Skip to content

docs(protocol): give INVALID_FIELD one published meaning — the name/resolution one its 22 producers send (#15800) - #15883

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15800-invalid-field-one-meaning
Sep 5, 2026
Merged

docs(protocol): give INVALID_FIELD one published meaning — the name/resolution one its 22 producers send (#15800)#15883
baozhoutao merged 1 commit into
mainfrom
claude/issue-15800-invalid-field-one-meaning

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15800

INVALID_FIELD was published with two different meanings. content/docs/api/error-catalog.mdx:71 publishes a name/resolution cause; content/docs/protocol/kernel/error-handling.mdx:302-304 published «Field value has wrong type», with an example message no producer emits. This PR rewrites that one block so the wire code has one published meaning. ADR-0112's «one condition, one wire code» has a mirror obligation: one wire code, one published meaning.

⛔ Scope is exactly the #### INVALID_FIELD block of error-handling.mdxerror-catalog.mdx is untouched, no other block of the page is touched (the page as a whole is the subject of decision #15632), and no code changes.

Measurement — re-run before editing, with a firing control

Sweep 1, INVALID_FIELD assignment sites (non-test), at merge base e75a9040b:

git grep -nE "code(: | = )'INVALID_FIELD'" -- 'packages/**/*.ts' ':!**/*.test.ts'   → exit 0, 22 hits / 5 files
  13  packages/metadata-protocol/src/protocol.ts
   3  packages/objectql/src/engine.ts
   1  packages/objectql/src/summary-backfill.ts        ← new since triage's sweep (PR #15708 has landed)
   2  packages/rest/src/error-response.ts
   3  packages/services/service-analytics/src/analytics-service.ts

Triage read 21 in 4 files; the 22nd is summary-backfill.ts:297, the roll-up producer PR #15708 was adding at filing time. It is name-shaped like the rest ("names N roll-up(s) this run cannot find").

Every one of the 22 message texts was read. 22/22 are name/resolution-shaped — no site refuses a record field's value for having the wrong type, and none renders anything resembling Field 'age' must be a number. Two of the 22 (protocol.ts:9610, :9624) refuse the shape of the searchFields parameter value — still a list-of-field-names condition, not a record value's type; that nuance is not covered by the catalog's Cause clause and is filed separately as #15882, ⛔ not fixed here.

Sweep 2, type-shaped refusal text (non-test):

git grep -nE "must be a (number|string|boolean)" -- 'packages/**/*.ts' ':!**/*.test.ts'   → exit 0, 14 hits

None of the 14 carries INVALID_FIELD (checked ±25 lines around each hit; zero INVALID_FIELD occurrences in every window). The only hit that is both type-shaped and carries a wire code is packages/plugins/plugin-auth/src/auth-plugin.ts:2225, and it uses INVALID_REQUEST. The nearest thing to the deleted example — packages/spec/src/system/validation-message.ts:96, invalid_number: '{{label}} must be a number' — reaches ApiError.details.fields[].code and never error.code, as packages/runtime/src/dispatcher-error-vocabulary.ts:970-980 records for that exact key (ADR-0114 D2 / ADR-0112 D6). So the platform does have a "value has the wrong type" condition; it does not answer it with INVALID_FIELD.

Firing control (a grep that MUST hit, so the zero above is a reading and not a broken pattern):

git grep -n "Unknown field" -- packages/rest/src/error-response.ts   → exit 0, 2 hits (:1131, :1655)

premise_still_valid: true. The card's sharper reading holds: «Field value has wrong type» was a meaning without an emitter.

HTTP status — unchanged at 400, as the producers send it

Every one of the 22 sites sets status = 400 / status: 400. The REST mapping in packages/rest/src/error-response.ts:1149 reads:

if (error?.code === 'INVALID_FIELD' && !isSandboxOrigin(error)) {
    const name = error?.object ?? object;
    return { status: 400, body: { error: String(error?.message ?? 'Request references a field that does not exist'), code: 'INVALID_FIELD', ... } };
}

Before

#### `INVALID_FIELD`
**HTTP Status:** 400
**Meaning:** Field value has wrong type

**Example:**
    {
      "success": false,
      "error": {
        "code": "INVALID_FIELD",
        "message": "Field 'age' must be a number",
        "details": { "field": "age", "expected_type": "number", "actual_type": "string", "value": "twenty-five" }
      }
    }

After

#### `INVALID_FIELD`
**HTTP Status:** 400
**Meaning:** A field name in the request does not exist on the target object — a name that
resolves to nothing, not a value of the wrong type. On a list read it also covers an
unreserved query parameter, which `GET /data/:object` reads as a field filter.

**Example:**
    {
      "success": false,
      "error": {
        "code": "INVALID_FIELD",
        "message": "Unknown field 'age' on object 'contact'",
        "details": { "field": "age", "object": "contact" }
      }
    }

The [error catalog's `INVALID_FIELD` entry](/docs/api/error-catalog#invalid_field) carries
the authoritative cause text — it enumerates every read axis this one code answers on
(`select`, `expand`, `searchFields`, `groupBy`, `aggregations[].field`) and the
off-request `backfillSummaryNulls` case.

The example message is quoted from a real producer: packages/rest/src/error-response.ts:1655 builds Unknown field 'FIELD' on object 'OBJECT' (the object clause is appended when the object is known), rendered here with concrete values, and packages/objectql/src/engine.ts:1252 emits the identical sentence. The details keys are narrowed to the two the producer actually sets (field, object, per error-response.ts:1149); the invented expected_type / actual_type / value keys are gone. The block keeps its shape: status line, meaning, example JSON.

Verification

  • node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack → 39 commands derived. All 39 run; reconciliation with --ran: "Run reconciliation — 39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN."
  • pnpm check:doc-anchors → exit 0 — "✅ check-doc-anchors: 311 internal #fragment link(s) across 410 source file(s) all resolve to a real heading" (this covers the new #invalid_field link).
  • pnpm check:error-status-conformance → exit 0 — "✓ every derivable runtime status is documented, and every documented status is reachable." This gate reads content/docs/protocol/kernel/error-handling.mdx by name (scripts/check-error-status-conformance.mjs:219), so it is the gate that grades this page's status line.
  • pnpm check:nul-bytes → exit 0; plus a direct control-byte scan of the edited file, grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' → no hits.
  • pnpm --filter @objectstack/spec exec vitest run src/api/error-catalog-docs.test.ts → exit 0, 3 passed. (It reads only error-catalog.mdx, which this PR does not touch.)
  • Five gates first refused with PREREQUISITE NOT MET (exit 3 / a "not built" exit 1) and were re-run to a real verdict after turbo run build for the @objectstack/lint, @objectstack/formula and @objectstack/client-react closures: check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift, spec check:docs, spec check:skill-examples — all exit 0 afterwards.
  • NOT MEASURED, by name: the three families dispatch-gates declares as taking a value from the workflow — check-cross-package-test-inputs --union-into "$RUNNER_TEMP/…", check-shard-attestation --shard …, check-test-completeness "$RUNNER_TEMP/test-core.log". Their argv exists only inside a CI run.
  • Governed surfaces: node scripts/pm/check-governed-merges.mjs --test content/docs/protocol/kernel/error-handling.mdx → exit 0, "✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list."
  • Mergeability, via a sound probe only: git clone --bare --shared of the worktree, git --git-dir=probe.git config --get merge.os-regen.driver → empty (exit 1, no driver registered), then git merge-tree --write-tree ef3a1388d d56fb5dec → exit 0, tree 4769460269e32cffd083524eb3958a7df728e6cc. Clean against origin/main as of ef3a1388d.
  • Open-PR overlap: all 32 open PRs' file lists enumerated via GET /repos/objectstack-ai/objectstack/pulls/N/files. Zero open PR touches content/docs/protocol/kernel/error-handling.mdx. PR feat(objectql,cli): backfillSummaryNulls accepts recomputeUndefinedOnEmpty — a just-declared min/max/avg roll-up can be filled on request (#15064) #15708 has landed (it is no longer open; its producer is the 22nd site above) and its docs half was error-catalog.mdx, not this page. Three open PRs touch neighbouring error docs and none touches this file or the catalog's INVALID_FIELD entry: docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD #15750 and feat(spec)!: the twelve api/ duration keys carry their unit in the key name (#15677, stack 2/6) #15837 (error-catalog.mdx), fix(runtime): refuse POST /packages/:id/duplicate on a source that is not a base #15849 (error-code-ledger.mdx).

Docs-only, no user-visible package change ⇒ skip-changeset.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

`error-handling.mdx` published «Field value has wrong type» with the example
message `Field 'age' must be a number`, while `error-catalog.mdx` publishes the
name/resolution meaning. A repo-wide producer sweep finds 22 assignment sites in
5 files, all name/resolution-shaped, and none of the 14 type-shaped refusal
texts carries `INVALID_FIELD` — so the type meaning had no emitter and the
example message quoted no producer.

Rewrite only the `#### INVALID_FIELD` block: the Meaning now matches the
catalog, and the example message is the real producer text from
`packages/rest/src/error-response.ts:1655` (`Unknown field '<field>' on object
'<object>'`) with the `field`/`object` details that producer actually sets.
Status stays 400, as every producer sets and `error-response.ts` maps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Flipped ready + auto-merge enabled (12:47Z, method: MERGE). All seven required contexts on d56fb5dec read non-failing: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Governed Surface Queue Guard success; Build Core, Temporal Conformance (live PG + MySQL) skipped; no other red. Mergeability through the sound bare-clone probe (no driver) → EXIT=0 against b398ad258. Watched; on landing #15800 closes.


Generated by Claude Code

Merged via the queue into main with commit 4f37912 Sep 5, 2026
37 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15800-invalid-field-one-meaning branch September 5, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants