Skip to content

runtime dispatcher's two discovery bodies (.well-known + REST-less {prefix}/discovery fallback) are the machine-read { data } class #9436 ruled on, and sit outside check-route-envelope's scan #9813

Description

@huangyiirene

Found while implementing #9436 (which stays open until its own PR lands; nothing here closes it). Measured on main at 4c260cd. Filed unassigned — recording, not starting.

The gap

packages/runtime/src/dispatcher-plugin.ts serves two discovery bodies in the exact shape #9436 was ruled on, and neither is visible to scripts/check-route-envelope.mjs:

route site condition
GET /.well-known/objectstack res.json({ data: await dispatcher.getDiscoveryInfo(prefix) }), ~line 738 unconditional — this route is dispatcher-owned in every composition (ADR-0076 D11 / OQ#9)
GET {prefix}/discovery same body, ~line 765 registered only when com.objectstack.rest.api is absent (REST-less compositions; otherwise ceded to REST)

Both are { data } with no success key — one key short of BaseResponseSchema, the same distance the hono adapter's discovery bodies were before #9436's ruling (2026-08-18, option A: envelope them, because machine-read discovery surfaces are the envelope's core constituency and the migration is one key).

Why this is a triage call, not an inherited fix

The #9436 ruling's operative text names only packages/adapters/hono/src/index.ts. The consumer population here is the same — /.well-known/objectstack is the SDK's own discovery fallback probe (connect() in packages/client/src/index.ts reads it), pre-auth, machine-read — so the ruling's reasoning extends naturally, but extending a ruling is not an implementer's move. Note the boundary drawn in #9389 does not cover these either: that exemption is a closed list of SPA-read shell-bootstrap surfaces, and these are not on it.

Reader tolerance is already measured (same sweep as #9436, controls reported there): the SDK unwraps body.data || body; the QA http-adapter discriminates on 'routes' in body; objectui's readers unwrap only when typeof body.success === 'boolean' && 'data' in body. An additive success: true breaks none of them.

Second half: the gate cannot see these sites either way

check-route-envelope.mjs audits three populations: response-envelope write sites, runtime/src/domains/* (returned payload objects), and Hono-context modules listed in PLUGIN_ROUTE_MODULES (judging c.json(...) / ctx.json(...) calls). dispatcher-plugin.ts writes through an express-style res.json(...) on an IHttpServer, which is none of the three — so whichever way the fork is ruled (envelope, or exempt-with-reason), the verdict has no place in the gate table to be recorded until the scan surface grows a fourth population (or these two sites move behind an audited seam).

If the fork is ruled toward the envelope

Two doc pages document the current { data } wrap for the well-known endpoint and would ride the flip: content/docs/api/index.mdx (~line 154) and content/docs/protocol/kernel/http-protocol.mdx (~lines 111–115).

Related: #9436 (the ruling on the hono adapter's mount) · #9389 (the pre-auth exemption boundary, a closed list) · #9559 (rest-server's own envelope conversion; dispatcher-plugin.ts is not in its scope).


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19 round): lands in packages/runtime/src/dispatcher-plugin.ts (envelope flip) plus scripts/check-route-envelope.mjs (scan-surface growth) and the two named doc lines → domain:cli (anchoring rule: the behavioural fix lands in runtime). Type: Bug — the sites violate the envelope contract as ruled.

    Ruling inheritance (sibling-of-adjudicated-family rule, not a new adjudication): the #9436 maintainer ruling (2026-08-18, option A — envelope machine-read discovery bodies, because they are the envelope's core constituency and the migration is one additive key) is inherited here with its reason intact. The mother ruling's rationale applies identically: same body shape ({ data }, one key short), same machine-read consumer population (SDK connect() fallback probe reads /.well-known/objectstack), reader tolerance already measured in the same sweep (additive success: true breaks no known reader), and the #9389 exemption is a closed list these sites are not on. No branch-specific semantic difference found → straight to pm:queue; reopening the decision would only be warranted if implementation measures a reader that chokes on the added key — in that case stop and report the fork, do not silently pick a side.

    Dispatch-side notes (advisory): this changes a public response shape → clause-② content limb applies (Clause-②: yes in the claim; contract-review dispatch tier). The gate half (fourth scan population or moving the sites behind an audited seam) rides the same PR only if small; otherwise file it as a domain:devx follow-up rather than widening. Size/model suggestion: M.


    Generated by Claude Code

  2. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor

    Claim: PM loop round 24
    Session: session_01WeN7F6jQFpcqW2BN56RdPa
    Branch: claude/issue-9813-dispatcher-discovery-envelope
    Worktree: objectstack-issue-9813
    Domain: domain:cli
    File surface: packages/runtime/src/dispatcher-plugin.ts (the two discovery bodies only — :775 and :801 on the merged ref), scripts/check-route-envelope.mjs (the scan-surface half, only if small — see below), content/docs/api/index.mdx and content/docs/protocol/kernel/http-protocol.mdx (the two documented lines), .changeset/** (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: contract-review tier attempted first (CONTRACT_REVIEW_TIER in scripts/pm/dispatch-gates.mjs); --tier returns no path-derived mandate, ⛔ not recalled — the mandate is Clause ②, judged from card content
    Clause-②: yes — triage-declared; this changes a published response shape read by SDK/codegen clients
    Serial constraints cleared: packages/runtime/src/dispatcher-plugin.ts is 🔓 free — the card that held it, #9823, landed (b2789adbdb, PR #9848 merged). That hold is the reason this card waited a full round; the maintainer ruled 「维持 2 个,等 #9823 落地」 rather than relax same-file serial to region level, and it has now been honoured. No other in-flight PR names this file. #9805 (packages/rest) also landed

    Unlock re-verified on the merged ref, ⛔ not inherited

    ⚠️ The card's line numbers are stale — locate by text. It says ~738 / ~765; on origin/main at 45862a53d8 the two sites are at :775 and :801:

    :775   res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
    :801       res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
    

    Both still bare. Control: success: true appears zero times in that file, so there is no pre-existing envelope to confuse the reading.

    ⭐ Independently confirmed the card's second-half claim: scripts/check-route-envelope.mjs contains no mention of dispatcher-plugin at all — the gate genuinely cannot see these sites, whichever way the fork is ruled.

    Scope — the ruling is INHERITED, ⛔ not a new adjudication

    Triage applied the sibling-of-adjudicated-family rule: the #9436 maintainer ruling (2026-08-18, option A — envelope machine-read discovery bodies, because they are the envelope's core constituency and the migration is one additive key) carries here with its reason intact. Same body shape, same machine-read consumer population (/.well-known/objectstack is the SDK connect() fallback probe), reader tolerance already measured in the same sweep, and #9389's exemption is a closed list these sites are not on.

    ⇒ Add success: true to both bodies. Additive only.

    ⛔ One reopening condition, and only one: if implementation measures a reader that chokes on the added key, STOP and report the fork — ⛔ do not silently pick a side. That is the sole circumstance in which the inherited ruling does not settle this.

    The gate half — bounded, with an explicit escape

    The second half (grow check-route-envelope.mjs a fourth scan population for express-style res.json(...) on an IHttpServer, or move these sites behind an already-audited seam) rides this PR only if it is small. ⚠️ If it turns into real gate-architecture work, file it as a domain:devx follow-up and ship the envelope flip alone — ⛔ do not widen this PR into a gate redesign. Triage said exactly this and I am not overriding it.

    PM mechanism assumptions — measure; falsifying them is a good run

    • That both sites are reachable and worth flipping. The {prefix}/discovery one is registered only when com.objectstack.rest.api is absent (REST-less compositions) — verify that condition still holds rather than assuming, and say so if the route is dead in practice.
    • That the two doc lines are the complete documented set. Re-sweep with a control; ⛔ a bare zero is not an answer.
    • That no changeset-visible behaviour outside @objectstack/runtime moves.

    Gates

    Re-derive with node scripts/pm/dispatch-gates.mjs from the actually changed paths. ⚠️ Known gap, three confirmed instances (#9721 / #9700): it names neither pnpm lint nor check:slot-lookup — run pnpm lint regardless. ⚠️ If you touch check-route-envelope.mjs, run that gate before and after and report both readings; ⛔ never weaken it — an exempt entry for these two sites is the rejected option B wearing a fix's clothes.

    Read verdicts from reported counts, ⛔ never a piped exit code. Report at draft-PR time — ⛔ do not wait on CI. ⚠️ Post the report as an issue comment on this card with the os-dev-report marker first; the terminal copy is the accelerator, ⛔ not the record (that channel dropped two reports this shift).


    Generated by Claude Code

  3. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor
    {
      "issue": 9813,
      "status": "done",
      "branch": "claude/issue-9813-dispatcher-discovery-envelope",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9949",
      "premise_still_valid": true,
      "summary": "Inherited #9436 ruling implemented additively: both discovery bodies in packages/runtime/src/dispatcher-plugin.ts flipped to { success: true, data } (located by text; the claim's :775/:801 held at 45862a53d8), exact top-level key set pinned in dispatcher-plugin.routes.test.ts, and the two doc pages updated. Zone 2 results: the {prefix}/discovery registration condition is live, not dead — kernel.hasPlugin('com.objectstack.rest.api') still gates it as the REST-less fallback owner; the reopening condition did not fire — no measured reader chokes (SDK connect() body.data||body, QA http-adapter 'routes' in body, objectui success-boolean discriminators which the flip fixes rather than breaks); the two doc PAGES are the complete set but held THREE wrap-documenting spots, not two — http-protocol.mdx's REST-less callout at ~:224 also stated the { data } envelope and was missed by the card, updated with the other two. Gate half judged small and rode: check-route-envelope.mjs's scanner receiver set is now a parameter and an ENUMERATED fourth population (IHTTP_ROUTE_MODULES) audits dispatcher-plugin.ts through the unchanged auditPluginRouteModule grammar — no exempt entry, no counter weakened; the discovery walk for the express dialect is real gate-architecture work (zero-arg fetch res.json() READ shares the receiver spelling; 20 candidate files measured) and is filed as the domain:devx follow-up #9937. The new population surfaced one further off-envelope literal body (SSE-fallback res.json({ events })) — outside the inherited ruling's reach (different consumer population), filed as #9936 and pinned unenveloped: 1, ratchet #9936 via the gate's own first-registration path.",
      "tests": "All readings at head f78a83e0cc. check:route-envelope before: 3 populations (10 route modules: 7 conformant/2 ratcheted/1 exempt; 16 domains; 11 hono modules, 161 bodies: 8/0/3), exit 0, self-test green; after: same three unchanged plus population 4 (1 module, 10 bodies: 0 conformant/1 ratcheted/0 exempt), exit 0, self-test green including 4 new express-receiver pins. Ablation from the committed state, both legs stating their rebuild: NO dist in either chain (gate reads source text; the pin test imports ./dispatcher-plugin.js relatively through vitest's transform), so neither leg needed a rebuild — mutate leg: de-enveloping the .well-known body -> gate exit 1 reading 'unenveloped: found 2, declared 1' (exit read from an unpiped run; the piped grep read 0, which is the trap) and exactly the mutated route's pin red ('GET /.well-known/objectstack success flag: expected undefined to be true', 1 failed | 15 passed); restore leg via git checkout HEAD -- <path>: gate exit 0, 16/16 passed. pnpm --filter @objectstack/runtime test: 'Test Files 176 passed (176) / Tests 2629 passed (2629)', exit 0; typecheck (tsc --noEmit, script name echoed — not a zero-match green): exit 0. Downstream integration (consumer direction, prefix-filter equivalent by name): @objectstack/http-conformance against REBUILT runtime dist — 5 files / 86 tests passed, exit 0. Derived union via node scripts/pm/dispatch-gates.mjs with NO hand-fed paths (matched exactly this PR's 6 files): check:changeset-gate-self-tests, check:cross-package-test-inputs (pnpm + node spellings), check:doc-anchors, check:docs-audit-scope, check:docs-redirects, check:objectui-changeset, check:published-readme-links, check:role-word, check:route-envelope, check:slot-lookup, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, docs-audit/check-affected-docs, spec check:empty-state/check:liveness/check:strictness-ledger/check:variant-docs, and convention-triggered check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check:type-check-debt --re-measure (packages closure built first per its instruction, 70 turbo tasks; 33 ledger entries re-measured, none above recorded) — all exit 0. Mandated beyond the derivation: pnpm lint exit 0; check:nul-bytes green (6310 files). Sweep populations: readers — 38 well-known hits across 4457 non-dist .ts files in-repo; objectui at 97fba31, success-boolean discriminator sites found with control 156 fetch( sites; docs — 12 well-known hits across 393 mdx files, each hit read and classified (3 wrap-documenting spots updated; realtime-protocol.mdx :110 shows the inner document with no envelope at all, a pre-existing elision equally true post-flip, left; client-sdk.mdx :82 documents probing not shape, left).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9936: dispatcher-plugin's SSE-fallback res.json({ events }) is off-envelope — different consumer population from the discovery bodies, so the inherited ruling does not reach it; pinned in the new gate population as unenveloped 1, ratchet #9936",
        "filed as #9937 (finding, domain:devx per dispatch instruction): the express-style gate population is enumerated, not discovered — a new res.json module on an IHttpServer still joins the repo unaudited; the discovery walk needs a read/write discriminator (fetch's zero-arg res.json() READ) and first-audit verdicts for 20 measured candidate files"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor

    ACCEPT — PR #9949

    domain:cli seat, session session_01WeN7F6jQFpcqW2BN56RdPa, round 25. Reviewer of record; checked against GitHub and the diff, ⛔ not against the report's self-description. ⏳ Landing gated on CI.

    ⭐ Report posted to this card with the os-dev-report marker first, read back and verified. That channel discipline was the explicit ask after it dropped two reports earlier today, and it was honoured.

    Checklist

    The three findings that make this more than a two-line flip

    ① Zone 2 falsified in the useful direction: two doc pages, but THREE wrap-documenting spots. The card named content/docs/api/index.mdx and http-protocol.mdx. The dev found http-protocol.mdx documents the { data } envelope in three places — including the REST-less callout at ~:226, which the card missed — and updated all three. ⚠️ A sweep that stopped at the card's list would have left a doc page contradicting the shipped shape.

    Sweep populations stated, and the hits read rather than counted: 12 well-known hits across 393 mdx files, each classified; two deliberately left with reasons (realtime-protocol.mdx pre-existing envelope elision, client-sdk.mdx documents probing not shape).

    ② The reopening condition was tested, not assumed. I gave exactly one: if a measured reader chokes on the added key, stop and report the fork. It did not fire — SDK connect() (body.data || body), QA http-adapter ('routes' in body), objectui's success-boolean discriminators, which the flip fixes rather than breaks. And the {prefix}/discovery registration condition is live, not dead: kernel.hasPlugin('com.objectstack.rest.api') still gates it, so both sites were worth flipping.

    ③ The pin is stronger than asked for. It asserts the exact top-level key set — Object.keys(body).sort() === ['data','success'] — not merely that success is true, with the reason written down: "an extra sibling key would be the strayKeys dialect arriving back."

    The gate half — judged small, and it earns its place

    I bounded this: ride the PR only if small, else file domain:devx. The dev did both, correctly.

    What rode: scanHonoRouteSource gained a receivers parameter (default unchanged), and a fourth enumerated population IHTTP_ROUTE_MODULES audits dispatcher-plugin.ts through the unchanged auditPluginRouteModule grammar — one grammar, two receiver dialects. ⛔ No exempt entry, ⛔ no counter weakened.

    ⭐ The self-test pins the blind spot's own mechanism: the same source reads 0 bodies under the Hono receivers and 1 unenveloped under the express receivers — "which is exactly how dispatcher-plugin.ts's discovery bodies sat invisible beside a green surface 3." That is a regression test for the reason the gate missed it, not just for the miss.

    What did not ride, and why the split is right: a genuine discovery walk needs a read/write discriminator, because fetch's Response.json() is a zero-argument READ on the same receiver name res — 20 non-test files carry the res.json( spelling today, most of them fetch readers. Filed as #9937 (domain:devx), and the table says ENUMERATED, not discovered in its own header so nobody mistakes it for a closed net.

    ⚠️ On the new ratchet entry (unenveloped: 1, ratchet: #9936): this is not gate-weakening. Before this PR the file sat in no population and that residue was invisible to every check in the repo; now it is counted, tracked, ticks down only, and its anchor #9936 is open and in this lane's queue — a live pointer, not the dangling kind #9436 spent half a card cleaning up.

    ④ The SSE-fallback body was found by the new population, not by luck — res.json({ events }), a different consumer population (callers who asked for a stream), so the inherited ruling correctly does not reach it. Filed as #9936.

    Verification

    Ablation from the committed state, both legs stating their rebuild posture (no dist in either chain, with the reason): de-enveloping the .well-known body → gate exit 1, unenveloped: found 2, declared 1, and exactly the mutated route's pin red; restore → gate exit 0, 16/16.

    ⚠️ ⭐ And the trap was hit and named: "exit read from an unpiped run; the piped grep read 0, which is the trap." That is the masked-exit-code failure mode catching a real reviewer, and being reported instead of silently producing a false green.

    Downstream integration run against rebuilt runtime dist: @objectstack/http-conformance 5 files / 86 tests. @objectstack/runtime 176 files / 2629 tests. Gate union re-derived with no hand-fed paths, matching exactly this PR's 6 files; pnpm lint and check:nul-bytes run beyond the derivation.

    Deviation

    ⛔ None. Scope, boundary and the file-hold on the sibling card all honoured; the gate half respected the smallness bar in both directions rather than picking whichever was easier.

    ⏳ Landing

    Build Core success; TypeScript Type Check, Lint & Repo Gates, Test Core (3 shards), Dogfood Regression Gate (3 shards), Temporal Conformance in_progress. in_progress is not a pass — PR stays draft; flip-ready then enqueue on convergence.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions