feat(spec,cli): ADR-0087 P1+P2 — conversion layer, migration chain & change manifest#2897
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 104 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 samenormalizeStackInputseamdefineStack/validate/lint/info/doctorshare. Old-shape authoring keeps loading with zero action; each rewrite emits a structuredOS_METADATA_CONVERTEDnotice, surfaced byobjectstack validate.Seed table (retroactive protocol-11 renames):
flow-node-http-callout-renameflow.node.typehttp_request/http_call/webhook→httppage-kind-jsx-to-htmlpage.kind'jsx'→'html'(ADR-0080)flow-node-crud-filter-aliasflow.node.config.filterconfig.filters→config.filterPD #12 retirement demonstrated: the
filters→filteralias is removed from the service-automation executor'sreadAliasedConfigfallback and promoted into the declared/loud/tested/expiring conversion above; executors now read canonicalcfg.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?)foldsfromMajor+1..currentand 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_FLOORis an explicit release-policy knob. Seeded protocol-11 step: three graduated conversions (mechanical) + the two non-lossless live windows (titleFormatcomposite →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.composeSpecChangesfolds the conversion table (D2) and migration set (D3) across majors and joins the release-time api-surface diff; per-major manifests compose into onefrom→toview (the generated guide and P3's MCPspec_changesare 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.normalizeStackInputgains an optionalconvert: false(map→array only) somigrate metareplays the conversions itself against the raw authored source, attributing each rewrite to a chain hop.Verification
@objectstack/spec— 6748 passed (32 new: 15 conversion + 17 migration/spec-changes),tsc --noEmitclean,check:api-surfacegreen (additions purely additive, 0 breaking).@objectstack/service-automation— 255 passed (CRUD-alias test rewritten for the executor-shim vs. load-time-conversion split, incl. an end-to-endfilters→filterconversion-then-run case).@objectstack/downstream-contract— 14 passed.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