Skip to content

feat(migrate,metadata-protocol): os migrate meta --stored rewrites sys_metadata rows in place (#4327) - #4464

Merged
os-zhuang merged 6 commits into
mainfrom
claude/metadata-migration-stored-rewrite-jxlz2k
Aug 1, 2026
Merged

feat(migrate,metadata-protocol): os migrate meta --stored rewrites sys_metadata rows in place (#4327)#4464
os-zhuang merged 6 commits into
mainfrom
claude/metadata-migration-stored-rewrite-jxlz2k

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4327.

Why

#4317 answered #3903 from the read side: every stored-row rehydration seam replays the full ADR-0087 conversion chain, retired entries included, so a row written under any past protocol is served canonical forever. What it deliberately did not do is make the rows themselves canonical — a pre-17 row keeps its legacy bytes, the chain re-lowers it on every load, and each affected row logs one conversion notice per process. Until now the only things that ever rewrote such a row were a Studio re-save and duplicatePackage.

What

protocol.migrateStoredMetadata() walks sys_metadataactive + draft, every org — replays the same applyConversionsToStoredItem pass, and re-saves each changed body through saveMetaItem. So a rewritten row gets everything an author's save gets: the schema gate, a sys_metadata_history row, a fresh checksum, the mutation projectors and the watch event Studio's HMR consumes. Three deliberate arguments:

  • parentVersion: row.checksum — a true optimistic lock. A concurrent writer that moved the row gets a 409, reported as failed, never a clobber.
  • force: true — the destructive diff compares the stored body (which getMetaItem already serves converted) against the same conversion. Empty by construction, and there is no author here for a prompt to reach.
  • source: 'migrate-stored' — a later history diff distinguishes an upgrade from somebody's edit.

os migrate meta --stored is the operator surface. Archived/deprecated rows are never read (they are a record of what was), and sys_metadata_history is appended to, never rewritten — converting a version body would break the checksum↔body pairing the contract depends on.

os migrate meta --stored                    # preview: per-row report, writes nothing
os migrate meta --stored --apply            # rewrite the rows (prompts)
os migrate meta --stored --apply --yes --json   # CI / scripts
os migrate meta --stored --type view        # restrict to a type (repeatable)

Two deliberate deviations from the issue text

Preview is the default; --apply is the only writing mode. The issue sketched "optionally --dry-run", i.e. writing by default. Both siblings (os migrate value-shapes, os migrate files-to-references) and #3617's "a dry run changes nothing" establish the opposite, and the reason applies with more force here because what moves is metadata: every affected row's checksum and one history entry per row. An apply run also refuses to start while another process holds the SQLite database, for the same reason the file migration does (--force overrides).

No sys_migration flag row. The issue is explicit that this is not load-bearing, and #3855's conclusion stands — an operator-run migration cannot be relied upon, so the read path remains the guarantee. A flag row would advertise a gate that does not exist. The "explicit, verifiable my data is on protocol N" the issue asks for is delivered by the re-run instead: nothing left to do exits 0, work remaining exits 1, so the claim becomes a CI check rather than a belief.

What it declines, and names

Reported, never counted as done:

Not rewritten Why
flow rows Flow-node conversions carry ADR-0078's open-namespace conflict guard, which needs the automation engine's live executor registry (reservedNodeTypes). Flows canonicalize at registerFlow. Filed as #4454
Types with no repository write path (agent) saveMetaItem routes those down the legacy raw-engine branch: no history row, and a forced state: 'active' that would promote a draft. A historyless half-write is worse than leaving the row to the read path
Rows that still fail the schema after conversion A genuine contract violation, not chain-owned history. The write path's 422 is correct and is not bypassed; the row keeps reading through the chain and stays fixable in Studio

An empty scan says it attests nothing rather than reading as a pass — same lesson as the value-shape scan: "nothing to convert" and "nothing was looked at" are different claims, and a run from the wrong project root produces the second.

No speculative API was added for the flow case: a canonicalizeFlow hook nothing can pass would advertise a capability the runtime does not deliver (PD #10). #4454 sketches the automation-engine entry point it needs.

One bug fixed on the way

oclif's dependsOn cannot express "stored-only": a boolean with default: false and an env-backed string both read as provided, so dependsOn: ['stored'] on --database-url would make a merely-exported OS_DATABASE_URL break os migrate meta --from N. The guard reads raw argv instead (storedOnlyFlagsIn), which also closes a declared ≠ enforced gap: --apply on the authored-source chain is now refused rather than silently ignored.

Verification

  • 26 new tests — 18 in protocol.stored-migration.test.ts (preview writes nothing; apply rewrites in place; history row + fresh checksum + migrate-stored source; re-run is a no-op; drafts stay drafts; all orgs; archived untouched; type filter; each carve-out; unparseable body; schema refusal; optimistic-lock conflict does not clobber), 8 in meta.stored-flags.test.ts (the argv guard incl. the env-var trap, and the flag declarations).
  • metadata-protocol · cli · objectql · rest · metadata · runtime · spec all green after merging main (which carried three commits touching these packages; one import conflict in protocol.ts, resolved keeping both sides).
  • pnpm lint, tsc --noEmit, and the doc-authoring / type-check-coverage / published-files / changeset-fixed gates pass.
  • Real run, not just unit tests: seeded a pre-17 conditionalRequired row into examples/app-todo's SQLite → preview exits 1 and names the conversion → --apply rewrites → DB shows the same row id with a canonical body, a new checksum, and a sys_metadata_history row {source: 'migrate-stored', previous_checksum: <the legacy checksum>} → re-run reports canonical and exits 0.

Docs

content/docs/deployment/cli.mdx gains an os migrate meta --stored section (including the occupancy-gate row and the "hygiene, not a gate" framing), and ADR-0087 gains a 2026-08-01 addendum recording the decision — why no flag, why the write gate is not bypassed, why history stays verbatim, and what the pass does not cover.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f


Generated by Claude Code

claude added 3 commits August 1, 2026 08:08
…s_metadata rows in place (#4327)

#4317 closed the correctness gap from the read side: every stored-row
rehydration seam replays the full ADR-0087 conversion chain, retired entries
included, so a row written under any past protocol is served canonical
forever. The rows themselves stayed legacy — the chain re-lowers them on every
load and each logs a conversion notice per process. Until now the only things
that rewrote such a row were a Studio re-save and duplicatePackage.

`os migrate meta --stored` walks sys_metadata (active + draft, all orgs),
replays the same applyConversionsToStoredItem pass, and re-saves each changed
body through saveMetaItem — so a rewritten row gets a sys_metadata_history
entry, a fresh checksum and the mutation projectors, exactly like an author's
save. The history row's source is `migrate-stored`, distinguishing an upgrade
from an edit. parentVersion is the row's own checksum, so a concurrent writer
produces a 409 the report names rather than a clobber.

Preview is the default and --apply the only writing mode, matching its two
siblings and #3617's "a dry run changes nothing"; an apply run refuses to
start while another process holds the SQLite database. Nothing gates on this
having run (#3855) and no sys_migration flag is recorded — a flag would
advertise enforcement that does not exist. What a run buys is hygiene plus an
assertable verdict: nothing left to do exits 0, work remaining exits 1.

Three carve-outs are reported rather than counted as done: flow rows (their
seam is AutomationEngine.registerFlow, which holds the executor registry the
node-type conflict guard needs), types with no repository write path (agent),
and rows that still fail the current schema after conversion. An empty scan
says it attests nothing rather than reading as a pass.

Also: protocol.migrateStoredMetadata() returns the same structured report an
admin route would render, and saveMetaItem takes an optional `source` for its
history/audit rows — server-stated, never request-derived.

Closes #4327

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
…sh line (#4454)

The addendum said flows were "tracked separately" without saying where.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
…tion-stored-rewrite-jxlz2k

# Conflicts:
#	packages/metadata-protocol/src/protocol.ts
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 1, 2026 9:22am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/metadata-protocol.

23 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 3 commits August 1, 2026 08:31
The v17 entry for #3903 described the read-path guarantee and stopped there,
and the upgrade checklist listed the two per-deployment migrations without
this one. Both now point at `os migrate meta --stored`, marked optional —
it opens no gate, unlike its two neighbours in that list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] os migrate meta --stored: rewrite sys_metadata rows in place so the read-path chain has a finish line

2 participants