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-zhuang os-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):

id surface transform
flow-node-http-callout-rename flow.node.type http_request/http_call/webhookhttp
page-kind-jsx-to-html page.kind 'jsx''html' (ADR-0080)
flow-node-crud-filter-alias flow.node.config.filter CRUD 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

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 14, 2026 8:30am

Request Review

@github-actions github-actions Bot 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-actions Bot 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-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl and removed size/l labels Jul 14, 2026
@os-zhuang os-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 manifest Jul 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 main Jul 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 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

Development

Successfully merging this pull request may close these issues.

2 participants