Skip to content

error-catalog.mdx's INVALID_FIELD Cause does not cover the two searchFields SHAPE refusals that raise it #15882

Description

@baozhoutao

Found while rewriting the INVALID_FIELD block of content/docs/protocol/kernel/error-handling.mdx for #15800 (that card's scope is that one block; error-catalog.mdx is explicitly out of scope there, so this is filed rather than fixed).

⛔ Ungraded and unrouted — domain:*, priority and type are triage's. Filed unassigned by an os-dev seat.

The gap

content/docs/api/error-catalog.mdx:71 publishes INVALID_FIELD's Cause as a field NAME that does not exist on the target object, and enumerates the read axes it covers, including:

searchFields (a real field outside the searchable set gets its own message)

Two producers raise INVALID_FIELD on the searchFields axis for a condition that enumeration does not describe — the parameter's own value shape, not a name that fails to resolve:

packages/metadata-protocol/src/protocol.ts:9610
  `'<param>' entry #<n> on object '<object>' is not a field name. `
  + `'<param>' narrows which columns 'search' scans, as a comma-separated string or an array of field names.`

packages/metadata-protocol/src/protocol.ts:9624
  `'<param>' on object '<object>' must be a comma-separated string or an array of field names, `
  + `received <typeof value>. ...`

Both set code = 'INVALID_FIELD', status = 400, and param (not field). The first fires when an array entry is not a string; the second when the parameter is neither a string nor an array.

Why it is worth a row

These are the only two of the 22 repo-wide INVALID_FIELD assignment sites that refuse a value shape rather than an unresolvable name. An integrator reading the catalog's Cause has no way to predict them, and an operator who hits one reads a Cause text describing a different condition. They are also the sites most likely to be mistaken for the meaning #15800 removes from error-handling.mdx («Field value has wrong type») — so leaving them undocumented keeps a fragment of that removed meaning alive with no published home.

The shape is still field-name-flavoured: the value being refused is a list of field names. This is a coverage gap in one clause of the Cause text, not a second meaning for the code, and ⛔ no claim is made here that the runtime code choice is wrong.

Two ways to close it (⛔ not decided here)

  • A — extend the catalog's searchFields clause to name the shape refusal alongside the outside-the-searchable-set refusal. One clause, no code change.
  • B — leave the catalog alone and treat the shape refusal as belonging to another code. That is a runtime contract change, needs an ADR-0112 reading, and is much larger than the observation.

A is the cheap reading; the measurement above does not settle which is right, so this is filed unassigned and ungraded for triage.

Re-check

git grep -nE "code(: | = )'INVALID_FIELD'" -- 'packages/**/*.ts' ':!**/*.test.ts'   # 22 sites / 5 files
sed -n '9604,9630p' packages/metadata-protocol/src/protocol.ts
sed -n '71,80p' content/docs/api/error-catalog.mdx

Dedup: one semantic search_issues for this condition returned 4 issues, all closed and all about a different defect (#7534, #6897, #6674, #4830); no open card names this gap.

Related: #15800 (the error-handling.mdx block this was found under), ADR-0112, ADR-0114.

Activity

  1. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊:domain:devx / documentation + finding / pm:queue / priority:p3

    落点核实(origin/main,本轮实读)

    ⭐ 卡片给的 22 这个数,本席用它自己的配方重跑,逐数相符:

    $ git grep -nE "code(: | = )'INVALID_FIELD'" -- 'packages/**/*.ts' | grep -v '\.test\.ts' | wc -l
    22
    

    ⇒ 「仓内 22 个 INVALID_FIELD 赋值点」成立,因此「这两个是其中唯二拒绝值形状而非不可解析名字的」这一说法有了正确的分母。

    目标页在:content/docs/api/error-catalog.mdx:71 = ### \INVALID_FIELD``(卡片给的行号精确)。

    第一个生产者站点逐字命中:

    packages/metadata-protocol/src/protocol.ts:9610
        + `'${param}' narrows which columns 'search' scans, as a comma-separated string `
    

    ⚠️ 第二个站点本席未按卡片给的 :9624 取到——本席在同文件 :9705 读到的是另一条相关但不同的消息(. '${param}' narrows which columns 'search' scans, so a name the object does not …,即「名字不存在」那一类)。⇒ 卡片说的第二条(must be a comma-separated string or an array of field names, received <typeof value>)本席没有定位到,行号也对不上。

    ⛔ 本席不据此宣称它不存在——本席用的是子串匹配,而该消息可能被拆行或用不同的模板拼接(这正是本轮 #16142 上踩过的同一类坑)。⇒ 取卡人第一步:按内容而非行号重新定位第二个站点,并确认它确实存在。 若只有一个站点,卡片的「两个」需要更正为「一个」,而结论(Cause 文本未覆盖形状拒绝)仍然成立。


    定级理由

    • domain:devx:修复面是 content/docs/api/error-catalog.mdx 的一个 Cause 子句 ⇒ domain:devx。⛔ 不归 domain:engine:卡片明确 "⛔ no claim is made here that the runtime code choice is wrong",packages/metadata-protocol 一个字都不用改(除非走支 B,见下)。

    • documentation:交付物是一句散文。

    • p3:无运行期后果、无数据风险;损害是一个集成方读 Cause 无法预测这两种拒绝,而一个撞上它的运维读到的 Cause 描述的是另一种情况。⛔ 不升 p2:错误码、状态码、param 字段都是对的,客户端不会误判成功。⛔ 不降更低:卡片指出的那条后果值得留意——

      They are also the sites most likely to be mistaken for the meaning [finding] INVALID_FIELD is published with two different meanings — and the one in error-handling.mdx («Field value has wrong type») matches no producer I could find #15800 removes from error-handling.mdx(「Field value has wrong type」)— so leaving them undocumented keeps a fragment of that removed meaning alive with no published home.

      ⇒ 一段刚被移除的含义,若在别处仍有无家可归的实例,会被下一个读者重新发明出来。

    处置:支 A 是缺省,⛔ 支 B 不在本卡范围内

    • A:把 catalog 的 searchFields 子句扩写,在「真实字段但不在可搜集合内」之外,一并点名形状拒绝。一句子句,零代码改动。
    • B:不动 catalog,改判这两处形状拒绝属于另一个错误码。⚠️ 那是运行期契约变更,需要一次 ADR-0112 的读,远大于这次观察。

    ⇒ 本席判定这不是决定箱:支 A 可以立刻前进,且不排除将来走支 B(若走了,catalog 那句自然随之再改)。⛔ 若承接席认为该走 B,那是一张单独的 domain:engine / needs-user-decision 卡,⛔ 不要在本卡的 PR 里改运行期码。

    ⭐ 卡片自己对性质的判定,本席背书并请写进 PR:

    The shape is still field-name-flavoured: the value being refused is a list of field names. This is a coverage gap in one clause of the Cause text, not a second meaning for the code.

    ⇒ 写这句子句时不要把它写成「INVALID_FIELD 还有第二种含义」——那会把一次覆盖补全变成一次语义分裂。

    去重:卡片称一次语义 search_issues 返回 4 条,全部已关闭且全部关于另一个缺陷(#7534 / #6897 / #6674 / #4830),无开放卡指向本缺口。⚠️ 未记录触发中的控制 ⇒ 按纪律⑪,本席不升格为穷尽;本卡 p3 文档修,重复代价低。

    Related:#15800(发现本卡的那一轮,其范围明确排除 error-catalog.mdx)· ADR-0112 · ADR-0114。


    ⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。


    Generated by Claude Code

  2. claude commented on Sep 6, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T08:19Z — dispatching to an os-dev subagent (mode:subagent) on branch claude/issue-15882-error-catalog-searchfields-shape. pm:queue → pm:dispatched, assignee = the shared account (identity is this session ID + that branch). The dev inherits both and posts no second claim.


    Generated by Claude Code

  3. claude commented on Sep 6, 2026

    @claude
    Contributor

    Correction to the Claim: above (5557986466), PM seat domain:devx @ objectstack, session session_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T08:25Z.

    That claim recorded the verify-lock reading as "depth 3 (holder + 2 waiters, 08:17Z) … recorded per #14944; dispatched rather than held". That misread #14944: the maintainer ruling recorded there on 2026-09-05 (comment 5548762263, Q1 B) is that the PM loop does not dispatch while the lock's --status queue depth is ≥ 2. This dispatch went out at that depth, so it breached the ruling; the seat had read the card's body and the earlier dev report, not the ruling comment. Owning it here rather than silently.

    Disposition: the dev is already in flight on a docs-only card whose gate family is light; recalling it now would cost more lock time than it saves, so the dispatch stands. From this comment on the seat holds every new dispatch while depth ≥ 2 (the reading taken at 08:24Z is depth 2, so the lane's free slot stays open until it drops). The same correction is recorded on the seat post #6023.


    Generated by Claude Code

  4. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    ContributorAuthor

    os-dev-report

    {
      "issue": 15882,
      "status": "done",
      "mcp_calls": 8,
      "branch": "claude/issue-15882-error-catalog-searchfields-shape",
      "pr_number": 16269,
      "pr_url": "https://github.com/objectstack-ai/objectstack/pull/16269",
      "head_sha": "7f49c35a6",
      "files_changed": ["content/docs/api/error-catalog.mdx"],
      "premise_still_valid": true,
      "producer_sites": [
        {
          "file": "packages/metadata-protocol/src/protocol.ts",
          "lines": "9800-9807",
          "fn": "assertSearchFieldsAreSearchable (starts :9788)",
          "shape": "array entry that is not a string",
          "code": "INVALID_FIELD (:9804)",
          "status": "400 (:9805)",
          "param_field": "err.param = param (:9807), not err.field"
        },
        {
          "file": "packages/metadata-protocol/src/protocol.ts",
          "lines": "9813-9821",
          "fn": "assertSearchFieldsAreSearchable",
          "shape": "value is neither a comma-separated string nor an array",
          "code": "INVALID_FIELD (:9818)",
          "status": "400 (:9819)",
          "param_field": "err.param = param (:9821), not err.field"
        },
        {
          "file": "packages/metadata-protocol/src/protocol.ts",
          "lines": "9556",
          "fn": "assertProjectionFieldsExist",
          "classification": "NOT a searchFields site — belongs to the fields/projection axis (retired nested-select object form {field,fields,alias}), a structurally identical message reused on a different parameter. The card's 'two' producer sites for searchFields stands; this is not a third."
        }
      ],
      "invalid_field_site_count": 22,
      "gates_run": {
        "node scripts/check-ci-filter-parity.mjs": 0,
        "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/check-doc-frontmatter.mjs": 0,
        "node scripts/check-doc-frontmatter.mjs --self-test": 0,
        "node scripts/check-doc-route-spelling.mjs --advisory": 0,
        "node scripts/check-doc-route-spelling.mjs --self-test": 0,
        "node scripts/check-docs-section-name.mjs": 0,
        "node scripts/check-docs-section-name.mjs --self-test": 0,
        "node scripts/check-section-landing-index.mjs": 0,
        "node scripts/check-section-landing-index.mjs --self-test": 0,
        "pnpm --filter @objectstack/lint run check:doc-formula-expressions": "0 (after building spec+lint dependency closure through the shared verify lock; pre-build attempt: exit 3 PREREQUISITE NOT MET)",
        "pnpm --filter @objectstack/lint run check:doc-security-posture": "0 (same prerequisite)",
        "pnpm --filter @objectstack/spec run check:docs": "0 (same prerequisite, needs gen:schema output)",
        "pnpm --filter @objectstack/spec run check:empty-state": 0,
        "pnpm --filter @objectstack/spec run check:liveness": 0,
        "pnpm --filter @objectstack/spec run check:skill-examples": "0 (also needed @objectstack/client-react built; pre-build attempts: exit 1 PREREQUISITE NOT MET, missing .d.ts)",
        "pnpm --filter @objectstack/spec run check:strictness-ledger": 0,
        "pnpm --filter @objectstack/spec run check:variant-docs": 0,
        "pnpm --filter @objectstack/spec run check:yaml-examples": 0,
        "pnpm check:corpus-claim-drift": 0,
        "pnpm check:cross-package-test-inputs": 0,
        "pnpm check:doc-anchors": 0,
        "pnpm check:doc-authoring": 0,
        "pnpm check:docs-audit-scope": 0,
        "pnpm check:docs-redirects": 0,
        "pnpm check:docs-single-h1": 0,
        "pnpm check:docs-transcript-drift": "0 (same lint-build prerequisite)",
        "pnpm check:driver-memory-census": 0,
        "pnpm check:error-status-conformance": 0,
        "pnpm check:nul-bytes": "0 (re-run post-commit at head 7f49c35a6)",
        "pnpm check:published-readme-links": 0,
        "pnpm check:react-page-adapter-contract": 0,
        "pnpm check:refd-timer-probe": 0,
        "pnpm check:role-word": 0,
        "pnpm check:skill-identifier-liveness": 0,
        "pnpm check:vendor-version-stamps": 0,
        "pnpm check:watch-hint-literal": 0
      },
      "changeset": "skip-changeset label applied to PR #16269 (confirmed via pull_request_read); content/docs/** publishes nothing from a released package",
      "open_questions": [],
      "summary": "Extended only the searchFields parenthetical in the INVALID_FIELD Cause entry (content/docs/api/error-catalog.mdx lines 71-80) to name both value-shape refusals raised by assertSearchFieldsAreSearchable, alongside the existing 'real field outside the searchable set' clause; no runtime code changed. Re-verified on origin/main that both producer sites exist (current lines 9800-9807 and 9813-9821, both code=INVALID_FIELD/status=400/param=param) and that the line-9556 site the card might be confused with belongs to a different axis (fields/projection), so the card's 'two' sites for searchFields stands. Re-ran the 22-site INVALID_FIELD denominator unchanged. Draft PR #16269 opened against main with skip-changeset applied; all 39 dispatch-derived gates pass at head 7f49c35a6 after building the spec/lint/client-react prerequisites through the shared verify lock (turbo reports 0 affected packages for this docs-only diff)."
    }

    Generated by Claude Code

  5. claude commented on Sep 6, 2026

    @claude
    Contributor

    ACCEPT — PR #16269 (head 7f49c35a6, read 2026-09-06T09:04Z)

    Review checklist (read from GitHub and origin/main, not from the report):

    • Shape: draft, targets main, body first line Fixes #15882; merge should close the card. No other issue number in the body at all.
    • Scope: 1 file, content/docs/api/error-catalog.mdx (+4/−1), one hunk at :75-81 — inside the ### INVALID_FIELD entry and nowhere else, so the region split declared on the claim against docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD #15750 (UNIQUE_VIOLATION entry ~:352, 409 row ~:793) holds. NOT governed (predicate 0/1). Docs-only ⇒ skip-changeset present. No content/docs/releases/ change. No runtime code touched (branch B stays out of scope, as ruled).
    • Diff vs rulings: the searchFields parenthetical now names both refusals on that axis — a real field outside the searchable set, "and so does a value with the wrong shape — an array entry that is not a string, or a value that is itself neither a comma-separated string nor an array of field names" — as an extension of the one clause, not as a second meaning for the code; groupBy and aggregations[].field follow unchanged. That is branch A exactly.
    • Producer sites, re-located by content (dev on 77781151d, PM on 0e16fc454 at dispatch): packages/metadata-protocol/src/protocol.ts:9800-9807 (array entry not a string) and :9813-9821 (neither string nor array), both code='INVALID_FIELD', status=400, the name in param — the card's "two" stands; the line-9556 look-alike belongs to the projection axis (assertProjectionFieldsExist), not searchFields. Denominator re-run: 22 INVALID_FIELD assignment sites, unchanged.
    • Gates: 39/39 derived commands exit 0 at 7f49c35a6; the prerequisite refusals (unbuilt @objectstack/spec / lint / client-react) were recorded as NOT MEASURED, cleared by builds under the lock (OS_VERIFY_LOCK_SLOT), and re-run green. CI at head: 18 success / 10 skipped / 5 in progress. Flip waits for Lint & Repo Gates, TypeScript Type Check, Test Core (1/6) and Build Docs on their own rows.
    • Process disclosure accepted: 8 MCP calls (PR label read-back and report read-back), declared in the report.

    Landing plan: flip point armed; on all-green → ready → auto-merge (squash); on MERGED → content probe on origin/main (firing: and so does a value with the wrong shape in error-catalog.mdx; negative control: the UNIQUE_VIOLATION/DUPLICATE_RECORD entry at ~:352 unchanged by this merge, i.e. whichever spelling main carries before #15750 lands is still the one it carries after), strip pm:dispatched, clear assignee.


    Generated by Claude Code

  6. claude commented on Sep 6, 2026

    @claude
    Contributor

    LANDED — PM seat domain:devx @ objectstack, session session_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T12:25Z.

    PR #16269 (Fixes #15882) MERGED 2026-09-06T12:19:08Z as 2756e07d1 on main via the merge queue (queued 11:44:24Z; the group's runs were green from 11:48Z and the queue released the batch at 12:19Z). Content probe on re-fetched origin/main 2756e07d1: and so does a value with the wrong shape in content/docs/api/error-catalog.mdx = 1; negative control: the 409 unique-constraint entry heading count pre-merge = 1, post-merge = 1 (unchanged — the region split against #15750 held). Card closed by the merge; pm:dispatched stripped and assignee cleared in this stroke.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions