Repository navigation
test(runtime): pin which sys_metadata row the /automation flow doors write; no caller holds a package to name - #23499
Conversation
…doors write The flow doors save naming no package because no caller holds one: the protocol resolves the one package's row, or refuses a name two packages hold with 409 RESOURCE_CONFLICT. Pinned over a real store at all three call sites in both seed orders, and the reason recorded on registerAndSaveFlow so the engine-held or body _packageId stamp is never threaded into the save. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
Drops the options cast the query-options-erasure ratchet counts: the engine's find accepts the where/context pair as typed. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
席位核对✅ ACCEPT: PR #23499 at
Checklist, read against GitHub and
Accepted deviations:
Out-of-scope findings, one line each:
Landing: Generated by Claude Code |
Fixes #23292
Clause-②: no
What this is
The card asks that
registerAndSaveFlow(packages/runtime/src/domains/automation.ts) name the package of the flow it registers whenever its caller has one, and that a pin hold the two-package case either way. Theplugin-securityhalf of the card is gone frommain(permission-set-projection.tsdeleted bydac99c2277,position-write-through.tswithstoredRowPackageArgby5c9bfb0d3f), so this PR is the runtime half only.Measured on
main: none of the door's three callers holds a package, so the save keeps naming none. This PR ships the pins the card asks for, over a real store, and records the measured reason on the function so the next reader does not thread a guessed package into the save. No runtime behaviour changes. This is the dispatch's mechanism (b), reported here as a deviation from its suggested route (thread an optionalpackageId): there is no source to thread from.The measurement
Real store (better-sqlite3, the real
ObjectQLengine, the realObjectStackProtocolImplementation), every request throughHttpDispatcher.dispatch(). Packagescom.qa.alphaandcom.qa.betaare installed writable bases.What package a row this door writes can carry. The protocol resolves a save that names no package (ADR-0048): the one package's row when the flow's active rows are bound to exactly one, else the package-unbound row.
/meta, thenPUT /automation/solo_flow:200, rows[com.qa.alpha], label updated, no package-unbound row minted.POST /automationof a new flow:200, rows[null](the package-unbound row).How a name comes to be held by two packages.
PUT /meta/flow/shared_flow?package=com.qa.alpha, then?package=com.qa.beta: both200, one active row per package. The automation door itself cannot mint a second package's row, because it never names a package.What each call site holds.
POST /andPUT /:nametake a definition and a path. The only package-shaped value either reaches is a_packageIdstamp, from one of two places:getMetaItemsForExecution, which the boot bind arms from) lists both rows as contenders.resolveFlowPrecedenceties two runtime contenders and keeps arrival order. Measured: seeded alpha then beta, the view lists beta first and the engine holds_packageId: com.qa.beta; seeded beta then alpha, it lists alpha first. So the held stamp is a driver-order pick. That is the guess the 409 refuses to make, not a package the caller holds.POST /:name/clonewrites a new name.cloneFlowDefinitiondrops the protection envelope,_packageIdincluded (FLOW_CLONE_DROPPED_KEYS, ADR-0126 §7.1). The source's package is not the target's.HttpProtocolContextcarries no package.What changes
packages/runtime/src/domains/automation-flow-door-package-address.test.ts(new, 12 pins):PUT /:namesaves to that package's row only, with no package-unbound row minted.POST /saves the package-unbound row./metadoor, and the engine holds one contender's stamp.PUT /:name,POST /onto the held name,POST /while the engine holds none, andPOST /:name/cloneonto the name each answer409 RESOURCE_CONFLICT.code+statusand the rows read back from the store. No refusal prose is pinned.packages/runtime/src/domains/automation.ts: one bullet inregisterAndSaveFlow's doc comment. It states why no package is named and what a two-package name answers, and that neither stamp is to be passed aspackageId. Comment only.Evidence
On the merged head
6c0c016164(main873151021fmerged in):pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --reporter=verbose src/domains/automation-flow-door-package-address.test.ts:Tests 12 passed (12).pnpm --filter @objectstack/runtime typecheck: exit 0,check:test-typecheck: OK, 190 errors and 68 signatures held.tsc -p tsconfig.test.json --listFileslists the new file, and it has 0 errors.localproject,pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2, on the merged head:Test Files 379 passed (379),Tests 5553 passed | 19 skipped (5572). Before the merge, at1b04501693, it was 377 files and 5499 tests passed.node scripts/ablation-replace.mjsin wrap mode). The subject resolves throughsrc, by the relative import of../http-dispatcher.js, so nodistleg is needed._packageIdtosaveMetaItem. 4 failed, 8 passed. The four arePUT /:nameandPOST /onto the held name in both seed orders, each answering200where409was pinned. Restored: the blob equals HEAD (0684fb34d327) andgit diff HEADis empty.restoreFlowRegistration(...)from the refused-save branch. 8 failed, 4 passed: every two-package refusal pin in both orders. Restored the same way. The first attempt at this mutation used a replacement that was a substring of its anchor, and the tool refused it before any test ran. It was re-run with--delete.pnpm exec eslint --no-inline-configover the 2 changed files: exit 0, 2 files linted, 0 errors, 0 warnings. These are the flagspnpm lintuses; the repo-wide scan is CI's.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with the--ranreconciliation in the seat report. On the first run,check:query-options-erasurewent red on one test-surface options cast (236 to 237). It was fixed incdbed1bf31by removing the cast and re-ran green.check:dual-build-cjs-loadsanswered PREREQUISITE NOT MET (no whole-repo build), so it is NOT MEASURED locally, and its reading is CI's.Changeset: none,
skip-changesetNothing publishes. The
srcchange is a comment, and the new file is a test. To measure it,@objectstack/runtimewas built and everydistfile in itsfiles[]was searched:index.js,index.cjs, both maps and both.d.ts.automation.tsstring literalFlow definition body requiredappears 2 times inindex.jsand 2 inindex.cjs.disttoo, so esbuild drops it.The dispatch named a
patchchangeset, premised on a fix. With no behaviour change, the repo's rule is theskip-changesetlabel.Acceptance notes
DELETE /automation/shared_flowon a two-package name answers409 RESOURCE_CONFLICT, both rows remain, and the engine re-registers the definition it held.unregisterAndDeleteFlowhas the same shape as the save half: loud, and restored. Nothing in this PR touches it.PUT /:name, soPUT /automation/shared_flow?package=com.qa.alphaanswers the identical 409. A caller following the remedy at this door loops; the save that works isPUT /meta/flow/shared_flow?package=.... This is reported to the seat as a finding.Generated by Claude Code