feat(runtime): route ledger + conformance guard for the dispatcher↔client surface (#3563) - #3569
Merged
Merged
Conversation
…ient surface (#3563) #3528's root-cause class — a route that exists and works while @objectstack/client has no way to express it — now has an inventory and a ratchet. route-ledger.ts records the audited disposition of every dispatcher route: sdk (with the client method named) / gap / server-only / public / dynamic / mismatch. The audit found 27 gaps across 6 domains — /actions is entirely unreachable from the SDK (the largest hole), /keys has no SDK path to mint an API key, /share-links and /security have zero expression, /packages exposes 6 of 17 lifecycle routes, plus 4 shape mismatches where the client speaks REST dialect at dispatcher-only routes. route-ledger.conformance.test.ts is the guard, and it bites in both directions (each verified to fail when broken): - a dispatcher domain registered with no ledger entry fails — a new route surface cannot land without a reviewed SDK disposition; - a ledger entry claiming a client method that does not exist fails — the ledger cannot claim coverage the SDK does not have; - the gap count is ratcheted at 27 — closing a gap lowers it, raising it is a reviewed decision. The client is imported as a built artifact via a relative path deliberately: a runtime→client package edge would close a turbo build cycle (client's own devDeps point back at runtime for its Hono tests). A vitest subpath alias for @objectstack/core/logger makes that dist resolvable under the test aliases. Full findings — including the stale DEFAULT_DISPATCHER_ROUTES spec table that CLIENT_SPEC_COMPLIANCE.md anchors its "FULLY COMPLIANT" claim on, mount-less dispatch branches, the RealtimeAPI stub, six phantom methods in the client README, five surfaces with zero tests, and the un-audited @objectstack/rest second surface — live in docs/audits/2026-07-dispatcher-client-route-coverage.md with proposed follow-up slicing. No runtime behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…y — CI has no client dist in runtime's test task Test Core failed on the first push: the conformance test imported the built client (`../../client/dist/index.mjs`), assuming the dist exists whenever tests run. Wrong assumption — turbo's per-package test tasks build only their own dependency closure, and runtime deliberately has no edge to client (the reverse edge exists, so adding one is a build cycle turbo rejects). In CI the dist therefore never exists for runtime's suite. The fix is structural, not a skip: each package now verifies its own half. - runtime keeps the dispatcher directions (registry domain ↔ ledger, both ways) plus ledger hygiene and the gap ratchet — no client import at all. - client gains `route-ledger-coverage.test.ts`, which instantiates the real ObjectStackClient and asserts every ledger-named method resolves to a function. The ledger is imported as a relative SOURCE file: it is pure data with zero imports, and it lives in runtime because that is where routes are declared. - the now-unneeded @objectstack/core/logger vitest alias is reverted. Skipping when the dist is missing was rejected: Test Core would never exercise the client direction, and the guard's whole point is that this failure mode cannot go quiet again. Verified: phantom-method negative check bites in the new home; client suite 117 passed, runtime suite 653 passed; eslint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5
os-zhuang
marked this pull request as ready for review
July 27, 2026 06:54
This was referenced Jul 27, 2026
Merged
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
…he 2 PR-1 (#3587) (#3609) The dispatcher tranche (#3569..#3579) closed its 27 gaps and guards them; this brings the same regime to the second, larger surface. All 89 routes @objectstack/rest mounts now carry a reviewed disposition in rest-route-ledger.ts: 38 sdk, 43 gap (ratcheted), 3 server-only, 3 public, 2 mismatch. The guard enumerates BOTH sources for real — RouteManager routes via the getRoutes() introspection seam that already existed, and the two RouteManager-bypassing registrars (package-routes.ts, external-datasource-routes.ts) via captured mock-server registrations — so no pinned-by-hand list anywhere. The client-method direction lives in packages/client next to the SDK (same package-boundary split as tranche 1; a rest->client edge is unbuildable). All five guard directions were negative-tested and fail with the offending route named. Mismatches the audit surfaced, ledgered not fixed: POST /api/v1/packages is a publish-vs-install shape collision between REST and the dispatcher (REST registers first and wins); REST's GET /ui/view/:object/:type path dialect is unreachable by the SDK's query-param dialect. The service-storage / service-i18n autonomous mounts are a third surface, explicitly scoped out and noted in the ledger header. No behavior change: data + tests only, plus a scope-note refresh in the runtime ledger. Refs #3587 Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5 Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
… tables under them (#3612) (#3616) Five client families built URLs no server surface mounts (dispatcher, REST, and autonomous service mounts all checked in the #3587 audit): permissions x3, realtime x6, workflow x3, views x5, notifications device/prefs x4. Every call was a guaranteed 404. Each family was underwritten only by an unconsumed spec DEFAULT_*_ROUTES table — the same disease DEFAULT_DISPATCHER_ROUTES had (#3586) — so DEFAULT_PERMISSION/VIEW/ WORKFLOW/REALTIME_ROUTES go with them; getDefaultRouteRegistrations() returns 9. ApiRouteType loses its client-only 'views'|'permissions' extras. Kept: events (local buffer, no HTTP), notifications list/markRead/ markAllRead (dispatcher-served), approvals.* (ADR-0019), and meta.getLegalNextStates (the real FSM read). Docs/README/integration-test spec swept; api-surface.json regenerated (-4 exports). Re-adding any of these surfaces now requires the server route to exist and a route-ledger row proving it (#3569/#3609 guards). The one external consumer (objectui useClientNotifications dead delegates) is trimmed in a companion objectui change. Closes #3612 Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5 Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR-1 of #3563: the inventory, the classification, and the ratchet. No runtime behavior change.
What this establishes
#3528's root cause was a class, not an instance: a route can exist, work, and be documented while
@objectstack/clienthas no way to call it — and nothing fails. This PR makes the class visible and un-growable:packages/runtime/src/route-ledger.ts— the audited disposition of every dispatcher route:sdk(client method named) /gap/server-only/public/dynamic/mismatch, each non-sdk row carrying its rationale.packages/runtime/src/route-ledger.conformance.test.ts— every dispatcher domain (enumerated live fromDomainHandlerRegistry.list(), plus the pinned legacy if-chain prefixes) must have a ledger entry, in both directions; ledger hygiene (sdk rows name their method, non-sdk rows carry a rationale); and the gap count ratcheted at 27 — closing a gap lowers the bound, raising it is an explicit reviewed decision.packages/client/src/route-ledger-coverage.test.ts— every ledger entry naming a client method must resolve to a real function on an instantiatedObjectStackClient. The ledger is imported as a relative source file: pure data, zero imports.docs/audits/2026-07-dispatcher-client-route-coverage.md— full findings + follow-up slicing.Headline findings
/actions(server-registered actions, 3 shapes)/keys/share-links,/security/packagesanalytics.meta/explain, both storage methods)DEFAULT_DISPATCHER_ROUTES(spec)CLIENT_SPEC_COMPLIANCE.mdanchors "FULLY COMPLIANT" on itclient.events(RealtimeAPI)Also documented: dispatch branches with no HTTP mount (the dispatcher has no catch-all —
/security/*,/share-links/*,/ui/view/*etc. are dead over HTTP in a plain runtime and only reachable through@objectstack/restor a host catch-all), and the un-audited REST second surface (search / forms / reports / sharing rules / …) proposed as the next tranche.Design notes
@objectstack/spec(and retiringDEFAULT_DISPATCHER_ROUTES, a spec-major) is follow-up work.@objectstack/client's own devDependencies point back at runtime (its Hono tests boot a real server), so a package edge closes a turbo build cycle (verified — turbo rejects the graph), and CI's per-package test tasks build only their own dependency closure, so a dist import is equally unavailable (the first push failed Test Core exactly this way). Skipping-when-absent was rejected: the client direction would never run in CI, and this guard exists precisely so that failure mode cannot go quiet.Test plan
/keysledger entry fails the runtime direction with the intended message; renamingautomation.resume→automation.resumeRunin the ledger fails the client direction.action-execution.tsTS6133s under baretsc -preproduce identically onmainand are untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5