Repository navigation
fix(spec,service-automation,metadata-protocol): the v18 stored preview lists every row protocol 18 refuses and no conversion rewrites - #23315
Conversation
…e through the TODO sink Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…h the stored preview Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…e listing; changeset Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…8-stored-row-fates
… row keeps loading — a refused flow does not Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…ion TODO is check:dispatcher-error-vocabulary classifies OS_METADATA_CONVERSION_TODO per (code, file); the detector list now hands back (refused, detail) and applyConversions builds the notice. Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…8-stored-row-fates
📓 Docs Drift CheckThis PR changes 3 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5d97dedb89e2a46b2fca531d159f6c6e6d1948ba && git checkout 5d97dedb89e2a46b2fca531d159f6c6e6d1948ba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc 086d6f1adca2add2f9d36ca75ec33b85c6c72786 && git checkout -B drift-repro 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc && git merge --no-ff 086d6f1adca2add2f9d36ca75ec33b85c6c72786
node scripts/docs-audit/affected-docs.mjs --json 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc
|
Contract reviewServed-tier: Read: cards #23269, #23270, #23271 with every comment (triage Check-runs on the head at 18:45Z: 40 runs, 29 success, 5 skipped, 6 ① Derived judgmentsEach accept-set and public-surface change the diff implies, named right or wrong.
② Semver levelThe changeset
Patch round, three edits, no behaviour change: (a) declare ③ Boundary flagsEvery deviation and out-of-scope entry in report
Implemented-by: VERDICT: FAIL |
…8-stored-row-fates
…dos the stored pass reads; the changeset is minor, Clause-② yes (widening) The contract-tier review on the family PR found the consumer reading a member the published contract never promised. Declared as the optional sibling of notices and conflicts; StoredFlowCanonicalization restates it. Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Second-round record on the patch round answering FAIL record Check-runs on the head at 20:10Z: 48 runs, 41 success, 7 skipped, 0 failed, 0 in progress. Test Core 1 to 6 (the six shards open at the first record), ① Derived judgmentsScope of this round, verified on the diff:
② Semver levelThe changeset
③ Boundary flagsEvery deviation in round report
Implemented-by: VERDICT: PASS |
Fixes #23269
Fixes #23270
Fixes #23271
Clause-②: yes (widening)
What this does
os migrate meta --storednow lists every stored row that protocol 18 refuses and that no conversion rewrites. Before, the preview counted each such row ascanonical("already on protocol"), while the parse or the boot refused it after the upgrade. All three cards get the same fate, refused and listed, through the stored preview's existing TODO lines. Each line names the step-18 D3 entry that refuses the shape, the site, and the choice the owner has to make. Nothing is rewritten, on preview or on--apply, andstoredMigrationCleandoes not move.{…}token, including aget_record/update_record/delete_recordfilter operandflow-value-slot-template-dialect-refusedcontrolled_by_parentwith two masters (both required, or neither), no master, a requiredlookuponly, an explicitrequired: falsecbp-master-relation-tiers-retireddatabinds the retired view-level API source, in every stored view body spelling and in an object'slistViewsview-data-api-provider-retiredThe one lossless shape in this family, a single
master_detailwithrequiredomitted, is still converted bycbp-master-detail-required-defaultedand is not listed.Public surface: Clause ②, yes (widening)
No door accepts or refuses anything it did not before. The published flow-canonicalization result gains one output member, declared in the contract and read through it:
IAutomationService.canonicalizeStoredFlow(...).todos?(@objectstack/spec,contracts/automation-service.ts): optional, the sibling ofnoticesandconflicts, so an implementation that reports none still conforms.AutomationEngine.canonicalizeStoredFlow(...).todos(@objectstack/service-automation): the engine always returns it.StoredFlowCanonicalization.todos?(@objectstack/metadata-protocol): restates the contract's optional member, soflowResult.todos ?? []reads a member the contract declares.Also widened: the published
ConversionTodoNotice.conversionIdmay now name a D3 entry as well as a conversion, and anonTodosink now also receives the refused-shape TODOs. All three packages shipminor(.changeset/23269-v18-stored-row-fates.md).The #23269 choice, measured: (b) refuse and list
The lane's ruling allowed (a) only if the chain already declares an operator-applied rewrite of stored rows that is separate from the load-path D2. It does not:
migrateStoredMetadata(the--storedpass) writes back the read path's own pass:applyConversionsToStoredItemfor most types, andAutomationEngine.canonicalizeStoredFlowfor a flow, which is theregisterFlowseam itself.--from 17(applyMetaMigrations) replays step conversions over authored source files, never over rows.flow-decision-mode-inclusive-explicit) shows the same boundary. It replays over authored sources only and is refused at rehydration byCONVERSIONS_NOT_REPLAYED_AT_REHYDRATION. For stored rows it ships a report-only list, not a rewrite.So (b) applies, and the changeset names it.
Zone 2 assumptions, each measured
--storednever reads D3 entries. What it does read is the conversion TODO sink (onTodo,CONVERSION_TODO_CODE). For a flow row,migrateStoredMetadatahardcodedtodos: []and ran onlycanonicalizeStoredFlow.FlowSchema.safeParseaccepts aget_recordfilter of{ id: '{record.id}' }, which I measured atsuccess: trueon this tree. The refusal isflowNodeValueTemplateRefusals, which onlyregisterFlow'svalidateFlowExpressionsruns, after canonicalization. Objects and views had the same gap: the preview never parses a row it does not rewrite, so a refused row read ascanonical.spec-changes.jsonprojects every D2 entry as "A lossless conversion applied at load". The detectors therefore live in their own in-package list (packages/spec/src/conversions/refused-shapes.ts). They are keyed by their D3 entry ids, they run at the end ofapplyConversionsonly when a caller passesonTodo, and they never rewrite.lookupturned into amaster_detailchanges delete behaviour. An explicitrequired: falseis a contradiction the builder refuses: writingtruewould change what the field accepts and override what the author wrote. No master leaves nothing to convert.dataSource, and deletingdatamakes the view query its object instead. The population is not provably empty: the save door accepted the arm before feat(spec)!: retire the view-levelapidata source — a view binds its object or static rows; an API call is the page element's binding (#23082) #23160, so I list instead of treating the population as empty.packages/spec/src/data/controlled-by-parent-master.ts. It is not re-exported, so spec: export the controlled_by_parent master predicate, so a producer does not restate ADR-0055 §1's exactly-one-master rule #23272 stays its own card. The refinement's messages are byte-identical. The view detector asksViewDataSchema.safeParseitself, and the flow detector callsflowNodeValueTemplateRefusals, so each listing is exactly what its door refuses.registry.tsis gitignored and regenerated at install, build and test. I mergedmainthree times, never with a conflict.Where it lands, and why outside the claim's file surface
The claim's surface is
packages/spec. For flows, measurement put the producer elsewhere, and the dispatch's file-surface clause says to fix at the producer and name it. Changingpackages/cli/src/commands/migrate/meta*(the dispatch's STOP path) was not needed: the CLI rendersformatStoredMigrationReport, which lives in metadata-protocol.packages/services/service-automation/src/engine.ts:canonicalizeStoredFlowpassesonTodoand returnstodosbesidenoticesandconflicts.registerFlowignores them, because its own refusal names the same sites.packages/metadata-protocol/src/stored-migration.ts:StoredFlowCanonicalizationgains an optionaltodos, so a caller-supplied canonicalizer that reports none still fits.packages/metadata-protocol/src/protocol.ts: the flow branch readsflowResult.todos ?? []. The skip reason of a row that only has TODOs no longer says "The row keeps loading unchanged", which this change would have made false for a refused flow. It now says the row is read as stored and that each TODO says what that leaves it doing.Inside
packages/spec:conversions/refused-shapes.ts(new),conversions/apply.ts(the call), and doc-only edits toconversions/types.ts(a TODO'sconversionIdmay name a D3 entry) andconversions/stored.ts;contracts/automation-service.ts:canonicalizeStoredFlow's return declarestodos?(the public-surface section above);data/controlled-by-parent-master.ts(new) anddata/object.zod.ts(the refinement delegates to it);--storeddoes with these rows.Docs: one row in
content/docs/deployment/cli.mdx's--storeddecline table. Changeset:.changeset/23269-v18-stored-row-fates.md(minorfor spec, service-automation and metadata-protocol).Both new spec modules (
refused-shapes.ts,controlled-by-parent-master.ts) are internal: neither is re-exported, so no export name changes. The one contract member they need is declared above.Reach decision: all value slots, not only CRUD filters
The flow detector lists every refusal of
flowNodeValueTemplateRefusals, not only the CRUD filter operands. That is the D3 entryflow-value-slot-template-dialect-refusedwhole, andregisterFlowrefuses every one of those sites the same way. Limiting it to filters would have taken extra code that filters the one judge, and the preview would have stayed silent about flows the boot refuses for the same retirement. The four conditions for a bounded in-place fix hold: same defect class, mechanical, no other claim on these files, same gate family.Pins
get_record,update_recordanddelete_record(field value,$gtcomparand,$inelement), and one inside a loop body: one TODO each, naming the flow, the node and the operand, with its CEL envelope;registerFlowrefuses the same site;--applywrites nothing, and the CEL form iscanonicaland registers.required: false, required lookup only, no master;canonical, writes nothing on--apply, and leavesstoredMigrationCleantrue.listViews: listed;from,reasonormessage;object,valueandschemaproviders: untouched.Verification
Patch round, at
HEAD086d6f1ad(merge base withorigin/main:404fc477e)The contract-tier review (record 6152721854) failed this PR on the semver and Clause-② section only. This round makes its three edits, with no change to any door, detector or predicate:
todos?;minorfor all three packages, withClause-②: yes (widening)naming the three members;Clause-②line matches.mainwas merged first:331fa99bb, no conflict.turbo run buildover the closures of@objectstack/service-automation^...and@objectstack/metadata-protocol^..., plus the@objectstack/client...and@objectstack/client-react...closures: 35 of 35 tasks, exit 0.pnpm --filter @objectstack/spec check:generated: exit 0, "All 14 generated artifacts are up to date",check:api-surfaceandcheck:export-originsamong them. Nothing was regenerated, because the api-surface snapshot records export names, not interface members.AutomationEngine.canonicalizeStoredFlow'stodoswidened toConversionTodoNotice[] | numberthroughscripts/ablation-replace.mjs.tsc --noEmitin service-automation went red as predicted:src/engine.ts(4364,5): error TS2416: Property 'canonicalizeStoredFlow' in type 'AutomationEngine' is not assignable to the same property in base type 'IAutomationService'. A contract withouttodoswould accept that member, so the red provestscread the rebuiltdistdeclarations. Restore proven: blob equal toHEAD, emptygit diff HEAD.typecheckfor@objectstack/spec,@objectstack/service-automationand@objectstack/metadata-protocol: exit 0 each.refused-shapes,storedandcbp-master-detail-required-defaulted, 61 tests;canonicalize-stored-flowandengine, 175 tests;protocol.stored-migrationandprotocol.flow-canonicalizer, 60 tests.pnpm --filter @objectstack/spec test:repo: measured in full, exit 0, 58 files, 1007 tests.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 115 commands at086d6f1ad, with no stale-tree warning. All exited 0, the Clause-② gates (check-changeset-no-major,check-adr-0087-registration) andcheck:api-surfaceincluded.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (exit 3), because 33 packages had nodistin the recreated worktree. Re-run after the battery, whose later gates built them, it gave exit 0: "108 published require entry point(s) across 67 package(s) load".origin/mainhas since gained87e902dcb,4d4040ccband37510aa0f, none under the three changed packages. Not merged.First round, at
HEADaf10b608d(merge base withorigin/main:9c7cfbf0a) unless a line names another committurbo run buildover the closures of@objectstack/service-automation^...and@objectstack/metadata-protocol^..., plus the@objectstack/client...and@objectstack/client-react...closures: 35 of 35 tasks, exit 0.pnpm --filter @objectstack/spec test: exit 0, 670 files, 20307 tests (1 todo).pnpm --filter @objectstack/metadata-protocol test: exit 0, 235 passed and 3 skipped files, 28336 tests.pnpm --filter @objectstack/service-automation test: exit 0, 185 files, 2451 tests, ate43c67948. The merge after it brought no change under that package; the pins re-ran onaf10b608d:canonicalize-stored-flow.test.ts, 11 of 11.typecheckfor@objectstack/spec,@objectstack/metadata-protocoland@objectstack/service-automation: exit 0 each.pnpm --filter @objectstack/spec test:repo: NOT MEASURED as a whole. I stopped my own run at 22 minutes of lock hold while another agent was queued (exit 143). Declared narrowing, ate43c67948: the 34src/tree-scan pins plusscripts/conversions-major18-merge.test.tsran, exit 0, 35 files, 482 tests. The other 23scripts/tests exercisepackages/spec/scripts/*, which this diff does not touch. CI runs the whole project.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 115 commands ataf10b608d, and every one exited 0. Reconcile: "✓ dispatch-gates --ran: 115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED (a DERIVED zero — all 115 recorded an exit code and none of them is 3)."check:dispatcher-error-vocabularysawOS_METADATA_CONVERSION_TODOstamped inrefused-shapes.ts, an unclassified site. That code is now stamped only inapply.ts, the classified one.origin/mainmoved four commits:280806912, the CLI organization-ownership ceremony;4c54486dd, a fix to the derivation script;5c9bfb0d3, plugin-security;404fc477e, plugin-auth. None touchespackages/spec,packages/metadata-protocolorpackages/services/service-automation, so I did not merge them then; the patch round merged them (331fa99bb).pnpm lint.eslint --no-inline-config --format jsonover the 16 changed source and docs files: 16 entries, 0 errors, 0 rule warnings.content/docs/deployment/cli.mdxis outside the population: eslint itself answers "File ignored because no matching configuration was supplied".eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules; the note aboveQUERY_OPTIONS_TEST_GLOBS). So this diff cannot move an untouched file's verdict through type information.dist/index.jsanddist/browser/index.js, both load and list a refused object and a refused view throughapplyConversionsToStoredItem.scripts/ablation-replace.mjs, run before the stamping move. Each restore was proven by blob equal toHEADand an emptygit diff HEAD:reportRefusedStoredShapescall removed fromapplyConversions:refused-shapes.test.ts18 failed and 13 passed, predicted red. The 13 are the not-listed and registry pins.flowResult.todos ?? []replaced by[]inprotocol.ts: only the v18 upgrade: stored flows whose get/update/delete_record filter operand is a {…} token are refused at boot with no conversion — every deployment's automations built on the old spelling stop running #23269 preview pin went red (1 of 46).todos.pushremoved: only the braced-operand engine pin went red (1 of 11).Acceptance notes
0f84c9chas 19 flow files (35 lines) whosefilter:carries a dotted-path braced token. Cloud's AI build wrote it while the skill taughtfilter: { id: '{record.id}' }, per v18 upgrade: stored flows whose get/update/delete_record filter operand is a {…} token are refused at boot with no conversion — every deployment's automations built on the old spelling stop running #23269.data(object-grid, and the other component whosedataisViewDataSchema) binds the retired API source is refused by the same schema but is not listed. feat(spec)!: retire the view-levelapidata source — a view binds its object or static rows; an API call is the page element's binding (#23082) #23160's D3 entry scopes the retirement to views and object list views, and this listing follows that scope. Carrier: none.registerFlowrefusals (text-slot{{ }}holes, unknown config keys, decision modes) are still not previewed. The general shape would be the stored pass runningregisterFlow's whole validation. Carrier: none.Generated by Claude Code