Repository navigation
docs(protocol): the error-handling page documents BOTH live error envelopes and states the door boundary - #18191
Merged
claude[bot] merged 5 commits intoSep 14, 2026
Conversation
…page Envelope section, auth codes and the validation codes, each measured against its writer. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
…andling page Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
…and security examples Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 14, 2026
claude
Bot
deleted the
claude/issue-15632-error-envelope-both-families
branch
September 14, 2026 15:10
This was referenced Sep 14, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ain-red premise is falsified (the failure is PR objectstack-ai#18191’s own) (objectstack-ai#18196) Fixes objectstack-ai#18193 Clause-②: no Docs-only. One hand-written MDX page under `content/docs/`; no `packages/**` byte changes, no runtime surface widened, no new authorable key. ##⚠️ The card's premise is falsified: `main` is NOT red — the failure belongs to PR objectstack-ai#18191 The card diagnoses a latent red on `main` and rests that on one mechanism claim: that `face.wireCodes` is derived from CODE only, so "no docs edit can move it". **That claim is wrong, and it is the whole diagnosis.** `deriveWireFace` (`scripts/check-error-status-conformance.mjs`) reconciles `StandardErrorCode` with *every other code a scanned page publishes a status for* — and it scans **two** pages: ```js const DOC_HANDLING = 'content/docs/protocol/kernel/error-handling.mdx'; // :219 const DOC_CATALOG = 'content/docs/api/error-catalog.mdx'; // :220 ``` `error-handling.mdx` is exactly the page PR objectstack-ai#18191 rewrites. The test file itself never reads it — which is what the card measured — but the derivation it imports does, one frame down. `reconciledVocabulary`'s own docblock calls that second half "the whole point": a ledger code is reconciled **because the docs publish a status for it**. ### Measured, four trees, same script | tree | `wireCodes.length` | `docPublishedBeyondStandard` | wire codes with no catalog heading | |:--|:--:|:--|:--| | `origin/main` @ `076bb975d` (today) | **51** | `INVALID_REQUEST`, `UNIQUE_VIOLATION` | *(none)* | | `main` @ `ba9b4981c` (PR objectstack-ai#18191's own base) | **51** | same two | *(none)* | | PR objectstack-ai#18191 head `50d799c03` | **52** | + `VALIDATION_FAILED` | `VALIDATION_FAILED` | | this branch | **52** | + `VALIDATION_FAILED` | *(none)* | The provenance line the run prints for the 52nd code on the PR head names the writer outright: ``` claimed VALIDATION_FAILED = [[400, ["content/docs/protocol/kernel/error-handling.mdx:346"]]] ``` That line is in PR objectstack-ai#18191's diff. And the test is green on `main`, twice measured: ``` BEFORE (unmodified tree, 076bb97) Test Files 1 passed (1) · Tests 5 passed (5) AFTER (this branch) Test Files 1 passed (1) · Tests 5 passed (5) ``` ### Ablation — the card's two assertions, reproduced byte-for-byte, only with objectstack-ai#18191 present Both files swapped under a trapped restore (absolute paths; each mutation proven on disk by a changed `git hash-object`; restore proven by `git checkout HEAD -- PATHS` reproducing the HEAD blob hashes and an empty `git diff HEAD`): ``` MUTATED handling=d174e08b8b93f10891366716e3677458f073944b catalog=6a529b3d11dd502d94c18c6c10dcd6f7f217e95b (handling hash == `git rev-parse probe-18191-head:content/docs/protocol/kernel/error-handling.mdx` — identity, not a grep) catalog claim now: **51 error codes reachable on the wire** catalog VALIDATION_FAILED headings: 0 AssertionError: wire code `VALIDATION_FAILED` has no catalog entry: expected false to be true ❯ src/api/error-catalog-docs.test.ts:95:82 AssertionError: expected 51 to be 52 ❯ src/api/error-catalog-docs.test.ts:125:31 Test Files 1 failed (1) · Tests 2 failed | 3 passed (5) RESTORED both files == HEAD blobs · git diff HEAD empty ```⚠️ One honest miss in that probe: the first swap also printed a `grep -c` marker count of **0**. The marker string was mine, invented from the PR's summary rather than read off its diff, so it never existed. The binding evidence is the blob-hash identity above, which fired; the grep proved nothing and is reported rather than quietly dropped. ### So why land this anyway, rather than returning a falsified-premise report with no PR The ruling's *intent* is served and the repair is correct on its own terms — it just is not a `main`-red stanch: 1. **The page already told readers to branch on a code it never catalogued.** `INVALID_REFERENCE`'s **Fix** line says *"branch on `VALIDATION_FAILED` + `fields[].code === 'reference_not_found'`"*, the dangling-reference callout publishes its full JSON body, and the `MISSING_REQUIRED_FIELD` callout names `400 VALIDATION_FAILED`. Seven prose mentions, zero entries. That hole is real with or without objectstack-ai#18191. 2. **It is green in both worlds, because the catalog page is itself one of the two scanned inputs.** Adding the entry under `## Validation Errors (400)` is what admits the code to the vocabulary, so the count moves 51 → 52 **on `main` alone** and stays 52 once objectstack-ai#18191 lands. Verified on a throwaway merge of this branch with `probe-18191-head`: `wireCodes.length = 52`, zero wire codes missing a heading, and the real test run **5 passed (5)** with objectstack-ai#18191's page swapped in. 3. **It unblocks objectstack-ai#18191**, which the seat deliberately did not arm for this reason. ⇒ If the intent was specifically "stanch a red on `main`", there is nothing to stanch and this can be closed unmerged at no cost. Everything below stands on its own. ## The entry, traced to its writers ⛔ Not a placeholder row added to satisfy a count. Every claim measured on this tree: | Claim now on the page | Writer it was read off | |---|---| | flat `/data` body is `{ error, code: 'VALIDATION_FAILED', fields: [...] }`, `fields` a **top-level sibling** | `structuredCodeAnswer`'s arm, `packages/rest/src/error-response.ts:1150-1159` | | flat `fields` is **always present**, `[]` when the producer named none | same arm: `fields: Array.isArray(error?.fields) ? error.fields : []` (:1156) | | the nested door carries the list at `error.details.fields` instead | `validationFailureDetails` returns `{ code, fields }` (`packages/types/src/validation-failure.ts:57-63`); `splitSemanticCode` **promotes** `code` to `error.code` and leaves the rest as `details` (`packages/runtime/src/error-envelope.ts:100-126`) | | a route that **builds** its own refusal sends the code with **no `fields` at all** | `sendError(res, 400, 'VALIDATION_FAILED', 'object and recordId are required')`, `packages/plugins/plugin-sharing/src/share-link-routes.ts:156` | | it is the code a **producer names**, unlike `VALIDATION_ERROR` which is **status-derived** | `HttpStatusErrorCodeMap[400]` for the latter; the former is matched on `err.code === 'VALIDATION_FAILED' \|\| err.name === 'ValidationError'` | | status is **400 and only 400** | the derivation's own emitted census: `emitted[VALIDATION_FAILED] = [[400, [...]]]` — `protocol.ts` `MalformedVersionTokenError`, `protocol.ts:2721`, `protocol.ts:13012`, `share-link-routes.ts:156` | | covers a batch row naming no record id | `rowRequiredIdError`, `packages/metadata-protocol/src/protocol.ts:2719-2724` | | `fields[]` entry keys (`field`, `code`, `message`, `label`, `constraint`, `value`, `options`) | `FieldValidationError`, `packages/objectql/src/validation/record-validator.ts:102-133`; `code` from `FieldErrorCode` (ADR-0114) | The count was read off `face.wireCodes.length` **after** the edit (52) and not transcribed from the card, per its acceptance criterion 2. ## Verification **Gates — 41/41 green**, derived with no path list (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`), exit codes captured to disk before any pipe, reconciled with `--ran` in the `:: exit N` form: ``` Run reconciliation — 41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN. EXIT CODES — all 41 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them. ✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3). ``` Exit-code census: **41 × exit 0**, zero non-zero, zero exit 3. `pnpm check:error-status-conformance` — the gate most exposed to this edit, since it is the same derivation — is one of them. Two prerequisite builds ran first (`@objectstack/spec` + `@objectstack/lint`, then the `@objectstack/client-react...` closure) through `OS_VERIFY_LOCK_SLOT=issue-18193 bash scripts/pm/os-verify-lock.sh`; the second printed `VERDICT command-exit 0`. **Lint — full, not narrowed.** `eslint . --no-inline-config --format json` over the whole repo in one foreground pass: **exit 0, 6752 files, 0 errors, 0 warnings**. No narrowing claim is needed. This file is not in eslint's population at all — **0** of the 6752 reported files end in `.mdx`. **Control characters.** `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over the edited file: no match (grep exit 1, zero lines). `pnpm check:nul-bytes` exit 0. **`skip-changeset` — measured on the publish surface, not asserted.** Of the **70** non-private workspace packages, **zero** list anything under `content/` or `docs/` in `files[]`, and `apps/docs` is `"private": true`. Positive control: the same reader prints `@objectstack/spec`'s real `files[]` (`["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]`), so the zero is a reading and not an empty walk. Nothing in this diff ships in a tarball. **Tree.** Derived and run on `6f3523462`, branched from `origin/main` `076bb975d`. ## Acceptance notes Observed while measuring, **not filed and not fixed here** — none is a reproducible defect, a declared-contract violation, or a metadata-authoring trap: - **Editing `error-handling.mdx` silently widens the wire face and reds a test about `error-catalog.mdx`.** A docs author on the protocol page has no local signal that publishing a status for a ledger code enlarges the catalog's advertised count. It is loud where it matters (the assertion names the code and the arithmetic) and it is the mechanism `reconciledVocabulary` was deliberately built to have, so this is the design working rather than a defect. **Successor: PR objectstack-ai#18191**, which is the PR that hit it and now has the repair landing alongside. - **The count sentence is self-referential.** Adding a catalog entry for a *ledger* code moves `wireCodes.length` by exactly +1 through the same edit, so "add the entry" and "fix the number" are one change, not two. Worth knowing before reading a `51 → 52` diff as two independent claims. Noted, not filed — no rule is violated and the gate is loud. **Successor: none.** - **`VALIDATION_ERROR` and `VALIDATION_FAILED` are adjacent in the catalog and easy to confuse**; the new entry states the distinction (status-derived vs producer-named) rather than leaving it to the reader. This is fixed here, not deferred — noting it only because the same confusion is visible in `packages/cli/src/utils/format.ts:236`'s comment, which is correct and needs nothing. ## 维护者速读(草稿) **改了什么**:错误码目录页 `content/docs/api/error-catalog.mdx` 补上 `VALIDATION_FAILED` 这一条(此前它在页面正文里被提到七次,却没有自己的条目),并把页头「51 error codes reachable on the wire」改成实测的 52。 **为什么改**:两件事,要分开看。① 这一页本来就有洞 —— `INVALID_REFERENCE` 条目明写「请改为 branch on `VALIDATION_FAILED`」,而目录里根本查不到这个码。② 派单说 `main` 红了,**这一条经实测不成立**:`main` 上这个测试是绿的(今天的 `076bb975d` 与 PR objectstack-ai#18191 自己的 base `ba9b4981c` 都测过)。真正会变红的是 PR objectstack-ai#18191 —— 它重写的 `error-handling.mdx` 恰好是 `deriveWireFace` 扫描的两个页面之一,该页新写的 `VALIDATION_FAILED` 400 把这个码带进了线上词表,目录页却没跟上。卡片认定「文档改动移不动词表」,这一点是错的。 **风险与代价(含回滚)**:纯文档,零运行时影响,`content/docs/**` 不进任何已发布包(70 个非私有包的 `files[]` 实测,带正控)。回滚 = revert 这一个 commit。因为目录页本身就是词表的输入之一,这个改动在「objectstack-ai#18191 已合」和「objectstack-ai#18191 未合」两种世界里都是绿的 —— 两种都实跑过。 **席位意见**:_(留空,待席位定稿)_ **你要做的**:如果你认同「目录页该有这一条」,直接 review 合并即可,它同时解开 PR objectstack-ai#18191 的阻塞。如果你当初批的是「去把 main 的红止住」,那没有红可止 —— 这张 PR 可以零代价关掉,证据都在上面。 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
… start of its own line — the enqueue gate reads that comment only (objectstack-ai#17800) (objectstack-ai#18208) Fixes objectstack-ai#17800 ## What changed `.claude/skills/pm-dispatch/SKILL.md` 〈执行座位职责〉 › 认领(先认领后动工), lines :472–:474 on base `99edfd008e`: three lines → three lines. 812/812 held; every changed line ≤ 120 bytes. Nothing else in the tree moves — ⛔ not the 〈模板与表〉 template (P5), ⛔ not `scripts/pm/check-clause2-carriers.mjs`, ⛔ not the reader tolerance that objectstack-ai#17098 is about (objectstack-ai#17098 remains open), ⛔ no label, sweep or gate, ⛔ not `references/core-rules.md` (P3). ```diff -- ② 认领评论(英文),固定形状见 模板与表 节;首行以字面 `Claim:` 开头是机器判据。 -- 巡查谓词只认 `Claim:` 这一个拼写且保持严格,修法是全舰队向它收敛,⛔ 不放宽谓词。 -- session ID 不可省;`mode:subagent` 的 dev 与 PM 同会话同 ID,甄别身份是分支。 +- ② 认领评论(英文)照抄 模板与表 节:`Claim:` 首行,`Branch:`、`Clause-②:` 各占一行且行首。 +- 条款②入队闸门只读认领评论且只读行首键:句中提及与另发评论皆读不到,卡即不可读。 +- 谓词保持严格、⛔ 不放宽;session ID 不可省,`mode:subagent` 下 dev 与 PM 同 ID,身份看分支。 ``` Bytes per line (`Buffer.byteLength`, utf8, measured 2026-09-14T15:05:43Z): before 109 / 114 / 94 → after **116 / 118 / 114**; the cap is `MAX_LINE_BYTES = 120` in `scripts/pm/check-skill-line-ratchet.mjs`. What each new line carries: - **:472 — the binding shape.** The claim comment copies the template: `Claim:` is the first line; `Branch:` and `Clause-②:` each occupy a line of their own, at the start of the line. 「照抄」 (copy) is the remedy the checker's own remedy text names — "COPY the template's line rather than composing one" (`TEMPLATE_POINTER`, check-clause2-carriers.mjs :1502). - **:473 — the why, in the line itself.** The clause-② enqueue gate reads only the claim comment and only line-initial keys; a key mentioned inside a sentence (the dispatch-order bullet form measured on objectstack-ai#17234, objectstack-ai#17620 and objectstack-ai#17948 — an instruction that describes the key inside a sentence IS the violation) or posted as a separate comment is unread, and the card is then illegible. 「条款②入队闸门」 is the charter's own name for it (:637). - **:474 — old :473 + old :474, compressed, content kept.** The readers stay strict and are never widened (the standing 「⛔ 不放宽谓词」); the session ID is mandatory; under `mode:subagent` the dev and the PM share one ID, so identity is the branch. Two clauses dropped as now-redundant: 「只认 `Claim:` 这一个拼写」 — the literal spelling is shown in :472 and in the template header (:799) — and 「修法是全舰队向它收敛」 — converging on the template is what 「照抄」 in :472 says. ## Triage direction (comment 5645818231, verbatim) > wherever the charter states the claim form, make the three lines non-optional (`Claim:` / `Branch:` / `Clause-②:` each at the start of its own line in the claim comment) and say why (the gate reads the claim comment only); ⛔ not the checker (right on both limbs); ⛔ not objectstack-ai#17098 (the reader's tolerance — a different defect) Where the charter states the claim form: SKILL.md :472 (the claim-discipline line) and 〈模板与表〉 :799–:811 (the template, already three-line). The rule line was the gap; the template was not. ## Premises, read against `origin/main` `99edfd008e` before writing **P1 — holds in substance; the instrument's predicted count was wrong.** :472 and :475 read exactly as the dispatch quoted (2026-09-14T14:57Z). The grep `各占一行|各自一行|own line|行首` over SKILL.md, `references/*.md`, os-dev.md and AGENTS.md returned **5 hits, not 0** (14:57:17Z; the control `机器判据` in SKILL.md → 2, as predicted). Each read: SKILL.md:111 (`Maintainer-action:` line-initial — a different key), state-machine.md:25 and :42 (`Blocked-by:` / `Release:` — different keys), platform-readings.md:352 (`CLAIM_COMMENT_MARKER` recognises line-initial `Claim:` — the marker, not the declaration), and platform-readings.md:350–:351 「认领腿 `CLAUSE2_KEY_LINE` 的轴是行首位置:引用块、项目符、粗体、反引号全容,冒号可在外。只有 `#` 标题与行中键破它」 — a READING of the checker's axis in the readings reference, not a rule binding the claim comment's shape, and not at the place the charter states the claim form. No line said the gate reads the claim comment only. The rule gap the card names is real; the new :473 is its first rule-layer statement and agrees with that readings line. **P2 — falsified in its literal form; direction unchanged, "why" stronger.** `grep -c 'Branch:' scripts/pm/check-clause2-carriers.mjs` → **23** (14:57:17Z). The checker DOES read the claim's `Branch:` line: it imports `claimedBranches` from `check-half-states.mjs` (:608), whose reader is line-anchored — `/^\s*(?:[-*+]\s+)?>?\s*Branch(?:es)?\s*:\s*(.*)$/gim` (check-half-states.mjs :4789) — and `governingClaim` picks "the newest claim comment from which at least one branch parses" (:4802). An inline `Branch:` parses to zero and the pair is reported UNJUDGED with the remedy "a `Branch:` line of its OWN … ⛔ not inside the `Claim:` sentence, which no reader parses" (check-clause2-carriers.mjs :3194–:3200, pinned at :5209–:5221). So the gate reads ONE comment and all three keys line-anchored: `Claim:` as the marker (`CLAIM_COMMENT_MARKER`, check-half-states.mjs :1134), `Branch:` to resolve which claim governs the pair, `Clause-②:` as the declaration (`CLAUSE2_KEY_LINE`, :762). The new :473 says exactly that — 「只读认领评论且只读行首键」 — and neither claims nor denies anything beyond it; :479 「分支名必须带 issue 号」 is untouched. Reported per the three-zone rule (mechanism assumption tested, falsification reported, ruling's intent kept), not as a stop. **P3 — SKILL.md only.** `references/core-rules.md` :109 reads 「认领原子对:一次标签写入完成认领与状态对调,随后留 `Claim:` 开头的评论并重读全线程。」 (151/151, headroom 0). It states the atomic pair and the `Claim:`-initial comment and nothing about the other two keys' placement, and the new :472 leaves that sentence true. The placement of `Branch:` / `Clause-②:` is form detail of the template (细则), not a principle (核心条款); 〈优先级〉's same-PR sync clause 「一条规则在本文与核心条款一处改动,另一处同 PR 同改」 fires on a rule that lives in both files and changes in one — no core-rules sentence changed meaning. Touching core-rules would have cost a trim in a 0-headroom file for a detail; not taken. **P4 — holds.** Base `pnpm check:pm-skill-ratchet` exit 0 at 2026-09-14T15:00:16Z: 「.claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom 0)」, 「widest table row is 342 bytes (pin 342; headroom 0)」; base `pnpm check:skill-frame-sync` exit 0 at 15:00:17Z. `git ls-remote --heads origin` at 15:13:32Z: only `claude/issue-17800-claim-form-three-lines` matches `issue-17497|issue-17800` — no objectstack-ai#17497 branch exists. After the edit `origin/main` advanced by one commit (`d4554d4f5a`, objectstack-ai#18191, a `content/docs` page); `git diff --stat 99edfd0 origin/main -- .claude/skills/pm-dispatch/ .claude/agents/os-dev.md AGENTS.md` is empty (15:15:22Z) — no governed file moved; the merge-base is still `99edfd008e`. **P5 — holds.** 〈模板与表〉 :799–:811 (read 14:57:13Z) carries `Claim: PM loop round N`, `Branch: …`, `Clause-②: yes | no` each on its own line at column 0. The template is untouched. ## Gates — this tree, `4c91f60ef2` `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; changeset from merge-base `99edfd008`: committed 1, working tree 0, untracked 0 — `.claude/skills/pm-dispatch/SKILL.md`) derived **16** commands. Each run in the foreground, exit captured by redirect-then-`$?`: | command | exit | |:--|--:| | `node scripts/check-closing-keyword-parity.mjs` | 0 | | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | | `node scripts/check-comment-mask-corpus.mjs` | 0 | | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 3 → 0 (see below) | | `pnpm check:agent-test-spelling` | 0 | | `pnpm check:doc-authoring` | 0 | | `pnpm check:driver-memory-census` | 0 | | `pnpm check:nul-bytes` | 0 | | `pnpm check:pm-governed-merges` | 0 | | `pnpm check:pm-governed-prose` | 0 | | `pnpm check:pm-skill-id-lint` | 0 | | `pnpm check:pm-skill-ratchet` | 0 | | `pnpm check:refd-timer-probe` | 0 | | `pnpm check:skill-frame-sync` | 0 | | `pnpm check:watch-hint-literal` | 0 | `check:doc-formula-expressions` first exited **3** — 「PREREQUISITE NOT MET — the workspace package `@objectstack/formula` is not built … Nothing was measured」 — not a reading. Built `@objectstack/formula` and `@objectstack/lint` under `scripts/pm/os-verify-lock.sh` (「VERDICT command-exit 0 · held the lock 219s (3m39s) · waited 0s」), rerun → **exit 0** at 15:13:30Z. `--ran` reconciliation (15:15:20Z): 「Run reconciliation — 16 derived, 16 run, 0 NOT-MEASURED, 0 UNRUN.」 … 「✓ dispatch-gates --ran: 16 derived famil(ies) accounted for — 16 run, 0 NOT-MEASURED (a DERIVED zero — all 16 recorded an exit code and none of them is 3).」 Post-edit ratchet (`pnpm check:pm-skill-ratchet` exit 0, 15:08:39Z): 「.claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom 0)」, 「widest table row is 342 bytes (pin 342; headroom 0)」. `pnpm check:skill-frame-sync` exit 0: 「the one declared copy of the decision frame is internally coherent」. `pnpm check:pm-skill-id-lint` exit 0: 「27 file(s) clean」 — the new lines cite no card number. `pnpm check:nul-bytes` exit 0; the control-character self-scan of SKILL.md → no hits. Not derived, not run: `pnpm lint` (repo-wide eslint, CI-owned; markdown is outside its population) and `docs:check-links`. Not run: `pnpm test` / `pnpm typecheck` — the diff touches no package. Changeset: none. `.claude/**` publishes nothing (fast lane: no `files[]` of any released package ships it). No label is written from this container. ## 维护者速读(草稿) - 改了什么/为什么改:认领评论里 `Clause-②:`(连同 `Claim:`、`Branch:`)必须各自独占一行、写在行首,因为条款②入队闸门只读认领评论、只按行首读键——写进派发令的句子里或另发一条评论,闸门都读不到,卡就不可读。模板不变、脚本不变,只把规则层这三行改成 3→3(812/812 不动)。 - 风险与代价(含回滚)/席位意见/你要做的:零运行时影响,纯规则文本,回滚即 revert 本 PR 的一个 commit;席位意见:(留空,席位定稿成评论);你要做的:授权审批后由席位落地(裁决 C)。 ## Acceptance notes - Governed rules layer: this PR is a DRAFT and stays one. Ruling C (objectstack-ai#17971) 「C. approve 后不管后续改动都由席位落地:」 governs the landing — the seat does the four-piece after an authorized approval; nothing here is readied, enqueued, auto-merged, approved or labelled from the dev container. - P2's literal claim (the checker does not read `Branch:`) was falsified by the tree (above) and reported as a mechanism-assumption correction; the ruling's direction was kept and the "why" written so it is true of all three keys. - P1's predicted grep count was 0; measured 5 — none a rule on the claim form (each hit read above). - noted, not filed: `references/platform-readings.md` :350–:351 already carries the checker-axis reading the new :473 agrees with; no drift, nothing to file. 承接者:无. - objectstack-ai#17497 (same file) stays queued behind this PR; objectstack-ai#17098 remains open and its reader tolerance is untouched. - Line-count payment: the 3→3 replacement pays its density inside the same subsection by merging old :473 and :474 — no re-wrap, no line elsewhere touched. Clause-②: no --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15632
Clause-②: no
Docs-only. No
packages/**byte changes, no runtime surface widened, no new authorable key — the whole diff is one hand-written MDX page undercontent/docs/.What the ruling asked for
Decision batch #107 item 3 (comment 5596253788), option A, maintainer reply verbatim: 「其他同意」.
Routes are not changed here. Zero bytes outside
content/docs/.Every shape published, traced to its writer
The ruling's execution note requires each envelope claim to be measured against a writer before it is written. Each claim in the new page and the writer it was read off:
{ success: false, error: { code, message, ...extra } }sendError,packages/types/src/response-envelope.ts:228extrais exactlycategory · httpStatus · details · requestId · declaredCode · userMessagePick<ApiError, …>onsendError's signature, same file :209detailsis emitteddetails:extras across the 7sendErrorroute modules;buildApiError'sdetailssplit; the rate limiter's{ retryAfterSeconds, resetAt }httpStatusis written by the dispatcher writer, not bysendErrorbuildApiErrorsets it unconditionally (packages/runtime/src/error-envelope.ts:119-126); nosendErrorcall site passes one (measured over the same 7 modules that yielded the 9details:hits — same grep, same population, so the zero has a firing control)requestIdrequestIdpassed intosendError's extra orbuildApiError'sextra/detailsat any call site. Firing control:details:fires 167× in the same populationtimestamptimestamp:inerror-response.ts,response-envelope.tsorerror-envelope.ts{ error: <string>, code?, …siblings }structuredCodeAnswerarms + the generic passthrough return,packages/rest/src/error-response.ts:2384-2392codeis optionalthrownCodeFieldsreturns{}when the producer named no code (:493-498)field,fields,developerMessage,dependentObject,dependentCount,currentVersion,currentRecord,datasource,reason,object,issues,declaredCode,userMessagestructuredCodeAnswerarms (:1007-1270) +withDeclaredUserMessage(:708-715)UNIQUE_VIOLATIONflat-onlyDUPLICATE_RECORDarm, :1229-1245DELETE_RESTRICTEDflat-only, two sentences,dependentCountabsent when withheldpackages/objectql/src/engine.ts:13786-13825(discloseCountgates the key)INVALID_FIELDflat-only,field/objectas siblingsRATE_LIMIT_EXCEEDEDnested-only, withhttpStatus+detailspackages/runtime/src/security/inbound-rate-limit.ts:353-361; code derived viaHttpStatusErrorCodeMap[429]VALIDATION_FAILEDis the per-field code,fieldsa top-level siblingFieldValidationError(packages/objectql/src/validation/record-validator.ts:102-133);codevalues fromFieldErrorCode(packages/spec/src/api/errors.zod.ts:233-277)VALIDATION_ERRORis the status-derived 400 codeHttpStatusErrorCodeMap(errors.zod.ts:173)EXPIRED_TOKENis not emitted by anythingStandardErrorCoderegistration aterrors.zod.ts:71. Firing control:INVALID_TOKEN, same grep, same population, returns two real producers instorage-routes.ts/data403 for a shared-record write isFORBIDDEN, notPERMISSION_DENIEDpackages/plugins/plugin-sharing/src/sharing-plugin.ts:1337-1344CODE:prefix is stripped before the body is writtenwithoutDeclaredCodePrefix,error-response.ts:537INTERNAL_ERROR_MESSAGE='Internal server error',packages/types/src/error-leak.ts:38)packages/runtime/src/observability/instrument.ts:103-121(res.header(requestIdHeader, requestId), defaultX-Request-Id); inbound echo is validated first —extractRequestId,request-context.ts:40-52One measurement that corrects the brief
Zone 2's map said
/meta/*answers nested. That holds for the runtime dispatcher's/metadoor only. The@objectstack/rest/metaregistrars report every thrown error throughhandleRouteError→resolveErrorResponse, i.e. flat — 18handleRouteError(res, error)call sites insideregisterMetadataEndpointsInner's range, against zerosendErrorcalls there. So the sameINVALID_REQUESTspelling refusal (packages/metadata-protocol/src/protocol.ts:274-283, a bare throw with.code+.status) answers nested through the dispatcher — which is whaterror-catalog.mdx's/metaexamples publish,httpStatusand all — and flat through@objectstack/rest.The page states this as the rule rather than as a path list, which is what Zone 3 asked for: the envelope is decided by the door, never by the code; a refusal a writer builds is envelope-exclusive, a refusal that is thrown takes whichever door caught it. The
/metasplit is called out explicitly in its own callout.The page also discloses, without enumerating, that the
@objectstack/restdoor still carries tracked non-conforming bodies, and namespnpm check:route-envelopeas the authority — rather than publishing a ratchet count that goes stale.Verification
Gates — 41/41 green, derived with no path list (
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack), exit codes captured to disk before any pipe, reconciled with--ran:The first sweep produced five non-zero results, all of them
PREREQUISITE NOT MET, none a finding:check:doc-formula-expressionsandcheck:doc-security-postureexit 3 on an unbuilt@objectstack/lint;check:docs-transcript-driftexit 3 on the same;check:docsexit 1 on a missingpackages/spec/json-schema;check:skill-examplesexit 1 onpackages/spec/distand then again onpackages/client-react/dist. After building those packages the whole sweep was re-run, not just the five. Builds went throughOS_VERIFY_LOCK_SLOT=issue-15632 bash scripts/pm/os-verify-lock.sh, all threeVERDICT command-exit 0.Lint — full, not narrowed.
eslint . --no-inline-config --format jsonran over the whole repo in one foreground pass: exit 0, 6752 files, 0 errors, 0 warnings. No narrowing claim is needed, and the file this PR touches is not in eslint's population at all (.mdxmatches none of the config'sfiles:globs — 0 of the 6752 reported files end in.mdx).Tree. Derived and run on
50d799c03, which already mergesorigin/main(the derivation's earlierSTALE TREEwarning is gone, and the derived family list is byte-identical before and after the merge).Control characters.
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file: no match.pnpm check:nul-bytesexit 0.skip-changeset. Measured on the publish surface, not asserted: of the 70 non-private workspace packages, zero list anything undercontentordocsinfiles[], andapps/docsis"private": true. Positive control: the same reader prints@objectstack/spec's realfiles[]array, so the zero is a reading. Nothing in this diff ships in a tarball.Acceptance notes
Observed while measuring, not filed and not fixed here — none is a reproducible defect, a contract violation, or a metadata-authoring trap:
rest-server.ts'sregisterMetadataEndpointsInnermixes four response shapes on one prefix —handleRouteError's flat body, hand-built{ error: { code, message } }with nosuccess,{ code, message }with noerrorkey at all (respond501), andrespondError→ the shared nestedsendError. This is exactly the driftcheck:route-envelope'sdialectOnlyratchet already tracks against#9559, so it has a successor and needs no new card.query-allowlist.ts,query-multiplicity.ts) each write one{ error: { code, message } }body with nosuccessabove it. Already carried in the same gate asunenveloped: 1apiece, ratcheted to#9559./datadoor drops a thrown error'sdetailsbag (the sharing plugin sets one atsharing-plugin.ts:1343; the generic 4xx passthrough does not relay it). Noted, not filed — the flat envelope has nodetailsslot by design, so this is the design working, not a leak.EXPIRED_TOKENis a registeredStandardErrorCodewith no producer — the same declared-but-dead shapeQUOTA_EXCEEDEDalready carries on this page. Noted, not filed: the ADR-0049 enforce-or-remove question for the error-code catalog is apackages/specdecision, not a docs one, and no PR or person is heading for it right now. Successor: none.维护者速读(草稿)
改了什么:协议文档《错误处理》整页重写。此前开篇写「每个错误都长这样」并给出一个嵌套信封,页面上十几条 per-code 示例全部重复该嵌套形状 —— 而平台实际上有两套活的错误信封。现在页面如实描述两套:新增「信封由哪一道门写决定,不由错误码决定」这条规则、两节分别写明嵌套与扁平各自的真实字段、一张路由族对照表,并把每一条 per-code 示例放回它真正所属的那一族。
为什么改:裁决 A(决策批次 #107 第 3 项,维护者原话「其他同意」)。此前照这页实现的客户端在
/data一族上整条解包路径都是错的 —— 会去读body.error.code,而线上给的是body.code。页面还发布了三个任何写手都不发射的字段(details在嵌套上是真的,但requestId/timestamp两者皆无),以及一个零生产者的错误码EXPIRED_TOKEN。风险与代价(含回滚):纯文档,零运行时影响,
content/docs/**不进任何已发布包(已在发布面实测)。回滚 = revert 这一个 commit。真正的代价是这页现在更长(955 → 1330 行):两套信封、一条判别规则、逐码归属,写全就是这个体量。若嵌套与扁平将来收敛为一套,这页可大幅收缩 —— 但那是一张协议卡,本次裁决明确没有开(未给A+)。席位意见:(留空,待席位定稿)
你要做的:如果同意「两套信封如实公开」这个方向,直接 review 合并即可。如果你希望开一张「收敛为一套信封」的协议卡,那是独立的一张,本 PR 不阻塞它。
🤖 Generated with Claude Code
https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Generated by Claude Code