Skip to content

fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) - #20817

Merged
objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-20679-automation-door-package-lock
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-20679-automation-door-package-lock

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20679
Clause-②: yes (widening)

The /automation definition doors now refuse a packaged flow, with the same locked-base verdict the metadata door gives. This is the public checklist item access-security.packaged-flow-write-door-parity: clauses 2 and 3 flip to PASS, and clauses 1 and 4 still pass, measured on a booted showcase. The reproduction stays withheld as filed. This body stays at the door level the public item already describes.

What changed

packages/runtime/src/domains/automation.ts

  • PUT /:name and DELETE /:name call refusePackagedFlowBaseChange before the engine is called, and relay its refusal.
    • Order at each door: manage_metadata authoring gate, then the body envelope check (PUT and POST only), then the lock, then the engine.
    • A refused write reads and registers nothing. A refused removal unregisters nothing.
  • Bounded in-place fix, adopted onto the claim's surface by the seat: POST / onto a name the engine already holds overwrites that flow. It now takes the same lock, right after the name check.
    • Evidence at 3690c44b, before the fix, from a throwaway probe (real protocol over a real SchemaRegistry, deleted afterwards): a create onto a packaged flow's name answered 200, registerFlow was entered once, and the packaged flow's label was overwritten.
    • Pinned now: 403 NOT_OVERRIDABLE, registerFlow never entered, and a create under a new name still succeeds.
  • The helper resolves the protocol slot with resolveServiceOrLoud, unscoped, exactly as the /meta domain resolves it. So both doors ask the same protocol instance.
    • If the slot is wired but fails to resolve, the error is re-raised: the write fails and does not proceed as unlocked.
    • A composition with no metadata protocol keeps today's behaviour. It has no /meta door to be at parity with.

packages/metadata-protocol/src/protocol.ts: the widening, a cross-lane surface the seat adopted.

  • New public method ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name, operation }). It returns the refusal the /meta door gives for writing ('save') or removing ('delete') an existing item that a code package ships, or null when that door would not refuse on this ground.
    • The /meta verdict (isArtifactBacked + isOverlayAllowed, and its emitters) was private to this class, so a second door could not ask it any other way.
  • The verdict is lifted, not copied. Two private helpers, refusePackagedBaseOverride and refusePackagedBaseRemoval, carry saveMetaItem's and deleteMetaItem's inline package-door code verbatim (proof below).
    • Both methods call the helpers where the inline code stood.
    • The helpers still throw, as that code did.
    • packagedBaseRefusal is the one place a throw becomes a value. It re-raises anything that is not a 403 NOT_OVERRIDABLE / ITEM_LOCKED.
  • Why runtime relays the refusal instead of stamping its own code. NOT_OVERRIDABLE and ITEM_LOCKED are ledgered under @objectstack/metadata-protocol (ADR-0112). @objectstack/runtime's owner key lists neither.
    • A runtime stamp would need a ledger row or a waiver in packages/spec.
    • It would also be a second emitter for one condition.
    • Relayed through errorFromThrown, the code, status and sentence are the producer's.
    • check:error-code-provenance: 330 stamp sites, 313 listed, 17 waived, OK.

What the lock keys on. The flow's NAME, looked up in the registry's artifact-only lookup (SchemaRegistry.getArtifactItem, packages/objectql/src/registry.ts:3919). That lookup scans the PACKAGE_ID:NAME entries the artifact loader registers.

  • The door hands the verdict { type, name, operation } and nothing else.
  • Neither the request body nor the engine's registered flow is consulted.
  • Pinned both ways: a packaged name is refused whatever provenance stamps the body carries, and a customer flow is not locked by a body that claims a package.

Two refusals on DELETE. The lock (403 NOT_OVERRIDABLE) answers before the engine's ADR-0126 §7.3 refusal (DELETE_RESTRICTED / 409, a packaged subflow that packaged callers still reach).

  • Every packaged flow is locked first, so at this door the §7.3 refusal is reached only where the lock admits the removal: with OS_METADATA_WRITABLE=flow.
  • The engine's guard is unchanged.
  • ⛔ No lock was added to registerFlow / unregisterFlow: the boot pull registers packaged flows through them.

What stays open.

Lift proof: each lifted helper body, before and after, whitespace-insensitive

git show f284ab26:packages/metadata-protocol/src/protocol.ts | sed -n '16014,16093p' | tee old-save.txt | wc -l     # 80: saveMetaItem's inline package door
git show f5ea0060:packages/metadata-protocol/src/protocol.ts | sed -n '14300,14379p' | tee new-save.txt | wc -l     # 80: refusePackagedBaseOverride body
diff -w old-save.txt new-save.txt | wc -l        # 0

git show f284ab26:packages/metadata-protocol/src/protocol.ts | sed -n '21920,21931p' | tee old-delete.txt | wc -l   # 12: deleteMetaItem's inline refusal
git show f5ea0060:packages/metadata-protocol/src/protocol.ts | sed -n '14401,14412p' | tee new-delete.txt | wc -l   # 12: refusePackagedBaseRemoval body
diff -w old-delete.txt new-delete.txt | wc -l    # 0
helper lines before lines after diff -w changed lines
refusePackagedBaseOverride 80 80 0
refusePackagedBaseRemoval 12 12 0

