Skip to content

fix(driver-sql): the boot drift line for an organization_id drop names the ownership ceremony on an uncertified database - #25188

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24956-boot-drift-line-names-ceremony
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24956-boot-drift-line-names-ceremony

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24956

Clause-②: no (operator log line text only)
Metadata-protocol: none

What changes

SqlDriver's boot reconcile (reconcileAndWarnDrift) logs each drift entry as [schema-drift] MESSAGE. When an entry is an organization_id drop_column, it now asks the driver's own reader, isTenantKeyCertified(), once per table and only when such a line is about to print. The reader goes through the engine-handed resolver over isOrganizationOwnershipCertified(): a birth marker, or a ceremony marker of the current shape generation. No second list is involved.

Files: packages/drivers/driver-sql/src/schema-drift.ts adds isCeremonyOwnedColumnDrop and ceremonyOwnedDropFinding. They are module exports only and are not added to the package's index.ts, so the public surface does not change. sql-driver.ts adds the branch. There is one new test file and a patch changeset.

Measured: a real create-objectstack@17.7.0 SQLite database, OS_AUTH_SECRET set

The fixture was scaffolded with npx create-objectstack@17.7.0; it ran on @objectstack/*@17.7.0 and booted once with objectstack dev -d file:./v17.db. It carries 43 tables with an organization_id column. Each door ran on a fresh copy, through this tree's CLI (packages/cli/bin/run.js) and a copy of the scaffold's app with engines.protocol set to ^18.

door main 82cfaf792 this branch bdd1ac563
os serve exit 1 (boot guard refusal); 28 [schema-drift] lines, 9 organization_id drop lines naming --allow-destructive exit 1 (same refusal); 28 lines, 0 naming --allow-destructive for organization_id, 9 naming os migrate organization-ownership
os migrate apply --yes exit 0; 28 lines, 9 as above exit 0; 28 lines, 0 as above, 9 naming the ceremony
os migrate plan exit 0; no [schema-drift] lines (its own renderer already names the ceremony, PR #24954) unchanged
os migrate apply --yes --allow-destructive (main) exit 1, refused by the drop gate; sys_job.organization_id still present not re-run (the gate is untouched)

The 9 tables are sys_metadata, sys_metadata_history, sys_metadata_commit, sys_migration, sys_migration_journal, sys_secret, sys_flow_credential, sys_job and sys_job_queue. The card counted 22 lines at 003e41ab5; today's main prints 28. On both serve and apply --yes, the 19 other drift lines are identical between main and this branch (diff empty).

Certified control. A fresh database from this branch (birth marker adr-0131-organization-ownership verified), plus a hand-added sys_user.organization_id column. Its boot prints [schema-drift] sys_user.organization_id: column exists in the database but not in metadata (orphaned) — "os migrate apply --allow-destructive" to drop it., unchanged. The resolver answers "certified" at boot time.

The fork the dispatch named did not occur. The driver's reader answers "not certified" for this card's population. That is the same reading the boot's recreate_index lines already print ("This database is not certified for the organization-ownership shape").

Pins and ablation

packages/drivers/driver-sql/src/sql-driver.ceremony-owned-drop-line.test.ts runs on real SQLite through initObjects, with a 17.x-shaped table:

  1. not certified (the resolver answers no): the organization_id line names "os migrate organization-ownership" and never --allow-destructive; the reader is asked once;
  2. no resolver handed over: the same line;
  3. certified: the line equals the differ's message exactly;
  4. control, not certified: an orphaned legacy_code drop is unchanged (exact);
  5. control, not certified: a tighten_not_null is unchanged (exact);
  6. a table with no organization_id drop never asks the reader.

Ablation (scripts/ablation-replace.mjs): the guard was prefixed with false &&, so the anchor went from 1 hit to 0 and the blob from 24448296bf26 to cb74d2820589. Pins 1 and 2 went red; pins 3 to 6 stayed green (2 failed, 4 passed). After restore the blob equals HEAD (24448296bf26) and git diff HEAD is empty. The test imports ./sql-driver.js from src, so no dist preflight applies.

Verification

  • pnpm --filter @objectstack/driver-sql typecheck: exit 0. The new test file is in the program (--listFiles count 1).
  • pnpm --filter @objectstack/driver-sql test: exit 0, Test Files 231 passed | 12 skipped (243), Tests 3748 passed | 212 skipped (3960). This is on the merged head 970f966b9, and again before the merge. It ran SQLite only; live PG and MySQL are left to CI.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: exit 0, 323 passed (323) files, 4769 passed tests (at bdd1ac563).
  • pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2: exit 0, 166 passed | 1 skipped (167) files, 1447 passed | 9 skipped tests (at bdd1ac563). It ran locally because the diff touches the driver boot path.
  • After merging origin/main (6251f5905): pnpm exec turbo run build --concurrency=2 exit 0 (77/77 tasks), then typecheck and the driver-sql suite as above.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 970f966b9 derived 70 commands, and all 70 exit 0. The first run, at bdd1ac563, had check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, unbuilt packages); it exited 0 after the full build. --ran: 70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED (a DERIVED zero).

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01NQBsTsRUMvji8sjVyHJkvY


Generated by Claude Code

…s the ceremony on an uncertified database

On a database the driver's reader (isTenantKeyCertified) answers not
certified, the boot's [schema-drift] line for an orphaned organization_id
column no longer prescribes 'os migrate apply --allow-destructive', which
the drop gate refuses there; it names 'os migrate organization-ownership'.
A certified database, every other column and every other drift kind print
the differ's message unchanged.

Claude-Session: https://claude.ai/code/session_01NQBsTsRUMvji8sjVyHJkvY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 37 pages)
  • 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 — 11 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 f7d740376d3953c7b4a7d22c77e0f689c77a228a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json f7d740376d3953c7b4a7d22c77e0f689c77a228a

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 970f966b9f
Local-runs: none

Scope: the net diff against the merge base (4 files, +163/−0):

  • packages/drivers/driver-sql/src/schema-drift.ts: the local CEREMONY_OWNED_DROP_COLUMN, isCeremonyOwnedColumnDrop() and ceremonyOwnedDropFinding();
  • packages/drivers/driver-sql/src/sql-driver.ts: the boot reconcile's warning loop asks isTenantKeyCertified() once per table, only for an organization_id drop_column about to be warned;
  • the new sql-driver.ceremony-owned-drop-line.test.ts;
  • the changeset.

Read with them:

① Derived judgments

  • One source decides, as the card asks: right.

    • The source. The line is changed only when isTenantKeyCertified() answers no. That is the driver's own reader over the engine-handed isOrganizationOwnershipCertified(). There is no second list and no copy of the CLI gate.
    • The constant. CEREMONY_OWNED_DROP_COLUMN names a column, not a table set. driver-sql cannot import the CLI's ORGANIZATION_COLUMN, so a local constant is the honest spelling.
  • This card's population answers no. The dev measured on a real create-objectstack@17.7.0 SQLite database (43 tables carry organization_id).

    • Before the fix. os serve and os migrate apply --yes each print 28 [schema-drift] lines, 9 of them organization_id drops naming --allow-destructive. apply --allow-destructive is refused.
    • After the fix. The same 9 name the ceremony, and the other 19 lines diff empty against main.
    • No fork. The boot's own recreate_index lines on that database already say "This database is not certified", so the fork the claim named did not occur.
  • The text is the finding plus the plan's wording. driftFindingWithoutRemedy removes exactly the drop_column remedy sentence. Then the line takes the entry wording os migrate plan uses (apply-drop-gates.ts, "a drop the organization-ownership ceremony owns, and it has not verified on this database").

    • The ending. The boot line ends with run "os migrate organization-ownership", which drops it itself., where the plan says "(see below)" and points at its footer. The boot has no footer, so naming the command is the right ending.
  • Nothing else moves. The new branch is a continue before the existing warn. What the boot applies, what it skips and driftWarned's de-duplication are unchanged.

    • One ask per table. The question is asked lazily, certified ??= await …, once per call, so once per table, and only when such a drop is about to be warned.
    • A yes is kept. The reader keeps a yes, and a no is asked again on the next table.
  • The pins have teeth. Six cases on real SQLite:

    • not certified: names the ceremony, never --allow-destructive, and the reader is asked once;
    • no resolver handed over: the same line;
    • certified: the differ's message byte for byte;
    • two not-certified controls, an orphan legacy_code drop and a tighten_not_null, unchanged;
    • a table with no such drop never asks.

    Ablation. With the guard prefixed false &&, pins 1 and 2 go red and 3 to 6 stay green; restored blob-equal to HEAD.

② Semver level

  • patch for @objectstack/driver-sql: right for a log line's remedy text. No response, status, error, outcome or export of the public entry moves. isCeremonyOwnedColumnDrop / ceremonyOwnedDropFinding are exported from schema-drift.ts but are not in src/index.ts's named re-export list from it (which carries driftFindingWithoutRemedy), so the published surface does not widen.
  • The changeset and the PR body carry the claim's Clause-②: no (operator log line text only) verbatim at a line start, and Metadata-protocol: none. none is right, since nothing a metadata author writes changes.

③ Boundary flags

  • Cross-lane: none. All four files are driver-sql's. The CLI drop gate and apply-drop-gates.ts are read, not edited. @objectstack/cli passed its unit (4769) and integration (1447) layers against the change.
  • Not governed: check-governed-merges matched 0 of 4 paths, and the PR has 163 changed lines. git merge-tree --write-tree against today's origin/main is clean.
  • The 18.0-born case stays outside. A database born on 18.0 (birth marker, so certified) whose object newly drops organization_id still prints --allow-destructive, which the CLI gate refuses. That is [Decision] An 18.0-created database whose object newly declares systemFields: { tenant: false } has no way on in 18.0 — the drift report prescribes --allow-destructive, the drop gate refuses it, and the ceremony is refused #24638's population (domain:cli, pm:blocked), as the claim fenced it.
  • The dev's observation, noted and not filed. Orphaned indexes keyed on organization_id (drop_index) still name --allow-destructive on a pre-ceremony database, at boot and in os migrate plan. The CLI gate judges drop_column only, and a run that carries those index drops on such a database also carries the column drops it refuses. Whether an index drop is held is the gate's question. It sits in the PR's Acceptance notes.
  • Deviations, all accepted:
    • the base moved (82cfaf792, then the merge of 6251f5905);
    • 28 lines today, not the card's 22, with all 9 organization_id drops covered;
    • the measurement setup (engines.protocol bumped on the scaffold, not on the database);
    • the CLI integration layer run as well;
    • the AGENTS.md commit trailers.
  • Open questions: both are answered A by the seat: name the ceremony rather than print no remedy, and a local constant. They sit inside the claim's fence.
  • Docs: the dev found no content/docs/** page that quotes the boot line. deployment/cli.mdx's drift table stays true.
  • CI on 970f966b9f: 2 of 40 check-runs still running at this read. The ACCEPT waits for green.

Implemented-by: claude/issue-24956-boot-drift-line-names-ceremony
Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-22T08:34Z · ACCEPT

Head: 970f966b9f. It carries the ceremony line, its pins and the changeset.

Card #24956 (p2, bug, area:devpath; filed by domain:cli seat 2 from #24925's dev report 6298327441): claim 6333744317, dispatch 6333771718. Dev report: 6335859221.

Item Reading
Shape Draft PR → main. The body opens Fixes #24956. 4 files, +163/−0. Not governed: check-governed-merges matched 0 of 4 paths.
Scope On a database SqlDriver.isTenantKeyCertified() answers "not certified", the boot's [schema-drift] line for an orphaned organization_id drop keeps its finding. It replaces — "os migrate apply --allow-destructive" to drop it. with the ceremony, in os migrate plan's entry wording. A certified database, every other column and every other drift kind print as before; nothing the boot applies or skips changes.
Seat's own read The one source is the driver's reader from PR #24893, asked once per table and only for such a drop; there is no copy of the CLI gate. The 18.0-born, certified case still prints --allow-destructive, and stays #24638's (domain:cli).
Review TIER_DEFAULT. Record 6336375080 is PASS on this head.
Declaration One changeset, @objectstack/driver-sql patch, with Clause-②: no (operator log line text only) and Metadata-protocol: none at line starts, as on the PR body.
Cross-lane None. The CLI's drop gate and apply-drop-gates.ts are read, not edited. @objectstack/cli passed its unit layer (4769 tests) and its integration layer (1447 tests).
Docs drift The bot listed nothing: no hand-written page names the five anchors. The dev found no content/docs/** page that quotes the boot line; deployment/cli.mdx's drift table stays true.
Gates Measured on a real create-objectstack@17.7.0 SQLite database. Before: 9 of 28 drift lines name --allow-destructive for organization_id, at both os serve and apply --yes. After: those 9 name the ceremony, and the other 19 diff empty against main. Dev at 970f966b9: 70 derived, 70 run, all exit 0. driver-sql passed 3748 tests, and the new pins 6/6. Ablation (the guard disabled): 2 red, 4 green, restored to HEAD.
CI At this read on 970f966b9f: ci-failure GREEN, all 41 check-runs completed, none failed. git merge-tree against today's origin/main (14e7239e73) is clean.

Disposition:

  • open_questions: both answered A by the seat:
    • the line names the ceremony, rather than printing no remedy;
    • the column name is a local constant, since driver-sql cannot import the CLI's.
  • deviations: all accepted:
    • the moved base;
    • 28 lines rather than the card's 22, with all 9 covered;
    • the measurement setup;
    • the extra CLI integration run;
    • the AGENTS.md commit trailers.
  • out_of_scope_findings: orphaned organization_id index drops (drop_index) still name --allow-destructive on a pre-ceremony database. The CLI gate judges drop_column only, and a run that carries those index drops on such a database carries the refused column drops too. Noted in the PR, not filed.

Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24956 then closes the card.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants