Skip to content

test(runtime): pin which sys_metadata row the /automation flow doors write; no caller holds a package to name - #23499

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23292-flow-door-package
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23292-flow-door-package

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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. The plugin-security half of the card is gone from main (permission-set-projection.ts deleted by dac99c2277, position-write-through.ts with storedRowPackageArg by 5c9bfb0d3f), 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 optional packageId): there is no source to thread from.

The measurement

Real store (better-sqlite3, the real ObjectQL engine, the real ObjectStackProtocolImplementation), every request through HttpDispatcher.dispatch(). Packages com.qa.alpha and com.qa.beta are 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.

  • A flow saved into alpha through /meta, then PUT /automation/solo_flow: 200, rows [com.qa.alpha], label updated, no package-unbound row minted.
  • POST /automation of 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: both 200, 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 / and PUT /:name take a definition and a path. The only package-shaped value either reaches is a _packageId stamp, from one of two places:
    • On the caller's body. These are the caller's bytes. Reading them as the write address would make the body a package selector this door never declared, which is a widening the dispatch rules out.
    • On the definition the engine holds. For a two-package name, the protocol's execution view (getMetaItemsForExecution, which the boot bind arms from) lists both rows as contenders. resolveFlowPrecedence ties 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/clone writes a new name. cloneFlowDefinition drops the protection envelope, _packageId included (FLOW_CLONE_DROPPED_KEYS, ADR-0126 §7.1). The source's package is not the target's.
  • HttpProtocolContext carries no package.

What changes

  • packages/runtime/src/domains/automation-flow-door-package-address.test.ts (new, 12 pins):
    • A flow one package holds: PUT /:name saves to that package's row only, with no package-unbound row minted.
    • A flow no package holds: POST / saves the package-unbound row.
    • A flow two packages hold, in both seed orders, so whichever contender the engine holds:
      • The state is reachable through the /meta door, and the engine holds one contender's stamp.
      • PUT /:name, POST / onto the held name, POST / while the engine holds none, and POST /:name/clone onto the name each answer 409 RESOURCE_CONFLICT.
      • Both rows are deep-equal before and after, and no third row is minted.
      • The engine is put back: the definition it held is re-registered (the same object), or the new name is withdrawn.
    • Refusals assert code + status and the rows read back from the store. No refusal prose is pinned.
  • packages/runtime/src/domains/automation.ts: one bullet in registerAndSaveFlow'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 as packageId. Comment only.

Evidence

On the merged head 6c0c016164 (main 873151021f merged 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 --listFiles lists the new file, and it has 0 errors.
  • Full runtime local project, 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, at 1b04501693, it was 377 files and 5499 tests passed.
  • Mutation proofs (one-shot, through node scripts/ablation-replace.mjs in wrap mode). The subject resolves through src, by the relative import of ../http-dispatcher.js, so no dist leg is needed.
    • Mutation 1, the forbidden route: pass the engine-held _packageId to saveMetaItem. 4 failed, 8 passed. The four are PUT /:name and POST / onto the held name in both seed orders, each answering 200 where 409 was pinned. Restored: the blob equals HEAD (0684fb34d327) and git diff HEAD is empty.
    • Mutation 2: delete 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.
  • Lint: pnpm exec eslint --no-inline-config over the 2 changed files: exit 0, 2 files linted, 0 errors, 0 warnings. These are the flags pnpm lint uses; the repo-wide scan is CI's.
  • Derived gate families: 53 commands from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with the --ran reconciliation in the seat report. On the first run, check:query-options-erasure went red on one test-surface options cast (236 to 237). It was fixed in cdbed1bf31 by removing the cast and re-ran green. check:dual-build-cjs-loads answered PREREQUISITE NOT MET (no whole-repo build), so it is NOT MEASURED locally, and its reading is CI's.

Changeset: none, skip-changeset

Nothing publishes. The src change is a comment, and the new file is a test. To measure it, @objectstack/runtime was built and every dist file in its files[] was searched:

  • A phrase from the new bullet appears 0 times in index.js, index.cjs, both maps and both .d.ts.
  • Positive control: the automation.ts string literal Flow definition body required appears 2 times in index.js and 2 in index.cjs.
  • The function's pre-existing doc comment is absent from dist too, so esbuild drops it.

The dispatch named a patch changeset, premised on a fix. With no behaviour change, the repo's rule is the skip-changeset label.

Acceptance notes

  • The removal half, read and not changed. DELETE /automation/shared_flow on a two-package name answers 409 RESOURCE_CONFLICT, both rows remain, and the engine re-registers the definition it held. unregisterAndDeleteFlow has the same shape as the save half: loud, and restored. Nothing in this PR touches it.
  • The relayed 409's remedy, at this door, read and not changed. The refusal text is the protocol's: "on the REST door add ?package= with the package id". The automation door reads no query on PUT /:name, so PUT /automation/shared_flow?package=com.qa.alpha answers the identical 409. A caller following the remedy at this door loops; the save that works is PUT /meta/flow/shared_flow?package=.... This is reported to the seat as a finding.

Generated by Claude Code

…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>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/runtime/src/domains/automation.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/runtime/src/domains/automation.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 394f65e891f0ac6c6be708fb807740d5cb446520 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

✅ ACCEPT: PR #23499 at 6c0c016164 (#23292). It lands on an all-green head; no contract review is owed · 2026-10-14T14:14Z

domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6168069112. Report 6170366180. Thread-read: 6170366180.

Checklist, read against GitHub and origin/main, not against the report:

  • PR shape: draft, base main, title test(runtime):, assignee sunhaolin, labels skip-changeset, tests, size/m. Line 1 is Fixes #23292, the body's only closing keyword; line 2 is Clause-②: no.
  • Scope: 2 files, +259 / −0, inside the claim's surface:
    • automation.ts gains one doc-comment bullet on registerAndSaveFlow and no code;
    • one new pin file, automation-flow-door-package-address.test.ts.
    • check-governed-merges --pr 23499: NOT governed. No content/docs/releases/ change.
  • The card's outcome, read against the claim's rule. The claim allowed one honest reduction: if no caller holds a package, the card reduces to the loud-refusal pin and the report says so. The dev measured exactly that over a real store through dispatch():
    • POST / and PUT /:name reach only a _packageId stamp. Either it is the caller's body bytes, and honouring it would be a new door input, which the claim forbids. Or it is the engine-held definition's, which for a two-package name is the contender the boot bind armed by arrival order: a guess.
    • The clone writes a new name and drops the envelope.
    • The request context carries no package.
    • I spot-read the source: flow-precedence.ts:119 reads _packageId off the item; engine.ts:5013 calls _packageId "the caller's bytes".
    • With no package to name, registerAndSaveFlow keeps saving with none. The card's three pins hold as written:
      • a one-package flow saves to that package's row only;
      • a two-package name is refused loudly: 409 RESOURCE_CONFLICT at PUT, POST overwrite, POST create and clone, with neither row changed, no third row, and the engine put back;
      • a flow no package holds saves the package-unbound row.
    • The plugin-security half was already gone (dac99c2277, 5c9bfb0d3f), as the claim records. So Fixes #23292 closes what remains.
  • Pins and reverse verification:
    • 12 tests, with both seed orders, at all three call sites. All 12 green at the final head.
    • Two one-shot mutations, each restored with blob == HEAD:
      • threading the engine-held _packageId into saveMetaItem turns 4 pins red (PUT and POST overwrite answer 200 where 409 is pinned);
      • deleting restoreFlowRegistration from the refused-save branch turns 8 pins red.
  • The doc-comment bullet, checked sentence by sentence:
    • It says "no package, because no caller here holds one" and gives the resolution order: the one package's row, else the package-unbound row, else 409. Both are what the pins measure.
    • Its ⛔ line forbids passing either stamp as packageId; that is the claim's own rule.
    • The bullet above it ("no organization … and no package") stays true.
  • Changeset: none, behind skip-changeset. The diff moves no published byte: a comment in src and a test. The dev measured a phrase of the new bullet 0 times in every dist file in files[], against a positive control. Writing the label was a deviation from the dispatch, and it is right; see below.
  • Gates:
    • Local, at 6c0c016164: the dev ran the 53 derived gates, 53 run and 0 unmeasured. check:type-check-debt exited 3 on an OOM-killed closure build on the shared box, then 0 when re-run alone. check:query-options-erasure went red pre-merge on a cast, and cdbed1bf31 removed the cast. The full runtime suite (379 files, 5,553 tests) and the typecheck exited 0. pnpm lint was taken as the proven narrowing.
    • Gate evidence: the check-runs pinned to 6c0c016164, read at this stamp. There are 43 runs: 31 success, 10 skipped, and 2 in progress (Test Core (1/6), (3/6)).
      • Every skip is on the expected-skip roster: Check PR Size, Auto Label, Check Changeset (behind skip-changeset), Packed-tarball smoke, Temporal Conformance, Build Docs, Dogfood Verify CLI, Console Pin Gate, and Dogfood Regression Gate (${{ matrix.shard }}/3).
      • Landing reads them again.
  • Contract review: none owed. The diff touches none of the three faces. It has no Clause-②: yes declaration, no published schema and no governed path, and the card's grade is Clause-②: no with no narrowing: runtime behaviour does not move. CI plus this seat's read (contract-review.md).
  • Docs drift: none owed; no behaviour moves.

Accepted deviations:

  • No packageId threaded, and no "drop the forward" ablation. The measurement reduced the card to hypothesis (b), as the dispatch foresaw. Two mutation proofs stand in for the ablation.
  • skip-changeset instead of the dispatch's @objectstack/runtime patch changeset. The dispatch premised a fix. The role file asks whether the publish surface moved, and it did not. The one label write is accepted.
  • pnpm lint as the proven narrowing, with three pieces of evidence. The PR's Lint & Repo Gates is success.

Out-of-scope findings, one line each:

Landing: pr_ready + automerge_enable through the relay once every check on the current head is success or a roster skip.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 14:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 14:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 111b582 Oct 14, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23292-flow-door-package branch October 14, 2026 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants