Skip to content

feat(docs): refactor Building Block documentation — per-block docs/, committed catalog with CI sync gate - #124

Open
Simone319 wants to merge 17 commits into
mainfrom
zimzha/docs-restructure
Open

feat(docs): refactor Building Block documentation — per-block docs/, committed catalog with CI sync gate#124
Simone319 wants to merge 17 commits into
mainfrom
zimzha/docs-restructure

Conversation

@Simone319

@Simone319 Simone319 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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

  • Per-block docs folders. @aws-blocks/blocks now ships one folder per Building Block at docs/<block>/ (README.md, API.md, DESIGN.md, and any block-specific guides), generated at build time from each package's source.
  • Committed Building Block catalog. The catalog + decision tree live in packages/blocks/README.md. The catalog table is generated between <!-- BEGIN:block-catalog --> / <!-- END:block-catalog --> markers; the decision-tree prose is static.
  • CI sync gate. A new Block Catalog Check workflow runs sync-catalog.mjs --check on 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 via require.resolve('@aws-blocks/blocks/docs/<file>') regardless of npm/pnpm/Yarn-PnP layout or hoisting.
  • Agent-first navigation (no links). Catalog/prose use plain-text block names — no markdown links. Agents locate docs by resolving the package:
    node -p "require('path').dirname(require.resolve('@aws-blocks/blocks/docs/README.md'))"
    (fallback node_modules/@aws-blocks/blocks/docs), then read <block>/{README,API,DESIGN}.md relative to it. This avoids agents fetching URLs or following links that break in non-flat node_modules.
  • Scaffolded AGENTS.md (create-blocks-app) updated with the single resolve-based locator bullet.
  • README additions: Security Considerations section; AWS-credentials + least-privilege note for 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 gitignored docs/ artifact at prebuild; 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 + bundled docs/ guarantees discovery without hard-coded paths.

Testing

  • npm run sync-docs:check passes (and fails correctly on simulated drift)
  • npm run build -w packages/blocks passes (prebuild regenerates docs/, doesn't dirty the committed README)
  • node --test → 41/41 pass
  • Verified require.resolve('@aws-blocks/blocks/docs/...') resolves the version-correct copy across nested-conflict, monorepo-hoist, and simple-install layouts (CJS + ESM)
  • Changeset: patch bump for @aws-blocks/blocks + @aws-blocks/create-blocks-app

@Simone319
Simone319 requested a review from a team as a code owner June 29, 2026 12:06
@Simone319
Simone319 marked this pull request as draft June 29, 2026 12:07
Comment thread .github/workflows/block-catalog-check.yml Fixed
@Simone319 Simone319 changed the title Zimzha/docs restructure feat(docs): refactor Building Block documentation — per-block docs/, committed catalog with CI sync gate Jun 29, 2026
@Simone319

Copy link
Copy Markdown
Contributor Author

📊 Agent Bench — token/efficiency vs main

Reconstructed from per-cell bench results (PR run 08f65c3 vs main baseline 58f77dd, 10 task·template cells):

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
svidgen previously approved these changes Aug 5, 2026

@svidgen svidgen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions. No blockers.

Comment thread scripts/gen-block-docs.mjs Outdated
Comment thread scripts/gen-block-docs.mjs
Comment thread scripts/gen-block-docs.mjs

@osama-rizk osama-rizk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-run0 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/blocks/README.md Outdated

## 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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/sync-catalog.mjs
const packagesDir = join(__dirname, '..', 'packages');
const readmePath = join(packagesDir, 'blocks', 'README.md');

const EXCLUDED = new Set(['blocks', 'data-common', 'foundations', 'create-blocks-app']);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/sync-catalog.mjs Outdated
}

function renderCatalogTable(entries) {
const rows = entries.map((e) => `| ${e.pkg} | ${e.blurb || '—'} | ${e.keywords || '—'} |`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/sync-catalog.mjs
function extractKeywords(content) {
const match = content.match(/\*\*Keywords?:\*\*\s*(.+)/i);
return match ? match[1].trim() : '';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

svidgen
svidgen previously approved these changes Aug 6, 2026
…ding, escape catalog pipes, clarify duplicated-discovery rationale (PR #124)
osama-rizk
osama-rizk previously approved these changes Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants