Skip to content

fix(cli): the rest of the read-only data doors answer a project with no database yet with empty work (#21552) - #21570

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21552-absent-db-family-closeout
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21552-absent-db-family-closeout

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21552
Clause-②: no

On a project whose database does not exist yet, os migrate account-issuer, os migrate audit-metadata-bodies, os migrate meta --stored, os secret orphans, os secret rewrap and os storage orphans now answer with empty work and exit 0. Before, each exited 1 from its own first read of a table its read-only boot had deferred. This completes the family that PR #21550 started for resume, recorded-by and value-shapes, under the same ruling (5965283666, applied to these six doors by triage's 5966537984): prefer "not asked"; where a read cannot be avoided, recognise its refusal only with isMissingTableError and only for the command's own table.

Measured at the public door

Fixture: one artifact with two app objects, --database-url file: a path that does not exist, run as node packages/cli/bin/run-dev.js ... --json. Base f83d0669d7 (origin/main when the branch was cut), head 491087ee0f (the src of the six doors is the same at the current head 69a1689f00; the patch round touched two test files only). files-to-references, summary-nulls, multi-value-columns and duplicates are the controls; duplicates has no --json flag and always prints its one document.

command (absent database) base head
migrate account-issuer exit 1, RESOURCE_CONFLICT, "Cannot enumerate sys_account" exit 0, scanned: 0, ok: true
migrate audit-metadata-bodies exit 1, failures: 3 (sys_audit_log, sys_activity, sys_metadata_audit) exit 0, failures: 0
migrate meta --stored exit 1, DATABASE_ERROR for sys_metadata exit 0, scanned: 0, clean: true
secret orphans exit 1, scan_failed for sys_secret exit 0, counts.total: 0, all three families enumerated
secret rewrap exit 1, scan_failed for sys_secret exit 0, counts.total: 0, all three families enumerated
storage orphans exit 1, DATABASE_ERROR for sys_file exit 0, filesScanned: 0, stranded: 0
the four controls exit 0 exit 0
database file left behind none none

Each of the six names, on stderr under --json and on stdout in human mode, the tables it read as no rows.

The boot path of each door (what the seam can and cannot say)

All six boot through bootSchemaStack with deferSchemaDdl and readOnlyProbe, so SchemaStack.tableAbsent is there to ask. Measured per door:

  • Five ask it (audit-metadata-bodies, meta --stored, secret orphans, secret rewrap, storage orphans) before the first read. A shared helper, packages/cli/src/utils/absent-table-reads.ts, is the one place a door asks. It adds no second mechanism: it wraps tableAbsent, and a refused read of an ordinary table is still issued and still refused.
  • account-issuer cannot ask. Its boot composes no auth plugin, so sys_account is not a registered object and the held-back sync never lists it: tableAbsent('sys_account') is false on every database. It is the isMissingTableError case the ruling names: the probe's read is not avoidable, and the door reads that one refusal, for sys_account only, as no rows. Registering the object would not be a fix: the probe selects the retired issuer column, which a registered sys_account no longer declares.
  • meta --stored hands its read to the metadata protocol, which holds a private engine this command cannot wrap. The preview answers an absent sys_metadata itself with the report the protocol returns for zero rows, typed as StoredMigrationReport so a new field is a compile error here.
  • secret orphans and secret rewrap read at driver level and through the reference union. The union is given a read-only view of the engine (secretUnionReadView): only find is carried onto the driver, so --delete and --apply take their write verbs from the unwrapped driver, and listDatasourceDefs stays absent when the engine has none (its absence is a declared gap).

The table the boot cannot measure: sys_activity

The control for audit-metadata-bodies showed tableAbsent('sys_activity') true on a booted database. sys_activity is rotation-managed: its rows live in sys_activity__rYYYYMMDD shard tables and its base name is a view. The deferred sync asks the driver hasTable for the base name, a view is not a table, so on SQLite it lists the base as create_table over a database that serves it. A door that believed it would have answered "Nothing to rewrite" over the cleartext credential copies the command exists to reach.

So absentTableReads takes a schema lookup, and for a rotation-managed object it does not consult the measurement: the read is issued, and only its missing-table refusal (isMissingTableError, for that object) reads as no rows. The control pins it: a cleartext copy seeded into the rotation shard is found and counted (sys_activity.rewritten: 1). The seam itself is untouched here (see Out of scope).

Documented exit (A3)

  • migrate account-issuer: its own description, "exits non-zero when the drop must not proceed". A database with no account table has no collision, and a booted database holding two clean accounts answers ok: true with exit 0.
  • migrate audit-metadata-bodies, migrate meta --stored: the platform checklist's migrate item (docs/qa/platform-checklist/areas/cli.json: "EVERY migrate subcommand with --json exits 0 on success"); meta --stored also documents "A second pass reporting every row canonical exits 0".
  • secret orphans, secret rewrap, storage orphans: report-only by their own documentation ("Report-only by default: without --delete it writes nothing", "A dry run by default", "Writes nothing"); their report mode exits 0 whenever the read succeeded.
  • Each is also the exit the same door gives the booted control (below).

content/docs/deployment/cli.mdx said the opposite in two places: the Data migrations edge paragraph ("Another dry run that reads a missing table can still fail and exit 1") and os secret orphans ("it refuses and exits 1 ... scan_failed"). Both now describe the empty-work answer, and keep the refusal for any other read that fails.

Tests

  • Integration pin, extended: packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts gains a second block. For each of the six, on the absent database: --json exit 0 with the empty-work document, no refused read of its own tables, the table named on stderr; human mode exit 0 on the empty-work sentence; no database file created. The control is a booted database holding one row of work per door (two legacy sys_account rows, a cleartext audit copy in sys_audit_log and in the sys_activity shard, a stored metadata row, an unreferenced sys_secret row, a stranded sys_file): each door READS it, reports the row, and says nothing about absent tables. The four doors that already exited 0 are pinned to still do. Every spawn runs in beforeAll.
  • Two refusal pins flipped: preview-read-only.integration.test.ts pinned exit 1 for meta --stored and audit-metadata-bodies on a missing database (the refusal [finding] seven more dry-run / report-only CLI commands boot the app seed loader and rewrite seeded rows (the family of #21349) #21391 declared). That is the behaviour the ruling reverses for these doors; both cases now assert exit 0 and the empty-work document, with the file header updated.
  • Unit: packages/cli/src/utils/absent-table-reads.test.ts, 17 cases: the helper's answers, the stderr/stdout split, and the rotation rule.
  • Patch round 1: two stale test doubles. CI Test Core (1/6) was red at 491087ee0f in rewrap.guards.test.ts. That suite, and orphans.guards.test.ts, replace bootSchemaStack and hand the command a stack of only kernel and shutdown. The doors' first ask is stack.tableAbsent, a member of SchemaStack since PR fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work #21550, so every case fell into the scan_failed catch. Root cause read off the case's own output (a throwaway copy of the suite that printed the payload): {"error":"scan_failed","message":"stack.tableAbsent is not a function"}, in all five cases of rewrap.guards.test.ts. Run locally against the unfixed head, the two suites had 7 red cases (5 in rewrap.guards, 2 in orphans.guards); CI listed four. The fix is on the double's side: both doubles now carry tableAbsent as the real boot returns it (false for every table on the plain boot of a writing run; rewrap.guards takes the measured-absent set as an option). No consumer-side tolerance was added. rewrap.guards.test.ts also gains one case: a dry run over tables the boot measured absent issues no read, reports counts.total: 0 with every family enumerated, and a present-table control still reads. The stack double in test/exit-signal.pin.test.ts (stackWith) has no tableAbsent either, and no path that pin drives reaches it (recorded-by --apply and resume --run short-circuit before the ask, and account-issuer does not use the seam), so it is left as it is.
  • Test files that mock bootSchemaStack or schema-migrate.js (git grep for vi.mock of the module over packages/cli): rewrap.guards, orphans.guards, test/exit-signal.pin, files-to-references.column-step-refusal and summary-nulls. The last two reach other commands, not these doors. The test files that reach a door through any other seam (a real boot, the command module, or a source scan) were found by git grep for the door names and modules: data-commands.absent-database, preview-read-only, meta.stored-flow-resolution, meta.report-order, meta.stored-flags, orphans.driver-contract, rewrap.driver-contract, platform-migrations-arming, schema-migrate.one-shot-family, resume.recorded-by, sys-secret-rewrap, one-shot-settings.pin, migrate-meta-engine-guidance, migrate-meta-strict-factories.
  • At 69a1689f00 (this branch merged with origin/main at 491087ee0f, which brought PR fix(cli): os init and os compile render each refusal once, not once on stdout and again as oclif's Error block (#21542) #21560's refusal pins; no later merge, the gate derivation does not call the tree stale):
    • The 13 files above that run in the package's two projects (the five mockers and the door-reaching files, summary-nulls and files-to-references.column-step-refusal among them), vitest run --maxWorkers=2, both projects: 13 files, 202 tests passed (it was 7 failed, 194 passed before the double fix). 7 of the 13 are integration-tier.
    • The round-1 integration files (data-commands.absent-database, preview-read-only, meta.stored-flow-resolution, platform-migrations-arming, schema-migrate.one-shot-family, resume.recorded-by), --project integration: 6 files, 137 passed, 1 skipped (the live PostgreSQL cell, not provisioned here).
    • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 253 files, 3702 passed (after pnpm --filter @objectstack/cli build, which the two published-subpath-* pins need).
    • pnpm --filter @objectstack/cli build and pnpm --filter @objectstack/cli typecheck (test layer included): exit 0.
    • NOT MEASURED: the other 69 files of the cli integration tier (82 files listed by vitest list --project integration; 13 run here: those 7 and the six round-1 files). Reason: the tier runs past the 10-minute foreground cap here, so the rest is declared to CI. The narrowing is the git grep above: none of the 69 names a door, mocks schema-migrate.js, or imports the new helper. And the two *.e2e.test.ts files that spawn the doors (test/migrate-meta.e2e.test.ts, test/json-stdout-purity.e2e.test.ts) belong to neither of the package's two vitest projects, so no local run selects them (the runner printed "matches no test file in this package").

Reverse verification

Each leg mutated the committed tree through scripts/ablation-replace.mjs: anchor 1 to 0, blob changed, restored to HEAD with an empty git diff HEAD and a clean status. The CLI spawns run packages/cli/src through tsx, so there is no dist hop. Two first attempts were refused by the tool itself because the replacement text contained the anchor or already occurred in the file; nothing ran, and each leg was redone with a distinct replacement.

  • Leg A, tableAbsent forced false (schema-migrate.ts). Predicted: every fresh-project pin of the five seam doors red, the booted controls green, account-issuer green (it does not use the seam). Observed: 19 failed, 25 passed, 1 skipped. The 19 are the 7 pins PR fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work #21550 added, the 10 new fresh-project cases (five doors, --json and human) and the two flipped preview-read-only cases. Every booted control and the four exit-0 controls stayed green, and account-issuer stayed green.

  • Leg B, the isMissingTableError branch of account-issuer disabled. Predicted: the two account-issuer fresh-project cases red, nothing else. Observed: 2 failed, 34 passed.

  • Leg C, the rotation rule disabled (absent-table-reads.ts). Predicted: the audit control red because sys_activity is skipped. Observed: integration 1 failed, 35 passed, with sys_activity.scanned 0 where a cleartext copy is stored; the unit file 2 failed, 15 passed.

  • Leg D, patch round: the not-asked answer disabled (absent-table-reads.ts, absent() always false), over rewrap.guards.test.ts. Predicted: only the new absent-tables case red. Observed: 1 failed, 6 passed. Restored to HEAD, empty git diff HEAD.

Legs A to C ran at 491087ee0f; src of the doors and the helper is unchanged since (the round touched two test files).

Gates

At 69a1689f00, after the final commit:

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 94 commands (the same list as at 491087ee0f); --ran, with an exit code per command, reconciled as "94 run, 0 NOT-MEASURED (a DERIVED zero)". All 94 read exit 0 on this pass, with every package built first (turbo run build --filter='!@objectstack/docs'). The derivation says no commit it can see touched what it derives from, so no merge was made.
  • pnpm lint, as a proven narrowing. eslint --no-inline-config --format json on the 12 changed TS files: 12 files, 0 errors, 0 warnings, none reported as ignored. The population comes from eslint.config.mjs (files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}); the config enables no type-aware linting (no parserOptions.project, and every block's parserOptions is { ecmaVersion, sourceType }), so this diff cannot move the verdict on a file it does not touch. The .md and .mdx files are outside it.

Acceptance notes

Out of scope (reported to the seat, not filed here)

  • SchemaStack.tableAbsent answers true for a rotation-managed object's base name on a booted SQLite database, because previewDeferredSchemaWork asks hasTable and a view is not a table. Measured through os migrate audit-metadata-bodies against a booted database whose sys_activity is the view over sys_activity__rYYYYMMDD. The same list is what os migrate plan prints for a host that composes the audit plugin (not measured at plan). The fix belongs in the driver's preview, not in a consumer.

Generated by Claude Code

claude added 7 commits October 3, 2026 07:56
…isMissingTableError (WIP)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ip to empty work (WIP)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ase yet with empty work (WIP: docs, changeset, helper test)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 22 documentable anchor(s).

44 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6dd99b82c38cd68b51c86241d53c5b7dda670a08.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 27 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 6dd99b82c38cd68b51c86241d53c5b7dda670a08 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6d7261ebb602afd9a66d2fd8500bb06ca891bb9d — the merge of head 69a1689f00764c05bed842224ae205b9100e0874 into base 6dd99b82c38cd68b51c86241d53c5b7dda670a08, 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 6d7261ebb602afd9a66d2fd8500bb06ca891bb9d && git checkout 6d7261ebb602afd9a66d2fd8500bb06ca891bb9d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6dd99b82c38cd68b51c86241d53c5b7dda670a08 69a1689f00764c05bed842224ae205b9100e0874 && git checkout -B drift-repro 6dd99b82c38cd68b51c86241d53c5b7dda670a08 && git merge --no-ff 69a1689f00764c05bed842224ae205b9100e0874

node scripts/docs-audit/affected-docs.mjs --json 6dd99b82c38cd68b51c86241d53c5b7dda670a08

⚠️ 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 6dd99b82c38cd68b51c86241d53c5b7dda670a08 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…sent, as the real boot returns it

The two mocked-boot suites built a stack with only kernel and shutdown, so the
doors' first ask (tableAbsent) threw 'stack.tableAbsent is not a function' and
every case fell into the scan_failed catch. The doubles now follow the real
SchemaStack shape; rewrap.guards also pins the not-asked dry run.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 10:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 10:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 1777a9b Oct 3, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21552-absent-db-family-closeout branch October 3, 2026 11:26
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…egistry does not resolve (objectstack-ai#21516) (objectstack-ai#21545)

Fixes objectstack-ai#21516

Clause-②: yes (narrowing)

An in-process engine verb now refuses an object name the schema registry
does
not resolve with the data door's own `OBJECT_NOT_FOUND` (404), instead
of
handing that name to the driver as a raw table name. One name space for
the
in-process verbs and the generic data door (triage ruling). The engine's
accept
set narrows; no surface is widened. The `yes` half of the clause line is
the one
new export, `objectNotFoundError`, in `@objectstack/core`.

## What changed

- **`packages/core` (new `objectNotFoundError`).** One factory for the
`OBJECT_NOT_FOUND` / 404 envelope, beside `recordNotFoundError` and for
the
same ADR-0076 D2 reason (the engine closure cannot import the package
where
the door's envelope was written). Both doors now build the refusal here.
- **`packages/metadata-protocol` (the door).** `assertObjectRegistered`
raises
  that shared factory: same wire status, same code.
- **`packages/objectql` (the engine).** `resolveObjectName` throws
`objectNotFoundError` for a name the registry does not resolve, rather
than
  returning it as a physical table name. Every in-process verb
(`find`, `findOne`, `count`, `aggregate`, `insert`, `insertMany`,
`update`,
  `delete`, `validate`) resolves through it, so all refuse uniformly: no
spelling allow-list, no per-caller marker. `judgeFilter` keeps judging
the
filter for an unresolved name (it reads nothing and reaches no driver),
as its
  contract states.
- **Three platform-internal, constant-name best-effort probes** that
read a
known system object and were already fail-soft on a missing table now
treat
  the engine's refusal (attributed to their own object) as the same "not
provisioned in this composition" case. A body cannot reach these paths:
  `ObjectQL.probeInstallOrganizations` (registry-presence guard),
`SeedLoaderService.resolveSoleOrganizationId` and
`SysMetadataRepository`'s
  history counters (refusal recognised by `code` and `object`).
- **`packages/spec`.** The `IObjectQLEngine.judgeFilter` docblock states
that
execution refuses an unknown object before admission (comment only; it
ships
  in the built type declarations).

## Why (classes, doors, roles, codes only)

An action body invoked through the actions door could name a protected
member
of the stored-metadata family by a spelling the registry does not
resolve and
receive its stored content, whether a member or an administrator invoked
it.
The in-process verb handed that name to the driver as a raw table name,
and
every name-keyed in-process guard (PR objectstack-ai#21513's reader seam among them)
was
addressed by the registered name only. The generic data door answers
`OBJECT_NOT_FOUND` for the same name. The engine now answers the same,
so the
two doors share one name space and no name-keyed guard can be stepped
around by
naming its target some other way.

## Census: does any legitimate platform reader rely on the raw-table
fall-through?

Instrumented the resolver's fall-through and ran the `objectql`,
`metadata-protocol` and `runtime` suites, plus a full boot, seed and
door drive
of four example apps (crm, showcase, todo, multi-package). The
instrument was
reverted in the branch; the net diff carries none of it.

| composition | fall-through reads | reader relies on it? |
|:--|:--|:--|
| 4 example apps booted, seeded, driven through the doors | **0** | no:
every platform reader addresses a registered object |
| unit/integration suites (partial registries + tolerant stub drivers) |
many | only test harnesses, plus the three constant-name probes below |

Every in-repo caller that passes a possibly-unresolved name, by
function:

| caller | object (constant) | pre-change | disposition |
|:--|:--|:--|:--|
| `ObjectQL.probeInstallOrganizations` | the org object | fail-soft on
missing table | moved: registry-presence, empty answer |
| `SeedLoaderService.resolveSoleOrganizationId` | the org object |
fail-soft on missing table | moved: recognise the refusal as
not-provisioned |
| `SysMetadataRepository` history counters | the history object |
fail-soft on missing table | moved: recognise the refusal as
not-provisioned |
| `ObjectQL.cascadeDeleteRelations` / `planCascadeAtomicity` /
`referenceExists` | a relation ref | already `try/catch` | unchanged:
already tolerate a throw |
| the data door's existence gate | the requested name | raised the 404
itself | now raises the shared factory |

Conclusion: **no production or example reader relies on the
fall-through.**

## Merge-queue fix

`os migrate account-issuer` reads `sys_account` through the driver the
engine routes that name to. Its read-only boot registers no
`sys_account`, and the engine now refuses an unregistered name. The
refusal is not read as absence, because that would report a table full
of accounts as a clean pre-flight. objectstack-ai#21570's missing-table reading for
`sys_account` is kept, and every other failure still throws.

## Fixture triage (the test-only fallout, per the seat's answer)

Every test that encoded the raw-table fall-through, by disposition. No
ADR text
is edited, and no pin ruled under objectstack-ai#7929 (the cross-field withholding
decision)
changes what it asserts. The table-keyed `captureExpectedReadRefusals`
noise
pins keep their subject and reshape their reading (item 3).

1. **The unregistered name is the deliberate probe: the test now asserts
the
refusal** (`code` + `status`, and where the test watched the driver,
that
   the driver saw nothing).
   - `objectql`: `engine-20822-no-field-map-type-blind-lowering`,
     `query-expression-conformance`, `engine.test`,
`engine-undeclared-update-field`, `engine-undeclared-field-preflight`,
     `engine-temporal-comparand-door`, `engine-aggregate-filter` /
`-having` / `-reference-verdict`, `engine-summary-recompute-context`,
     `registry-field-type-refused-at-door`, the `global-search-*` pins,
     `engine-judge-filter`, `engine-organization-probe-outage`.
- `protocol-unregistered-object.test.ts`, case B of the card 3770 gate:
the
     door's 404 assertion is unchanged; the engine assertion turns from
"serves the row" to "refuses `OBJECT_NOT_FOUND` / 404"; header item ②
     gains one sentence naming objectstack-ai#21516.
- dogfood `registry-gate-wiring`: the premise reads ground truth at the
driver (host code's declared internal path), then asserts the engine
     refuses the same name.
2. **The fall-through was incidental: the harness now registers what the
platform reader resolves** (registered after boot or DDL, so nothing new
is
   provisioned and every outage/absence subject keeps its meaning).
   - `objectql` metadata-write harnesses (delete, save, publish-meta,
     publish-package-drafts, protocol-derived-provenance,
     protocol-save-meta-repo-path, protocol-picklist,
     protocol-publish-canonical-fold): the stored-metadata family.
   - `rest` (14 harness files): the stored-metadata family, after DDL.
- `plugin-security` (4 files) and the `http-conformance` stack: the
authz
resolver's read set, unprovisioned, so the missing-table answer is still
     what they measure.
- `plugin-approvals` status-mirror cascade: the delegation object and
the
     org object, unprovisioned. `service-settings`: the secret and
     setting-audit objects.
3. **Noise pins: same subject, reshaped reading.** The org probe for an
unregistered org object is now refused before any driver, so a pin that
read
"the probe reached the driver and was withheld" now reads "the probe
reached
no driver" (`tablesSeen()` equal to empty where `silentChannels()` was
   read). `runtime` (about 17 files) and `trigger-record-change`.
`expected-read-refusal-noise.channel-asymmetry.test.ts` registers its
probe
object, unprovisioned, so the real driver refusal is still what its two
   channels measure.
4. **`cli` served-boot control** (`schema-migrate.host-composition`):
each
hook's probe read is witnessed by its recorded `OBJECT_NOT_FOUND`
answer, not
by a driver line; the SQL driver suppresses that line for its own
deferred
   set, so the line never was the subject.
5. `engine.test.ts`: one mock parameter typed (`name: string`), the
   `@objectstack/objectql#typecheck` red of the earlier heads.

## New pin: the measured public door

`packages/runtime/src/unresolved-object-name.actions-door.pin.test.ts`
boots
the plugin set `bootStack` uses and drives REST `/actions`. The target
is a
table that exists and holds a sentinel row, created out of band at the
driver
and registered nowhere (the class the card measured, naming no protected
table). For an administrator and a member, the action body's read
answers
`404 OBJECT_NOT_FOUND` and the sentinel appears nowhere in the answer.
Control:
the same body shape on a registered name is served. Reference: the
generic data
door's answer for the same name is the same 404. PR objectstack-ai#21513's reader-seam
pins
stay green.

## Ablation (one-shot; nothing left in the tree)

Mutation leg, `scripts/ablation-replace.mjs` on `engine.ts`: the refusal
in
`resolveObjectName` replaced by the old raw-table return plus a marker
branch
(marker on disk 1, refusal on disk 0); rebuilt;
`ablation-dist-preflight`:
marker present in 4 built files. Pins under mutation:

- objectql (`protocol-unregistered-object`, `engine-20822-...`,
  `query-expression-conformance`, `engine-judge-filter`):
  `5 failed | 245 passed (250)`
- runtime actions-door pin: `2 failed | 4 passed (6)` (both role cases)

Restore leg: `git checkout HEAD -- engine.ts`; blob equals the HEAD blob
`f5793bff919d`, `git diff HEAD` empty, porcelain clean; rebuilt; marker
absent
from all 14 built files. Pins restored: `250 passed (250)`, `6 passed
(6)`.
`engine.ts` is byte-identical on the final head (the later merge of
`main`
carried no `objectql` source).

## Verification on the final head `6752a29827` (merge of `origin/main`
at `1ca1eb0972`)

- `objectql` full suite: `367 files, 7384 passed`.
- `metadata-protocol` full suite: `206 passed, 3 skipped files; 3187
passed, 19 skipped`.
- `runtime` full suite: `317 files; 5176 passed, 19 skipped`.
- `service-analytics` full suite: `175 files; 4152 passed, 253 skipped`.
- `cli` unit tier: `252 files, 3685 passed`; integration tier, the three
files
  this branch or the merged `main` touched: `36 passed`.
- `mcp`: `35 files, 389 passed`; dogfood (`registry-gate-wiring` + the
two
files `main` added): `16 passed`; the `main`-added example, `metadata`
and
  `core` files: green.
- `plugin-security`, `plugin-approvals`, `service-settings`,
  `http-conformance`, `trigger-record-change` test tasks: turbo `38/38`
  (5 test tasks run, 33 cached builds).
- Build closure for the above: `63/63` turbo tasks.
- Before the merge (head `bab0903840`): typecheck of every touched
package
`76/76` tasks; `rest` repo project `177 passed`; the 14 `rest` harness
files
  `552 passed, 21 skipped`.
- CI on `6752a29827`: every check green except "Part-of PR must not also
close
  its card", which read the earlier body; this body is its input.

## Acceptance notes

- Recorded decision of card 3770 ("the engine deliberately does not
reject;
internal callers unaffected") is narrowed at the engine: the door's gate
is
unchanged and the engine now gives the same answer. ADR-0053's
type-blind
lowering is kept for a registered object with no field map, and removed
for an
  unregistered name (the bypass itself). No ADR text is edited here.
- Changesets: `@objectstack/core` minor (`Clause-②: yes`, the new
export);
`@objectstack/objectql` minor (`Clause-②: no (narrowing)`, with its
ADR-0087
disposition); `@objectstack/metadata-protocol` patch;
`@objectstack/spec`
  patch (docblock).
- `packages/cli/test/refusal-renders-once.e2e.test.ts` (added on `main`)
sits
in neither of the cli package's two vitest projects and was not run
here;
it drives refusal rendering of `os init` / `os compile`, which this
change
  does not reach.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3

---------

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/l tests tooling

Projects

None yet

2 participants