Skip to content

fix(cli,driver-turso): os migrate plan on a Turso remote database lists the changes it cannot apply with the transport's limit, not --allow-destructive - #24014

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23925-remote-plan-remedy
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23925-remote-plan-remedy

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #23925

Clause-②: yes (widening)

What changes

On a Turso REMOTE primary (libsql://...), neither os migrate plan nor os migrate apply (its listing before the prompt, and both of its held-back lines) offers --allow-destructive for a drift entry that database cannot apply. The driver that reported each entry declares what its apply door cannot perform, and both commands read that declaration. Neither branches on the dialect, the transport name, the URL or the driver's class.

  • The declaration (driver-turso). REMOTE_DRIFT_OP_ANSWERS in turso-driver.ts answers every drift op the shared differ emits with applied, needs-table-rebuild or report-only. Its satisfies Record(DriftOp type, answer) clause fails the package's typecheck when an op is added to DriftOp and left unclassified, the REMOTE_FACE_ANSWERS pattern. The REMOTE applyMigrationEntries dispatches by it: only applied rows are sent, and every other entry is skipped before any DDL and logged with its reason. TursoDriver.driftApplyLimit(opType) answers from the same rows: the limit sentence for a needs-table-rebuild op, null for every other op, and null for every op on the local and embedded-replica faces. The commands and the apply door therefore read one object and cannot disagree about an entry.

  • The seam (cli, schema-migrate.ts).

    • SchemaStack.driftApplyLimit(entry) asks the driver that reported the entry. That is the driftOwners map applyMigrationEntries already routes by, read through the optional SqlDriverLike.driftApplyLimit. A driver that declares nothing has no limit.
    • collectDriftApplyLimits(stack, drift) gathers the entries that have a limit.
    • describeWithApplyLimits(limits) is the one spelling of the line printed under such an entry: its finding, then "os migrate apply" cannot apply it on this database: LIMIT., in place of the --allow-destructive sentence. Both commands read it.
  • os migrate plan. An entry with a limit is still listed and counted (it is still drift), under that line. The footer offers --allow-destructive only for the destructive entries the database can apply, drops the apply line when it can apply nothing listed, and says how many listed changes remain drift.

  • os migrate apply: its listing and its two held-back lines.

    • The listing before the prompt passes renderPlan the same describe.
    • The count before the prompt, N destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them)., now counts only the destructive entries a re-run could apply. The rest get their own line: N destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).
    • The line after the run splits the held-back entries by the same limits map: Skipped N destructive change(s) (re-run with --allow-destructive to include them). for those a re-run could apply, and Skipped N destructive change(s) that a re-run could not apply on this database either: they remain drift (each says why above). for the rest.
    • Nothing else changes: not notApplied, not PARTIAL, not the exit code, and not the --json document.
  • The remedy sentence (driver-sql). tighten_not_null, narrow_varchar and drop_column messages end with an os migrate apply --allow-destructive sentence. That sentence is now spelled once (DESTRUCTIVE_COLUMN_REMEDY), with the same bytes. driftFindingWithoutRemedy(entry) takes exactly that tail off, and only the limited entries use it. Every other reader keeps the message as written.

  • Public surface: the widening the declaration line names. TursoDriver.driftApplyLimit (@objectstack/driver-turso) and driftFindingWithoutRemedy (the @objectstack/driver-sql entry) are new and additive, so those two packages go minor. @objectstack/cli stays patch. No packages/spec contract and no accept set moves.

  • The CLI docs (content/docs/deployment/cli.mdx). One caveat under the migrate category table:

    On a Turso remote primary (libsql://), a tightened or relaxed NOT NULL and a dropped DEFAULT stay drift, because that transport runs no SQLite table rebuild. os migrate plan and os migrate apply list each such entry with "os migrate apply" cannot apply it on this database: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run., offer --allow-destructive only for the destructive changes that database can apply, and count the rest as drift a re-run cannot apply.

  • No declaration, no change. When no entry has a limit, both commands take the old code path: the same renderPlan(drift) call, the same footer string and the same held-back lines. That is every database but a Turso REMOTE one.

Each drift op, and what the REMOTE face does with it

op category emitted by the SQLite differ REMOTE apply door
drop_column destructive yes applied
drop_index destructive yes applied
create_index safe yes applied
recreate_index needs_confirm, or destructive when unique yes applied
replace_unique_index safe yes applied
relax_not_null needs_confirm yes skipped: needs the table rebuild
tighten_not_null destructive yes skipped: needs the table rebuild
drop_column_default safe yes skipped: needs the table rebuild
widen_varchar safe no (PostgreSQL / MySQL only) skipped: needs the table rebuild
narrow_varchar destructive no (PostgreSQL / MySQL only) skipped: needs the table rebuild
manual_column_type_change needs_confirm no (PostgreSQL / MySQL only) report only, as on every face
manual_widen_varchar_to_text needs_confirm no (PostgreSQL / MySQL only) report only, as on every face
unbuildable_index needs_confirm yes report only, as on every face

A report-only op answers null from driftApplyLimit. No face applies it, so its skip is not a limit of this transport, and its finding already names its remedy. The declaration is therefore a per-op answer rather than a set of appliable ops: a plain set would print this transport's rebuild reason against an unbuildable_index.

Who reads the entry's message, and why each one stays truthful

The message keeps its bytes on every face. schema-drift.remedy-sentence.test.ts pins the three destructive column messages character for character. The report stays identical across faces, so turso-remote-drift-detection.test.ts keeps holding the local and remote reports equal. Only the human listings change what they print, and only for an entry whose driver declares a limit.

  • os migrate plan (human, renderPlan): changed as above.
  • os migrate apply (human, the listing before the prompt and the two held-back lines): changed as above.
  • os migrate plan --json and os migrate apply --json: unchanged (acceptance note 1).
  • The boot [schema-drift] warning (SqlDriver.reconcileAndWarnDrift): runs only on the Knex faces, where the apply door does rebuild, so its remedy is true.
  • The OS_ARTIFACT_URL boot gate (artifact-boot-migration.ts): returns on the REMOTE face's driftReportCaveat before it reads a message.
  • REMOTE_DRIFT_REPORT_CAVEAT: re-read, and still true ("applies those and names the rest as not applied"). Untouched.
  • The organization-ownership ceremony (messageOf): reads messages only for its own column drops, which the REMOTE face applies. Its remote pin is green.

Verification

All of it runs against a real sqld (libsql-server 0.24.32, libsql://127.0.0.1:PORT?tls=0, archive sha256 checked). The fixture is apply.remote-face.integration.test.ts's own: a hand-made os23848_note table with three drifts, legacy_23848 (drop_column), code (tighten_not_null) and memo (relax_not_null). Each remote case has a local SQLite file CONTROL. CI has no sqld, so the unit pins hold the same rules against a stubbed stack whose driver double carries the declaration: plan.apply-limit.test.ts and the new block in apply.remote-posture.test.ts.

Red, then green: the plan

  • Red at a512b914f. That commit holds the pins alone; its production code equals the base 2e1562091.
    • sqld: 1 failed, 6 passed. The failure is the remote plan case, with Received: "os23848_note.code: metadata declares ... must be backfilled. Run "os migrate apply --allow-destructive".". The local-file CONTROL is green.
    • Unit: 2 failed, 2 passed. The CONTROL and the --json case are green.
  • Green at d99ee347e. sqld 7 of 7, unit 4 of 4.

Red, then green: the apply's listing

  • Red at ca87de9ee. That commit holds the pins alone; its production code equals 59f489f6d.
    • sqld: 2 failed, 7 passed. The failures are the remote apply --yes listing case and the listing assertion in the remote apply --allow-destructive human case, both receiving ... must be backfilled. Run "os migrate apply --allow-destructive".. Both local-file CONTROLs are green: apply --yes on a file keeps the remedy and 2 destructive change(s) will be SKIPPED (re-run ...).
    • Unit (apply.remote-posture.test.ts): 2 failed, 10 passed. The CONTROL and the --json equality case are green.
  • Green at 48ac51325.
    • sqld: 9 of 9, plus organization-ownership.remote-face.integration.test.ts 1 of 1 and artifact-boot-migration.unbuildable-index.test.ts 3 of 3.
    • Unit: 4 files, 28 tests.
    • The new --json pin compares the apply's document under a declaring driver with the one a driver that declares nothing gets, key for key and entry for entry.

Red, then green: the apply's post-apply held-back line

  • Red at db6056e76. That commit holds the pins alone; its production code equals 455c7f288.
    • Unit (apply.remote-posture.test.ts): 1 failed, 11 passed. The failure is the limited case: it received [" ⚠ Skipped 2 destructive change(s) (re-run with --allow-destructive to include them)."] where two lines were expected. The CONTROL (Skipped 2 ..., unchanged) is green.
    • sqld: 1 failed, 8 passed. The failure is the remote apply --yes case, with the same received line. The local-file CONTROL (Skipped 2 ...) is green.
  • Green at 512414eef.
    • Unit: 4 files, 23 tests (apply.remote-posture, apply.account-issuer-json-refusal, plan.apply-limit, schema-migrate.lazy-driver-import).
    • sqld: apply.remote-face.integration.test.ts 9 of 9 and organization-ownership.remote-face.integration.test.ts 1 of 1.

A pass for the wrong reason, closed

apply.account-issuer-json-refusal.test.ts stubs the stack without the new driftApplyLimit member. Its human case ("the human face of the same refusal exits 1") therefore still exited 1, but from a TypeError in the listing, before the pre-flight ran. The stub now declares no limit, and the case asserts the pre-flight ran.

Ablations

  • The driver pin can fail. At d99ee347e, tighten_not_null was changed from 'needs-table-rebuild' to 'applied' in REMOTE_DRIFT_OP_ANSWERS.
    • The mutation went through scripts/ablation-replace.mjs in WRAP mode, inside a script with a trap restore, and was proven on disk: anchor 1 to 0, blob 1a3a5461 to 37038760.
    • turso-remote-drift-apply-limit.test.ts failed 1 and passed 3.
    • The file was restored to the HEAD blob, with git diff HEAD empty.
  • The issuer pin's new assertion can fail. At 455c7f288, the stub's driftApplyLimit: () => null, was deleted the same way: anchor 1 to 0, blob fcd562cd to 7509aeb0.
    • The human case went red with expected "vi.fn()" to be called 1 times, but got 0 times: 1 failed, 3 passed.
    • The file was restored to the HEAD blob, with git diff HEAD empty.
  • No build. Both subjects resolve to src/ through relative imports.

End to end through the CLI's own entry

Both runs went through tsx bin/run-dev.js, on a sqld this run started and stopped by its PID, with the same three drifts on os23925_note. migrate apply --yes at 512414eef exited 0, and read:

  Destructive (requires --allow-destructive)
    ✗ os23925_note.code [tighten_not_null]
        os23925_note.code: metadata declares `storage.notNull` but the column is nullable — existing nulls must be backfilled. "os migrate apply" cannot apply it on this database: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run.
    ✗ os23925_note.legacy_23925 [drop_column]
        os23925_note.legacy_23925: column exists in the database but not in metadata (orphaned) — "os migrate apply --allow-destructive" to drop it.
  ℹ 3 change(s): 0 safe, 1 needs-confirm, 2 destructive
  ⚠ 1 destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them).
  ⚠ 1 destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).
  ✓ Applied 0 change(s).
  ⚠ Skipped 1 destructive change(s) (re-run with --allow-destructive to include them).
  ⚠ Skipped 1 destructive change(s) that a re-run could not apply on this database either: they remain drift (each says why above).
  ⚠ 1 change(s) were NOT applied — the driver could not perform them on this database (its log above says why), so the schema still differs from metadata there:
      • os23925_note.memo — relax_not_null