The only added lines compute the locals the inline code read from its enclosing method:

  • overlayAllowed in the override helper;
  • overlayAllowed and artifactBacked in the removal helper.

Each is spelled exactly as in the calling method. deleteMetaItem keeps its own copies for its NOT_CREATABLE check.

Pins

  • packages/runtime/src/domains/automation-packaged-base-lock.test.ts (15 cases). Real ObjectStackProtocolImplementation over a real SchemaRegistry, with the packaged flow registered the way the loader registers it; the automation service is a spy.
    • PUT and DELETE refused on a host-config kernel and on an environment kernel.
    • The door's answer equals saveMetaItem's / deleteMetaItem's thrown refusal: code, status and sentence.
    • The POST create-overwrite.
    • Body stamps decide nothing.
    • The envelope check keeps its place.
    • Controls: customer flow, runtime-row and tenant-bound registry items, clone, toggle, the hatch, no protocol, a failing protocol (not fail-open).
  • packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts (9 cases):
  • packages/metadata-protocol/src/protocol.read-verb-canonical-fold.test.ts: the derived fold population gains packagedBaseRefusal ("all fourteen").
  • packages/qa/dogfood/test/packaged-flow-write-door-parity.dogfood.test.ts (5 cases), the real showcase composition over HTTP:
    • Clause 1 control: PUT /meta/flow/:name answers 403, code NOT_OVERRIDABLE, in the REST door's envelope.
    • Clauses 2 and 3: PUT / DELETE /automation/:name answer 403 NOT_OVERRIDABLE, and the definition reads back byte-identical.
    • Clause 4: no residue. The enabled/bound row is unchanged.
    • A customer flow is created, updated and removed through the same door.

Tests (measured)

At 3690c44b (merge over #20726):

  • @objectstack/runtime local project: 292 files, 4215 passed, 1 skipped.
  • @objectstack/runtime repo project: 727 passed.
  • @objectstack/metadata-protocol: 191 files passed (3 skipped); 2801 passed, 19 skipped.
  • @objectstack/metadata-protocol typecheck: exit 0.
  • @objectstack/objectql, the 20 files pinning NOT_OVERRIDABLE / NOT_CREATABLE / ITEM_LOCKED on saveMetaItem / deleteMetaItem: 362 passed.
  • @objectstack/rest, 3 files: 41 passed.

At f5ea0060 (head):

  • Every automation-*.test.ts in @objectstack/runtime: 25 files, 478 passed.
  • @objectstack/runtime typecheck: exit 0, check:test-typecheck OK.
  • The two protocol pin files: 27 passed.
  • Dogfood pin, after a runtime rebuild: 5 passed.

Reverse verification

Each leg was committed first, then mutated through scripts/ablation-replace.mjs (anchor hit 1, blob changed). Dist legs were proven by ablation-dist-preflight (marker in 2 built files). Every restore was proven (blob == HEAD, git diff HEAD empty, tree clean, marker absent from dist). Predicted and measured agree on every leg.

leg head suite predicted measured
door helper disabled (runtime src) 3690c44b runtime pin 7 red / 7 green 7 red / 7 green
same, runtime rebuilt 3690c44b dogfood pin 3 red / 2 green 3 red / 2 green
packagedBaseRefusal returns null (rebuilt) 3690c44b protocol pin 6 red / 3 green 6 red / 3 green
same 3690c44b runtime pin 6 red / 8 green 6 red / 8 green
re-raise discriminator widened 3690c44b protocol pin 1 red / 8 green 1 red / 8 green
POST call removed f5ea0060 runtime pin 1 red / 14 green 1 red / 14 green

After every restore, all pins were green again.

Gates

  • dispatch-gates --commands at f5ea0060: 67 derived. --ran reconciliation: 67 run, 0 NOT-MEASURED, 0 unrun, every exit 0.
  • Also run: check:error-code-casing and @objectstack/spec check:error-code-provenance, both exit 0.
  • pnpm lint, narrowed and proven:
    1. The population comes from eslint.config.mjs (the **/*.{ts,…} and packages/**/*.{ts,…} blocks). All 6 changed .ts files are in it.
    2. --format json counted 6 files linted, 0 errors, 0 warnings, at f5ea0060.
    3. The config never enables type-aware linting (eslint.config.mjs states it: no parserOptions.project, no typed rules), and its four plugins are local AST rules. So this diff cannot move any untouched file's verdict.

Acceptance notes (observed, not filed)

  • The ADR-0126 §2 refusal wording. §2 says the refusal names the sanctioned path. For a packaged flow, the shared NOT_OVERRIDABLE sentence names "edit the source artifact and redeploy" and the OS_METADATA_WRITABLE hatch. It does not name clone (§7.1). This holds on both doors, because the sentence is one emitter. It is reported to the seat, not changed here.
  • Two envelopes for one refusal. On this composition /meta answers through the REST server's refusal envelope, { error, code } with a flat string error. /automation answers through the dispatcher's, { success, error: { code, message } }. Pre-existing. The dogfood pin reads each where it lives.
  • @objectstack/rest inlines the protocol. It declares @objectstack/metadata-protocol as a devDependency, so its built dist/ carries its own copy of the protocol class. Pre-existing. It is rebuilt with the same source.

Generated by Claude Code

… the packaged-base lock

PUT /automation/:name and DELETE /automation/:name now ask the metadata
protocol's own locked-base verdict (packagedBaseRefusal, lifted out of
saveMetaItem / deleteMetaItem unchanged) before the engine is called, and
relay its refusal verbatim.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…ver on body stamps

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…em / deleteMetaItem verbatim

