Repository navigation
Commit 3f98c2a
Fixes #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 #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 #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 #18191's own base) | **51** | same two |
*(none)* |
| PR #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 #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 #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 #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 #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 #18191's page swapped in.
3. **It unblocks #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 #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 #18191 自己的 base `ba9b4981c` 都测过)。真正会变红的是
PR #18191 —— 它重写的 `error-handling.mdx` 恰好是 `deriveWireFace`
扫描的两个页面之一,该页新写的 `VALIDATION_FAILED` 400
把这个码带进了线上词表,目录页却没跟上。卡片认定「文档改动移不动词表」,这一点是错的。
**风险与代价(含回滚)**:纯文档,零运行时影响,`content/docs/**` 不进任何已发布包(70 个非私有包的 `files[]`
实测,带正控)。回滚 = revert 这一个 commit。因为目录页本身就是词表的输入之一,这个改动在「#18191 已合」和「#18191
未合」两种世界里都是绿的 —— 两种都实跑过。
**席位意见**:_(留空,待席位定稿)_
**你要做的**:如果你认同「目录页该有这一条」,直接 review 合并即可,它同时解开 PR #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>
1 parent f0b2db3 commit 3f98c2a
1 file changed
Lines changed: 57 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
77 | 132 | | |
78 | 133 | | |
79 | 134 | | |
| |||
810 | 865 | | |
811 | 866 | | |
812 | 867 | | |
813 | | - | |
| 868 | + | |
814 | 869 | | |
815 | 870 | | |
816 | 871 | | |
| |||
0 commit comments