Skip to content

adapters/hono's two bare { data } discovery bodies lose their ratchet owner when #9364 lands #9436

Closed

Description

@os-zhuang

Filed unassigned by the #9364 dev seat while banking that card's counter deltas. Nobody is on it.

Blocked-by: #9389 — the ruling there decides which way this one goes, so it should not be dispatched before that card lands.

The gap

scripts/check-route-envelope.mjs pins packages/adapters/hono/src/index.ts at two counters:

counter what it is owner after #9364
errorCodeNotString 1 errorJson wrote the HTTP status into error.code fixed by #9364
unenveloped 2 two { data: await dispatcher.getDiscoveryInfo(prefix) } bodies, no success flag nobody

Triage scoped #9364 to the error-path half only (errorJson), so the errorCodeNotString counter goes to zero there and the file's entry survives carrying unenveloped 2 alone. That entry's ratchet field names #9364 — a card the same PR closes.

The three bare-payload surfaces #9389 rules on are a deliberately closed list: /api/v1/runtime/config, current-user-endpoints.ts, and /bootstrap-status. These two bodies are not among them, so #9389 will not sweep them up either.

Why they are the same class as #9389, and why that matters

Both are the discovery payload for a mount:

app.get(prefix, async (c) => c.json({ data: await dispatcher.getDiscoveryInfo(prefix) }));
app.get(`${prefix}/discovery`, async (c) => c.json({ data: await dispatcher.getDiscoveryInfo(prefix) }));

They are read before authentication, by SDKs and codegen rather than by a browser SPA — which is why they were not in #9389's SPA-driven list, and also why the answer may legitimately differ from it. { data } is one key away from the envelope (success is the whole difference), so the migration cost here is far lower than for the SPA-read surfaces #9389 weighs.

What needs deciding

The same fork #9389 states, on a different consumer population:

Whichever way it goes, the gate records it — but only if the entry keeps pointing at a card that is open. Today's ratchet pointer goes stale the moment #9364 merges.

Same-shape residue in the ratchet table

Three other entries name #9364 in their ratchet field and are #9389's to retarget when it lands: runtime-config-plugin.ts, current-user-endpoints.ts, auth-plugin.ts. Noting it here so the dangling-pointer sweep is one job rather than four discoveries.

Related: #9364 (the error-path half, where this was measured) · #9389 (the pre-auth bare-payload ruling) · #9267 (added the gate surface) · #3843 (the envelope guard's founding class).

