From 423d61f96737d60dbc58c3f29b632b658d4bcaea Mon Sep 17 00:00:00 2001 From: sohey Date: Mon, 12 Oct 2026 20:47:37 +0200 Subject: [PATCH] ci(base-std-sync): route base-std docs/b20 to the B20 section and scope changelog entries to B20 base-std#240 moves every B20 doc under docs/b20/ so each Base standard gets its own docs// folder. Update the route table to match: - Point the 15 B20 product-doc rules at docs/b20/... (same Base Docs targets). - Route docs/b20/integration-architecture.md (previously unrouted) to the B20 overview and the precompiles page; ignore docs/b20/README.md and AGENTS.md. - Limit changelog-entry rules to B20 products (B20*, PolicyRegistry, ActivationRegistry). Other standards (e.g. BaseTime, see base/docs#2095) are reported as unrouted and their rows are skipped on the B20 changelog summary. - Skip renames with no content change (new unchanged_renames list derived from the commit API) so a folder move does not send every page to the model. - Add a base-std#240 fixture, tests for the changelog scope and pure moves, and fix a stale errors/events expectation. --- .github/workflows/base-std-docs-sync.yml | 30 +++++-- scripts/sync-from-base-std/README.md | 32 ++++++- .../__tests__/base-std-routing.test.mjs | 88 ++++++++++++++----- .../fixtures/code-change-docs-b20-move.json | 79 +++++++++++++++++ scripts/sync-from-base-std/index.mjs | 38 ++++++-- scripts/sync-from-base-std/route-table.json | 62 +++++++++---- 6 files changed, 269 insertions(+), 60 deletions(-) create mode 100644 scripts/sync-from-base-std/fixtures/code-change-docs-b20-move.json diff --git a/.github/workflows/base-std-docs-sync.yml b/.github/workflows/base-std-docs-sync.yml index 02e013e16..402634c0e 100644 --- a/.github/workflows/base-std-docs-sync.yml +++ b/.github/workflows/base-std-docs-sync.yml @@ -787,10 +787,16 @@ jobs: # from client_payload — and let the script skip them for routing. # A rename counts its previous name as removed. # - # Best-effort: an API failure leaves removed_paths empty and logs a + # A rename with zero content changes (status `renamed`, `changes == 0`) + # also lists its new name under `unchanged_renames`: a pure move + # carries nothing to sync, so the script skips it for routing. Without + # this, a folder move such as base-std#240 (docs/ -> docs/b20/) would + # hand the model a header-only diff for every B20 page. + # + # Best-effort: an API failure leaves both lists empty and logs a # warning rather than failing a sync whose content is otherwise fine. # Runs for every non-release dispatch so the payload's own - # removed_paths, if any, is always overwritten. + # removed_paths / unchanged_renames, if any, are always overwritten. if: env.PAYLOAD_KIND != 'release' env: SOURCE_REPO: ${{ env.PAYLOAD_SOURCE_REPO }} @@ -803,9 +809,9 @@ jobs: set -euo pipefail if [[ -z "${SHA:-}" ]]; then - jq '.removed_paths = []' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp" + jq '.removed_paths = [] | .unchanged_renames = []' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp" mv "$PAYLOAD_PATH.tmp" "$PAYLOAD_PATH" - echo "No sha in payload; removed_paths cleared." + echo "No sha in payload; removed_paths and unchanged_renames cleared." exit 0 fi @@ -817,12 +823,12 @@ jobs: "https://api.github.com/repos/${SOURCE_REPO}/commits/${SHA}") || code="000" if [[ "$code" != "200" ]]; then echo "::warning title=Removed paths unavailable::reading commit ${SHA:0:7} on ${SOURCE_REPO} failed (HTTP ${code}); deletions will route like edits this run" - jq '.removed_paths = []' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp" + jq '.removed_paths = [] | .unchanged_renames = []' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp" mv "$PAYLOAD_PATH.tmp" "$PAYLOAD_PATH" exit 0 fi jq -e '(.files // []) | all(.[]; (.filename | type == "string"))' "$commit" >/dev/null \ - || { echo "::warning title=Removed paths unavailable::commit ${SHA:0:7} returned malformed file metadata"; jq '.removed_paths = []' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp"; mv "$PAYLOAD_PATH.tmp" "$PAYLOAD_PATH"; exit 0; } + || { echo "::warning title=Removed paths unavailable::commit ${SHA:0:7} returned malformed file metadata"; jq '.removed_paths = [] | .unchanged_renames = []' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp"; mv "$PAYLOAD_PATH.tmp" "$PAYLOAD_PATH"; exit 0; } jq --argjson cap "$MAX_REMOVED_PATHS" --argjson bytes "$MAX_REMOVED_PATH_BYTES" ' [ .files[]? | select(.status == "removed" or .status == "renamed") @@ -830,10 +836,18 @@ jobs: | select(type == "string" and length > 0 and length <= $bytes) ] | unique | .[0:$cap] ' "$commit" > "$RUNNER_TEMP/trusted-removed-paths.json" + jq --argjson cap "$MAX_REMOVED_PATHS" --argjson bytes "$MAX_REMOVED_PATH_BYTES" ' + [ .files[]? + | select(.status == "renamed" and ((.changes // 1) == 0)) + | .filename + | select(type == "string" and length > 0 and length <= $bytes) + ] | unique | .[0:$cap] + ' "$commit" > "$RUNNER_TEMP/trusted-unchanged-renames.json" jq --slurpfile removed "$RUNNER_TEMP/trusted-removed-paths.json" \ - '.removed_paths = $removed[0]' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp" + --slurpfile moved "$RUNNER_TEMP/trusted-unchanged-renames.json" \ + '.removed_paths = $removed[0] | .unchanged_renames = $moved[0]' "$PAYLOAD_PATH" > "$PAYLOAD_PATH.tmp" mv "$PAYLOAD_PATH.tmp" "$PAYLOAD_PATH" - echo "::notice title=Trusted removed paths derived::sha=${SHA:0:7} removed_paths=$(jq length "$RUNNER_TEMP/trusted-removed-paths.json")" + echo "::notice title=Trusted removed paths derived::sha=${SHA:0:7} removed_paths=$(jq length "$RUNNER_TEMP/trusted-removed-paths.json") unchanged_renames=$(jq length "$RUNNER_TEMP/trusted-unchanged-renames.json")" - name: Derive trusted release routing inputs # client_payload.changed_paths is attacker-controlled JSON. For a diff --git a/scripts/sync-from-base-std/README.md b/scripts/sync-from-base-std/README.md index 69a2b1491..3da79bfb0 100644 --- a/scripts/sync-from-base-std/README.md +++ b/scripts/sync-from-base-std/README.md @@ -28,6 +28,7 @@ bucket, logged as `[routing]` and reported in the PR body (or an issue): | ignored | matched only a `kind: "ignored"` rule | nothing; deliberately unsynced (upstream README, authoring templates) | | unrouted | matched no rule | listed under **Unrouted source files** with a guideline-derived placement proposal | | removed | deleted in the source commit (`removed_paths`) | never routed; listed under **Removed source files** | +| moved | renamed with no content change (`unchanged_renames`) | never routed; listed under **Moved source files** | `removed_paths` is derived by the workflow from the commit API for the verified sha, never from the dispatcher's payload. A deleted documentation file carries @@ -38,6 +39,28 @@ has been removed" banners on reference pages (base/docs#1928); the validator now rejects callouts that describe repository housekeeping (`validateCallouts` in `safety.mjs`). +`unchanged_renames` comes from the same commit API call: the new name of every +file GitHub reports as `renamed` with zero changes. A pure move carries nothing +to sync, so a folder move like base-std#240 does not send every B20 page to the +model with a header-only diff. A rename that also edits content still routes by +its new path. + +### Feature folders in base-std + +base-std is the Base Standard Library: one `docs//` folder per Base +standard (base-std#240). Only B20 is mapped today: + +| base-std source | Base Docs target | +|---|---| +| `docs/b20/**` | the B20 specification under `docs/specifications/b20/` and the Build on Base task pages each rule names | +| `changelog/NN___.md` where Product is `B20*`, `PolicyRegistry`, or `ActivationRegistry` | its own entry page under `docs/base-chain/specs/reference/b20/changelog/`, plus a row on the B20 changelog summary | +| any other `docs//` folder or changelog product (e.g. BaseTime) | unrouted: listed in the PR or issue body until a rule maps it | + +To onboard a new feature, add `product-doc` rules for its `docs//` +files pointing at the Base Docs pages that own it, and widen the changelog +entry `source_pattern` (or add a second `changelog-entry` rule with its own +`page_template`). + ### Placement proposals from the IA guidelines Unrouted Markdown sources go through one Haiku call (`proposePlacement`) that @@ -89,9 +112,12 @@ LLM_GATEWAY_API_KEY=... \ --payload scripts/sync-from-base-std/fixtures/code-change-ib20.json ``` -`fixtures/code-change-docs-restructure.json` is the real file list from -base-std@be6d045 with its `removed_paths`; run it the same way to exercise the -docs-tree routes and the placement pass (set `RUNNER_TEMP` to see the routing +`fixtures/code-change-docs-b20-move.json` is the real file list from +base-std#240 (docs moved under `docs/b20/`) with its `removed_paths` and +`unchanged_renames`; the routing tests use it. `fixtures/code-change-docs-restructure.json` +is the older base-std@be6d045 file list (pre-#240 paths, so its docs files are +now unrouted); run either the same way to exercise the docs-tree routes and the +placement pass (set `RUNNER_TEMP` to see the routing report written as `sync-review.md`). Configuration knobs are optional positive numbers: diff --git a/scripts/sync-from-base-std/__tests__/base-std-routing.test.mjs b/scripts/sync-from-base-std/__tests__/base-std-routing.test.mjs index 3fa2cdd80..40a78a1f3 100644 --- a/scripts/sync-from-base-std/__tests__/base-std-routing.test.mjs +++ b/scripts/sync-from-base-std/__tests__/base-std-routing.test.mjs @@ -315,7 +315,7 @@ test("loadKnownRoutes: the central Interfaces page is reachable", async () => { // the sync edited pages from an all-minus diff and dropped 15 new files // silently. These tests pin the behavior that replaced that. -const RESTRUCTURE_FIXTURE = "scripts/sync-from-base-std/fixtures/code-change-docs-restructure.json"; +const RESTRUCTURE_FIXTURE = "scripts/sync-from-base-std/fixtures/code-change-docs-b20-move.json"; async function loadRouteTable() { return JSON.parse( @@ -330,39 +330,55 @@ test("upstream docs tree routes to the pages the IA guidelines assign", async () // Architecture: chain-generic precompile mechanics → Base Protocol → Execution; // B20 component map → spec overview; execution/versioning invariants → invariants page. - const arch = await pagesFor("docs/architecture.md"); + const arch = await pagesFor("docs/b20/architecture.md"); for (const expected of [ "docs/specifications/base-protocol/execution/precompiles.mdx", `${B20_REFERENCE_ROOT}/index.mdx`, `${B20_REFERENCE_ROOT}/reference/interfaces.mdx`, ]) { - assert.ok(arch.includes(expected), `docs/architecture.md should route to ${expected}`); + assert.ok(arch.includes(expected), `docs/b20/architecture.md should route to ${expected}`); } assert.ok(!arch.some((p) => p.includes("/i-policy-registry/")), "architecture must not fan out to the policy registry subtree"); // Concepts feed the spec overview key-concept sections plus the owning reference pages. - const multipliers = await pagesFor("docs/concepts/multipliers.md"); + const multipliers = await pagesFor("docs/b20/concepts/multipliers.md"); assert.ok(multipliers.includes(`${B20_REFERENCE_ROOT}/index.mdx`)); assert.ok(multipliers.includes(`${B20_REFERENCE_ROOT}/reference/interfaces.mdx`)); assert.ok(multipliers.includes("docs/build-on-base/issue-rwa/apply-a-multiplier.mdx")); // Guides feed the existing Build on Base task pages (ia-guidelines: Tokenize Assets / Issue Stablecoins). - assert.ok((await pagesFor("docs/guides/scheduling-stock-splits.md")).includes("docs/build-on-base/issue-rwa/apply-a-multiplier.mdx")); - assert.ok((await pagesFor("docs/guides/announcing-corporate-actions.md")).includes("docs/build-on-base/issue-rwa/announce-a-distribution.mdx")); - assert.ok((await pagesFor("docs/guides/restricting-transfer-initiators.md")).includes("docs/build-on-base/issue-rwa/restrict-transfer-initiators.mdx")); - const seize = await pagesFor("docs/guides/seizeing-assets.md"); + assert.ok((await pagesFor("docs/b20/guides/scheduling-stock-splits.md")).includes("docs/build-on-base/issue-rwa/apply-a-multiplier.mdx")); + assert.ok((await pagesFor("docs/b20/guides/announcing-corporate-actions.md")).includes("docs/build-on-base/issue-rwa/announce-a-distribution.mdx")); + assert.ok((await pagesFor("docs/b20/guides/restricting-transfer-initiators.md")).includes("docs/build-on-base/issue-rwa/restrict-transfer-initiators.mdx")); + const seize = await pagesFor("docs/b20/guides/seizeing-assets.md"); assert.ok(seize.includes("docs/build-on-base/issue-rwa/seize-and-cancel-units.mdx")); assert.ok(seize.includes("docs/build-on-base/issue-stablecoins/recover-funds.mdx")); assert.ok(seize.includes(`${B20_REFERENCE_ROOT}/reference/interfaces.mdx`)); // Reference tables feed the supporting pages. - assert.deepEqual(await pagesFor("docs/reference/constants.md"), [`${B20_REFERENCE_ROOT}/reference/constants.mdx`]); - assert.deepEqual(await pagesFor("docs/reference/errors.md"), [`${B20_REFERENCE_ROOT}/reference/errors.mdx`]); - assert.deepEqual(await pagesFor("docs/reference/events.md"), [`${B20_REFERENCE_ROOT}/reference/events.mdx`]); - assert.deepEqual(await pagesFor("docs/reference/interfaces.md"), [`${B20_REFERENCE_ROOT}/reference/interfaces.mdx`]); - - // Scaffolding is explicitly ignored, and the retired flat tree no longer routes anywhere. - for (const src of ["docs/guides/template.md", "docs/README.md", "README.md", "docs/B20/Asset.md", "docs/PolicyRegistry/README.md"]) { + assert.deepEqual(await pagesFor("docs/b20/reference/constants.md"), [`${B20_REFERENCE_ROOT}/reference/constants.mdx`]); + assert.deepEqual(await pagesFor("docs/b20/reference/errors.md"), [`${B20_REFERENCE_ROOT}/reference/errors.mdx`, `${B20_REFERENCE_ROOT}/reference/events.mdx`]); + assert.deepEqual(await pagesFor("docs/b20/reference/events.md"), [`${B20_REFERENCE_ROOT}/reference/errors.mdx`, `${B20_REFERENCE_ROOT}/reference/events.mdx`]); + assert.deepEqual((await pagesFor("docs/b20/integration-architecture.md")).sort(), [ + `${B20_REFERENCE_ROOT}/index.mdx`, + "docs/specifications/base-protocol/execution/precompiles.mdx", + ]); + assert.deepEqual(await pagesFor("docs/b20/reference/interfaces.md"), [`${B20_REFERENCE_ROOT}/reference/interfaces.mdx`]); + + // Scaffolding is explicitly ignored; the retired flat trees (pre-#213 docs/B20/, pre-#240 docs//) + // and other features' folders no longer route anywhere. + for (const src of [ + "docs/b20/guides/template.md", + "docs/b20/README.md", + "docs/README.md", + "README.md", + "docs/B20/Asset.md", + "docs/PolicyRegistry/README.md", + "docs/overview.md", + "docs/concepts/policies.md", + "docs/reference/errors.md", + "docs/basetime/overview.md", + ]) { assert.deepEqual(await pagesFor(src), [], `${src} must not route`); } }); @@ -383,21 +399,47 @@ test("removed source files never route, even when a rule still matches them", as assert.deepEqual(work[0].sourceFiles, ["src/interfaces/IB20Asset.sol"]); }); -test("classifyChangedPaths separates routed, ignored, unrouted, and removed for the be6d045 fixture", async () => { +test("classifyChangedPaths separates routed, ignored, unrouted, removed, and moved for the base-std#240 fixture", async () => { const routeTable = await loadRouteTable(); const fixture = JSON.parse(await fs.readFile(path.join(REPO_ROOT, RESTRUCTURE_FIXTURE), "utf8")); - const c = classifyChangedPaths(routeTable, fixture.changed_paths, { removedPaths: fixture.removed_paths }); + const opts = { removedPaths: fixture.removed_paths, unchangedRenames: fixture.unchanged_renames }; + const c = classifyChangedPaths(routeTable, fixture.changed_paths, opts); assert.deepEqual(c.removed, fixture.removed_paths); - assert.deepEqual(c.ignored.sort(), ["README.md", "docs/README.md", "docs/guides/template.md"]); - assert.deepEqual(c.unrouted, [], "every surviving file in the restructure has a rule"); - assert.equal(c.routed.length, fixture.changed_paths.length - c.removed.length - c.ignored.length); + assert.deepEqual(c.moved, fixture.unchanged_renames); + assert.deepEqual(c.ignored.sort(), ["AGENTS.md", "README.md", "docs/README.md", "docs/b20/README.md"]); + assert.deepEqual(c.unrouted, [], "every surviving file in the move has a rule"); + // Every renamed doc that changed content still routes by its new docs/b20/ path. + for (const f of ["docs/b20/overview.md", "docs/b20/concepts/token-types.md", "docs/b20/reference/errors.md", "docs/b20/reference/interfaces.md"]) { + assert.ok(c.routed.includes(f), `${f} should route`); + } + // Pure moves produce no work; only files whose content changed are routed. + const work = await routeCodeChange(routeTable, fixture.changed_paths, { repoRoot: REPO_ROOT, ...opts }); + const sources = new Set(work.flatMap((w) => w.sourceFiles)); + for (const f of fixture.unchanged_renames) assert.ok(!sources.has(f), `${f} is a pure move and must not route`); + for (const f of fixture.removed_paths) assert.ok(!sources.has(f), `${f} was removed and must not route`); + assert.ok(sources.has("docs/b20/reference/errors.md")); // A file outside every rule is reported, not dropped. - const c2 = classifyChangedPaths(routeTable, ["docs/concepts/brand-new-topic.md", "foundry.toml"]); - assert.deepEqual(c2.unrouted, ["docs/concepts/brand-new-topic.md", "foundry.toml"]); - assert.ok(isDocSource("docs/concepts/brand-new-topic.md")); + const c2 = classifyChangedPaths(routeTable, ["docs/b20/concepts/brand-new-topic.md", "foundry.toml"]); + assert.deepEqual(c2.unrouted, ["docs/b20/concepts/brand-new-topic.md", "foundry.toml"]); + assert.ok(isDocSource("docs/b20/concepts/brand-new-topic.md")); assert.ok(!isDocSource("foundry.toml")); }); +test("changelog entries route to the B20 changelog only for B20 products", async () => { + const routeTable = await loadRouteTable(); + const pagesFor = async (src) => + (await routeCodeChange(routeTable, [src], { repoRoot: REPO_ROOT })).map((w) => w.page); + const entryDir = "docs/base-chain/specs/reference/b20/changelog"; + assert.deepEqual(await pagesFor("changelog/03_Denim_B20_token_receiver.md"), [`${entryDir}/03-denim-b20-token-receiver.mdx`]); + assert.deepEqual(await pagesFor("changelog/02_Cobalt_B20Asset_multiplier.md"), [`${entryDir}/02-cobalt-b20asset-multiplier.mdx`]); + assert.deepEqual(await pagesFor("changelog/03_Denim_PolicyRegistry_not_policy.md"), [`${entryDir}/03-denim-policyregistry-not-policy.mdx`]); + assert.deepEqual(await pagesFor("changelog/04_Ember_ActivationRegistry_example.md"), [`${entryDir}/04-ember-activationregistry-example.mdx`]); + // Other Base standards in base-std are reported as unrouted, never filed under B20. + const baseTime = "changelog/03_Denim_BaseTime_millisecond_timestamp.md"; + assert.deepEqual(await pagesFor(baseTime), []); + assert.deepEqual(classifyChangedPaths(routeTable, [baseTime]).unrouted, [baseTime]); +}); + test("filterPlacementProposals keeps only real sources and existing candidate pages", () => { const sources = ["docs/concepts/brand-new-topic.md"]; const candidates = [`${B20_REFERENCE_ROOT}/index.mdx`]; diff --git a/scripts/sync-from-base-std/fixtures/code-change-docs-b20-move.json b/scripts/sync-from-base-std/fixtures/code-change-docs-b20-move.json new file mode 100644 index 000000000..84838b297 --- /dev/null +++ b/scripts/sync-from-base-std/fixtures/code-change-docs-b20-move.json @@ -0,0 +1,79 @@ +{ + "$comment": "Real file list from base/base-std#240 (docs: organize documentation by feature): moves every B20 doc from docs/ to docs/b20/ so each Base standard gets its own docs// folder. removed_paths and unchanged_renames are what the workflow derives from the commit API (renames list their previous name as removed; renames with zero changes also list their new name as an unchanged rename). Used by the routing tests.", + "kind": "code-change", + "source_repo": "base/base-std", + "sha": "", + "pr_number": "240", + "pr_title": "docs: organize documentation by feature", + "changed_paths": [ + "AGENTS.md", + "README.md", + "changelog/03_Denim_B20_transfer_executor_enforcement.md", + "docs/README.md", + "docs/architecture.md", + "docs/b20/README.md", + "docs/b20/architecture.md", + "docs/b20/concepts/multipliers.md", + "docs/b20/concepts/policies.md", + "docs/b20/concepts/roles-and-pause.md", + "docs/b20/concepts/token-types.md", + "docs/b20/guides/announcing-corporate-actions.md", + "docs/b20/guides/restricting-transfer-initiators.md", + "docs/b20/guides/scheduling-stock-splits.md", + "docs/b20/guides/seizeing-assets.md", + "docs/b20/guides/template.md", + "docs/b20/integration-architecture.md", + "docs/b20/overview.md", + "docs/b20/reference/constants.md", + "docs/b20/reference/errors.md", + "docs/b20/reference/events.md", + "docs/b20/reference/interfaces.md", + "docs/concepts/multipliers.md", + "docs/concepts/policies.md", + "docs/concepts/roles-and-pause.md", + "docs/concepts/token-types.md", + "docs/guides/announcing-corporate-actions.md", + "docs/guides/restricting-transfer-initiators.md", + "docs/guides/scheduling-stock-splits.md", + "docs/guides/seizeing-assets.md", + "docs/guides/template.md", + "docs/integration-architecture.md", + "docs/overview.md", + "docs/reference/constants.md", + "docs/reference/errors.md", + "docs/reference/events.md", + "docs/reference/interfaces.md" + ], + "removed_paths": [ + "docs/architecture.md", + "docs/concepts/multipliers.md", + "docs/concepts/policies.md", + "docs/concepts/roles-and-pause.md", + "docs/concepts/token-types.md", + "docs/guides/announcing-corporate-actions.md", + "docs/guides/restricting-transfer-initiators.md", + "docs/guides/scheduling-stock-splits.md", + "docs/guides/seizeing-assets.md", + "docs/guides/template.md", + "docs/integration-architecture.md", + "docs/overview.md", + "docs/reference/constants.md", + "docs/reference/errors.md", + "docs/reference/events.md", + "docs/reference/interfaces.md" + ], + "unchanged_renames": [ + "docs/b20/architecture.md", + "docs/b20/concepts/multipliers.md", + "docs/b20/concepts/policies.md", + "docs/b20/concepts/roles-and-pause.md", + "docs/b20/guides/announcing-corporate-actions.md", + "docs/b20/guides/restricting-transfer-initiators.md", + "docs/b20/guides/scheduling-stock-splits.md", + "docs/b20/guides/seizeing-assets.md", + "docs/b20/guides/template.md" + ], + "diff_truncated": false, + "diff_artifact_run_id": "", + "diff_artifact_name": "" +} diff --git a/scripts/sync-from-base-std/index.mjs b/scripts/sync-from-base-std/index.mjs index 1389f7e9e..444a9a32a 100644 --- a/scripts/sync-from-base-std/index.mjs +++ b/scripts/sync-from-base-std/index.mjs @@ -310,7 +310,10 @@ export async function routeCodeChange(routeTable, changedPaths, options = {}) { // deletion implies shows up in the diffs of the files that still exist. // Routing it used to hand the model an all-minus diff and an invitation to // write "the source file has been removed" banners on live pages. - const removed = new Set(options.removedPaths || []); + // A file renamed with no content change (trusted `unchanged_renames`) is a + // pure move: its docs pages already describe it, and its diff is a rename + // header with no hunks. Skip it the same way. + const removed = new Set([...(options.removedPaths || []), ...(options.unchangedRenames || [])]); const work = new Map(); // page → {transformer, sourceFiles[], kinds[]} for (const filePath of changedPaths || []) { if (removed.has(filePath)) continue; @@ -348,18 +351,25 @@ export async function routeCodeChange(routeTable, changedPaths, options = {}) { * unrouted — no rule matched; surfaced in the PR/issue body so a human * can add a rule instead of the file being dropped silently * removed — deleted in the source commit (trusted `removed_paths`) + * moved — renamed with no content change (trusted `unchanged_renames`); + * never routed, listed so a reviewer sees why nothing changed * Pure function over the route table; no filesystem access. * - * @returns {{routed: string[], ignored: string[], unrouted: string[], removed: string[]}} + * @returns {{routed: string[], ignored: string[], unrouted: string[], removed: string[], moved: string[]}} */ -export function classifyChangedPaths(routeTable, changedPaths, { removedPaths = [] } = {}) { +export function classifyChangedPaths(routeTable, changedPaths, { removedPaths = [], unchangedRenames = [] } = {}) { const removedSet = new Set(removedPaths); - const out = { routed: [], ignored: [], unrouted: [], removed: [] }; + const movedSet = new Set(unchangedRenames); + const out = { routed: [], ignored: [], unrouted: [], removed: [], moved: [] }; for (const filePath of uniq(changedPaths || [])) { if (removedSet.has(filePath)) { out.removed.push(filePath); continue; } + if (movedSet.has(filePath)) { + out.moved.push(filePath); + continue; + } const rules = (routeTable.code_changes || []).filter((r) => ruleMatches(r, filePath)); if (rules.length === 0) out.unrouted.push(filePath); else if (rules.every((r) => r.kind === "ignored")) out.ignored.push(filePath); @@ -765,6 +775,17 @@ export function routingReportRows({ classification, proposals = [], source, sha rows.push(""); for (const f of removed) rows.push(`- \`${f}\``); } + const moved = classification?.moved || []; + if (moved.length > 0) { + rows.push(""); + rows.push("## Moved source files"); + rows.push(""); + rows.push( + "Renamed in the source commit with no content change, so no docs page was edited for them. Each still routes by its new path the next time its content changes.", + ); + rows.push(""); + for (const f of moved) rows.push(`- ${link(f)}`); + } if (rows.length > 0) rows.push(""); return rows; } @@ -1788,9 +1809,12 @@ async function main() { const removedPaths = Array.isArray(payload.removed_paths) ? payload.removed_paths.filter((x) => typeof x === "string" && x.length <= 512).slice(0, 200) : []; - console.log(`[sync] changed_paths: ${changed.length}${removedPaths.length ? ` (removed upstream: ${removedPaths.length})` : ""}`); - work = await routeCodeChange(route, changed, { removedPaths }); - classification = classifyChangedPaths(route, changed, { removedPaths }); + const unchangedRenames = Array.isArray(payload.unchanged_renames) + ? payload.unchanged_renames.filter((x) => typeof x === "string" && x.length <= 512).slice(0, 200) + : []; + console.log(`[sync] changed_paths: ${changed.length}${removedPaths.length ? ` (removed upstream: ${removedPaths.length})` : ""}${unchangedRenames.length ? ` (moved unchanged: ${unchangedRenames.length})` : ""}`); + work = await routeCodeChange(route, changed, { removedPaths, unchangedRenames }); + classification = classifyChangedPaths(route, changed, { removedPaths, unchangedRenames }); for (const [label, list] of Object.entries(classification)) { if (label === "routed" || list.length === 0) continue; console.log(`[routing] ${label} (${list.length}): ${list.slice(0, 20).join(", ")}${list.length > 20 ? ", …" : ""}`); diff --git a/scripts/sync-from-base-std/route-table.json b/scripts/sync-from-base-std/route-table.json index cfe4c3c60..6d8a53fc2 100644 --- a/scripts/sync-from-base-std/route-table.json +++ b/scripts/sync-from-base-std/route-table.json @@ -159,8 +159,8 @@ "transformer": "claude" }, { - "$comment": "base-std docs/ tree (audience-layered since base-std#213). Placement follows docs/ia-guidelines.md and docs/content-guidelines.md: architecture material is a component map on the spec overview plus invariants, chain-generic precompile mechanics go to Base Protocol \u2192 Execution, concept pages feed the spec overview key-concept sections and the owning reference pages, guides feed the existing Build on Base task pages, reference tables feed the B20 supporting pages. No rule here creates a page.", - "source_prefix": "docs/overview.md", + "$comment": "base-std docs/b20/ tree (audience-layered since base-std#213; moved under docs/b20/ by base-std#240 so each Base standard gets its own docs// folder). Every docs/b20/ source maps to the B20 section of Base Docs. Other docs// folders are unrouted until a rule maps them. Placement follows docs/ia-guidelines.md and docs/content-guidelines.md: architecture material is a component map on the spec overview plus invariants, chain-generic precompile mechanics go to Base Protocol \u2192 Execution, concept pages feed the spec overview key-concept sections and the owning reference pages, guides feed the existing Build on Base task pages, reference tables feed the B20 supporting pages. No rule here creates a page.", + "source_prefix": "docs/b20/overview.md", "kind": "product-doc", "pages": [ "docs/build-on-base/issue-rwa/create-an-asset-token.mdx", @@ -169,7 +169,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/architecture.md", + "source_prefix": "docs/b20/architecture.md", "kind": "product-doc", "pages": [ "docs/specifications/base-protocol/execution/precompiles.mdx", @@ -179,7 +179,17 @@ "transformer": "claude" }, { - "source_prefix": "docs/concepts/multipliers.md", + "$comment": "Integrator-facing companion to architecture.md: what code sits at a B20 token address, how tooling should read it, and which guarantees are contractual. Same owners as architecture.md minus the interfaces page.", + "source_prefix": "docs/b20/integration-architecture.md", + "kind": "product-doc", + "pages": [ + "docs/specifications/b20/index.mdx", + "docs/specifications/base-protocol/execution/precompiles.mdx" + ], + "transformer": "claude" + }, + { + "source_prefix": "docs/b20/concepts/multipliers.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/index.mdx", @@ -189,7 +199,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/concepts/policies.md", + "source_prefix": "docs/b20/concepts/policies.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/index.mdx", @@ -201,7 +211,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/concepts/roles-and-pause.md", + "source_prefix": "docs/b20/concepts/roles-and-pause.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/index.mdx", @@ -213,7 +223,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/concepts/token-types.md", + "source_prefix": "docs/b20/concepts/token-types.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/index.mdx", @@ -224,7 +234,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/guides/scheduling-stock-splits.md", + "source_prefix": "docs/b20/guides/scheduling-stock-splits.md", "kind": "product-doc", "pages": [ "docs/build-on-base/issue-rwa/apply-a-multiplier.mdx", @@ -233,7 +243,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/guides/announcing-corporate-actions.md", + "source_prefix": "docs/b20/guides/announcing-corporate-actions.md", "kind": "product-doc", "pages": [ "docs/build-on-base/issue-rwa/announce-a-distribution.mdx", @@ -242,7 +252,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/guides/seizeing-assets.md", + "source_prefix": "docs/b20/guides/seizeing-assets.md", "kind": "product-doc", "pages": [ "docs/build-on-base/issue-rwa/seize-and-cancel-units.mdx", @@ -252,7 +262,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/guides/restricting-transfer-initiators.md", + "source_prefix": "docs/b20/guides/restricting-transfer-initiators.md", "kind": "product-doc", "pages": [ "docs/build-on-base/issue-rwa/restrict-transfer-initiators.mdx", @@ -262,13 +272,13 @@ }, { "$comment": "Upstream authoring template, not documentation.", - "source_prefix": "docs/guides/template.md", + "source_prefix": "docs/b20/guides/template.md", "kind": "ignored", "pages": [], "transformer": "claude" }, { - "source_prefix": "docs/reference/constants.md", + "source_prefix": "docs/b20/reference/constants.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/reference/constants.mdx" @@ -276,7 +286,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/reference/errors.md", + "source_prefix": "docs/b20/reference/errors.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/reference/errors.mdx", @@ -285,7 +295,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/reference/events.md", + "source_prefix": "docs/b20/reference/events.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/reference/errors.mdx", @@ -294,7 +304,7 @@ "transformer": "claude" }, { - "source_prefix": "docs/reference/interfaces.md", + "source_prefix": "docs/b20/reference/interfaces.md", "kind": "product-doc", "pages": [ "docs/specifications/b20/reference/interfaces.mdx" @@ -302,7 +312,7 @@ "transformer": "claude" }, { - "$comment": "Upstream navigation index.", + "$comment": "Upstream docs index (lists the docs// folders).", "source_prefix": "docs/README.md", "kind": "ignored", "pages": [], @@ -315,6 +325,20 @@ "pages": [], "transformer": "claude" }, + { + "$comment": "Upstream agent guide (documentation map for coding agents); not reader-facing.", + "source_prefix": "AGENTS.md", + "kind": "ignored", + "pages": [], + "transformer": "claude" + }, + { + "$comment": "B20 docs folder index; the pages it links are routed individually.", + "source_prefix": "docs/b20/README.md", + "kind": "ignored", + "pages": [], + "transformer": "claude" + }, { "source_prefix": "CHANGELOG.md", "kind": "changelog-index", @@ -332,9 +356,9 @@ "transformer": "claude" }, { - "$comment": "Per-feature entries (NN_Hardfork_Product_feature.md). The docs page is derived from the filename via page_template: each named group is lowercased with underscores replaced by hyphens, matching the naming convention in docs/content-guidelines.md. AGENTS.md / TEMPLATE_* never match.", + "$comment": "Per-feature entries (NN_Hardfork_Product_feature.md) for B20 products only: B20, B20Asset, B20Stablecoin, B20Factory, PolicyRegistry, ActivationRegistry. Entries for other Base standards in base-std (e.g. 03_Denim_BaseTime_*) do not match: the file is reported as unrouted and its changelog/README.md row is skipped on the B20 changelog summary. The docs page is derived from the filename via page_template: each named group is lowercased with underscores replaced by hyphens, matching the naming convention in docs/content-guidelines.md. AGENTS.md / TEMPLATE_* never match.", "source_prefix": "changelog/", - "source_pattern": "^changelog/(?\\d{2})_(?[A-Za-z0-9]+)_(?[A-Za-z0-9]+)_(?[A-Za-z0-9_]+)\\.md$", + "source_pattern": "^changelog/(?\\d{2})_(?[A-Za-z0-9]+)_(?B20[A-Za-z0-9]*|PolicyRegistry|ActivationRegistry)_(?[A-Za-z0-9_]+)\\.md$", "kind": "changelog-entry", "transformer": "claude", "page_template": "docs/base-chain/specs/reference/b20/changelog/{ordinal}-{hardfork}-{product}-{feature}.mdx",