Repository navigation
Description
Filed by the domain:engine-core seat (#6019, session session_01VGAePF7iGGUYUT8oX1cVgx) from PR #7868's open question 2. ⛔ Nothing shipped depends on this — #7868 landed (8b90d68) and is coherent either way. This is about whether two doors should agree.
The question
A non-system caller names the hidden __search companion column explicitly. Two doors, one column, two different answers:
| door | spelling | answer today |
|---|---|---|
$searchFields override |
__search |
400 — "is hidden" |
projection / select |
?select=__search |
silent drop — 200, key absent (as of #7868) |
Should the projection door also refuse with a 400, or is silence correct there?
Premises, each with its re-check command
$searchFieldsrefuses the spelling with a 400 —git grep -n "is hidden" origin/main -- packages/objectql/src- The projection door drops it silently, added by fix(objectql): strip the hidden
__searchcompanion from every record body (#7642) #7868 —git grep -n "stripSearchCompanionFromRead" origin/main -- packages/objectql/src/engine.ts assertProjectionFieldsExistgates only on whether a field is known, not on whether it is returnable —git grep -n "assertProjectionFieldsExist" origin/main -- packages/objectql/src- A system caller naming it still receives it (the
plugin-pinyin-searchbackfill depends on this) —git grep -n "isSearchCompanionRequested" origin/main -- packages/objectql/src
Options
A — keep the silent drop (status quo, shipped).
The projection door's existing contract for a field it will not return is silence, not refusal. A 400 here is a new refusal on a spelling that used to answer 200.
Cost: the two doors keep disagreeing, and a client that asks for the column gets a 200 whose body silently lacks what was asked for.
B — make the projection door refuse too (400 "is hidden").
One column, one answer, whichever door you knock on. Matches how the platform already answers this exact spelling elsewhere.
Cost: a behaviour change on a request that currently succeeds; anything that today passes select=__search and tolerates the missing key would start failing. Needs the system-caller carve-out preserved.
C — rule that the divergence is intended and record why.
The two doors mean different things: $searchFields is authoring input (a wrong value is an authoring error), select is a read projection (a field you cannot see is simply not in the body).
Cost: none mechanically; the cost is that the next reader re-asks this question unless the reasoning is written where they will meet it.
Recommendation: C, with the reasoning pinned in the code.
The divergence looks accidental but is defensible: the two doors are different kinds of surface, and B changes a currently-succeeding request for tidiness rather than for a user-visible defect. But C is only worth anything if the reasoning is written at stripSearchCompanionFromRead and at the $searchFields refusal, so this does not get re-litigated.
plugin-pinyin-search's backfill projects ['id', …sources, '__search'] under isSystem and compares stored-vs-recomputed; breaking it makes the walk rewrite every row of every object on every pass.
Four-lens
- Platform long-term coherence — B shrinks special-casing (one column, one answer); C keeps two rules but gives them a stated reason. A leaves an unexplained inconsistency, which is the worst of the three for coherence.
- Measured business pull — zero today. No in-tree caller passes
select=__search; the column is a private index and the only deliberate reader is the system backfill. Under the zero-pull default this is defer-or-record, not build — which is what makes C the proportionate answer and B the one that needs a reason beyond symmetry. - AI-agent error-resistance — B is strongest: a loud refusal on a spelling that cannot work is much harder for an AI author to misuse than a 200 whose body quietly lacks the requested key. This is the one lens that genuinely favours B, and it is the reason this is a decision rather than a shrug.
- Startup scope discipline — C costs two comments; B costs a behaviour change plus a regression surface on a zero-pull path. Cheapest honest answer wins unless lens 3 outweighs it.
Related: #7642 (origin) · PR #7868 (8b90d68) · #2486 (the companion column).
Activity
huangyiirene commentedon Aug 12, 2026
Maintainer ruling — 2026-08-12
裁定:方向 C —— 分歧是有意的,把理由钉进代码。
要点:
$searchFields是作者输入(错值是作者错误,400 正确),select是读投影(不可见的字段就是不在 body 里,静默正确)。stripSearchCompanionFromRead和$searchFields的拒绝处各写一条注释,把上述理由与本裁定钉住,防止下一个读者重开这题。plugin-pinyin-searchbackfill 依赖isSystem下投影__search)不许碰。select=__search(零业务拉力)—— 若未来有真实调用方被静默丢弃坑到,再以那次实测为据重议 B。裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话
session_01GZKbx4xyF7U5WXj6ch49BM代笔落卡。转pm:queue(两条注释,小卡)。Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
Claim: PM loop round 1
Session:
session_014C8pAprWdmtecFsEprZax4Branch:
claude/issue-7876-search-door-divergence-pinWorktree:
objectstack-issue-7876Domain:
domain:engine-coreFile surface:
packages/objectql/src/engine.ts(stripSearchCompanionFromRead,:4794-4860) +packages/objectql/src/search-companion.ts(:224) — comments only (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opusSerial constraints cleared:
engine.ts— sites measured onorigin/mainat:4794-4860/:7035/:7126/:7270; in-flight #5320 at:3813(gap 981), in-flight #7933 at:9257(gap 1,987), unlanded PR #7989 at:7922-8727(gap 652 from the nearest site). No pair can become one hunk at three lines of context.search-companion.tsis free — #7642 and #7641 both landed.needs-user-decision→pm:queueswap that followed was correct. The ledger went stale against a ruling recorded by a different session under the same account. Same correction applies to #7865 and #7378; #7929 is not this lane's at all (domain:drivers). The lane's decision box is empty.Seat #6019,
domain:engine-core. The 03:05Z ruling is carried into the dispatch verbatim and is not reopenable.Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
{ "issue": 7876, "status": "done", "branch": "claude/issue-7876-search-door-divergence-pin", "pr": "https://github.com/objectstack-ai/objectstack/pull/8079", "premise_still_valid": true, "summary": "Direction C implemented as two comments, zero behaviour change (54 added lines, every one a continuation line inside an existing block comment, zero deletions). Comment 1 sits at `ObjectQL.stripSearchCompanionFromRead` as the ruling directs; comment 2 sits at the ACTUAL `$searchFields` refusal, which is `assertSearchFieldsAreSearchable` in `packages/metadata-protocol/src/protocol.ts` — not in objectql at all, so the PM's assumption that `search-companion.ts:224` might be the second home was falsified. That file turned out to be the better place for the ruling anyway: `assertProjectionFieldsExist`, the projection door's own gate, is 220 lines above it in the same file, so the reader comparing the two axes is standing exactly where the question forms. The PM's other hypothesis was half right — engine.ts:4830 does already name the 400, and the carve-out bullet at :4848 already explains why a non-system caller does not KEEP the column, but neither answers why that is silence rather than a refusal, so I extended rather than duplicated and left search-companion.ts:224 untouched.", "tests": "pnpm --filter @objectstack/objectql test -- --maxWorkers=2 → 'Test Files 189 passed (189) / Tests 3356 passed (3356)'. pnpm --filter @objectstack/metadata-protocol test -- --maxWorkers=2 → 'Test Files 75 passed (75) / Tests 1094 passed (1094)'. pnpm --filter @objectstack/objectql typecheck → 'Done' (metadata-protocol declares no typecheck script). npx eslint on both changed files → exit 0. Gates re-derived at implementation time for the REAL two-file surface via `node scripts/pm/dispatch-gates.mjs packages/objectql/src/engine.ts packages/metadata-protocol/src/protocol.ts` — adding protocol.ts pulls in one gate the dispatch list did not have, check:cross-package-test-inputs. All green: check:adr-anchors ('OK (48 anchored file(s), every governing ADR still referenced; ... 22789 citation(s) across 3807 file(s) resolve)', exit 0), check:durability-log-level, check:engine-double-contract ('OK — 166 pinned, 133 in the DEBT ledger, 2 exempt'), check:stack-collection-maps, check:cross-package-test-inputs, check:nul-bytes ('OK (scanned 7336 text file(s) ... no raw ASCII control bytes)'), node scripts/check-engine-split-ratio.mjs. Plus a control-byte self-scan over both changed files (grep -naP over the C0 range + DEL): clean. The objectql suite is unchanged by construction, not just by count: no line was deleted and every added line is a block-comment continuation, so the token stream the compiler sees is identical.", "open_questions": [], "out_of_scope_findings": [ "filed as #8080: the 400 that four in-tree comments cite as the `$searchFields` half of this contract is asserted by no test — it follows by composition of two pinned facts (`resolveSearchFields` excludes hidden fields; the gate refuses known-but-unsearchable names), but nothing names `__search` and asserts the code+status envelope on it. Observation-class, `finding` label, no `pm:queue`, unassigned." ], "comments_verbatim": { "packages/objectql/src/engine.ts — stripSearchCompanionFromRead": " * ## Why this door is SILENT where the `$searchFields` door returns a 400\n *\n * Asked on #7876 and ruled there on 2026-08-12 (direction C): the divergence\n * is intended, and it is not reopenable on symmetry alone. The two doors are\n * two KINDS of surface.\n *\n * - `$searchFields` is AUTHORING input — it tells the server how to RUN the\n * query. A value the server will not honour has to be said out loud, or\n * the caller gets a WIDER answer than the one they narrowed to, in a\n * response with nothing to distinguish it from a satisfied one. That is\n * why `assertSearchFieldsAreSearchable` refuses the name with a 400\n * (#4254) — the refusal is protecting the ROW SET.\n * - `select` is a READ PROJECTION — it names what the caller would like\n * back. Dropping a column the caller may not see leaves the answer\n * correct: the rows are still the rows that were asked for, one key\n * lighter. {@link omitInternalFields} directly above answers\n * `?select=id,key` exactly this way, for exactly this reason (#7728), so\n * silence here is the platform's existing read-path rule, not an\n * exception to it.\n *\n * ⛔ Do not add a refusal here to make the two doors agree. That was option B\n * on #7876, weighed and declined: it turns a request that answers 200\n * today into a failure, for tidiness, on a spelling no non-system caller in\n * this tree uses — the companion's only deliberate reader is the backfill\n * carved out above. If a REAL caller is ever burned by the silent drop, that\n * measurement reopens it; the asymmetry by itself does not.", "packages/metadata-protocol/src/protocol.ts — assertSearchFieldsAreSearchable": " * ## The PROJECTION axis answers the same name differently — on purpose\n *\n * A name this gate refuses can still be spelled in `select` and come back\n * 200 with the key simply absent: {@link assertProjectionFieldsExist} gates\n * on whether a field is KNOWN, not on whether it is RETURNABLE, and the\n * engine's read path then drops what the caller may not see\n * (`omitInternalFields` for `internal: true` columns,\n * `stripSearchCompanionFromRead` for the hidden `__search` companion).\n * Measured on `__search`: `searchFields=__search` is a 400 here, while\n * `select=__search` is a 200 whose body lacks it.\n *\n * That is not a gap someone forgot to close — it was asked as its own\n * question and ruled intended on 2026-08-12 (#7876, direction C). The two\n * axes are different KINDS of surface. `searchFields` is AUTHORING input:\n * it tells the server how to RUN the query, so a value the server will not\n * honour changes WHICH ROWS come back — the fail-open this gate exists for.\n * `select` is a READ PROJECTION: it names what the caller would like back,\n * the row set is untouched either way, and a column the caller may not see\n * is simply not in the body.\n *\n * ⛔ Do not close the asymmetry by teaching the projection gate to refuse\n * unreturnable columns. That was the alternative on #7876 and it was\n * declined: it converts requests that answer 200 today into failures, for\n * symmetry, on spellings with no measured callers. A real caller burned by\n * a silent drop reopens the question on THAT measurement; the asymmetry\n * alone does not." } }Issue body completeness (verified): the fetched body is complete — it ends with the
Related: #7642 (origin) · PR #7868 (8b90d68) · #2486line, and all six sections (question, premises, options A/B/C, recommendation, four-lens, related) are intact with no truncation. The issue carries two comments, not one: the 03:05:16Z maintainer ruling and this seat's 13:56:35Z claim comment.Placement decisions, for review: the ruling asked for a comment at "the
$searchFieldsrefusal". Located by symbol, that isassertSearchFieldsAreSearchable(metadata-protocol), not anything in objectql — a hidden field is excluded from the searchable set (search-fields.ts:112), lands in theunsearchablebucket and throws400 INVALID_FIELD; the "is hidden" wording is specifically the auto-default branch's reason, and an object that declaressearchableFieldsgets the same 400 with the declared-set message instead.search-companion.ts:224was left untouched on purpose: it already narrates both doors in one sentence, and a third copy of that explanation is the drift this card exists to prevent.Serial constraints: engine.ts edited at 4856-4882 — the declared neighbours (#5320 at :3813, #7933 at :9257, PR #7989 at :7922-8727) are all far away, and #7933's live worktree shows its engine.ts edit elsewhere. protocol.ts was NOT on the PM's cleared list, so I checked it myself: of the open PRs touching it, #8046's hunks are at :2769/:3157 and #8045's end at :5195 — my insert at :6157 is ~960 lines from the nearest, no overlap at three lines of context.
Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
ACCEPTED on substance — PR #8079. ⛔ Ready-flip held: six jobs are still
in_progress.Engine-core seat #6019,
session_014C8pAprWdmtecFsEprZax4. Verified against GitHub andorigin/main, ⛔ not against the report's self-description.The dispatch was wrong about where the second door is, and the dev proved it
I declared the file surface as
engine.ts+search-companion.ts, on my own assumption that the 400 lived in objectql. It does not.assertSearchFieldsAreSearchableis inpackages/metadata-protocol/src/protocol.ts, and the dev located it by symbol as instructed rather than trusting my coordinates.⭐ What makes the placement right and not merely correct is the detail it went on to find:
assertProjectionFieldsExist— the projection door's own gate — sits 220 lines above it in the same file. A reader comparing the two axes is standing exactly where the question forms. That is a better siting argument than the one I dispatched.It also handled a serial constraint I did not clear:
protocol.tswas absent from my constraints line, so it measured the file itself — open PRs #8046 (:2769/:3157) and #8045 (ends:5195) against its insert at:6157, nearest gap ~960 lines. And it re-derived gates for the real two-file surface, picking upcheck:cross-package-test-inputs, which my dispatch list did not contain because my file surface was wrong. Both are the dispatch's gaps, closed by the dev.Zero behaviour change — structurally, not just by test count
+54 / −0across two files, every added line a continuation inside an existing block comment. No line deleted means the token stream the compiler sees is identical, so "the objectql suite is unchanged" is a property of the diff rather than a lucky measurement — the dev said so in exactly those terms, which is the right way to justify an unchanged number. Suites: objectql 189 files / 3356 tests, metadata-protocol 75 / 1094.The comments earn their place
The bar for direction C was never "record that a ruling happened" — it was whether a future reader is stopped. Both comments state the authoring-input vs read-projection reasoning, cite the ruling and this card, decline option B by name, and name the single thing that reopens it (a real caller measured as burned by the silent drop — ⛔ never symmetry alone).
⭐ And comment 1 supplies an argument the dispatch did not:
omitInternalFields, directly above, answers?select=id,keythe same way for the same reason (#7728). That converts C from "defensible asymmetry" into "the read path's existing rule, applied consistently" — which is a materially stronger thing for the next reader to hit, and it was measured from the code rather than reasoned from the card.search-companion.ts:224was left alone deliberately — it already narrates both doors, and a third copy is the drift this card exists to prevent. Correct call.Verified independently
Fixes #7876— correct here: this is the whole deliverable, so the merge should close the cardget_files)packages/objectql/src/engine.ts,packages/metadata-protocol/src/protocol.ts— ⛔ nodocs/adr/**, no.claude/skills/**, noskills/**. Clean; the maintainer-merge branch does not applyskip-changesetlabelled — correct for comments-only; ⛔ an empty-frontmatter changeset would have been rejected bycheck-empty-changeset.mjspackages/metadata-protocolisdomain:metadata's (#6367) — declaration filed there with a veto window before any flip, per declare-and-proceed. It was owed because my surface was wrong, not because the dev driftedfinding, observation-class, carries its own re-check commandWhy it is not flipped yet
Read from the jobs themselves at ~14:5xZ: 20 concluded, 0 failure — ESLint, Build Core, Temporal Conformance, Dogfood 1/3 + Verify CLI, Check Changeset, ADR maintainer approval all
success.Still
in_progress:TypeScript Type Check,Test Core1/3 · 2/3 · 3/3,Dogfood Regression Gate2/3 · 3/3. The first of those is the only job that runscheck:type-check-debt. This seat's standing commitment 1 exists because it inherited a PR publicly ACCEPTed while exactly that job was red — so the substance verdict is posted now and the flip waits for conclusions, ⛔ not for an absence of visible failures. Next patrol re-reads them; all-green ⇒ flip ready + enqueue SQUASH.Generated by Claude Code
docs(objectql,metadata-protocol): pin why the two `__search` doors an…
test(objectql): pin the `$searchFields=__search` 400 that four docblo…