Repository navigation
test(runtime): route-ledger conformance checks that every route the dispatcher plugin mounts has a ledger row - #26565
Conversation
…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
📓 Docs Drift Check
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
席位核对 · ACCEPT ·
|
| 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
Requested by Leehom · project thread
Fixes #26513
What changes
packages/runtime/src/route-ledger.conformance.test.tsgains a per-route arm: every routedispatcher-plugin.tsmounts on its hostIHttpServermust have aROUTE_LEDGERrow. The mounted side is recorded, never typed: the plugin's ownstart()runs against a recording server (the same technique asautomation-api-contract-mounts.test.ts), and the recordedVERB /pathlist is the population. No count is written anywhere./discovery; it cedes that route when REST is present), a serveableanalyticsservice (the capability-conditional/analyticsroutes mount), andscoping.enableProjectScoping: trueunder the defaultprojectResolution: 'auto'(every environment-scoped twin mounts beside its unscoped route). The other compositions are strict subsets. A vacuity test asserts the boot actually reached those three branches.VERB /api/v1/patternrow, or a* /prefix/**family row whose base the mount sits at or under. That is the same reading the live-mount parity gate gives a wildcard row. A scoped twin answers to its unscoped row, because the plugin mounts it by calling the same registrar with the scoped base anddispatch()strips the scope before routing.ai:routeshook. They are dynamic by construction, and the* /ai/**family row speaks for them. The plugin's own/ai/*mounts are recorded.@objectstack/runtimecode changes; this PR adds no listing method.#17041pointers (theroute-ledger.tscensus header, this suite's header,scripts/check-route-ledger-census.mjs) are re-pointed to test(runtime): route-ledger.conformance has no per-route reverse check, so 26 deleted ledger rows stay green; the decision it defers to (#17041) does not resolve #26513, and each says what the new arm does and does not cover. [finding] The route-ledger conformance suite asserts only at DOMAIN granularity, so ANY single route row can vanish from ANY of the 11 ledgers with nothing red — measured by deleting one #17041's open question is left as it is: whether the ledger should be checked per route beyond what the plugin mounts stays an open maintainer question, now tracked at test(runtime): route-ledger.conformance has no per-route reverse check, so 26 deleted ledger rows stay green; the decision it defers to (#17041) does not resolve #26513.check-route-ledger-censusstays. Whether it retires is 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's call, after this lands. It is still the only per-row guard for the rows the arm cannot see (next section).route-ledger.tstop header said this suite fails "when an entry claims a client method that does not exist". That direction lives in the client package's guards, so the sentence now says so.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:
dispatch()answers behind a catch-all host (the hono adapter'sapp.all(prefix/*)): every/metaand/datarow,/share-links,/security/suggested-bindings,/ui/view;/approvals/act,/webhooks/redeliver,/automation/hooks/:flowName/:hookId;servedByspecializations:POST /actions/global/:action,POST /actions/_activation/:object/:action;* /auth/**,* /apps/**,* /share-links/**) andGET /mcp/skill, which* /mcp/**absorbs.GET /meta/typesis not mounted by the plugin; onobjectstack serveREST mounts it, and on a catch-all hostdispatch()resolves it inside the/metadomain's inline if-chain. The two/actionsrows areservedByspecializations that are registered nowhere. So a mount-derived arm cannot red on them. Turning those red would need route-granularity enumeration ofdispatch()itself, which today exists only as inlineparts[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
维护者速读
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
main16ca320c92vs this head. Ablation throughscripts/ablation-replace.mjs, deleting the rowGET /automation/actions(mid-ledger, in a domain with siblings). The mutation landed (anchor 1 → 0) and was restored to the HEAD blob withgit diff HEADempty both times. The ablation is not committed.Ablation readings
main, row deleted:route-ledger.conformance.test.ts6/6 passed (exit 0). The old suite cannot see it.Dispatcher mounts with no route-ledger row: GET /api/v1/automation/actions, GET /api/v1/environments/:environmentId/automation/actions.GET /meta/typesdeleted (the card's example): 8/8 passed (exit 0), as measured above: that row has no mount to derive from.Test budget: no server is booted. The arm adds one
createDispatcherPlugin().start()against a recording server, shared by both tests throughbeforeAll. Suite wall: 15.45 s onmain, 16.72 s here (shared box; almost all of it is theimportphase, 15.28 s vs 16.54 s, from loadingdispatcher-plugin.ts). Test time is 12 ms vs 16 ms.Acceptance notes
dispatch()answers stay census-guarded, as listed above.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(underos-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-configon the three touched files: exit 0, no findings.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 70 commands, each exit code captured before any pipe. 68 exit 0, includingpnpm check:route-ledger-census(its--self-test, thenreads 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) andpnpm check:dual-build-cjs-loads(needs a fullpnpm build).--ran:✓ 70 derived famil(ies) accounted for — 68 run, 2 NOT-MEASURED.Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses (scoped to the touched files).main, one pull request.skip-changeset. Both declaration lines are filled in.packages/specnot touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oFmiRCo3V9eo4wD3y
Generated by Claude Code