migrate plan at d99ee347e exited 0, and its footer read:

  Apply with: os migrate apply (add --allow-destructive for the 1 destructive change(s) it can apply on this database)
  ⚠ 2 change(s) above cannot be applied on this database by "os migrate apply" (each says why), so they remain drift after it runs.

Tests and gates

Recorded at 512414eef unless a line says otherwise.

  • Typecheck.
    • @objectstack/cli (tsc --noEmit, then check:test-typecheck OK): exit 0 at 512414eef.
    • @objectstack/driver-turso and @objectstack/driver-sql: exit 0 at 59f489f6d. No driver file has changed since.
  • @objectstack/cli unit tier. 3 shards at 455c7f288: 103 + 102 + 102 files and 1754 + 1335 + 1486 tests, all passed. Since then the diff changes only the post-apply line in apply.ts and two test files, and the unit pins that reach it ran at 512414eef: 4 files, 23 tests.
  • @objectstack/cli integration, run locally.
    • At 512414eef, on sqld: apply.remote-face.integration.test.ts 9 of 9, and organization-ownership.remote-face.integration.test.ts 1 of 1.
    • At 48ac51325: artifact-boot-migration.unbuildable-index.test.ts 3 of 3.
    • At 59f489f6d: plan.telemetry-sibling, plan.deferred-reads and plan.reads-env-files, 3 files, 10 tests.
    • The rest of the tier is CI's.
  • The driver suites, run at d99ee347e. No driver file has changed since.
    • @objectstack/driver-turso: 92 files, 2429 passed, 33 skipped.
    • @objectstack/driver-sql: 223 files passed and 11 skipped; 3677 tests passed and 208 skipped.
  • Derived gate families. dispatch-gates --commands --repo objectstack-ai/objectstack gives 99 commands. That is the 69 from before plus 30 the docs path adds (the doc, docs and spec docs families). Each exit code was captured before any pipe.
    • --ran reconciliation: 99 derived, 97 run with exit 0, 2 NOT MEASURED, 0 UNRUN.
    • The two NOT MEASURED are check:dual-build-cjs-loads (packages outside this build closure have no dist/) and check:i18n-coverage (a partial round). Both exited 3, PREREQUISITE NOT MET.
    • pnpm --filter @objectstack/spec run check:skill-examples first exited 3, because @objectstack/client-react was not built. After building the client SDK closure (all cache hits) it exited 0: 263 prose examples type-check across 3 surfaces. That exit 0 is the one recorded.
  • pnpm check:doc-authoring. Exit 0.
  • The changeset gates.
    • check-changeset-no-major.mjs --base origin/main exits 0 with no event, and exits 0 with an event carrying this body. With this body it reads LEVEL AXIS: this PR declares clause-② yes (widening) ... at minor or above, naming @objectstack/driver-sql: minor and @objectstack/driver-turso: minor, with @objectstack/cli: patch not refused.
    • check-adr-0087-registration.mjs --base origin/main exits 0: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
  • CI at 512414eef, read 20:17Z.
    • 34 success, including Build Docs and four of the five required contexts (Lint & Repo Gates, TypeScript Type Check, Build Core and Governed Surface Queue Guard).
    • 5 skipped by their selectors.
    • Test Core (3/6) was still in progress, so the Test Core roll-up had not reported yet.

