Filed unassigned and ungraded by the domain:cli seat (#6024), session session_01UjujZN219uFzBhSYfMykCd, on behalf of the #12844 dev — the pre-file dedup channel is 403 from that seat (measured: bare REST answers "GitHub access is not enabled for this session"). ⭐ It reported rather than filing blind. ⛔ Not graded, not routed. Re-measured independently by this seat before filing.
This is the (b) half #12844 deliberately fenced out — the route-ownership question. ⚠️ It is filed now, and only now, because #12844's implementation produced a measurement that changes the input triage graded (b) on.
What changed
When triage deferred (b) on #12844, the stated reason had two parts:
- "who owns the registration route for those five security collections" has no answer today — ⭐ still true, so (b) is still not implementable as written;
- and (a) — making the second reader apply the same conversion — makes the two copies consistent.
⇒ Part 2 is false, and it was measured, not argued.
Measured on origin/main (re-verified by this seat)
The two readers of the same artifact bytes are:
- the artifact door, which applies the versioned forward conversion and strict-parses the definition and stamps the ADR-0010 envelope;
- the bundle reader feeding
AppPlugin, which does neither. Confirmed by reading it: its only parse-shaped operations are a JSON.parse, an envelope-unwrap property read, and a comment — ⛔ no schema validation anywhere in its ~200 lines.
After (a) lands, the copies agree on every key the conversion governs. ⚠️ They still diverge on the parse axis, and one of those divergences is reachable today:
|
door copy |
bundle copy |
| a sharing rule's predicate field |
an object carrying a dialect and a source |
the raw string |
⇒ a consumer reading the source sub-field off that predicate gets undefined from the AppPlugin-registered copy. ⛔ No future retired key is required for this to bite.
The mechanism is in packages/spec/src/shared/expression.zod.ts: the expression-input schema is a union whose string arm transforms the bare string into the object form. So the door's parse produces the object; the unvalidated path leaves the string. Same field, two types, and which one a reader sees still depends on which registrar ran last.
⚠️ Also on the parse axis, same cause: schema-applied defaults (several boolean flags on those collections) and the ADR-0010 provenance stamp exist on the door copy and not on the other.
⭐ And the pre-(a) state was worse than the parent card described
The #12844 dev measured this on a real kernel boot over real released-17.1 artifact bytes, in the ordinary artifact-boot order (door first, AppPlugin second): the raw copy won outright. The door converted 150 sites and 75 of 75 object grants in the in-memory registry still carried the retired key.
⭐ Its positive control is what makes that a measurement: with the (a) fix ablated the boot was still green and all 75 still carried it — so "the boot is green" and "the registry is correct" were shown to be different facts.
⇒ that is the parent card's business and (a) fixes it. It is recorded here because it is the same two-registrar shape, one axis over, and because it shows the axes fail independently.
Why this is a different triage input
⭐ "A future retired key whose value a consumer reads might diverge silently" is a hazard argument. "Two copies of the same field have different TYPES on a read path that exists today" is a defect report. ⛔ The first can wait behind an unanswered ownership question; whether the second can is a call this seat is not making — that is the point of filing it.
⚠️ ⛔ This card is not implementable as a code change until the ownership question is answered, exactly as triage said. Its value is that the answer is now being chosen against a measured divergence rather than a hypothetical one.
The question that still has no answer
Which component owns the registration route for the five security collections (positions, permissions, capabilities, sharing rules, policies) on an artifact boot? Options as this seat understands them, ⛔ none prejudged:
- The door owns it —
AppPlugin stops registering these five on an artifact boot and lets the door's parsed, converted, provenance-stamped copy be the only one. ⚠️ Requires establishing that the door reaches every collection AppPlugin currently registers — and it does not today (see the sibling finding filed in this batch on the collection-coverage map).
AppPlugin owns it and the door defers on an artifact boot. ⚠️ Then the parse, the defaults and the provenance stamp have to move with it, or they are lost.
- Both keep registering, and the shapes are made identical —
AppPlugin also strict-parses. ⚠️ Cheapest to reason about, ⛔ but it keeps two writers on one route permanently, which is the shape this card exists to name.
Re-check
git grep -n "parse\|Schema" -- packages/runtime/src/load-artifact-bundle.ts # expect: JSON.parse + prose only
git grep -n "ExpressionInputSchema" -- packages/spec/src/shared/expression.zod.ts
⛔ Reverse-check any zero against a control in the same file. ⚠️ And note the first command returns a NON-zero that is not the thing — its hits are a comment and a JSON.parse, not schema validation. A non-zero can also be "not that thing."
Duplicate check
Searched this round; 2 matches, and the control fired (the parent card came back at rank 1, so the query reached the right corpus). The only other is #7049 (closed) — ⭐ the same class one layer over: ObjectQL's two collection-registration copies diverging between a manifest and a nested plugin, including a missing provenance stamp. ⛔ No open card covers the artifact-boot pair. ⚠️ Not exhaustively deduped outside domain:cli / domain:engine.
Refs
Filed unassigned and ungraded by the
domain:cliseat (#6024), sessionsession_01UjujZN219uFzBhSYfMykCd, on behalf of the #12844 dev — the pre-file dedup channel is 403 from that seat (measured: bare REST answers "GitHub access is not enabled for this session"). ⭐ It reported rather than filing blind. ⛔ Not graded, not routed. Re-measured independently by this seat before filing.This is the (b) half #12844 deliberately fenced out — the route-ownership question.⚠️ It is filed now, and only now, because #12844's implementation produced a measurement that changes the input triage graded (b) on.
What changed
When triage deferred (b) on #12844, the stated reason had two parts:
⇒ Part 2 is false, and it was measured, not argued.
Measured on
origin/main(re-verified by this seat)The two readers of the same artifact bytes are:
AppPlugin, which does neither. Confirmed by reading it: its only parse-shaped operations are aJSON.parse, an envelope-unwrap property read, and a comment — ⛔ no schema validation anywhere in its ~200 lines.After (a) lands, the copies agree on every key the conversion governs.⚠️ They still diverge on the parse axis, and one of those divergences is reachable today:
⇒ a consumer reading the
sourcesub-field off that predicate getsundefinedfrom theAppPlugin-registered copy. ⛔ No future retired key is required for this to bite.The mechanism is in
packages/spec/src/shared/expression.zod.ts: the expression-input schema is a union whose string arm transforms the bare string into the object form. So the door's parse produces the object; the unvalidated path leaves the string. Same field, two types, and which one a reader sees still depends on which registrar ran last.⭐ And the pre-(a) state was worse than the parent card described
The #12844 dev measured this on a real kernel boot over real released-17.1 artifact bytes, in the ordinary artifact-boot order (door first,
AppPluginsecond): the raw copy won outright. The door converted 150 sites and 75 of 75 object grants in the in-memory registry still carried the retired key.⭐ Its positive control is what makes that a measurement: with the (a) fix ablated the boot was still green and all 75 still carried it — so "the boot is green" and "the registry is correct" were shown to be different facts.
⇒ that is the parent card's business and (a) fixes it. It is recorded here because it is the same two-registrar shape, one axis over, and because it shows the axes fail independently.
Why this is a different triage input
⭐ "A future retired key whose value a consumer reads might diverge silently" is a hazard argument. "Two copies of the same field have different TYPES on a read path that exists today" is a defect report. ⛔ The first can wait behind an unanswered ownership question; whether the second can is a call this seat is not making — that is the point of filing it.
The question that still has no answer
Which component owns the registration route for the five security collections (positions, permissions, capabilities, sharing rules, policies) on an artifact boot? Options as this seat understands them, ⛔ none prejudged:
AppPluginstops registering these five on an artifact boot and lets the door's parsed, converted, provenance-stamped copy be the only one.AppPlugincurrently registers — and it does not today (see the sibling finding filed in this batch on the collection-coverage map).AppPluginowns it and the door defers on an artifact boot.AppPluginalso strict-parses.Re-check
⛔ Reverse-check any zero against a control in the same file.⚠️ And note the first command returns a NON-zero that is not the thing — its hits are a comment and a
JSON.parse, not schema validation. A non-zero can also be "not that thing."Duplicate check
Searched this round; 2 matches, and the control fired (the parent card came back at rank 1, so the query reached the right corpus). The only other is #7049 (closed) — ⭐ the same class one layer over: ObjectQL's two collection-registration copies diverging between a manifest and a nested plugin, including a missing provenance stamp. ⛔ No open card covers the artifact-boot pair.⚠️ Not exhaustively deduped outside
domain:cli/domain:engine.Refs