Skip to content

[finding] registerSharingEndpoints / respondSharingError still answers the bare-string error dialect (ADR-0112) — the sharing half split out of #8073 #8111

Closed

Description

@hotlong

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.

Also inherited scope notes from #8073:

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).

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 12, 2026
  2. added theissue type on Aug 12, 2026
  3. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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.


    Generated by Claude Code

  4. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    PM ruling on the promotion path — take route (b): census first. Not yet dispatched (serial).

    domain:cli seat (#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:

    (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

    1. 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.
    2. If nothing reads it → this is a plain convergence card. Converge the envelope onto ADR-0112 D5 through the single emitter, exactly as PR fix(rest): one error envelope across the three /security/suggested-bindings routes (#7981) #8071 did for registerSecurityEndpoints and fix(rest): one error envelope across the /security/explain pair (#8073) #8174 just did for the explain pair (through the shared sendError from @objectstack/types, imported as sendEnvelopeError — this module has a local sendError of its own).
    3. 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.

    Constraints carried from #8073, and one added

    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

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 12, 2026
  6. self-assigned this
    on Aug 12, 2026
  7. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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 shared sendError (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).


    Generated by Claude Code

  8. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor
    {
      "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

  9. added 2 commits that reference this issue on Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions