feat(docs): refactor Building Block documentation — per-block docs/, committed catalog with CI sync gate - #124
feat(docs): refactor Building Block documentation — per-block docs/, committed catalog with CI sync gate#124Simone319 wants to merge 17 commits into
Conversation
…ME.md, export ./docs/*
…block-docs); --write is catalog-only
…e (no curl/broken-link confusion)
# Conflicts: # scripts/sync-block-docs.mjs
…ack tolerates pre/post-rename)
📊 Agent Bench — token/efficiency vs
|
| Metric | PR | main |
Δ |
|---|---|---|---|
| Total tokens (in+out) | 12.85M | 14.29M | −10.1% |
| Duration | 2,456s | 3,049s | −19.4% |
| Cycles | 326 | 345 | −5.5% |
Net: no efficiency regression — the docs restructure is aggregate cheaper and faster. Biggest gains: kb-chat-agent (−33.7% tokens), sql-kb-catalog (−25.1%), observability-api (−23.7%), oidc-dsql-notes (−60% duration).
svidgen
left a comment
There was a problem hiding this comment.
A few suggestions. No blockers.
…ckage docs/ folders (PR #124 feedback)
osama-rizk
left a comment
There was a problem hiding this comment.
Reviewed the docs-restructure end-to-end. I verified the core mechanism actually works rather than trusting the description: docs is in the files array so it ships despite being gitignored; require.resolve('@aws-blocks/blocks/docs/README.md') resolves correctly under the new ./docs/* export (I built a nested node_modules and confirmed); Node blocks ../ traversal outside docs/ through that export; sync-catalog --check passes on the committed README; the scaffolded templates all declare @aws-blocks/blocks so the resolve-based locator works from an app root. The require.resolve + committed-catalog + CI-gate design is a genuinely good answer to "make docs reachable by agents without hard-coded node_modules paths."
Most of my comments are at the systemic/consistency altitude — the code is correct; the risks are about the generated artifact's lifecycle and whether the docs the README promises actually all exist. One (the publish/prebuild coupling) I'd want addressed before this ships, since it can silently defeat the whole feature. Nothing else blocks.
I deliberately did not spend much on line-level correctness of the two scripts (the marker injection, blurb regex, sort) — they're small, I ran them, and CI exercises them. The higher-leverage question for a docs-infrastructure change is "does the artifact reliably ship and match its own promises," which is where the findings sit.
| }, | ||
| "scripts": { | ||
| "prebuild": "node ../../scripts/sync-block-docs.mjs", | ||
| "prebuild": "node ../../scripts/gen-block-docs.mjs", |
There was a problem hiding this comment.
Publish fragility — this can silently ship a package with no docs, defeating the whole feature. docs/ is gitignored and only (re)generated by this prebuild hook, i.e. it exists only after npm run build runs for this package. But scripts/publish/publish.ts explicitly does not build — its comment reads "don't rebuild — pack what was already built and tested," and it only verifies dist/ exists (errors with "No build output found" if not). dist/ and docs/ are decoupled: a tree can have a valid dist/ from an earlier tsc run while docs/ is absent (fresh clone, or docs/ cleaned).
I simulated it: rm -rf packages/blocks/docs && npm pack --dry-run → 0 docs/ entries in the tarball. Every require.resolve('@aws-blocks/blocks/docs/...') in the README and scaffolded AGENTS.md then throws for the installed consumer.
CI's blocks-integrity job regenerates docs before packing, so CI is safe — but that guards the diff, not the publish. The publish path relies on an unstated invariant ("a full build always immediately precedes publish"). Make it robust: add a prepack hook (npm runs prepack on both npm pack and npm publish) that runs gen-block-docs.mjs, so the docs artifact is generated at pack time regardless of build state. prebuild alone doesn't cover the pack-without-build path.
There was a problem hiding this comment.
Fixed in 7d2dbf1 — added a prepack hook to packages/blocks/package.json (node ../../scripts/gen-block-docs.mjs) so docs/ is generated at pack AND publish time, regardless of build state (npm runs prepack on both npm pack and npm publish). Verified: rm -rf packages/blocks/docs && npm pack --dry-run now ships the full docs/ tree (95 docs/ entries). Good catch — this closes the pack-without-build gap the prebuild-only hook left open.
|
|
||
| ## Building Block documentation | ||
|
|
||
| Every Building Block ships its full docs — `README.md`, `API.md`, `DESIGN.md`, and `CHANGELOG.md` — inside the `@aws-blocks/blocks` package under `docs/<block>/`. To read them, locate the bundled folder: |
There was a problem hiding this comment.
This tells agents every block ships README.md, API.md, DESIGN.md, and CHANGELOG.md under docs/<block>/, and line ~250 + the scaffolded AGENTS.md instruct agents to read <block>/API.md and <block>/DESIGN.md. But gen-block-docs.mjs only mirrors whatever *.md each package happens to have — it doesn't guarantee the set. I generated the artifact and checked: core is missing DESIGN.md; hosting and pipeline are missing both API.md and DESIGN.md.
Failure scenario: an agent follows the documented convention and reads docs/hosting/API.md → ENOENT. Agents are the stated primary consumer, and a confidently-wrong path is worse for them than for a human who'd just glance at the folder. Two options: (a) soften the wording to "README.md, plus API.md / DESIGN.md / CHANGELOG.md where present," or (b) have gen-block-docs.mjs assert the promised files exist (or emit a stub) so the docs contract can't drift from what actually ships. I'd lean (a) now + (b) as the durable fix.
There was a problem hiding this comment.
Fixed in 7d2dbf1 (option a) — softened the README 'Building Block documentation' section and the scaffolded AGENTS.md to say API.md / DESIGN.md '…where present' (a missing file is not an error), so agents won't treat e.g. docs/hosting/API.md as a broken path. Filed the durable fix (option b — assert/stub the promised set in gen-block-docs.mjs) as a follow-up: #322.
| const packagesDir = join(__dirname, '..', 'packages'); | ||
| const readmePath = join(packagesDir, 'blocks', 'README.md'); | ||
|
|
||
| const EXCLUDED = new Set(['blocks', 'data-common', 'foundations', 'create-blocks-app']); |
There was a problem hiding this comment.
Design note (non-blocking): the EXCLUDED set and the getPackages() discovery logic are duplicated verbatim here and in gen-block-docs.mjs, and both files carry a comment saying this is "kept independent on purpose." I'd push back gently on that rationale. These two scripts must agree on which packages are blocks — the catalog table (this script) and the shipped docs/<block>/ folders (the other) are two views of the same set. If they drift (someone adds a block to one EXCLUDED list but not the other), the catalog and the docs folders silently disagree, and neither CI gate catches it because each script is internally consistent. "Independent on purpose" is a reasonable call for unrelated logic, but this is a shared invariant. Consider extracting getPackages() + EXCLUDED into one tiny shared module both import. If independence is truly intentional (e.g. avoiding a require cycle at build time), the comment should say why independence is worth the drift risk, not just that it's intended.
There was a problem hiding this comment.
Intentional for now — we keep EXCLUDED/getPackages() duplicated so each script stays dependency-free and independently runnable. You're right it's a shared invariant (the block set), so in 7d2dbf1 I updated the comment in both files to say why and to flag 'extract a shared module if this grows; keep the two in sync when editing.' If drift becomes a real risk we'll consolidate into one module — for two small scripts the coupling didn't feel worth it yet.
| } | ||
|
|
||
| function renderCatalogTable(entries) { | ||
| const rows = entries.map((e) => `| ${e.pkg} | ${e.blurb || '—'} | ${e.keywords || '—'} |`); |
There was a problem hiding this comment.
Latent correctness bug (low prob today, but the CI gate makes it a hard failure when it lands): catalog cells are interpolated raw into a markdown table, but blurb comes from extractBlurb() (first sentence of a block README) and keywords from a free-text **Keywords:** line — neither escapes |. A future block whose opening sentence or keyword list contains a pipe (... key-value | cache ...) will produce a malformed table row, and since --check compares exact strings, the mismatch surfaces as a confusing CI failure that npm run sync-docs "fixes" by committing the equally-broken table. Cheap guard: .replace(/\|/g, '\\|') on blurb/keywords in renderCatalogTable. I confirmed no current block README trips this, so it's prevention, not a live bug.
There was a problem hiding this comment.
Fixed in 7d2dbf1 — added an escapeCell() that escapes | (and guards undefined) for blurb/keywords in renderCatalogTable, so a future value containing a pipe can't produce a malformed row / a confusing --check failure. No-op on the current committed table (no value trips it today), so it's prevention as you noted.
| function extractKeywords(content) { | ||
| const match = content.match(/\*\*Keywords?:\*\*\s*(.+)/i); | ||
| return match ? match[1].trim() : ''; | ||
| } |
There was a problem hiding this comment.
Test coverage: neither new script (sync-catalog.mjs, gen-block-docs.mjs) has a unit test — CI invocation is the only exercise. That's a defensible line to draw for build tooling, but two behaviors are worth pinning because they're the ones that silently corrupt output: extractBlurb (h1 detection, sentence truncation, the <!--/# break conditions) and injectCatalog / extractBetweenMarkers (missing marker → throw on write but exit-1 on check; end < begin handling). A handful of table-driven cases over fixture README strings would lock the contract the CI gate depends on. Not a blocker; flagging since a regression here fails every future PR's catalog check.
There was a problem hiding this comment.
Agreed these are the two worth pinning (extractBlurb, and injectCatalog/extractBetweenMarkers). Since CI exercises the happy path and you flagged it non-blocking, I filed a follow-up to add table-driven fixtures rather than expand this PR: #323.
…ding, escape catalog pipes, clarify duplicated-discovery rationale (PR #124)
…ut (unbreak pack under npm 10 prepack)
What
Restructures how AWS Blocks documentation is bundled and discovered, optimized for AI coding agents (Claude Code, the AWS MCP Blocks skill, etc.) reading docs from the installed package.
Changes
@aws-blocks/blocksnow ships one folder per Building Block atdocs/<block>/(README.md,API.md,DESIGN.md, and any block-specific guides), generated at build time from each package's source.packages/blocks/README.md. The catalog table is generated between<!-- BEGIN:block-catalog -->/<!-- END:block-catalog -->markers; the decision-tree prose is static.Block Catalog Checkworkflow runssync-catalog.mjs --checkon every PR and fails if the committed catalog is stale, with the fix instruction (npm run sync-docs)../docs/*export. Added to the package so docs are resolvable viarequire.resolve('@aws-blocks/blocks/docs/<file>')regardless of npm/pnpm/Yarn-PnP layout or hoisting.node -p "require('path').dirname(require.resolve('@aws-blocks/blocks/docs/README.md'))"node_modules/@aws-blocks/blocks/docs), then read<block>/{README,API,DESIGN}.mdrelative to it. This avoids agents fetching URLs or following links that break in non-flat node_modules.AGENTS.md(create-blocks-app) updated with the single resolve-based locator bullet.sandbox/deploy.Scripts
scripts/sync-catalog.mjs—--write(inject catalog into README markers) /--check(CI gate).npm run sync-docs/sync-docs:check.scripts/gen-block-docs.mjs— generates the gitignoreddocs/artifact atprebuild; never modifies the committed README.Why
Docs need to be reliably reachable by agents from the installed package, version-matched, and impossible to silently drift. Committed catalog + CI gate guarantees freshness;
require.resolve+ bundleddocs/guarantees discovery without hard-coded paths.Testing
npm run sync-docs:checkpasses (and fails correctly on simulated drift)npm run build -w packages/blockspasses (prebuild regeneratesdocs/, doesn't dirty the committed README)node --test→ 41/41 passrequire.resolve('@aws-blocks/blocks/docs/...')resolves the version-correct copy across nested-conflict, monorepo-hoist, and simple-install layouts (CJS + ESM)@aws-blocks/blocks+@aws-blocks/create-blocks-app