The two helpers now carry the original inline lines unchanged (they throw,
as that code did); packagedBaseRefusal is the one place a throw becomes a
value, and it re-raises anything that is not the refusal. The changeset
states the widening.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…-base lock too

POST /automation onto a name the engine already holds is an overwrite; it
now asks the same locked-base verdict as PUT /:name before anything is read
or registered. Bounded in-place fix, adopted onto the claim's surface.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime, touching 19 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/deployment/validating-metadata.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via deleteMetaItem (symbol, a method of class ObjectStackProtocolImplementation), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/system-context.mdx (via handleAutomationRequest (symbol, a top-level function))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via automation.toggle (sdk, the route ledger binds it to POST /automation/:name/toggle, selected by route anchor /:name/toggle), /:name/toggle (route, a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: /automation/:name (route, 36 pages)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 31 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 79114850f0f559dcb4fb432f5bcfc12947a03de6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56 — the merge of head e4006d68594d433159b1a0e8addd9585e5929f59 into base 79114850f0f559dcb4fb432f5bcfc12947a03de6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56 && git checkout 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79114850f0f559dcb4fb432f5bcfc12947a03de6 e4006d68594d433159b1a0e8addd9585e5929f59 && git checkout -B drift-repro 79114850f0f559dcb4fb432f5bcfc12947a03de6 && git merge --no-ff e4006d68594d433159b1a0e8addd9585e5929f59

node scripts/docs-audit/affected-docs.mjs --json 79114850f0f559dcb4fb432f5bcfc12947a03de6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 79114850f0f559dcb4fb432f5bcfc12947a03de6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

The refusal's code and status match the metadata door everywhere; its
sentence matches wherever the metadata protocol's own package door answers.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e4006d68594d433159b1a0e8addd9585e5929f59
Local-runs: none

This is the record of record for PR #20817 at e4006d68. It is a delta re-review over the record rendered on f5ea0060, and its ①–③ judgments are carried forward because the code is byte-identical.

Inputs:

Disclosure is kept at the public item's level: doors, roles, codes and statuses.

Seat verification on adoption:

  • diff -w of saveMetaItem's inline package door at the merge-base (80 lines) against the lifted refusePackagedBaseOverride reads 0 changed lines;
  • the same for deleteMetaItem's inline refusal (12 lines) against refusePackagedBaseRemoval: 0 changed lines. The first lines of each pair match as a control;
  • automation.ts and protocol.ts carry identical blobs at f5ea0060 and e4006d68;
  • git diff f5ea0060 e4006d68 is the single changeset line.

① Derived judgments

Delta: the changeset sentence now reads "the same code and status the metadata door gives (403 NOT_OVERRIDABLE), and … the same sentence wherever the metadata protocol's own package door answers". RIGHT on both topologies.

  • On an environment kernel, the /automation doors relay the protocol door's exact refusal object.
  • On a host-config kernel, /meta's refusal comes from SysMetadataRepository.assertAllowed: the code and status agree, the sentence does not, and the wording claims exactly that.

The first record's finding is closed.

Carried forward, with the code unchanged:

  • (a) packagedBaseRefusal({ type, name, operation }), a new public method on ObjectStackProtocolImplementation: RIGHT. It is a faithful exposure of the /meta door's own locked-base verdict.
    • Both inline doors are lifted verbatim into private helpers that saveMetaItem and deleteMetaItem still call, at the same positions.
    • The helpers still throw. The method alone turns a 403 NOT_OVERRIDABLE / ITEM_LOCKED into a value, and re-raises everything else (pinned).
    • /meta's behaviour is unchanged. The one non-identical detail is a repeated pure registry read in deleteMetaItem, with no behavioural consequence.
  • (b) The lock keys on the server-held fact for the NAME: RIGHT. The verdict reads the registry's loader-registered PACKAGE_ID:NAME artifact entries (SchemaRegistry.getArtifactItem), never the request body or the engine's registered body. That is the automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761 ruling's rules 1 and 2. It is pinned both ways: a packaged name is refused whatever the body claims, and a customer flow is written whatever the body claims.
  • (c) Refuse before mutate: RIGHT, on PUT /:name, DELETE /:name and POST / onto an existing packaged name. POST / is the seat-adopted bounded in-place fix: measured 200 with an overwrite before the fix, and refused after it.
    • The authoring gate runs first, then the lock, then the engine. On refusal, registerFlow / unregisterFlow are never entered (pinned on both topologies).
    • On DELETE, the lock precedes the engine's DELETE_RESTRICTED / 409.
    • The engine's own registration is untouched, so boot still registers packaged flows.
  • (d) What stays open: RIGHT, each as before and pinned:
    • customer-authored flows;
    • the clone door (a new name, and FLOW_CLONE_NAME_TAKEN on a held one);
    • the toggle door;
    • the OS_METADATA_WRITABLE=flow hatch, which opens both doors alike;
    • a composition with no protocol service, which is a declared decision. A wired slot that fails to resolve is re-raised, not fail-open.
  • (e) Code and status: RIGHT. 403 NOT_OVERRIDABLE is the producer's ledgered code, relayed unchanged through errorFromThrown, never re-stamped. No new code and no new status.
  • (f) The changeset: accurate. @objectstack/runtime patch and @objectstack/metadata-protocol minor, with Clause-②: yes (widening). It carries no stray tracker number and no model identifier, and it claims nothing the diff does not deliver.

Surface inventory:

  1. packagedBaseRefusal: a widening, and right.
  2. The three /automation definition doors refuse a packaged flow: a runtime security and integrity refusal that restores ADR-0126 §2. Not a clause-② narrowing.
  3. Nothing else: no route, no query set, no schema and no code. The test-only files are two protocol pins, the runtime pin and the dogfood pin.

② Semver level

  • minor for @objectstack/metadata-protocol (an added public member) is right.
  • patch for @objectstack/runtime is right. The refusal pulls the door back to the declared contract, which is the negative boundary of clause ②, so no BREAKING banner or ADR-0087 marker is owed.
  • (widening) is the right arm. The fixed version group keeps runtime and metadata-protocol in lockstep.

③ Boundary flags

  • Deviations: all nine are answered:
    • the adopted cross-lane surface and the yes (widening) re-declaration;
    • the POST / fix on the amended claim;
    • the dogfood and fold-population pins on the amended claim;
    • the mechanism assumptions, verified;
    • the verdict key, verified;
    • the four merges of main, with no overlap;
    • the tooling slip, which had no effect;
    • check-changeset-no-major run locally without a payload (CI's Check Changeset is green);
    • CI, now complete.
  • Out-of-scope findings:
  • Residuals, none blocking: an absent protocol slot keeps the doors open, which is declared and pinned. The checklist item's "EXPECTED FAIL today" notes go stale on landing, a checklist-author follow-up.

Implemented-by: claude/issue-20679-automation-door-package-lock
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

Merged via the queue into main with commit 4b45afa Sep 30, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20679-automation-door-package-lock branch September 30, 2026 09:53
This was referenced Sep 30, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…sion in words instead of a tracker number (stage 2) (objectstack-ai#20830)

Part of objectstack-ai#20513
Clause-②: no

**Stage 2 of 5 of this lane (`metadata-protocol`), under the
maintainer's A / A ruling on the card.** The card stays open for stages
3-5, so this PR carries no closing keyword. Text only: no error `code`,
field name, HTTP status, export or control flow moves. Every changed
source line is a string-literal line (108 changed lines in 11 `.ts`
files, checked line by line against the merge base).

## What this does

The metadata protocol's refusals, hints and log lines sent the reader to
a tracker number for the reason behind them. Each rewritten string now
says that reason in words (form D, as the migration-entry rewrite and
stage 1 applied it). Where the sentence already stated what was decided,
only the citation goes. Where it did not, the decision is added in
words:

| Where | Cited | The sentence now says |
|---|---|---|
| `protocol.ts` `insertManyData` refusal (thrown) | 3172 | `insertMany`
is the partial-success batch insert: an outcome per row, so a bad row
neither fails the whole batch nor makes the good rows run their
`beforeInsert` hooks twice. |
| `protocol.ts` unknown metadata type refusal (400) | 8586 | A plugin
cannot declare a type either: `additionalTypes` was retired because
nothing ever read it. |
| `protocol.ts` stored non-canonical type refusals on publish and on
revert (`STORED_TYPE_NOT_CANONICAL`) | 7894, 8957 | The `/meta` URL door
now folds a type to its canonical spelling before it writes, so such a
row predates that; the stored migration's `skipped` report "with that
same reason" loses only its citation. |
| `runtime-authoring-gate.ts` schedule-flow `organization_id` hint |
6153 | The author's value wins, and the engine fills only an
organization the run resolved, so for a schedule run the author is the
one source. |
| `plugin.ts` the three `kernel:ready` "migration skipped" warnings |
5839, 8629, 8686 | What the migration that did not run would have
ensured: view-name uniqueness among ACTIVE rows only; the NULL-safe
`sys_setting` row identity on tenant and global rows; the adoption of
untenanted seed rows and their autonumber counter. |
| `sys-metadata-repository.ts` history-counter abort (`error`) | 4867 |
The old path answered 1 because it took a failed read for an empty
table. |
| `protocol.ts` publish-closure degrade (`warn`) | 10377 | Validation
falls back to the LIVE declarations without the batch's own drafts in
the closure. |
| `protocol.ts` cold-boot org-scoped audit (`warn`) | 6190, 6992 | The
write refusal it points at covers declared types only; the trailing
"See" keeps ADR-0005. |
| everything else: the overlay, `sys_view_definition` and `sys_setting`
index migration messages (ADR-0120 D4 stays), the seed/API tenancy
repair, its receipt and its skips, the batch-row withhold, the
object-existence gate's no-registry warning, the nested-select, overlay
and non-canonical-registry refusals, and the live-MySQL testkit error |
6418, 8725, 8629, 5839, 6417, 8686, 9451, 9261, 8502, 3770, 4196, 6190,
4432, 10382 | The sentence already said what was decided; only the
citation goes. |

Each claim was checked against today's code, not only against the cited
card: `saveMetaItem` folds the request type before it persists;
`additionalTypes` is a `retiredKey()` tombstone in `packages/spec`; the
org-scoped write refusal is live in `orgScopedWriteRefusal`. One cited
number (10382) answers 404 and was read through its landing commit
`ee09d2119`; the testkit sentence already says what it guards.

## Order inside the stage

All 46 id-bearing literals (50 occurrences) fit one PR, under the stop
line, so the stage lands whole, in three commits in the ordered
sequence:

1. `dc8a1a112` author-visible text: 10 literals (thrown refusals, the
stored-type refusals, the hint);
2. `730cbcaf6` log lines: 35 literals, plus the two re-pinned tests;
3. `442473234` the `src/`-shipped testkit string, and the changeset.

Each commit recomputes the ledger, so every commit on the branch is
green on `check:doc-authoring`.

## Pins re-pinned: 3 assertion lines in 2 test files

- `migrations/view-definition-active-index.test.ts` 342-343 asserted the
two numbers in the MySQL degradation line. They now assert the two gaps
in words: "an archived view keeps occupying its name slot" and "two
same-name ACTIVE shared views (owner NULL)".
- `protocol.batch-row-driver-text.test.ts` 396 asserted the number in
the withhold warning. It now asserts "must not be quoted back on
response data", the decision itself.

A one-off mutation proves each new pin can fail, run on the committed
head with `scripts/ablation-replace.mjs` (anchor hit once,
disk-verified) under a shell trap. Changing "name slot" gives 1 failed /
27 passed. Changing "(owner NULL)" gives 1 failed / 27 passed. Changing
"quoted back" gives 2 failed / 14 passed: the re-pin, and a knock-on in
the next test, because the failed test never reached its `mockRestore`.
After each leg, the blob equals HEAD and `git diff HEAD` is empty. The
tree is clean after the run, and no test file was left behind.

No string here is compared byte for byte with a twin in another package.
The consumer pins outside the package read unchanged fragments:
`runtime`'s `batch-row-driver-text-real-driver.integration.test.ts`
reads the withhold prefix,
`seed-tenancy-autonumber-split.integration.test.ts` reads "backfill
skipped", and `meta-field-overlay-lock.test.ts`, `objectql`'s
`protocol-meta.test.ts` and `rest`'s
`meta-unknown-type-read-refusal.test.ts` read "is not a metadata type".
I ran the three `runtime` files against the rebuilt `dist/`, and they
passed.

## Ledger burn-down

`scripts/doc-authoring-prose-id.baseline.json` was regenerated with
`node scripts/check-doc-authoring.mjs --census-ledger` into a scratch
file, so the growth refusal ran against the checked-in baseline, and
then copied into place. Only `metadata-protocol` rows moved, and every
one of them leaves:

| File | Occurrences before | after | (file, id) pairs before | after |
|---|---:|---:|---:|---:|
| `metadata-protocol/src/protocol.ts` | 15 | 0 (row leaves) | 11 | 0 |
| `metadata-protocol/src/migrations/seed-tenancy-backfill.ts` | 14 | 0
(row leaves) | 3 | 0 |
| `metadata-protocol/src/migrations/view-definition-active-index.ts` | 7
| 0 (row leaves) | 3 | 0 |
| `metadata-protocol/src/migrations/overlay-index.ts` | 4 | 0 (row
leaves) | 2 | 0 |
| `metadata-protocol/src/migrations/sys-setting-identity-index.ts` | 4 |
0 (row leaves) | 1 | 0 |
| `metadata-protocol/src/plugin.ts` | 3 | 0 (row leaves) | 3 | 0 |
| `metadata-protocol/src/migrations/live-mysql-database.testkit.ts` | 1
| 0 (row leaves) | 1 | 0 |
| `metadata-protocol/src/runtime-authoring-gate.ts` | 1 | 0 (row leaves)
| 1 | 0 |
| `metadata-protocol/src/sys-metadata-repository.ts` | 1 | 0 (row
leaves) | 1 | 0 |
| **whole ledger** | 861 | 811 | 572 | 546 |

The ledger's file count goes from 224 to 215, and other packages' rows
moved: 0. The census's 40 messages reconcile with the ledger's 50
occurrences. The gate counts 46 string literals: 45 in the census
population plus the testkit string, which the census filed as
test-facing. The census folds a `+` chain into one message, so 5
two-literal chains make 45 literals into 40 messages. Four literals
carry two ids each, which makes 46 literals into 50 occurrences.

## Verification (head `442473234`)

- build: turbo over `@objectstack/runtime...` (30/30), then the whole
workspace (72/72), then `@objectstack/metadata-protocol` directly. The
new sentences are in `dist/index.js`, and the only citations left in
`dist/` are docblocks.
- `@objectstack/metadata-protocol` test: Test Files 190 passed, 3
skipped (193); Tests 2792 passed, 19 skipped. The skips are the live
MySQL/PostgreSQL files: this container has no server.
- `@objectstack/metadata-protocol` typecheck: exit 0; `tsc --listFiles`
reads 193 of 193 test files.
- `@objectstack/runtime`, the three consumer files above: 3 files, 33
tests passed.
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths; 13 paths
against merge base `261c529f0`): 70 commands, all exit 0.
`check:dual-build-cjs-loads` and `check:type-check-debt` first answered
exit 3 PREREQUISITE NOT MET on the partial build. After the full build
(and a direct rebuild of this package, whose `dist` a turbo cache hit
had left older than the restored sources), both exit 0;
`check:dts-closure` and `check:lean-entry-closure` were re-run there
too. `--ran`: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.
- `check:doc-authoring`: sibling-package prose ids hold the baseline, no
growth, no burn-down unrecorded.
- narrowed lint: `eslint --no-inline-config --format json` over the 11
touched `.ts` files reports 11 files, 0 errors and 0 warnings. The
resolved `parserOptions` for these files are `ecmaVersion` and
`sourceType` only, with no `project` or `projectService`. So no
type-aware rule can move an untouched file. The repo-wide `pnpm lint` is
CI's.
- NOT MEASURED locally (CI's): the live PG/MySQL files, whose messages
changed only ahead of the pinned MySQL statement lead-in; the Test Core
shards; Dogfood; the workspace type-check lanes.

## Acceptance notes

- PR objectstack-ai#20817 (another lane, draft) also edits `protocol.ts`. Whichever
lands second merges `main` and recomputes the ledger with
`--census-ledger`.
- Docblocks and `//` comments in this package still cite numbers. They
are out of scope here: comments are the sanctioned home for internal
anchors, and a separate card owns stale ones.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…flow for a shipped flow name with a stored row, as the layered read does (objectstack-ai#21002) (objectstack-ai#21116)

Fixes objectstack-ai#21002
Clause-②: yes (widening)

The published-snapshot read of a flow name a managed package ships now
answers the package's flow when a stored row of that name is at rest.
This holds on the REST route and on its runtime-dispatcher twin. This is
the second half of objectstack-ai#21002, as triage ruled in `5924438659`: option A,
scoped by the decision, not by type. The first half, the layered read,
landed in PR objectstack-ai#21043.

⚠️ This body follows the objectstack-ai#20761 family's disclosure discipline. It talks
about doors, roles, codes and statuses only. It has no request body,
header or field spelling, and no seeding steps.

## What changed

**`packages/rest/src/rest-server.ts`**, the `GET
/meta/:type/:name/published` handler:
- It still reads the layered answer first. When that answer has a stored
layer, the door now asks the protocol's `isShippedFlowName` about the
answer's own type and name.
- When the predicate holds, the layered read has put the loader's body
over the stored row. The door then serves the effective layer, which is
the loader's body.
- In every other case it serves the stored layer, exactly as before. A
protocol that brings no such predicate also keeps today's answer.

**`packages/runtime/src/domains/meta.ts`**, the dispatcher twin of that
route:
- The same change, in the same place.
- `MetaDomainProtocol` gains the predicate as an optional member. It is
`Pick`ed from `ObjectStackProtocolImplementation`, not restated, so a
rename at the producer is a compile error here. This is the same move
`domains/automation.ts` makes for `packagedBaseRefusal`.

**`packages/metadata-protocol/src/protocol.ts`**, the declared
cross-lane surface the claim allows:
- `isShippedFlowName` changes from `private` to public. Its body is
unchanged.
- A docblock paragraph names the doors that ask it.
- `getMetaItemLayered`'s effective-layer decision (PR objectstack-ai#21043) is not
touched.

**`.changeset/21002-published-door-shipped-flow.md`**:
`@objectstack/metadata-protocol` `minor`, `@objectstack/rest` `patch`,
`@objectstack/runtime` `patch`.

## How the doors learn the decision

There is one decision point, the predicate PR objectstack-ai#21043 already calls.
- No per-type list, no new response key, no fourth precedence path, and
no second copy of the rule.
- Each door asks the protocol's own predicate about the type and name
the layered answer reports. The layered answer's type is the canonical
singular, so the predicate gets exactly the arguments
`getMetaItemLayered` used.
- `object` is never named. Its effective layer differs from its stored
layer by folding and governance, not by this decision, so it is served
byte-identically.

The predicate was private to the protocol class. A door in another
package could not reach it except by copying the rule (the flow-only
scoping plus `packagedArtifactOwner`), which the ruling forbids. So
making it public is the minimal reachability change.

## Clause ② reads `yes (widening)`, not the claim's `no`

The claim's own reading said the dev re-reads the line against the real
diff. The real diff adds one public member to a class
`@objectstack/metadata-protocol` exports, so its published declaration
grows.
- The rule `check-changeset-no-major.mjs` quotes says a purely additive
widening of a published package's public surface takes at least `minor`.
- The two in-family precedents read it the same way. PR objectstack-ai#20817 made
`packagedBaseRefusal` public, and PR objectstack-ai#20853 made
`tenantAuthoredWriteRefusal` public. Both were declared `Clause-②: yes
(widening)` with `@objectstack/metadata-protocol` at `minor`.
- No wire shape moves. No request key, response key or accept set
changes. `rest` and `runtime` stay `patch`: `MetaDomainProtocol` is not
exported from the runtime package entry.

If the seat rules this `no`, the revert is two lines: this body's second
line, and the changeset's `minor` back to `patch` with its own Clause
line.

## Reproduction

Showcase composition on a database file, cold boot, signed-in admin. The
stored rows were written on a first boot and read on the second. The
base is `63d1a7c378`.

| case | door | before | after |
|:---|:---|:---|:---|
| shipped flow name, stored row | REST | `200`, the stored body | `200`,
the loader's body |
| shipped flow name, stored row | dispatcher twin | `200`, the stored
body | `200`, the loader's body |
| flow name no package ships, stored row | REST and twin | `200`, the
stored body | unchanged, same bytes |
| `object`, published stored layer | REST and twin | `200`, the stored
layer | unchanged, same bytes |

"Same bytes" means the SHA-256 of the served document is equal before
and after, on both doors. The dispatcher figures come from a throwaway
probe that drove `HttpDispatcher` in-process over the booted kernel. The
probe was deleted.

## Pins

- **REST unit**, `packages/rest/src/meta-published-overlay.test.ts`: 5
new cases. They use the real protocol and the file's own engine double,
with a registry that ships one flow from a package.
- A shipped name with a stored row answers the loader's body, equal to
the layered effective layer.
  - The plural type spelling reaches the same decision.
- Controls: a flow name no package ships keeps its stored row. An
`object`'s published stored row is served as stored, and its effective
layer is shown to differ. A protocol without the predicate keeps the
stored row.
- **Runtime unit**,
`packages/runtime/src/domains/meta-published-runtime-publish.test.ts`:
the same 5 cases, through the real `HttpDispatcher`.
- **Dogfood**, the new file
`packages/qa/dogfood/test/flow-shipped-name-published-door.dogfood.test.ts`:
5 cases, showcase, cold boot.
- A store check, plus the layered read putting the loader's body over
the stored row.
  - The REST door answers the loader's body.
  - The REST door's body equals the layered effective layer.
- Controls: a flow name no package ships keeps its stored body. An
`object`'s published stored layer is served unchanged, and its effective
layer is shown to differ.
- No existing `flow-shipped-name-*.dogfood.test.ts` file is edited.
Neither unit file gains an engine double, so
`scripts/engine-double-contract.pinned.json` is untouched.

**Why the dispatcher twin is not in the dogfood file.** The verify
harness mounts no dispatcher `/meta` catch-all. That route is reached
only on hosts that mount `@objectstack/hono`'s catch-all, so in this
composition the twin is not served at all. Driving `HttpDispatcher`
in-process from the dogfood package means importing runtime source.
`check:test-source-alias` then refuses four new dist-resolved imports
for the dogfood package (`metadata-protocol`, `observability`, `rest`,
`service-datasource`). Its remedy is to alias them to source in the
dogfood vitest config, which is outside this claim's file surface and
would change every isolated dogfood test's resolution. So the twin is
pinned at the unit level, with the real protocol and the real
dispatcher.

## Ablation

The fix was committed first. Both mutations went through
`scripts/ablation-replace.mjs`, replacing the predicate clause with a
constant false. Each leg is shown below.

- **REST door** (`rest-server.ts`), at head `292cc60f45`:
- The anchor went from 1 to 0 hits, and the blob from `a97cfde7c227` to
`418bc95a799c`.
- Unit (source-resolved): 2 failed (shipped name, plural spelling), 12
passed.
- Rebuild of `@objectstack/rest`. Then `ablation-dist-preflight
--absent` found the predicate call absent from all 6 built files.
- Dogfood (dist-resolved): 2 failed (the REST door, and its equality
with the effective layer), 3 passed (the store check and both controls).
- Restore: blob equals HEAD `a97cfde7c227` and `git diff HEAD` is empty.
After the rebuild, the preflight found the call present in 2 built
files, and the tree was clean.
- **Dispatcher twin** (`meta.ts`), at head `292cc60f45`:
- The anchor went from 1 to 0 hits, and the blob from `0c4ddceecbb8` to
`1e10bb639fc9`.
- Unit (source-resolved): 2 failed (shipped name, plural spelling), 8
passed.
  - Restore: blob equals HEAD and `git diff HEAD` is empty.

## Verification

All at head `292cc60f45` unless a line names another.

- **Unit pins:** `meta-published-overlay.test.ts` 14 passed (9 existing,
5 new). `meta-published-runtime-publish.test.ts` 10 passed (5 existing,
5 new). Both at `92f242cee4`, and neither file has changed since.
- **Whole packages:**
- `@objectstack/rest`: 259 files, 5035 passed, 143 skipped, at
`b973faeca2`.
- `@objectstack/runtime` (`--project local`): 297 files, 4254 passed, 5
skipped, at `b973faeca2`.
- The only later change in either package is the reworded docblock and
one dropped fixture key in its unit file. Both files were re-run green
after it.
- `@objectstack/metadata-protocol`: 196 files passed and 3 skipped; 2930
tests passed and 19 skipped, at `92f242cee4`.
- **Dogfood:** the new file, 5 passed.
- **Typecheck:** `metadata-protocol`, `rest`, `runtime` (including
`check:test-typecheck`) and `dogfood` all exit 0. `tsc --listFiles`
counts each new or edited test file once in its own program.
- **Gates:** `dispatch-gates --repo objectstack-ai/objectstack
--commands` derived 68 families. All 68 were run, each exit code
recorded before any pipe, and every one is 0. `--ran` reports 68
derived, 68 run, 0 NOT-MEASURED, 0 UNRUN.
  - The first pass, at `92f242cee4`, had two non-zero exits.
- `check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET: 8
packages outside the diff had no dist. They were built (41 of 41 turbo
cache hits).
- `check:test-source-alias` exited 1 on the dogfood dispatcher leg,
which was then removed. The reason is under Pins.
- **Lint**, a proven narrowing:
- Population, from eslint's own config: 6 of the 7 touched paths are
linted. The changeset is ignored, with no matching configuration.
- Count, from `--format json` with the `pnpm lint` flags: 6 results, 0
errors, 0 warnings.
- Invariance: there is no type-aware linting. Every `parserOptions`
block is `ecmaVersion` and `sourceType` only. The config reads only two
baseline JSON files, and this diff touches neither. So no untouched
file's verdict can move.
- **Not measured locally, declared to CI:** the Test Core shards, the
full Dogfood Regression Gate, Temporal Conformance, Build Core, the
type-check lanes, and the runtime `repo` test project.

## Acceptance notes

- **Unchanged background fact**, per the review `5924388874`: in the
showcase composition, a shipped flow with no stored row answers `501`
`NOT_IMPLEMENTED` on the published door. That kernel has no code/package
store. This PR does not change that path.
- **Not merged with `origin/main`:** 9 commits landed since the base.
None of them touches the 7 files here. PR CI tests the merge ref.
- **Read, not edited:** `getMetaItemLayered` and the predicate's body.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ion run proved stale (objectstack-ai#21339)

Docs-only checklist revision from the 17.6.0 release-verification run
objectstack-ai#21330 (subject `617f25f8`, Console pin `31971ff1e28f`). Every change
follows the checklist README's lifecycle rule: the item's `revision`
bumps and one `history` entry says what changed, why, and cites the run.
No product code, no `content/docs/**`, no generated file.

## Stale clauses the run proved (each a FAIL in objectstack-ai#21330 with disposition
stale-clause / assertion-defect)

| item | rev | evidence | changed by |
|---|---|---|---|
| `access-security.audit-log-browser` | 2 → 3 | admin `GET
/data/sys_audit_log?filter={"action":"delete"}` → 0 rows; the row is
stored with correct attribution | `30c530e5` (objectstack-ai#21194): the ledger serves
a non-system reader, admins included, only rows about records it can
read |
| `api-backend.filter-comparand-conformance` | 2 → 3 | POST `/query` →
400 `VALIDATION_FAILED` at `query.where.f_number.$eq`; GET `$filter` and
engine → 400 `INVALID_FILTER`; no door returns rows | objectstack-ai#20116 (`cfc3bcf1`
objectstack-ai#20247, `dd1b8031` objectstack-ai#20325) — the split query-contract-matrix rev 3
already records |
| `api-backend.date-range-preset-matrix` | 1 → 2 | equality
`{"signed_on":"today"}` → 400 `INVALID_FILTER` (temporal door);
`$gte:"this_week"` → 400 with `bareDateRangePresetComparandMessage` | by
design: `18.filter-preset-ordering-comparand-refused.ts` judges ordering
positions only |
| `records-forms.import-transform-matrix` | 1 → 2 | 400
`UNSUPPORTED_TRANSFORM` names the missing sandbox, 0 rows — but no
`framework#2611` | `f115b1f` (objectstack-ai#21188): refusals state decisions in
words, not tracker numbers |
| `studio-authoring.view-authoring-live` | 1 → 2 | `GET
/meta/view?object=repair_asset` serves `repair_asset.default` /
`repair_asset.form` with the authored config; container name 0 hits | by
design: `expandViewContainer` (objectstack-ai#7163, objectstack-ai#7736, objectstack-ai#13407) |

## Expected-fail notes 17.6.0 has made pass (clauses held in objectstack-ai#21330;
only their framing was stale)

| item | rev | measured | fixed by |
|---|---|---|---|
| `automation.packaged-flow-subflow-disable-refusal` | 1 → 2 | caller
off → child's disable retry 200, ledger `active=false`; caller-first
enable 409 `RESOURCE_CONFLICT` | `36d043b` objectstack-ai#20724, `0d9349f` objectstack-ai#20759; the
enable guard is `679f95e` objectstack-ai#20711 (step 6 now enables the child first) |
| `automation.packaged-flow-clone-contract` | 1 → 2 | clone survives a
cold restart and fires; still unreachable from Studio | durability
`cb4c31d` objectstack-ai#20907; reachability now filed as objectstack-ai#21332 (clause unchanged,
still expected to fail) |
| `access-security.packaged-flow-write-door-parity` | 1 → 2 | `PUT` /
`DELETE /automation/showcase_urgent_task_alert` → 403 `NOT_OVERRIDABLE`,
flow unchanged | `4b45afae` (objectstack-ai#20817); knownGap names the existing pin
`packaged-flow-write-door-parity.dogfood.test.ts` |

No clause was weakened: each still refuses the original failure mode
(rows returned, a served delete row, a 200-with-zero-rows), and the
clone clause keeps its expected fail.

## Validation

- `node scripts/check-platform-checklist.mjs` → `OK — 15 areas, 269
items (265 active, 2 planned)`; symbol anchors and line-citation sweep
green.
- `api-backend.json` is re-serialized in its existing canonical 2-space
form; the other four files are edited in place in their existing mixed
formatting.

Not in this PR (listed on objectstack-ai#21330's close-out instead): the other
checklist-accuracy findings the run collected, and the two `planned`
picklist items, which can only be promoted by a run in which they pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6

Co-authored-by: Claude <noreply@anthropic.com>
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.

access-security.packaged-flow-write-door-parity clauses 2 and 3 fail on main — detail withheld pending maintainer

2 participants