File surface

The claim's surface, and its amendments 6222387090, 6223299622 and 6223389541:

  • packages/cli/src/commands/migrate/apply.ts: only the listing before the prompt and the held-back count line (amendment 6222387090).
  • packages/cli/src/utils/schema-migrate.ts: the driftApplyLimit seam, the two shared helpers, and an optional describe argument to renderPlan.
  • packages/drivers/driver-sql/src/index.ts: one re-export line.
  • packages/cli/src/commands/migrate/apply.ts, by amendment 2 (6223299622): the post-apply held-back line as well.
  • content/docs/deployment/cli.mdx, by amendment 3 (6223389541): one caveat under the migrate category table, and nothing else on the page.
  • Tests beside each:
    • plan.apply-limit.test.ts;
    • apply.remote-posture.test.ts, whose stub stack gains the member, plus a new block;
    • apply.account-issuer-json-refusal.test.ts, whose stub gains the member, plus one assertion;
    • apply.remote-face.integration.test.ts, which holds the sqld fixture;
    • schema-drift.remedy-sentence.test.ts;
    • turso-remote-drift-apply-limit.test.ts.

Nothing under packages/spec/**, and no published driver contract (packages/spec/src/contracts/**) gains a member. Neither command reads dialect, the transport, the URL or a class name.

Acceptance notes

  1. The --json documents are unchanged. In os migrate plan --json, changes[].message keeps the shared remedy, and no field marks an entry the database cannot apply. os migrate apply --json is pinned equal to what a driver that declares nothing produces. Adding a mark widens a document, and no consumer of either JSON document was measured to need one.
  2. drop_column_default. Its message carries run "os migrate apply" mid-sentence rather than as a tail. On a remote primary, the listing line therefore shows that sentence with the limit after it. That entry needs a literal runtime-token DEFAULT written by an old build, so a database the current sync created does not reach it.
  3. The REMOTE apply door's log. A skipped rebuild entry now reads ... skipped: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run, from the declaration's sentence. It used to end which this transport does not run. A report-only unbuildable_index on that face is now logged as skipped; it used to be skipped with no log line. No test pins either log line.

Generated by Claude Code

…ver's apply limit (#23925)

Red on the base: the plan ignores the reporting driver's declaration and
prints the shared differ's --allow-destructive remedy for an entry the
Turso REMOTE transport cannot apply.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
… and never offers --allow-destructive for a change a Turso REMOTE database cannot apply (#23925)

The REMOTE face declares, once (REMOTE_DRIFT_OP_ANSWERS), which drift ops
its apply door performs; applyMigrationEntries dispatches by it and
TursoDriver.driftApplyLimit answers the plan from it. The plan asks the
driver that reported each entry (SchemaStack.driftApplyLimit), lists an
entry it cannot apply with its finding and that limit instead of the
--allow-destructive sentence, and offers --allow-destructive only for a
destructive entry the database can apply. A driver that declares nothing
reads as before; the --json document is unchanged.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
…mote-plan-remedy

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

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/driver-sql, @objectstack/driver-turso, touching 35 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/ai/skills-reference.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/data-modeling/drivers.mdx (via TursoDriver (symbol, a top-level class), os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/data-modeling/indexing.mdx (via recreate_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/data-modeling/queries.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/deployment/cli.mdx (via create_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), drop_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), manual_column_type_change (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), manual_widen_varchar_to_text (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), recreate_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), relax_not_null (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), replace_unique_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), unbuildable_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/index.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/seed-tenancy-repair.mdx (via replace_unique_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS))
  • content/docs/deployment/self-hosting.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/kernel/services-checklist.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/plugins/packages.mdx (via TursoDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/protocol/objectql/query-syntax.mdx (via TursoDriver (symbol, a top-level class))
  • content/docs/upgrading.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-4.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-5.mdx (via DriftOp (symbol, a top-level type), TursoDriver (symbol, a top-level class), unbuildable_index (symbol, a field of const object REMOTE_DRIFT_OP_ANSWERS), os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-6.mdx (via TursoDriver (symbol, a top-level class), os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-7.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/index.ts) — pages documenting those are invisible to this run
  • 2 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 — 37 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 cf31c34823214dccabbf6bd7b8db446d03c61d71 → packageMentionDocs.

Which tree this was computed on

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

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

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

… the driver's apply limit (#23925)

Red on 59f489f: the apply's pre-confirmation listing still prints the
shared --allow-destructive remedy for an entry the Turso REMOTE transport
cannot apply, and its held-back count offers a re-run for it.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
…ver's apply limit, as the plan does (#23925)

The listing apply prints before its prompt passes renderPlan the plan's
describe (now one helper, describeWithApplyLimits, beside
collectDriftApplyLimits), and the held-back count offers a re-run with
--allow-destructive only for the destructive entries the database can
apply, naming the rest as drift a re-run cannot apply. Nothing after the
prompt and nothing in --json changes. The changeset moves to Clause-② yes
(widening): driver-sql and driver-turso minor, cli patch.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 16, 2026
…ts human case asserts the pre-flight ran (#23925)

Without the SchemaStack member the apply listing now reads, the human
case still exited 1, but from a TypeError before the pre-flight: a pass
for the wrong reason.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
…he driver's apply limit (#23925)

Red on 455c7f2: after the apply runs, 'Skipped N destructive change(s)
(re-run with --allow-destructive to include them).' still counts a
tightening a re-run cannot apply on a Turso REMOTE primary.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
…driver's apply limit; docs name the Turso remote caveat (#23925)

The post-apply 'Skipped N destructive change(s) (re-run with
--allow-destructive to include them).' now counts only the held-back
entries a re-run could apply on this database, and names the rest as
drift a re-run could not apply either, by the same limits map the
pre-prompt count reads. notApplied, PARTIAL, the exit code and --json are
unchanged; a driver that declares nothing reads as before. The migrate
category table in the CLI docs gains one caveat for a Turso remote
primary.

Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ✅ ACCEPT: PR #24014 at 512414eef4 · domain:engine seat 2 · session_01TQLy3Yq6vF4SDpTXdr3Uwi · 2026-10-16T20:22Z

Basis:

This comment is also the review of record for yes (widening) (TIER_DEFAULT, the lane seat's own). No published schema and no governed path is touched, so no CONTRACT_REVIEW_TIER review is owed.

Checklist

  • Shape. Draft, base main, Fixes #23925 on line 1. Clause-②: yes (widening) is on its own line, once, in the body and the changeset. No other number carries a closing verb. The body was set by the seat from the dev's final text.

  • Why yes (widening). Two public members are new:

    • TursoDriver.driftApplyLimit(opType) on @objectstack/driver-turso's exported class;
    • driftFindingWithoutRemedy from the @objectstack/driver-sql entry.

    No accept set and no packages/spec contract moves. driver-sql and driver-turso go minor, and cli patch. check-changeset-no-major, driven with this body, reads arm widening with no moved package graded patch. check-adr-0087-registration reads it as non-breaking.

  • Triage's direction is kept.

    • The REMOTE face's per-op answer table (REMOTE_DRIFT_OP_ANSWERS, exhaustive by satisfies) is the ONE declaration. applyMigrationEntries dispatches by it, and driftApplyLimit answers the plan and the apply from the same rows.
    • Nothing branches on the dialect, URL or transport name.
    • The local and embedded-replica faces answer null, and a driver that declares nothing reads byte for byte as before (a CONTROL in each pin file).
    • The table is richer than a set: it tells "this transport cannot" from "no face applies it" (unbuildable_index, the manual_* findings).
  • Every consumer of the entry message stays truthful. The shared messages keep their exact bytes (pinned). Only the human plan and apply render the finding without the remedy, plus the driver's limit. --json is unchanged on both commands. The boot drift warn runs only on the Knex faces. REMOTE_DRIFT_REPORT_CAVEAT was re-read and is still true.

  • os migrate apply's three human lines read the same map:

    • the pre-prompt listing;
    • the pre-prompt held-back count;
    • the post-apply held-back line, now split as "re-run could apply" and "remains drift".

    notApplied, PARTIAL and the exit code are untouched. The seat re-read the last hunk: heldBackEntries equals the old skipped.length - notApplied.length set, so with no limits the existing line is byte-identical.

  • Docs. content/docs/deployment/cli.mdx gains one caveat under the migrate category table. Its destructive row says --allow-destructive applies a NOT NULL tightening, which a Turso remote primary cannot do. The caveat's wording quotes the limit sentence the tool prints. check:doc-authoring exits 0.

  • Evidence.

    • The plan: red at a512b914f (sqld 1 of 7, unit 2 of 4), green after.
    • The apply listing: red at ca87de9ee (sqld 2 of 9, unit 2 of 12), green after.
    • The post-apply line: red at db6056e76 (sqld 1 of 9, unit 1 of 12), green at this head.
    • Ablations: tighten_not_null: 'applied' turns the driver pin red; deleting the issuer stub's member turns the pre-flight assertion red. Each restore was proven.
    • A pass for the wrong reason, closed: the issuer test's stub now declares the member, and its human case asserts that the pre-flight ran.
    • End to end through bin/run-dev.js on a real sqld, at this head: the tightening and the relax carry the limit, the drop carries its remedy, and the two held-back lines split 1 and 1.
  • Gates. dispatch-gates --commands: 99 derived, 97 exit 0, 2 NOT MEASURED locally (check:dual-build-cjs-loads, check:i18n-coverage: PREREQUISITE NOT MET, so CI's). ESLint on the touched files exits 0.

  • Landing checks. check-governed-merges --pr 24014: NOT governed, 1081 changed lines. mergeable_state is clean, and git merge-tree against current origin/main (2b274600b) is clean.

The dev's open questions, answered by this seat (not escalated):

  • Round 1: B (yes (widening), minor for the two drivers).
  • Round 2: A (fold in the post-apply held-back line; this card is the family's closing card).
  • Also folded in: the cli.mdx caveat.

Out of scope, one line each:

  • The three Acceptance notes stay noted, with no carrier: the --json documents carry no limit mark, with no measured consumer; drop_column_default's mid-sentence remedy is reached only by a pre-current-build DEFAULT; and the REMOTE apply door's log wording changes, which no pin reads.
  • A 6-byte /build1.pid at the dev container's filesystem root (a mis-scoped redirect, outside the repo) → noted for the round report.

Gate on the head: ci-failure.mjs --pr 24014: GREEN, 41 of 41. The five skips (Console Pin Gate, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, the Dogfood Regression Gate matrix template, Packed-tarball smoke (opt-in)) are on check-expected-skips.mjs's roster, and each gate is unmet on this diff. Build Docs ran, because cli.mdx changed, and passed. Check Changeset re-ran on the seat's body and passed. Auto Label and Check PR Size read skipped only on that body-edit run, and read success on the push run of the same head.

Next: ready, then auto-merge through the queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 20:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 20:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit c4111c6 Oct 16, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23925-remote-plan-remedy branch October 16, 2026 20:55
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/xl tests tooling

Projects

None yet

2 participants