Repository navigation
fix(cli): the rest of the read-only data doors answer a project with no database yet with empty work (#21552) - #21570
Conversation
… read (WIP) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…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
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…sent-db-family-closeout
📓 Docs Drift CheckThis PR changes 1 package(s): 44 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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
…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>
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 rewrapandos storage orphansnow 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 forresume,recorded-byandvalue-shapes, under the same ruling (5965283666, applied to these six doors by triage's5966537984): prefer "not asked"; where a read cannot be avoided, recognise its refusal only withisMissingTableErrorand 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 asnode packages/cli/bin/run-dev.js ... --json. Basef83d0669d7(origin/main when the branch was cut), head491087ee0f(thesrcof the six doors is the same at the current head69a1689f00; the patch round touched two test files only).files-to-references,summary-nulls,multi-value-columnsandduplicatesare the controls;duplicateshas no--jsonflag and always prints its one document.migrate account-issuerRESOURCE_CONFLICT, "Cannot enumerate sys_account"scanned: 0, ok: truemigrate audit-metadata-bodiesfailures: 3(sys_audit_log,sys_activity,sys_metadata_audit)failures: 0migrate meta --storedDATABASE_ERRORforsys_metadatascanned: 0, clean: truesecret orphansscan_failedforsys_secretcounts.total: 0, all three familiesenumeratedsecret rewrapscan_failedforsys_secretcounts.total: 0, all three familiesenumeratedstorage orphansDATABASE_ERRORforsys_filefilesScanned: 0, stranded: 0Each of the six names, on stderr under
--jsonand 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
bootSchemaStackwithdeferSchemaDdlandreadOnlyProbe, soSchemaStack.tableAbsentis there to ask. Measured per door: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 wrapstableAbsent, and a refused read of an ordinary table is still issued and still refused.account-issuercannot ask. Its boot composes no auth plugin, sosys_accountis not a registered object and the held-back sync never lists it:tableAbsent('sys_account')is false on every database. It is theisMissingTableErrorcase the ruling names: the probe's read is not avoidable, and the door reads that one refusal, forsys_accountonly, as no rows. Registering the object would not be a fix: the probe selects the retiredissuercolumn, which a registeredsys_accountno longer declares.meta --storedhands its read to the metadata protocol, which holds a private engine this command cannot wrap. The preview answers an absentsys_metadataitself with the report the protocol returns for zero rows, typed asStoredMigrationReportso a new field is a compile error here.secret orphansandsecret rewrapread at driver level and through the reference union. The union is given a read-only view of the engine (secretUnionReadView): onlyfindis carried onto the driver, so--deleteand--applytake their write verbs from the unwrapped driver, andlistDatasourceDefsstays absent when the engine has none (its absence is a declared gap).The table the boot cannot measure:
sys_activityThe control for
audit-metadata-bodiesshowedtableAbsent('sys_activity')true on a booted database.sys_activityis rotation-managed: its rows live insys_activity__rYYYYMMDDshard tables and its base name is a view. The deferred sync asks the driverhasTablefor the base name, a view is not a table, so on SQLite it lists the base ascreate_tableover 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
absentTableReadstakes 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 answersok: truewith 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 --storedalso 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--deleteit writes nothing", "A dry run by default", "Writes nothing"); their report mode exits 0 whenever the read succeeded.content/docs/deployment/cli.mdxsaid the opposite in two places: the Data migrations edge paragraph ("Another dry run that reads a missing table can still fail and exit 1") andos 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
packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.tsgains a second block. For each of the six, on the absent database:--jsonexit 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 legacysys_accountrows, a cleartext audit copy insys_audit_logand in thesys_activityshard, a stored metadata row, an unreferencedsys_secretrow, a strandedsys_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 inbeforeAll.preview-read-only.integration.test.tspinned exit 1 formeta --storedandaudit-metadata-bodieson 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.packages/cli/src/utils/absent-table-reads.test.ts, 17 cases: the helper's answers, the stderr/stdout split, and the rotation rule.491087ee0finrewrap.guards.test.ts. That suite, andorphans.guards.test.ts, replacebootSchemaStackand hand the command a stack of onlykernelandshutdown. The doors' first ask isstack.tableAbsent, a member ofSchemaStacksince 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 thescan_failedcatch. 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 ofrewrap.guards.test.ts. Run locally against the unfixed head, the two suites had 7 red cases (5 inrewrap.guards, 2 inorphans.guards); CI listed four. The fix is on the double's side: both doubles now carrytableAbsentas the real boot returns it (falsefor every table on the plain boot of a writing run;rewrap.guardstakes the measured-absent set as an option). No consumer-side tolerance was added.rewrap.guards.test.tsalso gains one case: a dry run over tables the boot measured absent issues no read, reportscounts.total: 0with every familyenumerated, and a present-table control still reads. The stack double intest/exit-signal.pin.test.ts(stackWith) has notableAbsenteither, and no path that pin drives reaches it (recorded-by --applyandresume --runshort-circuit before the ask, andaccount-issuerdoes not use the seam), so it is left as it is.bootSchemaStackorschema-migrate.js(git grepforvi.mockof the module overpackages/cli):rewrap.guards,orphans.guards,test/exit-signal.pin,files-to-references.column-step-refusalandsummary-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 bygit grepfor 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.69a1689f00(this branch merged with origin/main at491087ee0f, 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):summary-nullsandfiles-to-references.column-step-refusalamong 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.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 (afterpnpm --filter @objectstack/cli build, which the twopublished-subpath-*pins need).pnpm --filter @objectstack/cli buildandpnpm --filter @objectstack/cli typecheck(test layer included): exit 0.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 thegit grepabove: none of the 69 names a door, mocksschema-migrate.js, or imports the new helper. And the two*.e2e.test.tsfiles 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 toHEADwith an emptygit diff HEADand a clean status. The CLI spawns runpackages/cli/srcthrough 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,
tableAbsentforced false (schema-migrate.ts). Predicted: every fresh-project pin of the five seam doors red, the booted controls green,account-issuergreen (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,--jsonand human) and the two flippedpreview-read-onlycases. Every booted control and the four exit-0 controls stayed green, andaccount-issuerstayed green.Leg B, the
isMissingTableErrorbranch ofaccount-issuerdisabled. Predicted: the twoaccount-issuerfresh-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 becausesys_activityis skipped. Observed: integration 1 failed, 35 passed, withsys_activity.scanned0 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), overrewrap.guards.test.ts. Predicted: only the new absent-tables case red. Observed: 1 failed, 6 passed. Restored toHEAD, emptygit diff HEAD.Legs A to C ran at
491087ee0f;srcof 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/objectstackderived 94 commands (the same list as at491087ee0f);--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 jsonon the 12 changed TS files: 12 files, 0 errors, 0 warnings, none reported as ignored. The population comes fromeslint.config.mjs(files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}); the config enables no type-aware linting (noparserOptions.project, and every block'sparserOptionsis{ ecmaVersion, sourceType }), so this diff cannot move the verdict on a file it does not touch. The.mdand.mdxfiles are outside it.Acceptance notes
[ObjectQLPlugin] sys_metadata_activation is registered but could not be read. Triage ruled it out of this card.packages/cli/src/utils/absent-table-reads.ts(and its unit test). Five doors need the same view, the same notice line and the same rotation rule; five inline copies would drift.schema-migrate.tsis untouched.this.exitorthis.error: none. The exit-signal pin and 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 pin both ran green over the whole command table.rewrap.guards.test.tsandorphans.guards.test.tsare the two files added to the diff (see Tests).value-shapes(PR fix(cli,runtime): os migrate resume, recorded-by and value-shapes answer a project with no database yet with empty work #21550) reads through the same seam without the rotation rule. It composes no audit plugin, so none of its scanned objects is rotation-managed today; noted, not changed.Out of scope (reported to the seat, not filed here)
SchemaStack.tableAbsentanswers true for a rotation-managed object's base name on a booted SQLite database, becausepreviewDeferredSchemaWorkaskshasTableand a view is not a table. Measured throughos migrate audit-metadata-bodiesagainst a booted database whosesys_activityis the view oversys_activity__rYYYYMMDD. The same list is whatos migrate planprints for a host that composes the audit plugin (not measured atplan). The fix belongs in the driver's preview, not in a consumer.Generated by Claude Code