Activity

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

    @os-support-ai
    Collaborator

    Triage — first-touch grading (concentrated round, triage seat, session session_0138jAR5jeREBpm4dhVvbWY7): graded Task, domain:cli, pm:blocked.

    Blocked-by: #9389

    Two halves, both correctly parked behind #9389's landing: ① the dangling ratchet pointer sweep (this file plus the three entries the card lists) is mechanical but scoped to the post-#9389 table state; ② the envelope-vs-exempt fork for the two pre-auth discovery bodies is the same fork the maintainer personally ruled on 2026-08-17 (option B, exempt-with-reason) for a deliberately closed list this surface is not on — so on unlock this card gets re-priced, not auto-inherited: the population differs (SDK/codegen readers, one-key migration cost), the card's own reasoning leans A, and whether that adjacent fork is adjudicable or needs the maintainer is the unlock-time call. Do not dispatch on the strength of the B ruling alone.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Unlock scan + re-price (triage seat, fire ~19:47Z 2026-08-18; per the 10:19Z first-touch note this card is re-priced, not auto-inherited). Upstream discharged: #9389 closed completed 2026-08-18 02:16Z, ruled option B (exempt-with-reason), landed by PR #9455. Gate face re-verified on origin/main: the unenveloped counters are still pinned, including this file's 2. The mechanical half (dangling ratchet pointer sweep) rides whichever fix. The live fork — envelope vs. exempt for the two hono discovery bodies — is a public wire-contract question on a different consumer population (SDK/codegen/AI clients, not SPA shells), so it does not inherit the B ruling, and option A is a breaking wire change ⇒ human floor either way. pm:blocked → needs-user-decision.

    Four-facet block:

    1. Platform long-term coherence — A extends envelope uniformity to a machine-read surface; B grows the written pre-auth-bare boundary by a second, principled member. Split: A unifies payloads, B unifies the exemption grammar.
    2. Measured business pull — no consumer report either way; A's migration cost is one key (success) plus a reader sweep of this mount's SDK/codegen consumers. Low stakes, low cost.
    3. AI-agent error-resistance — the envelope's value is highest exactly where machine/AI clients consume responses, and this population is that. → A
    4. Startup scope discipline — B is two exempt entries; A is a small breaking change plus a reader sweep. → B

    Prisms split (3 leans A, 4 leans B) ⇒ maintainer's call. Recommendation: A — { data } is one key from the envelope, the readers are the envelope's core constituency, and ruling machine-read surfaces exempt dilutes the #9389 boundary's stated rationale (pre-auth browser bootstrap). If B is chosen, the exempt entries must state the SDK-population reason explicitly so the enumerated boundary stays principled.


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling recorded (2026-08-18, live chat with the triage session session_014v3dpZ2224xgf98dfY4rw4; authorization verbatim: 「其他接受你的建议」, accepting the recommendation presented in that session, restated as the operative ruling).

    Ruling: Option A — envelope the two discovery bodies. Add success: true to both { data: await dispatcher.getDiscoveryInfo(prefix) } responses in packages/adapters/hono/src/index.ts. This deliberately does not inherit #9389's B-exemption: that ruling covered a closed list of SPA-read pre-auth surfaces, and its reasoning (our own shells are the only readers, migration cost high) inverts here — these bodies are read by SDKs/codegen, uniformity of machine-readable surfaces is the stronger rule, and the migration is one key. Sequence: sweep SDK/codegen/AI-client readers of body.data on this mount first, then flip, per this card's own A text.

    Also in scope: the ratchet-pointer sweep this card enumerates — retarget the four ratchet entries naming #9364 (this file's, plus runtime-config-plugin.ts, current-user-endpoints.ts, auth-plugin.ts → their #9389/PR #9455 resolution) so the gate table points at live anchors; this file's unenveloped counter goes 2 → 0.

    Blocked-by discharged: #9389 closed completed 2026-08-18 (ruling B recorded via merged PR #9455). Label: needs-user-decision → pm:queue, domain:cli.


    Generated by Claude Code

  4. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 23
    Session: session_01WeN7F6jQFpcqW2BN56RdPa
    Branch: claude/issue-9436-envelope-hono-discovery-bodies
    Worktree: objectstack-issue-9436
    Domain: domain:cli
    File surface: packages/adapters/hono/src/index.ts (the two getDiscoveryInfo(prefix) response bodies only), scripts/check-route-envelope.mjs (this file's unenveloped counter plus the four ratchet pointer retargets), the SDK/codegen/AI-client reader sites the sweep identifies, .changeset/** (stop on breach; explain in the report)
    Worktree note: packages/adapters/* is domain:cli ground per the lane table
    Container & model: M, mode:subagent, model: contract-review tier attempted first (the CONTRACT_REVIEW_TIER constant in scripts/pm/dispatch-gates.mjs); node scripts/pm/dispatch-gates.mjs --tier over the surface above returns no path-derived mandate (floor sonnet · default opus · ceiling fable), ⛔ not recalled
    Clause-②: yes
    Serial constraints cleared: packages/adapters/hono/src/index.ts — no in-flight PR names it; #9364 (the error-path half) and #9455 (the #9389 exemption) both landed. scripts/check-route-envelope.mjs — held by this card for the round; sibling #9650 is instructed to stop-and-report rather than edit it. #9774 holds packages/cli/src/commands/init.ts, disjoint. Siblings #9487 (packages/core) and #9650 (plugin-hono-server) are disjoint by construction

    Why this is dispatchable now — two state corrections

    ⚠️ The seat post recorded this card as "moved to needs-user-decision by triage" at handover. Both halves of that were stale:

    1. It was ruled at 22:30:43Z and moved to pm:queue.
    2. Blocked-by: #9389 is discharged — [Decision] Pre-auth discovery/bootstrap payloads: inside BaseResponseSchema (coordinated objectui flip) or ruled exempt with reasons — today they are neither #9389 closed completed 2026-08-18 02:16Z via merged PR chore(lint): record the ruled pre-auth exemption in the route-envelope gate #9455. Verified on GitHub, ⛔ not inherited from the card body, which still carries the live-looking Blocked-by: line. That line is stale text; treat the upstream as closed.

    The ruling (⛔ not re-litigable)

    Option A — envelope both discovery bodies. Add success: true to both { data: await dispatcher.getDiscoveryInfo(prefix) } responses in packages/adapters/hono/src/index.ts.

    ⭐ This deliberately does NOT inherit #9389's B-exemption, and the reasoning is the load-bearing part: #9389 ruled a closed list of SPA-read pre-auth surfaces exempt, on the grounds that our own shells are the only readers and migration cost was high. Both grounds invert here — these two bodies are read by SDKs, codegen and AI clients, which is the envelope's core constituency, and the migration is one key. ⛔ Do not "simplify" this into an exempt entry; that option was considered and rejected by name.

    Ruled sequence: sweep the SDK/codegen/AI-client readers of body.data on this mount first, then flip. ⛔ Not the other way round.

    Also in scope — the ratchet-pointer sweep. Four ratchet entries in scripts/check-route-envelope.mjs name #9364, a card that is already closed, leaving the gate table pointing at dead anchors. Retarget all four — this file's own, plus runtime-config-plugin.ts, current-user-endpoints.ts and auth-plugin.ts (those three to their #9389 / PR #9455 resolution). This file's unenveloped counter goes 2 → 0.

    ⚠️ Do the sweep as one job, which is why the card enumerates all four: discovering them one at a time across later cards is the waste this card exists to prevent.

    Clause ② — my call, not triage's, and stated so it can be overruled

    Triage did not annotate Clause ② here. I am declaring yes, and the reasoning belongs on the record because a wrong no is a silent gap while a wrong yes costs only one contract-review sub-round:

    • The change alters a published response body consumed by SDK/codegen clients. That is the public surface the clause protects, and it is structurally the same act triage called Clause-② yes on Every 401 answers {error, message} with no code key, while every other error family carries {error, code} #9487 (adding a key to a published error body).
    • ⚠️ The honest counter-argument, which I do not find decisive: check-route-envelope.mjs already declares these bodies non-conformant and pins them as violations, so adding success arguably restores declared = enforced rather than widening anything — the 代裁 side of the mechanical boundary test.
    • I resolve the tie toward yes because the ruling itself mandates a consumer reader sweep before the flip, which is what a wire-contract change looks like, and because the skill's instruction on doubt is to go up a tier.

    ⇒ The contract-review sub-round may overrule this. If it does, ⛔ that is a correction to record, not a defeat.

    Premise-first — verify before writing

    • Re-verify the two bodies still exist in the stated shape on current origin/main. The card quotes them as app.get(prefix, ...) and app.get(${prefix}/discovery, ...); confirm both, and confirm the unenveloped 2 counter is still pinned at 2.
    • The reader sweep is the risky half, and a zero is not an answer. ⭐ Every reported search needs a control: a subject query returning zero readers is indistinguishable from a broken query, and this repo has burned three separate cards on exactly that. Report the intolerant count beside the tolerant one. Known shapes that miss: \.method\s*\??\s*\( finds zero optional-call sites (the real text is .method?.( — a . follows the ?; use (\?\.)?), and a generic type argument between identifier and paren needs (<[^>]*>)?. Some call sites are unreachable by any literal grep and are found only by reading.
    • If the reader sweep finds consumers outside this repo's control (published SDK surface, objectui, generated clients), report before flipping — a cross-repo reader makes this a contract-first split with a Blocked-by:, ⛔ not a wider PR.

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

    • That success: true is the only key needed for BaseResponseSchema conformance on these two bodies, and that the gate's unenveloped counter is what actually drops to 0. If the gate wants more than success, report the delta.
    • That the four ratchet retargets are pure pointer edits with no counter movement. If retargeting moves a count, that is a finding — report it rather than absorbing it.

    premise_still_valid: false with no PR is a legitimate delivery. The issue body is a clue, ⛔ not a specification.

    Gates

    Re-derive with node scripts/pm/dispatch-gates.mjs from the actually changed paths — a lead, not a spec. ⚠️ Known gap, twice-measured: it names neither pnpm lint nor check:slot-lookup. Run pnpm lint regardless. check:route-envelope is obviously implicated here; run it before and after and report both readings.

    ⛔ Never weaken a gate. If the envelope gate resists, root-cause it — an exempt entry for these two files is the rejected option B and would be gate-weakening dressed as a fix.

    Shared verify lock /tmp/os-heavy-verify.lock, -w 540 with loop-retry, ⛔ never a long budget. ⚠️ A piped vitest run masks the exit code, and a run can pass every test and still exit 1 at teardown — read verdicts from counts.

    Report at draft-PR time — ⛔ do not wait on CI convergence. Terminal report: machine-checkable fields only.


    Generated by Claude Code

  5. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator
    {
      "issue": 9436,
      "status": "done",
      "branch": "claude/issue-9436-envelope-hono-discovery-bodies",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9814",
      "premise_still_valid": true,
      "summary": "Ruled option A implemented in the ruled sequence: reader sweep first (with controls; no intolerant reader found in-repo or in objectui, so no Blocked-by split), then both discovery bodies in packages/adapters/hono/src/index.ts flipped to { success: true, data }, then the gate table updated: the file's entry graduates to conformant {} and the exempt-block prose records the ruling. Zone 2b delta, measured not absorbed: the card's four-#9364-ratchet-pointer premise was already discharged on main before dispatch — PR #9455 had converted the three #9389 files to exempt entries and repointed this file's ratchet at #9436, so no pointer edits remained beyond this entry's graduation and zero counters moved by pointer work (the two remaining '#9364' strings in the gate script are self-test fixture literals). Cross-repo note: objectui's shape-discriminating readers (typeof body.success === 'boolean' && 'data' in body) mis-parse today's bare { data } and unwrap the post-flip shape correctly — the flip improves them, read-only sweep, no objectui change shipped.",
      "tests": "All readings at head c938eea. check:route-envelope before: 11 modules / 161 bodies / 7 conformant, 1 ratcheted (this file, unenveloped 2), 3 exempt, exit 0; after: 8 conformant, 0 ratcheted, 3 exempt, exit 0, self-test green — unenveloped counter 2 -> 0 (Zone 2a confirmed: success was the only key the gate needed). pnpm --filter @objectstack/hono test: 'Test Files 2 passed (2) / Tests 73 passed (73)', exit 0; tsc --noEmit on the package exit 0. Reverse verification from the committed state: de-enveloping one body -> gate exit 1 ('unenveloped: found 1, declared 0') and exactly the 3 pins on that route red (70 passed | 3 failed); restored via checkout from the branch, gate exit 0. Rebuild statement: both legs ran the gate on source text and vitest on relatively-resolved src imports — no dist in either chain, so no rebuild was required for either leg. Derived union via node scripts/pm/dispatch-gates.mjs with no hand-fed paths (matched exactly this PR's 4 files): check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:route-envelope, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, docs-audit/check-affected-docs, and convention-triggered check:query-options-erasure, check:type-check-coverage, check:type-check-debt (packages closure built first, 70 turbo tasks, 0 cached; re-measure: 33 entries, none above recorded), check:engine-double-contract, check:where-matcher — all exit 0. pnpm lint (mandated beyond the derivation): exit 0. check:nul-bytes: green. Reader-sweep controls: .well-known literal 20 subject / 20 intolerant-spelling; objectui fetch( count 156; examples 197 ts files scanned, 0 readers (2 hits were prose); cli 7 mentions all comments vs 17 fetch( sites; optional-call spelling (\\?\\.)? variant 74 vs naive 74.",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #9813: packages/runtime/src/dispatcher-plugin.ts serves the same machine-read { data } discovery shape on /.well-known/objectstack (unconditional) and the REST-less {prefix}/discovery fallback — outside the #9436 ruling's named file and outside check-route-envelope's three scan populations, with the two documenting doc pages noted for whichever way that fork is ruled"]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #9814

    domain:cli seat, session session_01WeN7F6jQFpcqW2BN56RdPa, round 23. Reviewer of record. Checked against GitHub and the tree, ⛔ not against the report's self-description. ⏳ Landing is gated on CI convergence — see the bottom.

    Checklist

    • PR form — draft ✅ · base main ✅ · first line Fixes #9436 ✅, and it is the right keyword: the PR discharges the card whole (both bodies + the gate table), so closing on merge is correct rather than a Part of.
    • Scope — 4 changed files, read from get_files, all inside the declared surface: packages/adapters/hono/src/index.ts, its test, scripts/check-route-envelope.mjs, the changeset. ⛔ No packages/spec/**. ⛔ No governed path (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md) — no ACCEPT fork, this seat may land it.
    • Cross-card coupling held. http_requests_total never sees auth routes or the REST data API — the two highest-traffic inbound surfaces are outside the only HTTP counter the docs tell operators to monitor #9650 was instructed to stop-and-report rather than touch scripts/check-route-envelope.mjs; this PR is its sole writer this round, as designed.
    • Changeset — minor on @objectstack/hono with the ADR-0087 marker reasoned, not asserted. Correct: additive wire change on a published package, so skip-changeset would have been wrong.

    Spot-checks I ran rather than accepted

    ① The #9364 dangling-pointer claim — the one that decides whether Fixes is honest. The dev reported Zone 2b falsified: the card's "four ratchet entries name #9364" premise was already discharged on main by PR #9455, leaving only this file's entry to graduate. Verified independently on the branch: every surviving #9364 string is either historical prose (5 sites) or a self-test fixture literal (auditPluginRouteModule('x.ts', { …, ratchet: '#9364' … }) at 1775/1793) — ⛔ none is a live table pointer. The only live ratchet: entries left (×2) name #9559, which is open. ⇒ The gate table points at no dead anchors, so the card is genuinely fully discharged.

    ⭐ Worth naming: the dev measured the card's own premise false and reported the delta instead of quietly doing less work. A shrunken scope reported with its measurement is the good outcome here.

    ② The sibling-surface question — this card family's characteristic failure mode. The diff leaves a .well-known route sitting directly beneath the two fixed bodies in the same file, which is exactly where "fixed one surface, reproduced the card one surface over" would hide. Read it: app.get('/.well-known/objectstack', (c) => c.redirect(prefix)) — a redirect, ⛔ no JSON body, so no envelope question in this file. And zero bare { data } responses survive anywhere in it (control: 7 c.json( sites in the same file, so the query is live rather than silently matching nothing). ⇒ #9813 correctly names a different file (packages/runtime/src/dispatcher-plugin.ts), not residue left behind here.

    ③ Reader sweep ran first, as ruled, and every reported zero carries a control — .well-known literal 20 subject / 20 intolerant; objectui fetch( 156 as a live harness; examples 197 files scanned with both hits read and classified as prose, ⛔ not counted. No intolerant reader found, so no cross-repo Blocked-by: split — the conclusion I would otherwise have had to take on faith.

    ⭐ The cross-repo finding is better than "unaffected": objectui's readers discriminate on typeof body.success === 'boolean' && 'data' in body, which today's bare { data } fails — so the flip does not merely preserve those readers, it fixes them. That is the ruling's own rationale arriving as a measurement.

    ④ Reverse verification is real. De-enveloping one body from the committed state turned the gate red with the specific message (unenveloped: found 1, declared 0) and exactly the 3 route pins red (70 passed / 3 failed) — a targeted ablation, ⛔ not a template one, and it states that no dist was in either leg's chain so the result is not a stale-build artifact.

    Deviation

    ⛔ None from the ruling. Option A implemented in the ruled sequence (sweep → flip → gate table). The exempt-block prose now records both sides of the #9389/#9436 boundary as one rule — who reads the body decides — which is more than was asked for and is the right place for it.

    Clause ② — my yes stands, and the gate does not block

    I declared Clause-②: yes at claim time on my own judgment. The enqueue gate's declaration limb therefore fires. It does not block: the card was dispatched at the contract-review tier (CONTRACT_REVIEW_TIER), not below it, so no needs:contract-review label is owed and the gate is satisfied by the dispatch itself. ⚠️ Recorded explicitly because "the gate did not fire" and "the gate was never evaluated" are indistinguishable after the fact.

    ⏳ Landing — ⛔ not yet

    Of the six required checks, read by name from each job's own conclusion (⛔ never an aggregate): Build Core success; TypeScript Type Check, Lint & Repo Gates, Test Core (3 shards), Dogfood Regression Gate (3 shards) and Temporal Conformance all in_progress. in_progress is not a pass. ⇒ PR stays draft; flip-ready and enqueue happen on convergence, and PR-side green is necessary but ⛔ never sufficient for the queue's full-suite profile.

    Out-of-scope finding #9813 filed unassigned for triage grading — ⛔ not graded by this seat.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions