Skip to content

feat(cloud-connection,metadata,cli): ledger the six raw-app route mounters and guard them - #12152

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-11882-raw-mount-route-ledgers
Aug 25, 2026
Merged

feat(cloud-connection,metadata,cli): ledger the six raw-app route mounters and guard them#12152
os-trump merged 3 commits into
mainfrom
claude/issue-11882-raw-mount-route-ledgers

Conversation

@os-trump

@os-trumpos-trump commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11882

Six registrars across three packages mount HTTP routes on the host Hono app's framework-native
handle (http-servergetRawApp()). Those routes sit outside the dispatcher ledger, outside
RestServer.getRoutes(), and outside IHttpServer.getMountedRoutes() — the last one by the
contract's own words, "routes an adapter mounts on its framework-native handle behind
getRawApp are outside this table by construction"
(packages/spec/src/contracts/http-server.ts).
None carried a reviewed disposition anywhere.

This adds three per-package ledgers in the #3636 / #11863 pattern plus a guard for each.
Six new files, plus one line-level change to packages/cli/tsconfig.build.json (added in a
patch round on a maintainer ruling — see Changeset below; the file-surface extension was
authorized for that one file). No route behaviour changes.

Scope reading: "six ledgers" = three per-package ledgers covering the six mounters

Triage said "author the six ledgers per the #3636/#11863 pattern"; the claim's file surface said
"new per-package route-ledger files … in packages/cloud-connection/**, packages/metadata/**,
packages/cli/**"
. Those resolve the same way: #3636 is a per-PACKAGE pattern, and the six
mounters live in three packages (cloud-connection owns four of them). So the deliverable is three
ledgers covering six mounters, not six ledger files. Flagged explicitly rather than silently
picking one reading.

Population, re-verified against origin/main @ 2ba4329e — nothing dropped

The card's per-file table reproduced exactly, line numbers included. Nothing gained a ledger
since filing, so no mounter drops out. Every route below is a new row.

packages/cloud-connection — 16 routes, 4 registrars

mounting fileroutesdisposition
cloud-connection-plugin.ts (:231,276,335,438,484,531,592,634)GET /status, POST /bind/start, POST /bind/poll, POST /unbind, POST /install, GET /installation, GET /installed, GET /org-packages — all under /api/v1/cloud-connection8 × server-only
marketplace-install-local-plugin.ts (:246-251)POST + GET /api/v1/marketplace/install-local, DELETE …/:manifestId, POST …/:manifestId/{reseed,purge}-sample-data5 × server-only
marketplace-proxy-plugin.ts (:371)ALL /api/v1/marketplace/*public
runtime-config-plugin.ts (:875,877)GET /api/v1/runtime/config, GET /api/v1/studio/runtime-config2 × public

packages/metadata — 2 routes, 1 registrar

routes/hmr-routes.ts:126,185 (mounted via plugin.ts:468) — GET + POST /api/v1/dev/metadata-events. public (SSE stream) and server-only (build-tool loopback).

packages/cli — 4 routes, 2 plugin factories

utils/console.ts:536,540,543,608GET /, GET /_console, GET /_console/*,
GET /runtime/assets/:filename. All four static-asset (see below).

Total: 22 routes. One precision note, since the card is the population's own record: the body's
table is exact, but the routing comment's summary line — "cloud-connection alone carries 15 of
the ~24 routes"
— is off by one against that same table (8+5+1+2 = 16 of 22). The table
was right; the prose summary was not.

Two things the card's sweep did not name

  1. A computed-member mount.marketplace-proxy-plugin.ts:374-377 has a fallback arm that loops
    over ['get','head'] and mounts through rawApp[m]?.(…) on the marketplace prefix pattern — a
    computed member a literal grep walks straight past. It is the "unreadable mount" class
    check-auth-mount-ledger.mjs's constraint 4 exists for. It is declared, not skipped, in
    DECLARED_COMPUTED_MOUNTS, and reconciled in both directions: the guard fails if the spelling
    disappears (stale declaration) and if the ALL row that covers it disappears (uncovered
    mount). It adds no unledgered wire path — it mounts the same pattern the ALL row carries.
  2. A lane-only reach in cli.commands/serve.ts:4096 takes the raw app to install
    rawApp.use('*', …) (the unknown-hostname guard). A middleware lane is not a route, but the
    file is pinned so that the day it mounts one, the guard says so rather than the route going
    unledgered.

The cli family needed a sixth disposition, and that is the point of the card

The routing comment and the card body both flag that the cli console rows are static-asset
serving, not API surface
. None of the five REST-ledger words is true of them, and the nearest
one is actively misleading:

  • gap is the wrong kind of false — it means "should be in the SDK", is ratcheted to <= 0, and
    filing a file server there would assert @objectstack/client ought to grow a method for
    fetching index.html;
  • server-only means an inbound door or loopback; these are the opposite, outbound bytes to a browser;
  • public is true but insufficient, which is the trap. It would file a static file server
    alongside genuine anonymous API endpoints like GET /api/v1/runtime/config, so a reader
    auditing the platform's unauthenticated API surface would find four rows that are not API.

check-auth-mount-ledger.mjs states the governing rule for exactly this: "IF YOU CANNOT DECIDE,
DO NOT PICK THE NEAREST ALLOWED WORD."
Here the disposition is not undecided, so the ledger says
it in a word that is true: static-asset. Precedent for a per-package vocabulary extension is
plugin-auth, whose AUTH_ROUTE_LEDGER carries a sixth word of its own (disabled).

The extension is contained and the guard enforces the containment: the type is package-local,
every static-asset note must name the byte-serving or redirect mechanism, and the ledger must
stay static-asset-only — so an API route landing in this package cannot inherit the word by
sitting in the same file. It will fail and force a real disposition.

Guards read source, not lifecycles — and why that is the safer half

#11863's trigger-api guard drives the plugin's real lifecycle, which works because that plugin
resolves three services and mounts one route. Every registrar here mounts from inside a
kernel:ready hook behind resolutions (http.server/http-server, env-registry,
kernel-manager, manifest, metadata, objectql) each guarded by a try/catch that
returns quietly when the service is absent; the two cli factories return early unless a built
dist/ exists on disk. A lifecycle drive over that fails OPEN — it observes zero mounts and
every accounting assertion passes vacuously — precisely when a resolution changes. That is the
completed-census defect these ledgers exist to remove, so each guard reads SOURCE TEXT, the shape
check-auth-mount-ledger.mjs (#10534) established for rawApp mounts. No import, no module
resolution, no dist/ between the edit and the reading.

Each guard carries: a census-is-real limb (zero mounts is a broken scan, not a clean package);
exact accounting in both directions on METHOD /wire/path; a population identity (not a
count) so a second registrar cannot hide behind a ledger that reads as a completed census; and
hygiene + anti-vacuity — since no row is sdk, the client-method assertion would hold
vacuously, so the audit's actual finding is asserted instead (the service-datasource rule).

Two package-specific limbs:

No row is sdk, measured rather than assumed

@objectstack/client was grepped for all five families — cloud-connection, marketplace,
runtime/config, install-local, metadata-events. One hit in the entire package, and it is a
doc comment
(index.ts:1526, describing a payload shape "the same shape marketplace-install-local
consumes"). No client method builds any of these URLs.

The live half is enforced next door by omission: none of these ledgers is added to
client-url-conformance.test.ts's union, so a client method that started calling one of these
routes would fail there for matching no ledger row at all. Adding them would remove exactly that
protection.

Named non-SDK callers, which is what the server-only notes rest on rather than intent alone:
packages/cli/src/commands/package/install.ts:170 composes
${runtime}/api/v1/marketplace/install-local directly, and packages/cli/src/commands/dev.ts:553
documents the metadata-events POST as the endpoint it drives.

Verification

The final commit on this branch is 669232cdc (the patch-round tsconfig.build.json
exclusion). The ledger and guard results below were measured on fee81b753, whose six files
that patch does not touch; the gate union was re-derived and re-run on 669232cdc, and the
build/typecheck/guard re-verification for the exclusion is recorded in Changeset above. Nothing
in this section was carried over unmeasured.

Reverse verification — direction predicted in writing before the run

Predicted: removing one row reddens the accounting limb naming that exact route; for cli I
removed GET /_console, the strict prefix, predicting two failures if the right-boundary
property holds. Observed, under an unconditional trap … EXIT INT TERM:

ledgerrow removedablated result
cloud-connectionGET /api/v1/cloud-connection/status1 failed | 17 passed
metadataPOST /api/v1/dev/metadata-events1 failed | 18 passed
cliGET /_console2 failed | 15 passed — accounting and the prefix-pair pin

The mutation was observed on disk, never inferred from an editor's exit code: the anchored
regex reported substitutions=1 per file, the removed route literal went 1 → 0 in each, and
git diff --numstat showed 0 10 / 0 11 / 0 16. Findings name routes by identity with real
line numbers
GET /api/v1/cloud-connection/status (cloud-connection-plugin.ts:231),
POST /api/v1/dev/metadata-events (routes/hmr-routes.ts:185),
GET /_console (utils/console.ts:540) — not a count.

No rebuild leg applies, and that is stated rather than skipped: each guard imports its ledger
as a relative SOURCE module inside the same package, so no exports/dist resolution sits between
the edit and the reading. Restore was from HEAD and verified three ways: git status --porcelain
empty, and disk blob == HEAD: blob for all three files.

A defect the guards caught in themselves, kept as a pin: stripComments preserves string contents
(the census resolves wire paths out of them), so a structural count of getRawApp() call sites read
2 where the truth is 1 — the second occurrence was inside a log message. maskStrings fixes it
and pins both directions. Block-comment newlines are likewise preserved, or every finding would
point ~30-80 lines short of the real mount.

Suites

commandverdict line
pnpm --filter @objectstack/cloud-connection testTest Files 27 passed (27) · Tests 299 passed (299)
pnpm --filter @objectstack/metadata testTest Files 34 passed (34) · Tests 637 passed (637)
pnpm --filter @objectstack/cli typechecktsc --noEmit, exit 0 (script name echoed — not a zero-match no-op)
pnpm --filter @objectstack/cli testTests 1 failed | 2038 passed (2039) — one failure, diagnosed and cleared below

The three new guards contribute 54 assertions (18 + 19 + 17).

The one cli failure was an unbuilt-worktree artifact, and it is now cleared rather than merely
argued away.
test/serve-node-env-production-default.e2e.test.ts (#11113, NODE_ENV /
trusted-origin) spawns a server and probes it over a port. It failed differently on each run
first command serve not found (this worktree had no packages/cli/dist yet), then, after building
cli, TypeError: fetch failed … ECONNREFUSED 127.0.0.1:58370 while a sibling agent's
turbo run build --filter=@objectstack/cli held the shared container. Two different failures from
one unchanged tree is the signature of environment contention, not a regression. Re-run on the fully
built workspace: Test Files 1 passed (1) · Tests 3 passed (3). These six files are inert —
nothing imports the ledgers except their own guards.

Gates — derived, not recalled

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived on the final
commit 669232cdc
(provenance line: derived from the tree of 'objectstack-ai/objectstack' at
commit 669232cdc
; 7 paths vs merge base 2ba4329e4, three-dot — the added
tsconfig.build.json matched families already in the union and introduced no new one). Every gate
below was re-run on that commit. Exit codes captured before any pipe; each verdict is the
gate's own line.

gateverdict
check:cross-package-test-inputsOK: 16 package(s) read outside themselves, all declared
check:published-filesOK — 69 publishable package(s) … admits no test, test-harness config or build script
check:slot-lookupratchet holds: 107 unswept site(s) … none new · baseline verified against 2ba4329: no files added
check:test-source-aliasOK — 72 packages with tests scanned (+ --self-test OK)
check:type-source-resolutionOK — 93 tsc program(s) across 77 packages scanned (+ --self-test OK)
check:durability-log-level✓ read-seam invention … 66 read seam(s), none invents an unreported answer
check:plugin-teardown-shape✓ 63 Plugin implementation(s) across 4682 source(s) … baseline fully burned down
check:ci-filter-parityOK: all 96 declared cross-package glob(s) … covered
check:nul-bytesOK (scanned 6725 text file(s) … no raw ASCII control bytes) (+ 75 self-test assertions)
check:query-options-erasureat the ceiling · baseline verified against 2ba4329: no files added
check:type-check-coverageexit 0 — the structural half; the excluded ledger is not flagged as an orphan (it stays in the package's tsconfig.json program)
check:engine-double-contractexit 0 — 390 (file, verb) row(s) held by the RETAINED ledger
check:where-matcher0 silently-wrong and 0 unjudged matcher(s) … none new
check:i18ncheck-i18n-bundles: OK (9 package(s) — all bundles in sync)
check:i18n-coverageOK (12 config(s), 657 baselined untranslated string(s), none new)
check:type-check-debt (--re-measure)OK — 32 ledger entr(ies) re-measured in 266.2s, 1897 raw tsc error(s) total, none above its recorded number — the same 1897 as the pre-patch run, so the exclusion moved no count
docs-audit/check-affected-docs · check-drift-commentexit 0 · ✓ 56 cases pass across 5 fixture diff(s)

On the two gates that first refused — both are real measurements, not narrowings.
check:i18n-coverage initially returned COULD NOT MEASURE — 1 of 12 config(s) failed to lint
(examples/app-showcase could not resolve built dists: @objectstack/connector-mcp, then
@objectstack/connector-openapi). NOT MEASURED is not a pass, so rather than record it as one
I built the workspace and re-ran it; it is green above. Same for check:type-check-debt, whose
--re-measure needs the built closure. Its surplus note names @objectstack/plugin-approvals
(TEST_DEBT records 348, tsc now reports 347) — pre-existing, unrelated to this diff, and already
being handled in open PR #12110.

check:i18n-coverage is worth calling out on the patch round specifically: it drives the built
CLI
(scripts/cli-build-prerequisite.mjs), so it is the gate that would notice if excluding a
file from the cli build broke that build's output. It was re-run on 669232cdc against a fully
rebuilt workspace and is green, which exercises the exclusion end-to-end rather than only at the
ls dist level.

The one thing genuinely not run here is repo-wide pnpm lint. No narrowing is claimed for it
either — it is simply CI's run, and this PR asserts nothing about it.

Changeset: none, carrying skip-changeset — and the cli ledger no longer ships

#11885 (the trigger-api ledger, merged) landed with skip-changeset and no changeset on the
grounds that the ledger is package-internal and tsup never emits it. That argument held for two
of the three packages here and not for the third, so it was measured per package rather than
inherited — and the gap it exposed has now been closed rather than merely disclosed.

packages/cli compiles its whole include program instead of an entry graph, so its ledger was
emitted into the published tarball (dist/utils/console-route-ledger.js 2.8 KB + .d.ts 5.9 KB +
maps, ~10 KB of permanently dead module). Maintainer ruling: exclude it — a published binary
package should not carry ~10 KB nothing reads, and the exclusion makes cli's end state match its
two siblings rather than inventing a new posture. packages/cli/tsconfig.build.json now lists
src/utils/console-route-ledger.ts alongside the test globs it already excluded.

packagebuildledger in dist/?
cloud-connectiontsup, entry: ['src/index.ts']absent — unreachable from the entry
metadatatsup, 4 entriesabsent — same
clitsc -p tsconfig.build.jsonabsent — now excluded explicitly

The exclusion is safe against a future import, and that is measured, not assumed.exclude
filters the include glob; it does not remove a file that an included file imports — TypeScript
still pulls such a file in through the module graph and emits it. Proven in an isolated scratch
project, both directions: a file that is excluded and imported by an included file is still
emitted (excluded.js + excluded.d.ts present, tsc exit 0); the same file excluded and
imported by nothing is not emitted. So this line can under-exclude, never dangle. In this repo
the only importer is console-route-ledger.conformance.test.ts, which the test globs already
exclude — verified by grep across packages/cli/src and packages/cli/test.

Verification of the change itself, all grepped rather than inferred:

  • tsc --listFilesOnly on the build config: ledger in the program 1 → 0, with src/utils/console.ts
    still present as the positive control (489 files total), so the exclusion is targeted, not broad;
  • pnpm --filter @objectstack/cli build from a removeddist/ → exit 0, and
    dist/utils/console-route-ledger.*absent, while dist/utils/console.js (25,874 bytes) is
    emitted; CONSOLE_ROUTE_LEDGER appears nowhere under packages/cli/dist/. Re-confirmed absent
    after a full ./packages/** workspace build;
  • pnpm --filter @objectstack/cli typecheck → exit 0, zero error TS lines. The ledger is still
    type-checked: it remains in the package's tsconfig.json program (2 hits including its test),
    so only the build config drops it and check:type-check-coverage does not see an orphan;
  • the control that matters most: the conformance guard imports the ledger from source, so it
    must be untouched — all three guards re-ran green, 18 + 19 + 17 = 54 assertions.

Judgment: still no changeset, and now on stronger ground than before — with the exclusion in
place nothing user-visible ships in any of the three packages, and no dist/ byte moves at all.

Governed surfaces

skills/**, docs/adr/**, .claude/**, AGENTS.md, CLAUDE.md, content/docs/releases/** and
packages/spec are untouched. No route is added, removed, or behaviour-changed; no public API
surface moves. #11050's detector is untouched — these routes leaving its unmatched listing is a
consequence of the ledgers existing, not a change to it.

Filed alongside, not fixed here

#12140POST /api/v1/dev/metadata-events carries no authentication and MetadataPlugin
applies no environment gate of its own; the only isDev guard in the tree is on the CLI's
supplementary composition in serve.ts, and the plugin's own comment states the posture as a
prediction about callers ("production deployments simply won't have a CLI POSTing to this
endpoint"
) rather than as a gate. Surfaced by writing that row's disposition — which is what these
ledgers are for. Filed unassigned for triage; out of scope here, and the ledger row records the
posture without changing it.


Generated by Claude Code

…nters and guard them
Six registrars across three packages mount HTTP routes on the host Hono app's
framework-native handle (`http-server` -> `getRawApp()`), so their routes sit
outside the dispatcher ledger, outside `RestServer.getRoutes()`, and outside
`IHttpServer.getMountedRoutes()` -- the last one by the contract's own words,
"routes an adapter mounts on its framework-native handle behind `getRawApp` are
outside this table by construction". None carried a reviewed disposition
anywhere.
Adds three per-package ledgers in the #3636 / #11863 pattern plus a guard for
each. All six files are new; no existing file is touched and no route behaviour
changes.
The guards read package SOURCE rather than driving plugin lifecycles: every one
of these registrars mounts from inside a `kernel:ready` hook behind multi-service
resolutions that return quietly when a service is absent, so a lifecycle drive
would fail OPEN -- observing zero mounts while every accounting assertion passed
vacuously. That is the completed-census defect these ledgers exist to remove.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed tests labels Aug 25, 2026 — with Claude
@github-actions

github-actionsBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/cloud-connection, @objectstack/metadata, touching 29 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/cli/tsconfig.build.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/environment-routing.mdx(via /api/v1/... (route))
  • content/docs/concepts/metadata-lifecycle.mdx(via /api/v1/dev/metadata-events (route))
  • content/docs/deployment/environment-variables.mdx(via /api/v1/runtime/config (route))
  • content/docs/deployment/single-project-mode.mdx(via /api/v1/... (route))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/tsconfig.build.json) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 177: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.

Coarse fallback — 31 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 2c4c59ed20caf4f6cde3c979ae9205ee69e04256packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6bc9e6659df4e182942e191d43b291ba4ac82849 — the merge of head dc38f112133116ce8d8526e6fdabb93a8a5d7d57 into base 2c4c59ed20caf4f6cde3c979ae9205ee69e04256, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6bc9e6659df4e182942e191d43b291ba4ac82849 && git checkout 6bc9e6659df4e182942e191d43b291ba4ac82849
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c4c59ed20caf4f6cde3c979ae9205ee69e04256 dc38f112133116ce8d8526e6fdabb93a8a5d7d57 && git checkout -B drift-repro 2c4c59ed20caf4f6cde3c979ae9205ee69e04256 && git merge --no-ff dc38f112133116ce8d8526e6fdabb93a8a5d7d57
node scripts/docs-audit/affected-docs.mjs --json 2c4c59ed20caf4f6cde3c979ae9205ee69e04256

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2c4c59ed20caf4f6cde3c979ae9205ee69e04256 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…gram
`packages/cli` compiles its whole `include` program rather than an entry graph,
so the #11882 route ledger -- a review record read only by its own conformance
test, which the test globs already exclude -- was emitted into the published
tarball as ~10KB of permanently dead module. Its two sibling ledgers needed no
such line only because `tsup` bundles from `src/index.ts` and never reached
them; this makes cli's end state match theirs.
Safe against a future import, measured rather than assumed: `exclude` filters
the `include` glob but does NOT remove a file that an included file imports --
TypeScript still pulls such a file in through the module graph and emits it. So
this line can under-exclude, never dangle. The ledger remains in the package's
`tsconfig.json` typecheck program; only the build config drops it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa
@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Patch round — the dist-emission question is closed by ruling: option B.

669232cdc adds src/utils/console-route-ledger.ts to exclude in
packages/cli/tsconfig.build.json, so the cli ledger no longer ships. The Changeset section of
the PR body now records the ruling and its measurement in place of the earlier "reviewer's call"
paragraph.

The one semantic worth checking rather than trusting, because the whole change rests on it:
exclude filters the include glob but does not remove a file that an included file imports —
TypeScript still pulls such a file in through the module graph and emits it. I proved that in an
isolated scratch project rather than asserting it, both directions:

scratch caseresult
file excluded and imported by an included filestill emitted (excluded.js + excluded.d.ts), tsc exit 0
file excluded, imported by nothing (our situation)not emitted

So this line can only ever under-exclude; it cannot leave a dangling reference. If shipped code
ever imports the ledger, it comes back into the build automatically. In this repo the only importer
is console-route-ledger.conformance.test.ts, which the test globs already excluded — grepped
across packages/cli/src and packages/cli/test.

Re-verification, all grepped rather than inferred:

  • tsc --listFilesOnly: ledger in the build program 1 → 0, with src/utils/console.ts still
    present as the positive control (489 files) — targeted, not broad.
  • pnpm --filter @objectstack/cli build from a deleteddist/ → exit 0;
    dist/utils/console-route-ledger.*absent, dist/utils/console.js (25,874 bytes) present;
    CONSOLE_ROUTE_LEDGER nowhere under packages/cli/dist/. Still absent after a full
    ./packages/** rebuild.
  • pnpm --filter @objectstack/cli typecheck → exit 0, zero error TS lines. The ledger stays in
    the package's tsconfig.json program (2 hits incl. its test), so it is still type-checked and
    check:type-check-coverage reports no orphan — only the build config drops it.
  • The control: the guard imports the ledger from source, so it must be unaffected — all three
    guards green, 18 + 19 + 17 = 54 assertions.

Gates re-derived and re-run on 669232cdc (7 paths vs merge base 2ba4329e4; the added
tsconfig matched families already in the union and introduced no new one) — all green. Two worth
naming: check:i18n-coverage drives the built CLI, so it is the gate that would notice if this
exclusion broke the build output — OK (12 config(s), 657 baselined untranslated string(s), none new); and check:type-check-debt --re-measure reported 1897 raw tsc error(s) total, the same
1897
as before the patch, so no count moved.

Unchanged as instructed: still draft, skip-changeset still applies (with the exclusion in
place, nothing user-visible ships from any of the three packages and no dist/ byte moves), and I
have not flipped ready or armed auto-merge.


Generated by Claude Code


Generated by Claude Code

@os-trump
os-trump marked this pull request as ready for review August 25, 2026 15:25
@os-trump
os-trump added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit ea00591Aug 25, 2026
34 checks passed
@os-trump
os-trump deleted the claude/issue-11882-raw-mount-route-ledgers branch August 25, 2026 16:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xlskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Six more packages mount routes on the raw Hono app with no route ledger — the ledgers' blind spot is wider than trigger-api

2 participants

@os-trump@claude