Skip to content

feat(runtime): route ledger + conformance guard for the dispatcher↔client surface (#3563) - #3569

Merged
os-zhuang merged 2 commits into
mainfrom
claude/screen-flow-submit-resume-v9dqjx
Jul 27, 2026
Merged

feat(runtime): route ledger + conformance guard for the dispatcher↔client surface (#3563)#3569
os-zhuang merged 2 commits into
mainfrom
claude/screen-flow-submit-resume-v9dqjx

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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/client has 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.
  • The guard, split along the package boundary (each package verifies its own half):
    • packages/runtime/src/route-ledger.conformance.test.ts — every dispatcher domain (enumerated live from DomainHandlerRegistry.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 instantiated ObjectStackClient. 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

Finding Scale
Dispatcher routes with no SDK expression 27 gaps across 6 domains
/actions (server-registered actions, 3 shapes) entirely unreachable from the SDK — the largest functional hole
/keys no SDK path to mint an API key at all
/share-links, /security zero expression
/packages 6 of 17 lifecycle routes expressed
Shape mismatches (client speaks REST dialect at dispatcher routes) 4 (analytics.meta/explain, both storage methods)
DEFAULT_DISPATCHER_ROUTES (spec) consumed by nothing in runtime; lists 2 nonexistent domains, omits 8 real ones — and CLIENT_SPEC_COMPLIANCE.md anchors "FULLY COMPLIANT" on it
Client surfaces with zero tests 5 surfaces / 62 methods (~36% of the SDK)
Client README documents 6 methods that do not exist
client.events (RealtimeAPI) non-functional in-memory stub

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/rest or a host catch-all), and the un-audited REST second surface (search / forms / reports / sharing rules / …) proposed as the next tranche.

Design notes

  • The ledger lives in runtime, not spec, deliberately: routes are declared in runtime, and the ledger is the guard's data, not public API. Promotion to @objectstack/spec (and retiring DEFAULT_DISPATCHER_ROUTES, a spec-major) is follow-up work.
  • The guard is split across the two packages because no runtime→client edge is buildable: @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

  • Both guard halves verified to bite: deleting the /keys ledger entry fails the runtime direction with the intended message; renaming automation.resumeautomation.resumeRun in the ledger fails the client direction.
  • Full suites: runtime 653 passed, client 117 passed (guards included).
  • ESLint clean on all touched files; the pre-existing action-execution.ts TS6133s under bare tsc -p reproduce identically on main and are untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5

…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
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 6:46am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling and removed size/m labels Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/client, @objectstack/runtime.

26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/client)
  • content/docs/api/client-sdk.mdx (via @objectstack/client)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client)
  • content/docs/api/error-catalog.mdx (via @objectstack/client)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client)
  • content/docs/permissions/authentication.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/releases/v16.mdx (via @objectstack/client)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
os-zhuang marked this pull request as ready for review July 27, 2026 06:54
@os-zhuang
os-zhuang merged commit 48d5a1c into main Jul 27, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/screen-flow-submit-resume-v9dqjx branch July 27, 2026 06:54
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants