Skip to content

feat(spec,cli): ADR-0087 P1+P2 — conversion layer, migration chain & change manifest - #2897

Merged
os-zhuang merged 3 commits into
mainfrom
claude/adr-0087-metadata-protocol-yjefro
Jul 14, 2026
Merged

feat(spec,cli): ADR-0087 P1+P2 — conversion layer, migration chain & change manifest#2897
os-zhuang merged 3 commits into
mainfrom
claude/adr-0087-metadata-protocol-yjefro

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Implements P1 and P2 of the ADR-0087 epic (#2643) on top of the merged P0 handshake (#2650). Two stacked commits:


P1 — Conversion layer (D2)

A versioned, declarative, lossless table (packages/spec/src/conversions/) that rewrites old (N−1) metadata shapes to canonical protocol-N shapes at load — the same normalizeStackInput seam defineStack/validate/lint/info/doctor share. Old-shape authoring keeps loading with zero action; each rewrite emits a structured OS_METADATA_CONVERTED notice, surfaced by objectstack validate.

Seed table (retroactive protocol-11 renames):

idsurfacetransform
flow-node-http-callout-renameflow.node.typehttp_request/http_call/webhookhttp
page-kind-jsx-to-htmlpage.kind'jsx''html' (ADR-0080)
flow-node-crud-filter-aliasflow.node.config.filterCRUD node config.filtersconfig.filter

PD #12 retirement demonstrated: the filtersfilter alias is removed from the service-automation executor's readAliasedConfig fallback and promoted into the declared/loud/tested/expiring conversion above; executors now read canonical cfg.filter.

P2 — Migration chain (D3) + change manifest (D4)

D3 (packages/spec/src/migrations/): a permanent, ordered, per-major chain. Each major's step composes two feeders — the graduated D2 conversions (referenced by id, reusing transform + fixture) as mechanical transforms, and semantic changes with no lossless mapping surfaced as structured TODOs (surface · reason · acceptance criteria — never silence). applyMetaMigrations(stack, fromMajor, toMajor?) folds fromMajor+1..current and carries any past major to current in one run — cross-major is the designed-for case; each hop is checkpointed for per-hop verify / bisection. MIGRATION_SUPPORT_FLOOR is an explicit release-policy knob. Seeded protocol-11 step: three graduated conversions (mechanical) + the two non-lossless live windows (titleFormat composite → nameField; SQL-ish RLS predicate → CEL) as semantic TODOs. CI replays each conversion fixture through the full chain — a composability break is a release blocker.

D4:spec-changes.json — a Zod-defined { from, to, added, converted, migrated, removed } record. composeSpecChanges folds the conversion table (D2) and migration set (D3) across majors and joins the release-time api-surface diff; per-major manifests compose into one from→to view (the generated guide and P3's MCP spec_changes are projections of it).

CLI — objectstack migrate meta --from N (the command the P0 handshake error already names): replays the chain and prints a generated, ObjectStackDefinition-validated mechanical diff (path: old → new) plus the semantic TODOs; --to / --step (per-hop checkpoints) / --out <file.json> (canonical JSON snapshot) / --json. It does not silently rewrite TS config source (that AST rewrite is unsafe/lossy) — it emits a reviewable artifact for the consumer agent. normalizeStackInput gains an optional convert: false (map→array only) so migrate meta replays the conversions itself against the raw authored source, attributing each rewrite to a chain hop.

Verification

  • @objectstack/spec6748 passed (32 new: 15 conversion + 17 migration/spec-changes), tsc --noEmit clean, check:api-surface green (additions purely additive, 0 breaking).
  • @objectstack/service-automation255 passed (CRUD-alias test rewritten for the executor-shim vs. load-time-conversion split, incl. an end-to-end filtersfilter conversion-then-run case).
  • @objectstack/downstream-contract14 passed.
  • End-to-end proven: a protocol-10 stack authored with all three deprecated shapes migrates via applyMetaMigrations(…, 10) to a schema-valid canonical stack (3 mechanical rewrites + 2 semantic TODOs).

Scope / boundaries

Refs #2643, #2645, #2647 · ADR-0087 (D2/D3/D4) · AGENTS.md Prime Directive #12.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YA9vjVpDrKLuaK6nbm1s37

…ess, D2)
Ship the L1 rung of ADR-0087's preference ladder ("break invisibly"): a
versioned, declarative, lossless conversion table in @objectstack/spec that
rewrites old (N−1) metadata shapes to the canonical protocol-N shape at load —
the same normalizeStackInput seam defineStack / validate / lint / info / doctor
share — so a consumer still authoring the old shape keeps loading with zero
action while the runtime only sees the canonical shape. Each rewrite emits a
structured OS_METADATA_CONVERTED notice; `objectstack validate` surfaces them as
non-blocking deprecation warnings (and in --json).
Seeded with the retroactive protocol-11 renames the ADR names as its
calibration set:
- flow-node-http-callout-rename: flow callout node types
http_request / http_call / webhook → http
- page-kind-jsx-to-html: page kind 'jsx' → 'html' (ADR-0080)
- flow-node-crud-filter-alias: CRUD flow-node config.filters → config.filter
PD #12 retirement path demonstrated: the filters → filter alias is removed from
the service-automation executor's readAliasedConfig fallback and promoted into
the declared, loud, tested, expiring conversion entry above; the CRUD executors
now read the canonical cfg.filter directly.
The layer is the deliberate opposite of a PD #12 consumer-side dialect fallback
on every axis: one central versioned table (not scattered `cfg.a ?? cfg.b`),
declared in the spec, loud (a notice per application), tested (each entry ships
an old→new fixture pair driven through the load path), and expiring (applied for
one major, then graduated into the P2 migration chain — never deleted).
Immutable copy-on-write walkers touch only the changed path, so non-clonable
values (plugin instances) are preserved by reference. api-surface additions are
purely additive (no breaking removals).
Refs #2643, #2645 (ADR-0087 D2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YA9vjVpDrKLuaK6nbm1s37
@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 14, 2026 8:30am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/l and removed documentation Improvements or additions to documentation tests tooling labels Jul 14, 2026
@github-actions

github-actionsBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, packages/services, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx(via @objectstack/cli)
  • content/docs/api/environment-routing.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/cli, packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx(via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/data-service.mdx(via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/cli, packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx(via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/cli)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/cli, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via packages/services, @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/objectos/realtime-protocol.mdx(via @objectstack/cli)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)

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.

…es manifest (D3/D4)
Build the L2 rung ("break executably") and the machine-readable release record
on top of P1's conversion layer.
D3 — migration chain (@objectstack/spec migrations/): a permanent, ordered,
per-major chain. Each major's step composes two feeders — the graduated D2
conversions (referenced by id, reusing their transform + fixture, no
duplication) as the mechanical transforms, and semantic changes with no lossless
mapping surfaced as structured TODOs (surface, reason, acceptance criteria —
never silence). applyMetaMigrations(stack, fromMajor, toMajor?) folds the steps
fromMajor+1..current and carries any past major to current in one run;
cross-major is the designed-for case. Each hop is checkpointed for per-hop
verify / bisection. MIGRATION_SUPPORT_FLOOR is an explicit release-policy knob.
Seeded with the protocol-11 step: three graduated conversions (mechanical) plus
the two non-lossless live windows (titleFormat composite → nameField, SQL-ish
RLS predicate → CEL) as semantic TODOs. CI replays each conversion fixture
through the full chain — a composability break is a release blocker.
D4 — spec-changes.json: a Zod-defined machine-readable record
{ from, to, added, converted, migrated, removed }. composeSpecChanges folds the
conversion table (D2) and migration set (D3) across majors and joins the
release-time api-surface diff; per-major manifests compose into one from→to
view. Every downstream artifact (generated guide, P3 MCP spec_changes) is a
projection of this.
CLI — `objectstack migrate meta --from N` (the command the P0 handshake error
already points to): replays the chain, prints a generated + ObjectStackDefinition
-validated mechanical diff plus the semantic TODOs; --to / --step (per-hop
checkpoints) / --out <file.json> (canonicalized snapshot) / --json. It does not
silently rewrite TS config source (that AST rewrite is unsafe/lossy) — it emits
a reviewable artifact for the consumer agent.
normalizeStackInput gains an optional `convert: false` (map→array only) so
migrate meta replays the conversions itself against the raw authored source,
attributing each rewrite to a chain hop. Proven end-to-end: a 10.x stack with
all three deprecated shapes migrates to a schema-valid canonical stack. New
exports are purely additive.
Refs #2643, #2647 (ADR-0087 D3/D4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YA9vjVpDrKLuaK6nbm1s37
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/xl and removed size/l labels Jul 14, 2026
@os-zhuangos-zhuang changed the title feat(spec): ADR-0087 P1 — metadata conversion layer (load-time, lossless)feat(spec,cli): ADR-0087 P1+P2 — conversion layer, migration chain & change manifestJul 14, 2026
…ntime flow-load seam + conflict guard
Close two third-party-facing risks in the P1 conversion layer.
Risk 1 (data-loss regression) — retiring the executor's `filters` fallback
while the conversion only ran on the build/validate seam meant a stored flow
rehydrated from the DB (engine.registerFlow → FlowSchema.parse, which bypassed
the conversion) kept carrying `config.filters`; with the fallback gone the
executor saw an empty `filter` and a `delete_record`/`update_record` widened to
the whole table. Fix: run the D2 conversion in registerFlow BEFORE parse (new
applyConversionsToFlow), the runtime load seam ADR-0087 D2 calls for. Stored
old-shape flows (`filters`, `webhook`/`http_request` callouts) are canonicalized
on rehydration; the executor only ever sees the canonical shape. This is the
gap config-aliases.ts already described (graph-lint can't reach never-republished
prod flows) — now closed for `filters` by the conversion, not a fallback.
Risk 2 (silent third-party clobber) — `flow.node.type` is an OPEN namespace
(ADR-0018 removed the enum gate), so a retired official name could be
re-registered by a third party. The node-type rename is now conflict-aware:
the runtime seam passes its live executor registry as reservedNodeTypes; if a
retired alias (`http_request`/`http_call`/`webhook`) is a live custom node, the
rewrite is REFUSED and a loud, structured OS_METADATA_CONVERSION_CONFLICT
diagnostic (node path, conversion id, rename hint) is emitted instead of a
silent clobber (ADR-0078). The pure build/validate seam has no registry, so the
historical alias converts as before.
Mechanism/policy split kept clean: spec provides the conflict-aware conversion
(reservedNodeTypes context on apply); service-automation supplies the data. The
crud-config-aliases test is rewritten to prove the stored-`filters` flow now
keeps its filter (the risk-1 fix); a new flow-load-conversion test covers the
rehydration rename and the conflict guard. New exports are additive.
Refs #2643, #2645 (ADR-0087 D2), ADR-0078.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YA9vjVpDrKLuaK6nbm1s37
@os-zhuang
os-zhuang marked this pull request as ready for review July 14, 2026 08:51
@os-zhuang
os-zhuang merged commit 16b4bf6 into mainJul 14, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0087-metadata-protocol-yjefro branch July 14, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude