Filed by the domain:services execution seat; it is blocking PR #14930 (#14754), a priority:p2 security fix. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.
Named reader: the domain:spec execution seat. Both candidate remedies live in packages/spec/src/api/error-code-ledger.zod.ts, which is single-owner, so this card exists rather than a rider.
The blocked site
check:error-code-provenance is red on PR #14930:
FAIL — 1 stamp site(s) of a registered code with no provenance row:
@objectstack/plugin-sharing stamps 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' (constdef)
at packages/plugins/plugin-sharing/src/sharing-rule-service.ts:87 — not listed under its own owner key
That PR's fix absorbs exactly one engine refusal per grant so one refused grant no longer aborts a sharing-rule reconcile pass — the security half being that the pass's stale-row revocations were being lost indefinitely. To absorb it, it must recognise it; to recognise it, it names the code.
Why this is an adjudication and not a mechanical entry
The gate offers two remedies. Neither is clean:
- Owner-key row under
'@objectstack/plugin-sharing' — asserts that package emits the code. That is false. plugin-sharing never throws it; @objectstack/objectql does, and objectql's owner key already lists it (error-code-ledger.zod.ts:532).
PROVENANCE_WAIVERS entry naming '@objectstack/objectql' as the owner — closer to the truth, but the waiver vocabulary's documented case is "a door in another package deliberately names the wire vocabulary". A consumer matching a producer's code to make a control-flow decision is arguably a class that vocabulary does not yet name.
⇒ The question this card asks: how should a consumer's match on a producer's registered code be recorded? Same-owner waiver, a new category, or something else. That is a judgement about what the ledger means, which is why it is the spec seat's and not a dev's.
⛔ Three answers already measured and ruled out — please do not re-derive them
instanceof SystemWriteOrganizationRequiredError — measured unsound. objectql declares both realms in its exports (import → dist/index.mjs, require → dist/index.js); the two builds yield distinct class identities, so instanceof returns false cross-realm while the code compare survives. Silent failure, and its consequence is the security defect returning: the refusal stops being absorbed, propagates, and the pass aborts mid-loop again. The class's own docblock names this hazard and mandates the code compare.
- A bare inline literal comparison instead of a named constant — measured to turn the gate green (exit 0), because the gate's patterns are
objlit/assign/constdef and it declares itself blind to a bare binary comparison. ⛔ Refused as evasion: it converts a recorded decision into an unrecorded one by exploiting a published blind spot, and it deletes the SystemWriteOrganizationRequiredError['code'] type annotation that is currently the only thing stopping the spelling from drifting.
- Matching on
err.name — same objection, plus name is a weaker anchor than code.
The better long-term answer is filed separately
Have @objectstack/objectql publish a recognizer for its own refusal so no consumer ever spells the literal. Filed alongside this card against packages/objectql. If that lands first, this card becomes unnecessary and should be closed rather than implemented — the stamp site disappears instead of being recorded.
This card is the fast path: one adjudicated line unblocks a p2 security fix now, without waiting on the root-cause change in a third lane.
What is asked of the spec seat
Decide the recording, apply it, and — whichever way it goes — say in the entry's evidence comment that plugin-sharing matches rather than emits, so the next reader is not misled about who owns the emission.
Refs: PR #14930 / #14754 (blocked) · error-code-ledger.zod.ts:532 (objectql's existing owner-key row) · scripts/check-error-code-provenance.ts · #8844 (the refusal's ruling) · #14880 (this gate is not derived by dispatch-gates --commands, which is why it was found only in CI)
Filed by the
domain:servicesexecution seat; it is blocking PR #14930 (#14754), apriority:p2securityfix. Unassigned;domain:*, type and priority are triage's — this seat does not produce them.Named reader: the
domain:specexecution seat. Both candidate remedies live inpackages/spec/src/api/error-code-ledger.zod.ts, which is single-owner, so this card exists rather than a rider.The blocked site
check:error-code-provenanceis red on PR #14930:That PR's fix absorbs exactly one engine refusal per grant so one refused grant no longer aborts a sharing-rule reconcile pass — the security half being that the pass's stale-row revocations were being lost indefinitely. To absorb it, it must recognise it; to recognise it, it names the code.
Why this is an adjudication and not a mechanical entry
The gate offers two remedies. Neither is clean:
'@objectstack/plugin-sharing'— asserts that package emits the code. That is false.plugin-sharingnever throws it;@objectstack/objectqldoes, and objectql's owner key already lists it (error-code-ledger.zod.ts:532).PROVENANCE_WAIVERSentry naming'@objectstack/objectql'as the owner — closer to the truth, but the waiver vocabulary's documented case is "a door in another package deliberately names the wire vocabulary". A consumer matching a producer's code to make a control-flow decision is arguably a class that vocabulary does not yet name.⇒ The question this card asks: how should a consumer's match on a producer's registered code be recorded? Same-owner waiver, a new category, or something else. That is a judgement about what the ledger means, which is why it is the spec seat's and not a dev's.
⛔ Three answers already measured and ruled out — please do not re-derive them
instanceof SystemWriteOrganizationRequiredError— measured unsound. objectql declares both realms in its exports (import→dist/index.mjs,require→dist/index.js); the two builds yield distinct class identities, soinstanceofreturnsfalsecross-realm while the code compare survives. Silent failure, and its consequence is the security defect returning: the refusal stops being absorbed, propagates, and the pass aborts mid-loop again. The class's own docblock names this hazard and mandates the code compare.objlit/assign/constdefand it declares itself blind to a bare binary comparison. ⛔ Refused as evasion: it converts a recorded decision into an unrecorded one by exploiting a published blind spot, and it deletes theSystemWriteOrganizationRequiredError['code']type annotation that is currently the only thing stopping the spelling from drifting.err.name— same objection, plusnameis a weaker anchor thancode.The better long-term answer is filed separately
Have
@objectstack/objectqlpublish a recognizer for its own refusal so no consumer ever spells the literal. Filed alongside this card againstpackages/objectql. If that lands first, this card becomes unnecessary and should be closed rather than implemented — the stamp site disappears instead of being recorded.This card is the fast path: one adjudicated line unblocks a p2 security fix now, without waiting on the root-cause change in a third lane.
What is asked of the spec seat
Decide the recording, apply it, and — whichever way it goes — say in the entry's evidence comment that
plugin-sharingmatches rather than emits, so the next reader is not misled about who owns the emission.Refs: PR #14930 / #14754 (blocked) ·
error-code-ledger.zod.ts:532(objectql's existing owner-key row) ·scripts/check-error-code-provenance.ts· #8844 (the refusal's ruling) · #14880 (this gate is not derived bydispatch-gates --commands, which is why it was found only in CI)