You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] registerSharingEndpoints / respondSharingError still answers the bare-string error dialect (ADR-0112) — the sharing half split out of #8073 #8111
Split out of #8073 at triage (2026-08-12), per the domain:cli seat's serial/split analysis on that card (comment 5269022297): the explain pair and the /sharing/* family are different blast radii, and this half carries a question the explain half does not.
Fact (inherited from #8073's measurement — re-verify at claim)
registerSharingEndpoints's single emitter respondSharingError in packages/rest/src/rest-server.ts answers { code, error: msg.replace(...) } — a bare-string error — feeding 400 / 403 / 404 / 409 / 422 across the whole /sharing/* family. body.error.code, the one position ADR-0112 D5 declares, reads undefined on all of them.
Why this is held as a finding rather than queued
The envelope convergence itself is implementation of the settled ADR-0112 D5 ruling. But this emitter carries a second, unrelated protocol: msg.startsWith(CODE) — the error code is recovered by parsing the message text. Whether that string-prefix protocol survives (kept alongside the D5 envelope) or is retired with it must be ruled before dispatch, not during it — retiring it silently changes what programmatic consumers can read.
Promotion path: at the findings round, either (a) the prefix-protocol question goes to the maintainer and this queues after the ruling, or (b) if a measured consumer census shows nothing reads the prefix, it collapses into a plain convergence card.
Provenance: #8073 (measurement record), PR #8071 / #7981 (the pattern to follow), #7035 / PR #7293 (the dialect retirement).
Finding-grading round: promoted to pm:queue (domain:cli, type Bug).
The blocking question measured away: the CODE: prefix is a server-internal service→REST derivation — stripped at the wire (rest-server.ts:9337) and parsed only inside the server — so no consumer can read it and no maintainer ruling is owed. This collapses to plain ADR-0112 D5 envelope implementation, and it has no flat-shape pins in packages/rest (counter-checked), so it is lighter than its parent #8073, not heavier.
Serial constraint (binding for dispatch): same emitter file (rest-server.ts) as #8073 — never the same batch; sequence this after #8073 lands. Precedent: PR #8071 / #7981.
The prefix-protocol question is not mine to rule, and it may not be anyone's
I had this card down as "blocked on a PM ruling about whether msg.startsWith(CODE) survives." Re-reading it, that framing was wrong — the card already names the better path and I am adopting it:
(b) if a measured consumer census shows nothing reads the prefix, it collapses into a plain convergence card.
That is a measurement, not a judgment. Ruling on whether a protocol may be retired before establishing whether anything depends on it is guessing with extra steps — and this lane has spent the day finding that the specific detail everyone is confident about is the one that has gone stale. So: census first, and the census decides.
Dispatch shape, when this unblocks
Census the prefix protocol before touching anything. Establish, by measurement, whether any consumer recovers the code by parsing the message text — in-repo callers, the client SDK's error path, content/docs/**, and the dogfood suites. Report what was searched, not just the conclusion.
If something does read it → ⛔ STOP and report, with the reader named. That goes to the maintainer as a contract question with evidence attached. Do not remove it, and do not "preserve it just in case" either — an unmeasured preservation is how a retired dialect survives another year.
⛔ Migrate the existing flat-shape pins to the D5 position; never delete them.fix(rest): one error envelope across the /security/explain pair (#8073) #8174 did this correctly and additionally added a body.error.message assertion on the bare-string arm, because the position alone does not catch a bare string. Same applies here — this emitter is the bare-string dialect.
Borrow fix(rest): one error envelope across the /security/explain pair (#8073) #8174's derived cross-arm skeleton pin. One emitter feeding 400/403/404/409/422 is exactly the shape where per-arm literal pins can be satisfied by adding another matching literal case while the family quietly grows a third dialect. A derived skeleton assertion fails structurally instead.
Why it is not dispatched right now
packages/rest/src/rest-server.ts — the repo's hottest file (#5949) — currently has #8174 in the merge queue against it. Same-file serial, per this card's own note. This dispatches the moment #8174 merges, and after that the only PM input needed is reviewing the census result.
Serial block cleared — claimed and dispatched.domain:cli PM seat (#6024), session session_01B3Kurx8qufrDzNjk4rag7V, branch claude/issue-8111-sharing-envelope-convergence.
PR #8174 (#8073, the explain half) has merged, so packages/rest/src/rest-server.ts has no other writer and this card's same-file constraint is discharged.
Dispatched under route (b) per the ruling above: census first, and the census decides. The dispatch leads with the measurement, not the conversion — if any consumer recovers the code by parsing the message text, the developer stops and reports the reader by name rather than choosing for us.
#8174 landed the pattern this follows, and the brief points at it as a worked example rather than describing it second-hand: the single family-local emitter delegating to the sharedsendError (imported as sendEnvelopeError), the flat-shape pins migrated to the D5 position rather than deleted, the extra body.error.message assertion that the bare-string arm needs and the position alone does not catch, the derived cross-arm skeleton pin that fails structurally when a third dialect appears, and the SDK re-measurement run against these consumers. That last one is not a formality — #8174's re-run found err.details moving on every arm and had to discharge it by census.
⛔ Not licensed: removing the prefix protocol on a hunch, preserving it on a hunch, changing any status code, or touching registerSecurityExplainEndpoints / registerSecurityEndpoints (both already converged).
{
"issue": 8111,
"status": "done",
"branch": "claude/issue-8111-sharing-envelope-convergence",
"pr": "https://github.com/objectstack-ai/objectstack/pull/8212",
"premise_still_valid": true,
"summary": "CENSUS FIRST (the card's gate): nothing reads the `msg.startsWith(CODE)` prefix, so this proceeded as a plain convergence card. Method, not just conclusion — C1 `startsWith('<CODE>'/code)` across packages/apps/examples/scripts: 6 hits, 5 being rest-server.ts's own route mappings and 1 in plugin-approvals; C5 read that one in full — approval-service.ts:1126 re-checks a prefix on an error IT THREW ITSELF three lines above, in-process, not a wire consumer and not this family. C2 every mention of the five codes incl. content/**: producers in plugin-sharing/sharing-service.ts, docs document status+code pairs only. C3 route callers: SDK client.shares.* (ledger disposition: sdk). C4 ObjectStackClient.fetch: reads `errorBody?.code ?? errorBody?.error?.code`, never parses message text. C6 docs: no prefix-parsing guidance. C7 dogfood: no `shares` references at all. C10: the three 500 codes appear only in rest-server, the ledger and docs — no test pinned them. Structural reason behind the count: the emitter STRIPS the prefix before writing the body, so it has never been on the wire and no consumer could read it. It is therefore left exactly as it was; only the response SHAPE moved, and `stripsThePrefix` now pins the stripping. PROCEEDED TO CONVERT: all NINE refusal arms (respond501's flat `{code,message}`, respondSharingError's five `{code, error:'bare string'}` verdicts, and the three verb-specific 500s) now emit through one family-local `respondError` delegating to the shared `sendError` from @objectstack/types (imported as sendEnvelopeError). No status code moves; no code VALUE changes. ONE FINDING, and it is the card's real surprise: the 409 arm's `CONFLICT` was registered in NEITHER StandardErrorCode NOR ERROR_CODE_LEDGER, while ErrorCode — what ApiErrorSchema.code parses against — is exactly their union, so that body could never have parsed as the declared envelope. The arm is live (plugin-sharing/sharing-service.ts:1042, revoke on a rule-materialised share) and documented. Routing it through the typed shared writer surfaced it as a COMPILE ERROR — the closed vocabulary working as designed. Per the dispatch I neither invented nor renamed: the existing value is registered under @objectstack/rest, keeping the wire byte-identical, and the consolidation question is filed unassigned as #8211. Also worth the PM's eye: the promotion comment said this card has 'no flat-shape pins in packages/rest (counter-checked)' — there was one, rest.test.ts's 'POST surfaces VALIDATION_FAILED as 400', which I MIGRATED to the D5 position (plus a body.error.message assertion) rather than deleted.",
"tests": "pnpm --filter @objectstack/rest test → 'Test Files 105 passed (105) / Tests 1756 passed (1756)'. @objectstack/client → 'Test Files 23 passed (23) / Tests 296 passed (296)'. @objectstack/spec (ledger admission suite, green with the new CONFLICT entry) → 'Test Files 385 passed (385) / Tests 10195 passed (10195)'. New rest suite alone: 'Test Files 1 passed (1) / Tests 12 passed (12)'. typecheck clean on rest and client — three TS2307 reds in UNTOUCHED client test files were the stale-artefact trap (@objectstack/runtime unbuilt in the fresh worktree); building the closure cleared them, so they were false reds, not mine. check:type-check-debt → 'OK — 33 ledger entr(ies) re-measured, 1966 raw tsc error(s) total, none above its recorded number'; @objectstack/rest unchanged at 155 (absent from the surplus list) and I never touched the ledger file; measured only AFTER the full closure build the gate demands (70 turbo tasks, 8m12s). check:route-envelope green incl. --self-test, siblingCode LOWERED 73→69 per the ratchet's bank-your-progress rule; delta verified mine by measuring both ends — merge-base 73, head 69, the four vanished sites at merge-base lines 9480/9507/9541/9570 all inside registerSharingEndpoints, and ZERO head sites remaining in that range. stringError unmoved at 44 by construction (all four messages are computed — msg.replace(...), String(...).slice(0,500) — which that counter cannot see; respond501 was never counted by either dialect, having no `error` key). check-nul-bytes OK (7438 files); check-error-code-casing OK. REVERSE VERIFICATION — predictions stated before running, all confirmed. Verbatim failure text: \"AssertionError: expected undefined to be 'NOT_IMPLEMENTED'\" (and 'VALIDATION_FAILED' / 'PERMISSION_DENIED' / 'NOT_FOUND' / 'CONFLICT' / 'SHARING_NOT_ENABLED' / 'SHARES_LIST_FAILED'); the derived skeleton pin failed STRUCTURALLY as designed — \"AssertionError: 400 grant drifted: {code:VALIDATION_FAILED,error:x}: expected 'code:string|error:string' to be 'code:string|message:string'\"; \"AssertionError: body.error failed ApiErrorSchema ... [{expected:object, code:invalid_type, message:Invalid input: expected object, received undefined}]\"; \"AssertionError: expected undefined to be 'record account/a1 does not exist'\" (stripsThePrefix). Totals 'Tests 11 failed | 1 passed (12)'; gate went red as predicted with 'siblingCode: found 73, declared 69 — a NEW non-conforming body.' STAYED GREEN under revert, named as required: the healthy-path case (no envelope involved), rest.test.ts's 501 case (asserts status only), and the ENTIRE 9-case client SDK suite — it stubs the transport with literal bodies and never executes the server, so it is a compatibility measurement, not a server pin. Restored; 'git diff --stat HEAD' empty. SDK RE-MEASUREMENT (step 4, run against THESE consumers, not inherited): drove the real ObjectStackClient through shares.list/grant/revoke against a stubbed transport answering old and new bodies for 7 arms — code, message, httpStatus, category, retryable, fields IDENTICAL on every arm; details CHANGES on every arm (last fallback is `?? errorBody`, the whole body, and the body is what moved) — the same class of surprise #8174 found, discharged by census (C11: no in-repo consumer reads err.details off a shares.* call) and pinned explicitly rather than left implicit.",
"open_questions": [
{
"question": "The 409 arm's `CONFLICT` was on the wire while registered nowhere. I registered the existing value (wire byte-identical). Should the vocabulary keep it, or consolidate onto the standard catalog's `RESOURCE_CONFLICT`? Note @objectstack/rest now registers FOUR generic synonyms the catalog already covers: CONFLICT/RESOURCE_CONFLICT, NOT_FOUND/RESOURCE_NOT_FOUND, FORBIDDEN/PERMISSION_DENIED, INTERNAL/INTERNAL_ERROR. The ledger's header rule says to use the catalog instead of registering a synonym, but that rule is PROSE ONLY — the mechanical gate rejects only a literal StandardErrorCode member, which is why four accumulated without a decision.",
"options": [
"A — leave them registered. Zero client impact; cost is two words per condition permanently and a dead-letter prose rule the next author copies.",
"B — consolidate onto the standard catalog, one card per code with a deprecation window. Cost: a real breaking wire change per code (the SDK surfaces err.code verbatim), needing consumer sweeps across objectui and cloud.",
"C — keep them, but make the rule mechanical: extend the admission gate to reject a semantic synonym of a standard member unless explicitly waived with a reason."
],
"recommendation": "C now, B only if a specific code proves confusing in practice. Real business need: no user is hitting this today (after this PR all four are registered and parse), so B spends genuine client breakage on vocabulary tidiness with no measured pull. Long-term soundness: C converts the header's prose into something enforced, which is what declared=enforced asks, and stops the fifth synonym landing without deciding the fate of the existing four. Hard-to-get-wrong: C is the option that structurally prevents the mistake at authoring time rather than relying on a reader noticing a prose rule — A leaves exactly the silent-drift channel that produced these four. Filed as #8211; not urgent, hence `finding` and no pm:queue."
}
],
"out_of_scope_findings": [
"filed as #8211: @objectstack/rest registers four generic synonyms the standard catalog already covers (CONFLICT, NOT_FOUND, FORBIDDEN, INTERNAL) — the ledger's own anti-synonym rule is prose only, so the mechanical gate never enforced it; labelled `finding`, unassigned, with the three options above",
"NOT filed (already tracked): rest.test.ts's 'POST /reports surfaces VALIDATION_FAILED as 400' still pins the retired flat position for registerReportEndpoints, a different registrar on the same file. That is outstanding #7035 drift already covered by the route-envelope ratchet (siblingCode 69 remaining), not a new defect — left untouched as out of scope."
]
}
Activity
hotlong commentedon Aug 12, 2026
Finding-grading round: promoted to
pm:queue(domain:cli, type Bug).The blocking question measured away: the
CODE:prefix is a server-internal service→REST derivation — stripped at the wire (rest-server.ts:9337) and parsed only inside the server — so no consumer can read it and no maintainer ruling is owed. This collapses to plain ADR-0112 D5 envelope implementation, and it has no flat-shape pins inpackages/rest(counter-checked), so it is lighter than its parent #8073, not heavier.Serial constraint (binding for dispatch): same emitter file (
rest-server.ts) as #8073 — never the same batch; sequence this after #8073 lands. Precedent: PR #8071 / #7981.Generated by Claude Code
hotlong commentedon Aug 12, 2026
PM ruling on the promotion path — take route (b): census first. Not yet dispatched (serial).
domain:cliseat (#6024). Not a claim.The prefix-protocol question is not mine to rule, and it may not be anyone's
I had this card down as "blocked on a PM ruling about whether
msg.startsWith(CODE)survives." Re-reading it, that framing was wrong — the card already names the better path and I am adopting it:That is a measurement, not a judgment. Ruling on whether a protocol may be retired before establishing whether anything depends on it is guessing with extra steps — and this lane has spent the day finding that the specific detail everyone is confident about is the one that has gone stale. So: census first, and the census decides.
Dispatch shape, when this unblocks
content/docs/**, and the dogfood suites. Report what was searched, not just the conclusion.registerSecurityEndpointsand fix(rest): one error envelope across the /security/explain pair (#8073) #8174 just did for the explain pair (through the sharedsendErrorfrom@objectstack/types, imported assendEnvelopeError— this module has a localsendErrorof its own).Constraints carried from #8073, and one added
body.error.messageassertion on the bare-string arm, because the position alone does not catch a bare string. Same applies here — this emitter is the bare-string dialect./sharing/*consumers. Do not inherit [finding]registerSecurityEndpointsanswers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981's or fix(rest): one error envelope across the /security/explain pair (#8073) #8174's. fix(rest): one error envelope across the /security/explain pair (#8073) #8174's re-run is the standard to match: it founderr.code/err.messageidentical on all eight arms anderr.detailschanging on every one, then discharged it by census. Expect a similar surprise here and go looking for it.Why it is not dispatched right now
packages/rest/src/rest-server.ts— the repo's hottest file (#5949) — currently has #8174 in the merge queue against it. Same-file serial, per this card's own note. This dispatches the moment #8174 merges, and after that the only PM input needed is reviewing the census result.Generated by Claude Code
hotlong commentedon Aug 12, 2026
Serial block cleared — claimed and dispatched.
domain:cliPM seat (#6024), sessionsession_01B3Kurx8qufrDzNjk4rag7V, branchclaude/issue-8111-sharing-envelope-convergence.PR #8174 (#8073, the explain half) has merged, so
packages/rest/src/rest-server.tshas no other writer and this card's same-file constraint is discharged.Dispatched under route (b) per the ruling above: census first, and the census decides. The dispatch leads with the measurement, not the conversion — if any consumer recovers the code by parsing the message text, the developer stops and reports the reader by name rather than choosing for us.
#8174 landed the pattern this follows, and the brief points at it as a worked example rather than describing it second-hand: the single family-local emitter delegating to the shared
sendError(imported assendEnvelopeError), the flat-shape pins migrated to the D5 position rather than deleted, the extrabody.error.messageassertion that the bare-string arm needs and the position alone does not catch, the derived cross-arm skeleton pin that fails structurally when a third dialect appears, and the SDK re-measurement run against these consumers. That last one is not a formality — #8174's re-run founderr.detailsmoving on every arm and had to discharge it by census.⛔ Not licensed: removing the prefix protocol on a hunch, preserving it on a hunch, changing any status code, or touching
registerSecurityExplainEndpoints/registerSecurityEndpoints(both already converged).Generated by Claude Code
hotlong commentedon Aug 12, 2026
{ "issue": 8111, "status": "done", "branch": "claude/issue-8111-sharing-envelope-convergence", "pr": "https://github.com/objectstack-ai/objectstack/pull/8212", "premise_still_valid": true, "summary": "CENSUS FIRST (the card's gate): nothing reads the `msg.startsWith(CODE)` prefix, so this proceeded as a plain convergence card. Method, not just conclusion — C1 `startsWith('<CODE>'/code)` across packages/apps/examples/scripts: 6 hits, 5 being rest-server.ts's own route mappings and 1 in plugin-approvals; C5 read that one in full — approval-service.ts:1126 re-checks a prefix on an error IT THREW ITSELF three lines above, in-process, not a wire consumer and not this family. C2 every mention of the five codes incl. content/**: producers in plugin-sharing/sharing-service.ts, docs document status+code pairs only. C3 route callers: SDK client.shares.* (ledger disposition: sdk). C4 ObjectStackClient.fetch: reads `errorBody?.code ?? errorBody?.error?.code`, never parses message text. C6 docs: no prefix-parsing guidance. C7 dogfood: no `shares` references at all. C10: the three 500 codes appear only in rest-server, the ledger and docs — no test pinned them. Structural reason behind the count: the emitter STRIPS the prefix before writing the body, so it has never been on the wire and no consumer could read it. It is therefore left exactly as it was; only the response SHAPE moved, and `stripsThePrefix` now pins the stripping. PROCEEDED TO CONVERT: all NINE refusal arms (respond501's flat `{code,message}`, respondSharingError's five `{code, error:'bare string'}` verdicts, and the three verb-specific 500s) now emit through one family-local `respondError` delegating to the shared `sendError` from @objectstack/types (imported as sendEnvelopeError). No status code moves; no code VALUE changes. ONE FINDING, and it is the card's real surprise: the 409 arm's `CONFLICT` was registered in NEITHER StandardErrorCode NOR ERROR_CODE_LEDGER, while ErrorCode — what ApiErrorSchema.code parses against — is exactly their union, so that body could never have parsed as the declared envelope. The arm is live (plugin-sharing/sharing-service.ts:1042, revoke on a rule-materialised share) and documented. Routing it through the typed shared writer surfaced it as a COMPILE ERROR — the closed vocabulary working as designed. Per the dispatch I neither invented nor renamed: the existing value is registered under @objectstack/rest, keeping the wire byte-identical, and the consolidation question is filed unassigned as #8211. Also worth the PM's eye: the promotion comment said this card has 'no flat-shape pins in packages/rest (counter-checked)' — there was one, rest.test.ts's 'POST surfaces VALIDATION_FAILED as 400', which I MIGRATED to the D5 position (plus a body.error.message assertion) rather than deleted.", "tests": "pnpm --filter @objectstack/rest test → 'Test Files 105 passed (105) / Tests 1756 passed (1756)'. @objectstack/client → 'Test Files 23 passed (23) / Tests 296 passed (296)'. @objectstack/spec (ledger admission suite, green with the new CONFLICT entry) → 'Test Files 385 passed (385) / Tests 10195 passed (10195)'. New rest suite alone: 'Test Files 1 passed (1) / Tests 12 passed (12)'. typecheck clean on rest and client — three TS2307 reds in UNTOUCHED client test files were the stale-artefact trap (@objectstack/runtime unbuilt in the fresh worktree); building the closure cleared them, so they were false reds, not mine. check:type-check-debt → 'OK — 33 ledger entr(ies) re-measured, 1966 raw tsc error(s) total, none above its recorded number'; @objectstack/rest unchanged at 155 (absent from the surplus list) and I never touched the ledger file; measured only AFTER the full closure build the gate demands (70 turbo tasks, 8m12s). check:route-envelope green incl. --self-test, siblingCode LOWERED 73→69 per the ratchet's bank-your-progress rule; delta verified mine by measuring both ends — merge-base 73, head 69, the four vanished sites at merge-base lines 9480/9507/9541/9570 all inside registerSharingEndpoints, and ZERO head sites remaining in that range. stringError unmoved at 44 by construction (all four messages are computed — msg.replace(...), String(...).slice(0,500) — which that counter cannot see; respond501 was never counted by either dialect, having no `error` key). check-nul-bytes OK (7438 files); check-error-code-casing OK. REVERSE VERIFICATION — predictions stated before running, all confirmed. Verbatim failure text: \"AssertionError: expected undefined to be 'NOT_IMPLEMENTED'\" (and 'VALIDATION_FAILED' / 'PERMISSION_DENIED' / 'NOT_FOUND' / 'CONFLICT' / 'SHARING_NOT_ENABLED' / 'SHARES_LIST_FAILED'); the derived skeleton pin failed STRUCTURALLY as designed — \"AssertionError: 400 grant drifted: {code:VALIDATION_FAILED,error:x}: expected 'code:string|error:string' to be 'code:string|message:string'\"; \"AssertionError: body.error failed ApiErrorSchema ... [{expected:object, code:invalid_type, message:Invalid input: expected object, received undefined}]\"; \"AssertionError: expected undefined to be 'record account/a1 does not exist'\" (stripsThePrefix). Totals 'Tests 11 failed | 1 passed (12)'; gate went red as predicted with 'siblingCode: found 73, declared 69 — a NEW non-conforming body.' STAYED GREEN under revert, named as required: the healthy-path case (no envelope involved), rest.test.ts's 501 case (asserts status only), and the ENTIRE 9-case client SDK suite — it stubs the transport with literal bodies and never executes the server, so it is a compatibility measurement, not a server pin. Restored; 'git diff --stat HEAD' empty. SDK RE-MEASUREMENT (step 4, run against THESE consumers, not inherited): drove the real ObjectStackClient through shares.list/grant/revoke against a stubbed transport answering old and new bodies for 7 arms — code, message, httpStatus, category, retryable, fields IDENTICAL on every arm; details CHANGES on every arm (last fallback is `?? errorBody`, the whole body, and the body is what moved) — the same class of surprise #8174 found, discharged by census (C11: no in-repo consumer reads err.details off a shares.* call) and pinned explicitly rather than left implicit.", "open_questions": [ { "question": "The 409 arm's `CONFLICT` was on the wire while registered nowhere. I registered the existing value (wire byte-identical). Should the vocabulary keep it, or consolidate onto the standard catalog's `RESOURCE_CONFLICT`? Note @objectstack/rest now registers FOUR generic synonyms the catalog already covers: CONFLICT/RESOURCE_CONFLICT, NOT_FOUND/RESOURCE_NOT_FOUND, FORBIDDEN/PERMISSION_DENIED, INTERNAL/INTERNAL_ERROR. The ledger's header rule says to use the catalog instead of registering a synonym, but that rule is PROSE ONLY — the mechanical gate rejects only a literal StandardErrorCode member, which is why four accumulated without a decision.", "options": [ "A — leave them registered. Zero client impact; cost is two words per condition permanently and a dead-letter prose rule the next author copies.", "B — consolidate onto the standard catalog, one card per code with a deprecation window. Cost: a real breaking wire change per code (the SDK surfaces err.code verbatim), needing consumer sweeps across objectui and cloud.", "C — keep them, but make the rule mechanical: extend the admission gate to reject a semantic synonym of a standard member unless explicitly waived with a reason." ], "recommendation": "C now, B only if a specific code proves confusing in practice. Real business need: no user is hitting this today (after this PR all four are registered and parse), so B spends genuine client breakage on vocabulary tidiness with no measured pull. Long-term soundness: C converts the header's prose into something enforced, which is what declared=enforced asks, and stops the fifth synonym landing without deciding the fate of the existing four. Hard-to-get-wrong: C is the option that structurally prevents the mistake at authoring time rather than relying on a reader noticing a prose rule — A leaves exactly the silent-drift channel that produced these four. Filed as #8211; not urgent, hence `finding` and no pm:queue." } ], "out_of_scope_findings": [ "filed as #8211: @objectstack/rest registers four generic synonyms the standard catalog already covers (CONFLICT, NOT_FOUND, FORBIDDEN, INTERNAL) — the ledger's own anti-synonym rule is prose only, so the mechanical gate never enforced it; labelled `finding`, unassigned, with the three options above", "NOT filed (already tracked): rest.test.ts's 'POST /reports surfaces VALIDATION_FAILED as 400' still pins the retired flat position for registerReportEndpoints, a different registrar on the same file. That is outstanding #7035 drift already covered by the route-envelope ratchet (siblingCode 69 remaining), not a new defect — left untouched as out of scope." ] }Generated by Claude Code
Generated by Claude Code
docs(spec): regenerate api references for the CONFLICT ledger entry (#…
test(qa): migrate #8209's share-grant dogfood pin to the ADR-0112 D5 …
fix(rest): converge the record-sharing family onto the ADR-0112 D5 en…
fix(plugin-sharing): run the ADR-0111 D7 inert-grant guard for SYSTEM…
fix(rest): narrow the flat door's thrown `code` to the declared vocab…
fix(rest): narrow the flat door's thrown `code` to the declared ADR-0…
fix(rest): resolveErrorResponse's declared-4xx passthrough strips the…
docs(changeset): carry the cloud NOT-MEASURED census bound into the s…
fix(rest): converge every declared-4xx exit on the declared-code pref…
fix(spec): the error-code waiver reasons and the auth-feature registr…