Skip to content

Commit f91cd8d

Browse files
committed
fix(runtime,client): split the ledger guard along the package boundary — 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
1 parent 5ae4a77 commit f91cd8d

3 files changed

Lines changed: 52 additions & 27 deletions

File tree

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
"@objectstack/runtime": patch
3+
"@objectstack/client": patch
34
---
45

56
Route ledger + conformance guard for the dispatcher↔client surface (#3563)
@@ -8,8 +9,10 @@ Route ledger + conformance guard for the dispatcher↔client surface (#3563)
89
`@objectstack/client` has no way to express it — now has an inventory and a
910
ratchet. `route-ledger.ts` records the audited disposition of every dispatcher
1011
route (sdk / gap / server-only / public / dynamic / mismatch);
11-
`route-ledger.conformance.test.ts` fails when a dispatcher domain lands with no
12-
ledger entry, when an entry claims a client method that doesn't exist, and when
13-
the audited gap count (27 at PR-1) grows. Findings and follow-up slicing live
12+
The guard is split along the package boundary (a runtime→client edge is a
13+
build cycle): runtime's `route-ledger.conformance.test.ts` fails when a
14+
dispatcher domain lands with no ledger entry and ratchets the audited gap
15+
count (27 at PR-1); client's `route-ledger-coverage.test.ts` fails when a
16+
ledger entry claims a client method that doesn't exist. Findings and follow-up slicing live
1417
in `docs/audits/2026-07-dispatcher-client-route-coverage.md`. No runtime
1518
behavior change.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Route-ledger ↔ client-surface conformance (#3563) — the client half of the
5+
* guard whose dispatcher half lives in
6+
* `packages/runtime/src/route-ledger.conformance.test.ts`.
7+
*
8+
* Every ledger entry that names a client method must resolve to a real
9+
* function on an instantiated client — the ledger cannot claim coverage the
10+
* SDK does not have. This is the direction #3528 shipped through: the ledger
11+
* equivalent of the day would have said "resume → automation.resume" while no
12+
* such method existed.
13+
*
14+
* The ledger is imported as a relative SOURCE file deliberately: it is pure
15+
* data (no imports), and a client→runtime package edge for it would be
16+
* backwards — runtime is where routes are declared, so the ledger lives there,
17+
* and each package verifies its own half.
18+
*/
19+
20+
import { describe, it, expect } from 'vitest';
21+
import { ObjectStackClient } from './index';
22+
import { ROUTE_LEDGER } from '../../runtime/src/route-ledger';
23+
24+
describe('route ledger ↔ @objectstack/client surface', () => {
25+
const client = new ObjectStackClient({ baseUrl: 'http://localhost:9' });
26+
27+
const resolve = (path: string): unknown =>
28+
path.split('.').reduce<unknown>((o, k) => (o == null ? o : (o as Record<string, unknown>)[k]), client);
29+
30+
it('every ledger entry naming a client method resolves to a real function', () => {
31+
const broken = ROUTE_LEDGER.filter((e) => e.client != null)
32+
.filter((e) => typeof resolve(e.client!) !== 'function')
33+
.map((e) => `${e.route} → client.${e.client}`);
34+
expect(
35+
broken,
36+
`Ledger entries claiming a client method that does not exist: ${broken.join('; ')}`,
37+
).toEqual([]);
38+
});
39+
});

packages/runtime/src/route-ledger.conformance.test.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@
2121
*/
2222

2323
import { describe, it, expect } from 'vitest';
24-
// Relative import of the BUILT client, NOT a package dependency:
25-
// @objectstack/client's own devDependencies already point back at runtime
26-
// (its Hono tests boot a real server), so a runtime→client package edge would
27-
// close a turbo build cycle. Importing the dist keeps the graph acyclic — and
28-
// CI's Test job always runs after Build, so the dist is present. (The client
29-
// SOURCE can't be imported here: its module graph resolves through runtime's
30-
// vitest aliases and breaks on subpath imports like @objectstack/core/logger.)
31-
import { ObjectStackClient } from '../../client/dist/index.mjs';
3224
import { HttpDispatcher } from './http-dispatcher.js';
3325
import { ROUTE_LEDGER, LEGACY_CHAIN_PREFIXES } from './route-ledger.js';
3426

@@ -73,22 +65,13 @@ describe('route ledger ↔ dispatcher domain registry', () => {
7365
});
7466
});
7567

76-
describe('route ledger ↔ @objectstack/client surface', () => {
77-
const client = new ObjectStackClient({ baseUrl: 'http://localhost:9' });
78-
79-
const resolve = (path: string): unknown =>
80-
path.split('.').reduce<unknown>((o, k) => (o == null ? o : (o as Record<string, unknown>)[k]), client);
81-
82-
it('every `sdk`/`mismatch` entry names a client method that actually exists', () => {
83-
const broken = ROUTE_LEDGER.filter((e) => e.client != null)
84-
.filter((e) => typeof resolve(e.client!) !== 'function')
85-
.map((e) => `${e.route} → client.${e.client}`);
86-
expect(
87-
broken,
88-
`Ledger entries claiming a client method that does not exist: ${broken.join('; ')}`,
89-
).toEqual([]);
90-
});
91-
68+
// The client-instance direction — "every named client method actually exists"
69+
// — lives in packages/client/src/route-ledger-coverage.test.ts, next to the
70+
// SDK it introspects. It cannot live here: a runtime→client edge (package OR
71+
// dist import) is unbuildable — client's own devDeps point back at runtime
72+
// (turbo rejects the cycle), and CI's per-package test tasks build only their
73+
// own dependency closure, so the client dist does not exist for this suite.
74+
describe('route ledger hygiene', () => {
9275
it('every `sdk` entry names its client method; every non-sdk entry carries a rationale', () => {
9376
const sdkWithout = ROUTE_LEDGER.filter((e) => e.disposition === 'sdk' && !e.client).map((e) => e.route);
9477
expect(sdkWithout, 'sdk-disposition entries missing a client method name').toEqual([]);

0 commit comments

Comments
 (0)