Skip to content

test(runtime): route-ledger conformance checks that every route the dispatcher plugin mounts has a ledger row - #26565

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-gvuvy7
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-gvuvy7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26513

What changes

packages/runtime/src/route-ledger.conformance.test.ts gains a per-route arm: every route dispatcher-plugin.ts mounts on its host IHttpServer must have a ROUTE_LEDGER row. The mounted side is recorded, never typed: the plugin's own start() runs against a recording server (the same technique as automation-api-contract-mounts.test.ts), and the recorded VERB /path list is the population. No count is written anywhere.

Why

Deleting a single mid-ledger row left this suite green. Only the hand-kept row count noticed, and that count is the transcription-pin shape #26431 retires.

What the arm guards, measured

Per-row ablation over the recorded mount table: 71 of 105 rows now red this suite when deleted. The other 34 cannot, because they have no mount of their own spelling:

  • routes only dispatch() answers behind a catch-all host (the hono adapter's app.all(prefix/*)): every /meta and /data row, /share-links, /security/suggested-bindings, /ui/view;
  • doors a plugin serves from the raw app on a self-hosted kernel: /approvals/act, /webhooks/redeliver, /automation/hooks/:flowName/:hookId;
  • the two servedBy specializations: POST /actions/global/:action, POST /actions/_activation/:object/:action;
  • family rows (* /auth/**, * /apps/**, * /share-links/**) and GET /mcp/skill, which * /mcp/** absorbs.

⚠️ This includes all three rows the card named. GET /meta/types is not mounted by the plugin; on objectstack serve REST mounts it, and on a catch-all host dispatch() resolves it inside the /meta domain's inline if-chain. The two /actions rows are servedBy specializations that are registered nowhere. So a mount-derived arm cannot red on them. Turning those red would need route-granularity enumeration of dispatch() itself, which today exists only as inline parts[N] === 'literal' code. The order's red-first used a bridge-mounted row instead (Tests below). The seat has the question of the remaining 34 rows in the report.

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:运行时路由台账的一致性测试新增一条按路由的反向检查:分派插件实际挂载的每条路由,都必须在台账里有一行。
  • 为什么改:以前删掉台账中间某一行,这个测试照样全绿,只有手写的行数计数会报错(ci(gates): retire the transcription pins — tests and gates that hold a hand-typed count or table equal to the tree and red on every unrelated move (parent of eight) #26431 要退役的那种)。
  • 风险与回滚:只改测试和注释,不改产品代码,不发布。回滚就是 revert 这个 PR。
  • 先看:packages/runtime/src/route-ledger.conformance.test.ts(新增的 dispatcherPluginMounts 和 hasLedgerRow),packages/runtime/src/route-ledger.ts 的 CENSUS 注释段。
  • 两分钟验证:从 route-ledger.ts 删掉 GET /automation/actions 这一行,跑 pnpm --filter @objectstack/runtime exec vitest run --project local src/route-ledger.conformance.test.ts,应看到 1 个失败,并点名 GET /api/v1/automation/actions。
  • 需要你做的:无,席位落地。

Tests

Red first on main 16ca320c92 vs this head. Ablation through scripts/ablation-replace.mjs, deleting the row GET /automation/actions (mid-ledger, in a domain with siblings). The mutation landed (anchor 1 → 0) and was restored to the HEAD blob with git diff HEAD empty both times. The ablation is not committed.

Ablation readings
  • main, row deleted: route-ledger.conformance.test.ts 6/6 passed (exit 0). The old suite cannot see it.
  • This head, row deleted: 1 failed, 7 passed (exit 1): Dispatcher mounts with no route-ledger row: GET /api/v1/automation/actions, GET /api/v1/environments/:environmentId/automation/actions.
  • This head, GET /meta/types deleted (the card's example): 8/8 passed (exit 0), as measured above: that row has no mount to derive from.
  • This head, unmutated: 8/8 passed.

Test budget: no server is booted. The arm adds one createDispatcherPlugin().start() against a recording server, shared by both tests through beforeAll. Suite wall: 15.45 s on main, 16.72 s here (shared box; almost all of it is the import phase, 15.28 s vs 16.54 s, from loading dispatcher-plugin.ts). Test time is 12 ms vs 16 ms.

Acceptance notes

  • The arm guards what the plugin mounts. Rows only dispatch() answers stay census-guarded, as listed above.
  • No production listing method was added. The order allowed one only if no complete enumeration existed without it, and the recording server is complete for the mount. A listing of dispatch()'s inline arms would be a hand-written second ledger, not derived from anything that routes, so it is not offered here.

Verification

Run on 2eae6bd3, the final head.

Local runs
  • pnpm --filter @objectstack/runtime test (under os-verify-lock): Test Files 457 passed (457), Tests 7424 passed | 19 skipped. Exit 0.
  • pnpm --filter @objectstack/runtime typecheck: exit 0 (check:test-typecheck: OK).
  • eslint --no-inline-config on the three touched files: exit 0, no findings.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 70 commands, each exit code captured before any pipe. 68 exit 0, including pnpm check:route-ledger-census (its --self-test, then reads 105, array holds 105). 2 exit 3, NOT MEASURED (each gate refused its own prerequisite; CI runs both): node scripts/check-plugin-teardown-shape.mjs --self-test (the positive control is pinned to a commit this shallow clone cannot reach) and pnpm check:dual-build-cjs-loads (needs a full pnpm build). --ran: ✓ 70 derived famil(ies) accounted for — 68 run, 2 NOT-MEASURED.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (scoped to the touched files).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. Nothing publishes (test, a runtime-internal module's comment, a repo script), so skip-changeset. Both declaration lines are filled in.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the lane's required Chinese summary).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5oFmiRCo3V9eo4wD3y


Generated by Claude Code

…e dispatcher plugin's mounts

Every route dispatcher-plugin.ts registers on its host IHttpServer, recorded
as the plugin starts in its widest composition (no REST, a serveable analytics
service, environment scoping on), must have a ROUTE_LEDGER row. Deleting a
mounted route's row now reds the runtime suite instead of only the hand-kept
census. Rows with no mount of their own spelling stay census-guarded.

Re-points the dead #17041 references in route-ledger.ts, the conformance
header and check-route-ledger-census.mjs to #26513.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5oFmiRCo3V9eo4wD3y
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/runtime/src/route-ledger.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/runtime/src/route-ledger.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1c632311568790ca1f6014ad13ecab34be6e7389 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-27T08:58Z

Head 2eae6bd3, report 6437342937 (on #26513), claim 6436437376. No contract review is owed: there is no packages/spec/** diff, no governed path, no narrowing and no Clause-②: yes.

item reading
Shape Draft, base main, Fixes #26513, skip-changeset (test and comment only). 3 files: route-ledger.conformance.test.ts (the reverse arm), the route-ledger.ts header and check-route-ledger-census.mjs; the last two re-point the dead #17041 references. No runtime listing was added.
Fix Triage 6435737291, card option (a). The reverse arm is derived from a recording-server start of dispatcher-plugin (124 mounts), never from a typed count. 71 of the 105 ledger rows now turn the arm red when deleted.
Open question The dev's open_questions[0] (needs_decision): the order said deleting any of the card's three rows must turn the arm red. The dev measured that no mount-derived arm can do that. Those three rows and 31 others have no mount of their own spelling: /meta and /data are dispatch()-only behind the catch-all, and the /actions rows are servedBy. The seat takes option A. The "three must red" line was the seat's own order wording, not a ruling, and the triage ruling (derive from the mount) is met. The 34 rows stay guarded by check-route-ledger-census, which this PR keeps, and #26431 rules on the census with this measurement in hand. Option B (make dispatch() enumerable at route granularity) is a routing-core refactor well beyond this card; it can be filed as its own card if wanted. Option C (a hand-written listing) is rejected as the transcription shape #26431 retires.
Evidence CI on head 2eae6bd378: 36 succeeded, 16 expected skips, none failed or pending; mergeable clean. Derived gates ran green except two local NOT-MEASURED with exit 3 (check-plugin-teardown-shape --self-test on a shallow clone, and check:dual-build-cjs-loads without a full build); CI runs both.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 08:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 8e9abd9 Oct 27, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-gvuvy7 branch October 27, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants