Skip to content

The 400 that four comments cite as the $searchFields half of the __search contract is asserted by no test #8080

Closed

Description

@os-zhuang

Observation-class finding, noticed while implementing #7876 (PR #8079). Nothing is broken today — filing it so the triage round can grade it rather than sitting on it.

What is unasserted

"A $searchFields override naming __search is refused with a 400 ("is hidden")" is now stated in four places in the tree:

No test asserts it. Re-check:

git grep -rln "searchFields" -- packages | grep test | xargs grep -l "__search"

returns three files, and in all three the two names never meet in an assertion — search-companion.test.ts:173 pins the engine-side half (resolveSearchFields will not admit the column even when requestedFields names it), which is a different fact from the ingress gate's refusal.

Why it is only observation-class

The 400 does follow from two facts that ARE pinned, composed:

  1. resolveSearchFields excludes hidden fields (packages/spec/src/data/search-fields.ts:112), pinned by search-companion.test.ts:173;
  2. assertSearchFieldsAreSearchable refuses a known-but-unsearchable name with 400 INVALID_FIELD (packages/metadata-protocol/src/protocol.ts), pinned by query-expression-conformance.test.ts:1390 — on estimate, not on __search.

So the behaviour is real and reachable; what is missing is a test that names this column and asserts the ADR-0112 envelope (code AND status) on it.

Why it may still be worth closing

#7876's ruling makes the 400 load-bearing as a contrast: the projection door's silence is now documented as correct precisely because the authoring door refuses. A future change that let hidden columns clear the searchability gate would leave all four comments false with every suite green, and the one suite whose stated job is "on ANY door" is the one whose docblock asserts it in prose only.

Suggested shape if taken: one case in search-companion-read-projection-conformance.test.ts's matrix asserting code: 'INVALID_FIELD' and status: 400 for a searchFields override naming the companion.

Related: #7876 (the ruling) · PR #8079 (the comments) · #7642 (the projection strip) · #4254 (the gate)

Activity

  1. added theissue type on Aug 12, 2026
  2. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Finding-grading round: promoted to pm:queue (domain:engine-core, type Task).

    The decision it depended on is settled — #7876 closed via PR #8079 (option C), which makes the 400 load-bearing as the documented contrast. Verified: no test anywhere asserts INVALID_FIELD + status: 400 for the $searchFields half (prose only, three files). Deliverable is a test-only pin: one matrix case asserting code: INVALID_FIELD + status: 400.


    Generated by Claude Code

  3. self-assigned this
    on Aug 12, 2026
  4. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_014C8pAprWdmtecFsEprZax4
    Branch: claude/issue-8080-searchfields-hidden-400-pin
    Worktree: objectstack-issue-8080
    Domain: domain:engine-core
    File surface: packages/objectql/src/search-companion-read-projection-conformance.test.ts — test-only (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus
    Serial constraints cleared: in-flight #8118 (objectql/src accessor + plugin-webhooks) and #7168 (packages/formula) — neither touches this file. ⚠️ Just-landed #7876 (d91c558e0) wrote the two comments this card is about; merge origin/main first so the prose being pinned is the prose on main.

    ⚠️ Provenance worth stating: this card was filed by this lane's own #7876 dev as an out-of-scope finding, and triage promoted it at 17:59Z. ⛔ That does not make it pre-approved — it gets the same premise re-check as any other card, and the re-check command is on the card.

    Batch raised 3 → 5 for this round on the maintainer's 「继续加速处理」. ⛔ That widens the pipe, not the bar: every card still gets its full read, its own serial clearance, and its model chosen by the same criteria.

    Seat #6019.


    Generated by Claude Code

  5. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 8080,
      "status": "done",
      "branch": "claude/issue-8080-searchfields-hidden-400-pin",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8191",
      "premise_still_valid": true,
      "summary": "Premise re-verified on fresh origin/main (a0151e98e, with #7876's d91c558e0 an ancestor — no merge needed, the worktree branched from the post-#7876 tip): the card's own re-check command still returns exactly three files, and in none of them do `searchFields` and `__search` meet in an assertion. Added one case to `packages/objectql/src/search-companion-read-projection-conformance.test.ts` asserting `code: 'INVALID_FIELD'` + `status: 400` (plus `field`, `object`, `param: '$searchFields'` and the `/'__search' is hidden/` reason the four docblocks quote) on a `$searchFields` override naming the companion. Placed in the file the card names, but as a sibling `describe` rather than a row in the `describe.each` matrix — the matrix drives `engine.find` directly while the refusal lives at the metadata-protocol REST ingress, and the matrix's two provisioning states do not give the same answer (undeclared, the column is absent from the field map, so the gate refuses it as *unknown*, not 'hidden'). The surrounding matrix is untouched. One file, +91 lines, test-only.",
      "tests": "REACHES THE REAL GATE — proved by two reverse verifications, each direction predicted RED before running, each observed RED. (A) `hidden: false` on the provisioned companion column in `search-companion.ts`: the new case went RED ('promise resolved \"{ object: 'showcase_contact', ...(3) }\" instead of rejecting'), the other 24 cases in the file stayed green — and the resolved body still had no `__search` key, i.e. the projection door stayed silent while the authoring door failed open, which is exactly the #7876 contrast. (B) surgical neuter INSIDE `assertSearchFieldsAreSearchable` (metadata-protocol) letting hidden names clear the gate, package rebuilt: new case RED again, proving the assertion reaches the ingress gate in the other package's built dist rather than a nearer refusal. Under (B) `query-expression-conformance.test.ts` stayed 130/130 green — its `estimate`/`id` gate pins cannot see a hidden-column regression, so the new case was the only thing in the repo that went red. That is the blind spot this card describes, demonstrated. Both neuters restored, metadata-protocol rebuilt, suite green again (25/25). GREEN RUNS: `pnpm --filter @objectstack/objectql test` 191 files / 3391 tests all pass; `pnpm --filter @objectstack/objectql typecheck` clean; `pnpm check:query-options-erasure` 'ratchet holds: 67 unswept non-test site(s) ... none new. baseline key set verified against a0151e9: no files added'; `pnpm check:type-check-debt --re-measure` '33 ledger entr(ies) re-measured, 1966 raw tsc error(s) total, none above its recorded number' (no baseline raised; `--lower` NOT run); `pnpm check:nul-bytes` OK (7422 files); `pnpm check:durability-log-level` OK; `pnpm check:engine-double-contract` OK (175 pinned); `scripts/check-engine-split-ratio.mjs` OK. Gates derived at implementation time via `node scripts/pm/dispatch-gates.mjs` on the real file surface. Neither of the two same-day drift causes (missing `packageId`, missing `.js` under nodenext) appeared — objectql typecheck is clean and its debt entry did not move. `skip-changeset` applied and READ BACK: labels are exactly ['skip-changeset'] (it was the only label, so no bot write was wiped). Issue read via MCP GitHub tools (no raw HTTP); body complete through its closing 'Related:' line, both comments read.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPTED on substance — PR #8191. ⛔ Ready-flip held: CI started 19:24Z and is barely scheduled.

    Engine-core seat #6019, session_014C8pAprWdmtecFsEprZax4.

    ⭐ "Prove it reaches the real gate" was answered in the strongest available form

    I wrote that a pin which passes without reaching the gate is this card's own defect one level up. The dev did not argue the point — it broke the gate twice, in two different places, predicting RED each time:

    neuter where result
    A hidden: false on the provisioned companion column (search-companion.ts) new case RED; the other 24 in the file stayed green
    B surgical neuter inside assertSearchFieldsAreSearchable (metadata-protocol), package rebuilt new case RED again

    (B) is the one that settles it. It proves the assertion reaches the ingress gate in another package's built dist, not some nearer refusal that would have satisfied the test for the wrong reason. ⛔ Without it, "the case is red when I break the thing" is compatible with the case being red for a reason nobody checked.

    ⭐ And (A) produced an unrequested confirmation of the ruling this card exists to protect: with the authoring door failing open, the resolved body still had no __search key — the projection door stayed silent while the authoring door let the name through. That is #7876's direction-C contrast, observed rather than restated.

    ⭐ The blind spot is now demonstrated, not merely asserted

    Under neuter (B), query-expression-conformance.test.ts stayed 130/130 green. Its estimate / id gate pins cannot see a hidden-column regression, so the new case was the only thing in the repo that went red.

    That is precisely the gap the card was filed about, turned from a claim into a measurement. ⛔ Nothing in the tree was watching this contract before; exactly one thing is now.

    The placement was moved, with a measured reason

    The card suggested a row in the describe.each matrix. The dev put it in a sibling describe instead, because the matrix drives engine.find directly while the refusal lives at the metadata-protocol REST ingress — and because the matrix's two provisioning states do not give the same answer: undeclared, the column is absent from the field map, so the gate refuses it as unknown, not "is hidden".

    ⭐ That is the "say why you moved it" I asked for, and the reason is better than the suggestion: a row in that matrix would have asserted the wrong refusal in one of its two states. The surrounding matrix is untouched.

    Verified independently

    check reading
    premise re-checked on fresh origin/main (a0151e98e, with #7876's d91c558e0 an ancestor — no merge needed, the worktree branched from the post-#7876 tip). The card's own re-check command still returns exactly three files, and in none do searchFields and __search meet in an assertion
    assertion content code: 'INVALID_FIELD' + status: 400, plus field, object, param: '$searchFields' and the /'__search' is hidden/ reason the four docblocks quote — so the pin matches the prose it protects, ⛔ not just the status code
    path face one file, packages/objectql/src/search-companion-read-projection-conformance.test.ts, test-only, +91 — ⛔ no docs/adr/**, no skills root, ⛔ no cross-seat crossing. The metadata-protocol neuter was temporary and restored, with the package rebuilt and the suite green again (25/25)
    scope ⛔ one assertion, matrix untouched, refusal untouched — the fence held
    changeset skip-changeset, read back: labels are exactly ['skip-changeset'], and the dev noted it was the only label so no bot write was wiped
    ratchets both green; ⛔ no baseline raised, --lower not run. ⭐ Neither of the two same-day +N drift causes appeared — it checked for them specifically
    fetch path read via MCP tools, ⛔ no raw HTTP (which 403s in this fleet), body confirmed complete through its closing Related: line

    Why it is not flipped

    At ~19:2xZ: filter and a handful of cheap jobs concluded success, 0 failure — but TypeScript Type Check, ESLint, all three Test Core shards, all three Dogfood shards, Build Core and Temporal Conformance are in_progress, having started 19:24Z. ⛔ A run this early is not a green run. Next patrol reads each job's own conclusion; all-green ⇒ ready + enqueue SQUASH.


    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