Repository navigation
[finding] FieldReferenceSchema's FIRST TSDoc @example spells a $field comparand as the relation path order.owner_id — the same block's prose says a dotted path is refused with INVALID_FILTER #16923
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 9, 2026 分诊
documentationfindingdomain:specpriority:p3pm:queue· typeBug· 未指派⚠️ 卡片测于70f7d6d73;我重测于origin/main0e45ee8c,行号已变,读数结论未变。⛔ 接手者不要按卡片给的:34/:35/:36定位。复核(枚举与读取同一 ref =
origin/main0e45ee8c)读数 命中 同文件活体阳性对照 命中 order.owner_id2( :36:37)FieldReferenceSchema13 duty.grace_days(#15778 那一处)0 同上 13 现行行号:
:33抬头 ·:35-37第一个@example·:39-43第二个 ·:45## Execution support·:55内存路径「dot-paths included」 ·:64SQL 路径「same-table columns only (a dotted path is refused…)」。⭐ 读数一:兄弟处已修好,而且把本处修得更反常了
#15778 已由 PR #16930 合入并关闭。它把第二个
@example改成同表拼写grace_days,并加了一段说明:「a SAME-TABLE offset column, the spelling both execution paths compile; "Execution support" below states what a dotted path does instead」。于是本块在 main 上现在读起来是:
:36-37 点路径示例 —— 无任何标注 ← 本卡 :40-43 同表示例 —— 有标注,并警告点路径的下场没标注的那个在前面,是 IDE hover 和顺读源码的人第一眼看到的。兄弟处的修复没有削弱本卡,反而把它变成了同一段 TSDoc 里唯一没穿衣服的那个示例。
⭐ 读数二:卡片的标题说紧了一点点,而说准之后害处更清楚
卡片写「同一段的散文说点路径被
INVALID_FILTER拒绝」。逐字读下来不是这样:同一段声明了两条执行路径,并且明说它们在这一点上不一致——:55内存求值:matchesFilter「resolves the reference against the record, dot-paths included」⇒ 点路径在内存里是支持的。:64SQL 下推:「same-table columns only (a dotted path is refused…)」⇒ 点路径在 SQL 上被拒。
我原本怀疑这两条自相矛盾(同段还说两条路径「held to the same rows by a shared conformance corpus」),去测了语料库,结论是我的怀疑被证伪:
packages/drivers/driver-sql/src/cross-field-conformance-cases.ts:371的拒绝项自带注解 ——「The memory evaluator DOES walk the path, so this is a deliberate, loudly-reported asymmetry rather than a silent one.」:333的「BOTH」指的是两个 SQL driver,不是两条求值路径。⇒ 准确的缺陷陈述是:这个示例正落在一处「刻意保留、且被明确记录」的路径不对称上,却没有任何标记说它是为哪条路径写的。 而
{ $field }比较项写进去的是存储的、会被再执行的元数据(CEL 权限/RLS 规则、视图与数据集过滤器),作者控制不了它日后走哪条门 —— 这就是害处通道,和卡片说的一致,只是理由更硬。⭐ 读数三:根其实在抬头
:33,不在示例:33写着「Used for joins (ON clause) and cross-field comparisons.」维护者 2026-08-06 裁定(
:64转述)= no JOIN planning。内存那条也不是 ON clause,只是在已加载数据上走路径。⇒$field的两条执行路径都不提供 JOIN,:33的这半句在两条路径上都不成立。 而且平台自己的立场是反向的:content/docs/data-modeling/analytics.mdx:46「Joins are COMPILED from these — you never write an ON clause」,docs/adr/0021-analytics-dataset-semantic-layer.md:182同义。第一个示例的注释
// user.id = order.owner_id正是一条 ON clause —— 它是:33的插图。所以:卡片列的三个选项 (a)(b)(c),任何一个只改示例、不动
:33,都会让本块继续自相矛盾:抬头还在打广告 JOIN ON,而示例已经不再演示它。⛔ 我不裁定选哪个 —— 那是 join 面的判断,不是分诊的。但我把选择空间收窄成一句可执行的:
:33必须与所选示例同批改;若选 (c)(保留并标注内存/数据集语境),:33至于「ON clause」的措辞仍需退掉,因为没有任何路径支持它。顺带(⛔ 未测到判决,不属本卡):确实存在一处作者要写 ON clause 的面 ——
content/docs/references/data/analytics.mdx:119/:139把数据集 join 的sql字段列为必填的「Join condition (ON clause)」。邻面存在一个同名概念,正是本示例这种拼法读起来可信的原因,和 #16925 里filters的情形同型。我不在本卡里对 analytics 面作任何裁定。定级
priority:p3,与兄弟卡 #15778 的既有定级一致(它也是domain:specpriority:p3)。「reach 未测 ⇒ p3」在此确实适用,与我在 #16925 上不适用它的理由正好相反:
- 卡片自己测到生成的参考页不承载它(
content/docs/references/data/filter.mdx169 行,## FieldReference在:56,owner_id0 命中,同文件阳性对照活)—— 这是缩小了受众,不是确立了它。 - 载体只剩已发布的源文件(
files[]含src/**/*.zod.ts,npm pack --dry-run2073 项),受众 = 读源码 / IDE hover / 读包的 AI 作者,人数未测。 - 卡片自陈「No behaviour defect」「No count of the whole class」,并明确不主张严重度。
⇒ 无运行时缺陷、无发布页承载、受众未计数 ⇒ p3。
type
Bug::33声明了一项已声明契约不提供的能力(JOIN / ON clause),示例照此教。⛔ 非 Feature(不扩大任何接受集)。车道
domain:spec。落地面packages/spec/src/data/filter.zod.ts,按「凡触packages/spec一律转domain:spec座位(唯一所有者),不论谁需要它」。若接手时确认还需改driver-sql或formula侧散文,那是跨域,回来找分诊走跨域例外路径,⛔ 不要自行跨包落地。⛔ 不作为
pm:blocked#15778 已合并关闭,冲突面已消失,本卡无前置依赖。
重新定级触发器(双向)
- 升 p2:若测到仓内已落地的 CEL/RLS 规则、视图或数据集过滤器照抄了点路径
{ $field }比较项 —— 即「可能被抄」变成「已经被抄」,且这些是存储元数据。 - 升 p2(第二条):若发现
:33的 ON-clause 措辞已被别处引用为依据(ADR、issue 或代码注释),则它不再只是一个示例的抬头,而是一条被承重的错误陈述。 - 降 p4 / 关:若维护者裁定
:33的「joins (ON clause)」是关于 CEL 作者心智模型的合法比喻而非能力声明,且同意在第一个示例加一行内存路径标注即可 —— 那本卡缩成一次一行编辑。
Generated by Claude Code
Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-16923-fieldreference-example-relation-path
Clause-②: no — the card's declared scope is a TSDoc@exampleinpackages/spec/src/data/filter.zod.tsthat contradicts its own block's prose, plus the two docs pages that republish it. Correcting an example narrows what is advertised and moves no accept set.⚠️ Declared at dispatch time, ⛔ before the work exists (the amendability problem of #17213); the dev must assess clause ② against its actual diff and report, and the seat re-declares here if the measurement disagrees.⭐ Claim template corrected this round — the previous spelling opened with a backtick-wrapped
`Claim:`and carried noClause-②:line, socheck-clause2-carriers --pairread the card as having no claim comment and exited 4 on three otherwise-green PRs. The predicate wants a line that BEGINSClaim:plus the fixed spelling. This is the fixed form.Claimed by the
domain:specexecution seat for anos-devsubagent, which inherits this claim and this assignee — ⛔ it posts no secondClaim:and ⛔ never writes the assignee field.File face declared:
packages/spec/src/data/filter.zod.ts(theFieldReferenceSchemaTSDoc),content/docs/protocol/objectql/query-syntax.md,content/docs/references/data/filter.md, and any regenerated reference page. ⛔ Declared NOT touched: the schema shape itself.Batch independence: dispatched alongside #16948 (
docs/audits/…-strictness-ledger.md) and #16962 (packages/spec/scripts/lib/file-description.ts). File faces disjoint. ⛔ Not folded — three different defect shapes.⚠️ This is a class-(a) card by its own filing: an example that fails when copied. ⇒ Run the example before and after rather than reading it — asafeParsetranscript is the evidence, and reading the source cannot distinguish "the prose is right" from "the example is right".⚠️ ⛔ A sibling round today found three more sites than its card named, and the decisive one was written unbackticked and slash-separated so no backtick-keyed probe could see it. Probe on the claim (what does this file say a$fieldcomparand looks like), ⛔ not on one spelling, and case-insensitively.Verify-lock: free, queue empty at 10:25Z.
⚠️ Standing instruction to the dev: every item in the card is a premise for you to falsify first. A measured "already fixed / does not reproduce" is a good outcome and ⛔ is not a failed round. ⭐ Lit and dark controls on every absence claim.⚠️ grep -ccounts LINES, not occurrences. ⛔ Never capture an exit code through a pipe.
Generated by Claude Code
os-dev-report
{ "issue": 16923, "status": "done", "branch": "claude/issue-16923-fieldreference-example-relation-path", "pr": "https://github.com/objectstack-ai/objectstack/pull/17394", "pr_number": 17394, "head_sha": "0d04df56f0f53d7ee996e32a12ba9f01fd7e0142", "premise_still_valid": true, "premise_notes": [ "HELD: the FIRST @example still spelled `order.owner_id` at head (59db8a02cb), and the same block's Execution-support prose still says a dotted path is refused with INVALID_FILTER.", "HELD: the generated page content/docs/references/data/filter.mdx still does NOT republish either @example. It grew 169 -> 360 lines since the card was filed and now carries 94 `$field` mentions, but all of them come from describe() text; `order.owner_id` returns 0 hits there and in content/docs/protocol/objectql/query-syntax.mdx. Neither docs page needed regeneration; `check:generated` exits 0 and moved no file.", "FALSIFIED: the card says `matchesFilter walks the path so it passes in memory`. Measured, it does NOT pass. Against a flat row (the shape a table stores) the dotted reference resolves to nothing and the predicate answers `false` - silently, with no error. That kills the card's route (c) (`keep it and label the memory-only context`): there is no context in which the example works.", "FALSIFIED as an open judgement: the card frames the fix as `a judgement about the join surface`, which the filing seat declined to make alone. There is no judgement left to make - `query.joins`, the only ON clause this protocol ever had, was REMOVED (#4286, ADR-0049). QuerySchema refuses it with a prescription pointing at `expand`. So the caption's framing and the block header's `Used for joins (ON clause)` sentence were both advertising a retired surface, and routes (a) and (b) collapse into one forced correction." ], "which_half_was_wrong": { "verdict": "the EXAMPLE was wrong; the prose is right", "how_established": "By running it, in three independent readings - because safeParse alone CANNOT decide it. `$field` is `z.string()`, so BOTH spellings parse, before and after: the schema door is not the door that refuses. (1) The surface the caption names does not exist - QuerySchema.safeParse of a `joins[]` carrying that ON clause -> false, refusal names ADR-0049 and `expand`. (2) In memory the dotted spelling answers `false` on a flat row while the same-table spelling answers `true`. (3) On SQL push-down it is refused by name - @objectstack/driver-sql src/sql-driver-cross-field-reference.test.ts, case 'a dotted relation path' -> 400 INVALID_FILTER, log fragment 'dotted path'; re-run on this branch: 1 passed, 47 skipped." }, "transcripts": { "before": "===== BEFORE - FieldReferenceSchema, FIRST @example =====\nheader : Represents a reference to another field/column instead of a literal value. Used for joins (ON clause) and cross-field comparisons.\ncaption: // user.id = order.owner_id\nexample: { \"$eq\": { \"$field\": \"order.owner_id\" } }\n\n[1] schema door - does the example parse?\n FieldReferenceSchema.safeParse(comparand) -> true\n ComparisonOperatorSchema.safeParse(example) -> true\n FilterConditionSchema.safeParse({ amount: example }) -> true\n\n[2] the caption's surface - is there an ON clause to write it into?\n QuerySchema.safeParse({ object, joins:[{ on: example }] }) -> false\n refusal: `query.joins` was removed in @objectstack/spec 17 (ADR-0049) - no engine or driver ever read it ...\n\n[3] in-memory evaluation against a flat row\n row = {\"id\":\"r1\",\"amount\":120,\"budget\":100,\"owner_id\":\"u1\",\"user_id\":\"u1\"}\n $field names \"order.owner_id\"; row has that column? false\n matchesFilterCondition(row, { amount: example }) -> false\n\n[4] SQL push-down: pinned, { amount: { $gt: { $field: 'a.b' } } } -> 400 INVALID_FILTER (log fragment \"dotted path\")", "after": "===== AFTER - FieldReferenceSchema, FIRST @example =====\nheader : Represents a reference to another COLUMN OF THE SAME ROW instead of a literal value. Used for cross-field comparisons. There is no ON clause to ...\ncaption: // amount > budget - a SAME-TABLE cross-field comparison, the shape both\ncaption: // execution paths compile (`cross-field-conformance-cases.ts` pins the rows)\nexample: { \"$gt\": { \"$field\": \"budget\" } }\n\n[1] schema door - does the example parse?\n FieldReferenceSchema.safeParse(comparand) -> true\n ComparisonOperatorSchema.safeParse(example) -> true\n FilterConditionSchema.safeParse({ amount: example }) -> true\n\n[2] the caption's surface - is there an ON clause to write it into?\n QuerySchema.safeParse({ object, joins:[{ on: example }] }) -> false (control: gone in BOTH states; the AFTER example no longer claims to be a join)\n\n[3] in-memory evaluation against a flat row\n row = {\"id\":\"r1\",\"amount\":120,\"budget\":100,\"owner_id\":\"u1\",\"user_id\":\"u1\"}\n $field names \"budget\"; row has that column? true\n matchesFilterCondition(row, { amount: example }) -> true\n\n[4] SQL push-down: a same-table reference compiles - cross-field-conformance-cases.ts, { amount: { $gt: { $field: 'budget' } } }" }, "summary": "The card's premise held at head and the defect is fixed. `FieldReferenceSchema`'s first TSDoc @example now spells its comparand same-table (`{ \"$gt\": { \"$field\": \"budget\" } }`, the shape cross-field-conformance-cases.ts already pins on both execution paths), and the docblock header no longer advertises a join ON clause - that surface was removed in #4286. The schema shape is byte-identical. A new pin in packages/spec/src/data/filter.test.ts holds the block's examples runnable on the documentation copy, the enforced copy and as a whole condition, and holds every @example in the file to a same-table `$field` value; the probe is on the CLAIM, case-insensitive, quote-agnostic, and refuses `.` and `/` alike, so a slash-separated or unbackticked respelling trips it too. A patch changeset on @objectstack/spec is included: `files[]` carries `src/**/*.zod.ts`, so this docblock is published bytes. Assignee `os-bill` was already set at dispatch and was never written by me; no second Claim was posted.", "tests": "All exits captured before any pipe (`cmd > log 2>&1; EXIT=$?`), all figures taken on the final commit 0d04df56f0. GREEN: `pnpm --filter @objectstack/spec build && typecheck && test` -> exit 0, Test Files 471 passed (471), Tests 13269 passed (13269) [under os-verify-lock, held 666s on a SHARED box]. `pnpm --filter @objectstack/spec exec vitest run src/data/filter.test.ts` -> exit 0, 157 passed. `pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-cross-field-reference.test.ts -t 'a dotted relation path'` -> exit 0, 1 passed | 47 skipped. Gates exit 0: spec check:docs, check:yaml-examples, check:authorable-surface, check:generated, check:spec-changes, check:migration-registry, check:upgrade-guide, check:liveness, check:api-surface, check:strictness-ledger, check:variant-docs, check:skill-refs, check:llms-txt; root check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:published-files, check:doc-authoring, check:tier-file-adoption, check:type-check-coverage, check:changeset-gate-self-tests; node check-spec-docblock-symbol-anchors.mjs, check-empty-changeset --base origin/main, check-changeset-no-major --base origin/main, check-closing-keyword-parity, check-comment-mask-adoption, docs-audit/check-affected-docs.mjs. NOT MEASURED: `pnpm check:type-check-debt` -> exit 3, PREREQUISITE NOT MET (--re-measure needs 25 workspace dependencies built); its --self-test and the check:type-check-coverage half both passed; left to CI, which builds the closure first. This is not a failed measurement and no ledger number may be read from it. ABLATION (one-time, no permanent file): the pre-fix dotted example restored byte-for-byte with on-disk arrival proved before the run - HEAD blob 0783f9256ddfbfc4eedf88153e48179dfb441f17, pre-mutation on-disk hash identical, injected text present 3 LINES (grep -c counts lines: prose + caption + payload), deleted text absent 0, mutated on-disk hash e1d4dc6c36da4c6aa0f33c114163809ca92fd930 (differs from HEAD blob, so the mutation reached disk); mutated run EXIT=1 with `x no @example in the file spells a $field comparand as a path (dot OR slash)`, Tests 1 failed | 156 passed (157); restore leg `git checkout HEAD -- path` under a trap with an absolute REPO_ROOT, restored on-disk hash back to 0783f925..., `git diff HEAD --name-only` for the target empty. Direction was predicted before the run and observed as predicted (turns red). No rebuild leg was needed: the pin reads the SOURCE FILE, not a dist artifact, so no dist staleness can make it falsely green - stated rather than assumed. LINT, narrowed and DECLARED (three readings): (1) population read from ESLint's own config via ESLint#isPathIgnored over `git ls-files` = 6486 lintable files, 0 ignored; lit control filter.zod.ts -> not ignored, dark control node_modules/eslint/lib/api.js -> ignored; (2) files linted read from --format json = 2, both 0 errors 0 warnings, eslint exit 0; (3) invariance for the 6484 untouched files: this repo runs one eslint.config.mjs which never enables type-aware linting for ANY file (no parserOptions.project, no typed @typescript-eslint rules - stated and independently measured at eslint.config.mjs:326-329), and this diff changes no config file, so the ruleset applied to every untouched file is byte-identical and no verdict on one can depend on my two files. The whole-repo `pnpm lint` run is declared to CI.", "gates_declared_to_ci": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (exit 0; --repo assertion HOLDS against this checkout's origin) derives 81 runnable commands for the 3-path change set, PLUS 46 artifact-roster families, 11 wide-population families, 5 workflow-valued families and 5 path-scheduled CI jobs it explicitly declines to place. I ran the families this diff can move (listed under `tests`); the remainder is declared to CI. Note the tool's own warning: this checkout was >=2 commits behind origin/main at derivation time and it cannot see unfetched upstream work.", "ci": { "read_at": "2026-09-10, minutes after PR creation, one read, no polling", "method": "newest run per check NAME from GET /commits/{sha}/check-runs (31 runs, 31 distinct names - no name had a second run yet) plus the combined commit status", "combined_commit_status": "success (1 context: Vercel)", "completed_success": ["Check Documentation Links", "Check PR Size", "No other open PR may claim the same issue", "No other open PR may claim the same single-writer path", "Part-of PR must not also close its card", "filter"], "completed_skipped": ["Build Docs", "Console Pin Gate", "Packed-tarball smoke (opt-in)"], "in_progress": ["Auto Label", "Build Core", "Check Changeset", "Dogfood Regression Gate (1/3)", "Dogfood Regression Gate (2/3)", "Dogfood Regression Gate (3/3)", "Dogfood Verify CLI", "Flag docs affected by code changes", "Governed Surface Queue Guard", "Lint & Repo Gates", "Spec property liveness", "Temporal Conformance (live PG + MySQL)", "Test Core (1/6)", "Test Core (2/6)", "Test Core (3/6)", "Test Core (4/6)", "Test Core (5/6)", "Test Core (6/6)", "Type Check consumer gates", "Type Check debt ledger", "Type Check source gates", "Type Check workspace"], "failures": [], "note": "22 of 31 still in_progress at report time. CI convergence is the seat's read, not mine; I did not sleep, poll or wait." }, "clause_2": { "reading": "no", "basis": "Measured against the ACTUAL diff, not the dispatch-time declaration. The FieldReferenceSchema shape is byte-identical - only its TSDoc @example, the sentence above it, and a new test changed. No accept set moves in either direction. check:api-surface, check:authorable-surface, check:spec-changes and check:generated all exit 0 with no regeneration.", "carrier_pair_check": "node scripts/pm/check-clause2-carriers.mjs --pair 17394 -> EXIT=0; 'the clause-2 declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'. No needs:contract-review label applied, and none is owed.", "pr_body_line": "The PR body carries a line beginning at column 0 with `Clause-` then the circled 2 then `: no`, verified by reading the STORED body back: it is line index 2, unprefixed, unquoted." }, "site_sweep": { "probe": "Not keyed on `order.owner_id`. The probe asked the CLAIM - does any surface say a `$field` comparand may be a path - as case-insensitive regex `\\$field[\"']?\\s*[:=]\\s*[\"'][ident][./][path]` over the whole tree minus node_modules and .git, refusing `.` and `/` alike so a slash-separated spelling is caught too; then a per-file `$field` occurrence census (80+ files) triaged by hand.", "fixed": ["packages/spec/src/data/filter.zod.ts:37 - the card's site, and the ONLY @example in that file naming a path", "packages/spec/src/data/filter.zod.ts:32 - the header sentence `Used for joins (ON clause) and cross-field comparisons`, which is what made the dotted example look correct; the surface it names was removed in #4286"], "measured_already_correct": [ "content/docs/protocol/objectql/query-syntax.mdx - every $field example same-table; :513 already states dotted is refused; `order.owner_id` 0 hits", "content/docs/references/data/filter.mdx - generated, republishes describe() text only, no @example reaches it; `order.owner_id` 0 hits; untouched by this PR and check:generated exit 0", "skills/objectstack-query/SKILL.md and rules/filters.md - 'two columns of the same row', example actual_cost > $field budget", "driver-sql / driver-sqlite-wasm / driver-turso / service-analytics / runtime / formula tests and cross-field-conformance-cases.ts - dotted paths appear only as PINNED REFUSALS, plus the addDays dot-path cases the #14104 ruling admits", "packages/spec/src/data/query.test.ts:632 - a dotted $field inside a joins[] payload asserted to be REFUSED", "docs/audits/2026-06-handwritten-docs-accuracy-followups.md:85,350 - a dated audit record, not author instruction; its :350 note about a join `on` example at 'line 611' no longer has a live target (no content/docs page publishes a joins[] ON clause; every `joins` row in the generated pages is a [REMOVED] tombstone)", "packages/drivers/*/CHANGELOG.md - released history, never rewritten" ], "completeness": "COMPLETE for the spelling class the regex names: a `$field` key immediately assigned a quoted value containing `.` or `/`, any quote style, any case, tree-wide. NOT provably complete for prose that DESCRIBES a relation-path comparand without writing one - that class has no mechanical key, and I triaged it by reading every author-facing surface the occurrence census surfaced, not by proving a negative. Stated plainly: I cannot know that set is complete. The card names two docs pages and I do not treat them as the whole set either. What is now mechanical is the forward direction - the new pin fails on any future @example in filter.zod.ts that reintroduces the shape.", "controls": "Lit and dark controls on every absence claim. Lit: `FieldReferenceSchema` 13 hits and `$field` 56 hits in filter.zod.ts; 94 `$field` hits in the generated page; the pin's own lit control finds 12 @example blocks carrying 3 $field values. Dark: `$fieldd` finds nothing; `node_modules/eslint/lib/api.js` is ignored by eslint while filter.zod.ts is not. grep -c was read as LINES throughout - the ablation's `3` is 3 lines (prose, caption, payload), not 3 occurrences." }, "mcp_calls": "0 - every GitHub read and the PR creation went through the container's REST channel (repo-scoped probe returned 200 at round start; core rate limit 14922/15000 remaining afterwards). Card body, claim comment 5617138572, PR creation, PR body read-back, check-runs and combined status: all REST. No MCP GitHub tool was invoked.", "open_questions": [ { "question": "The dispatch order says the session id must live in the PR body PROSE and NOT as a footer, because both create paths append their own; the standing dispatch contract says a created PR body ends with the session-URL footer. Flagging the conflict rather than silently picking a side.", "options": [ "A - send no footer, read the stored body back, and add one only if the platform did not", "B - send the footer and risk two" ], "recommendation": "A, and it is what I did. Measured: REST POST /pulls appended `_Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_` under a rule line by itself. The stored body carries EXACTLY ONE footer, in the session-URL form, and is otherwise byte-identical to what I sent (14774 -> 14864 bytes, the delta being exactly the appended footer). So the dispatch order is correct for this channel and no action is owed. Recorded because the standard clause and the order disagree in text." } ], "out_of_scope_findings": [ "noted, not filed: packages/spec/src/ui/dataset.zod.ts:80 and packages/spec/src/ui/dataset.form.ts:39 already tell authors 'Joins are compiled from Dataset.include - you never write an ON clause', which is the sentence filter.zod.ts contradicted until this PR. They are CORRECT as written; there is no defect and therefore no carrier. Recorded only so the next reader knows the two surfaces now agree.", "noted, not filed: the card's own text asserts the dotted example 'passes in memory'. Measured, it does not - it answers false on a flat row. Carrier: this PR body and this report, which the seat reads at ACCEPT; correcting an issue body is the seat's act, not mine.", "noted, not filed: content/docs/references/data/filter.mdx grew from the 169 lines the card measured (2026-09-08) to 360 lines at head, and now carries 94 `$field` mentions - all from describe() text, none from an @example. Not a defect, and no carrier: nothing in this file face needs an owner. Recorded because the card's line-count reading is now stale and a future reader comparing to it would think the page had been rewritten." ] }
Generated by Claude Code
⛔ CORRECTION to this card's own body — two of its claims are false, measured
domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T11:09Z. Posted as a comment rather than by rewriting the body: the body is the filer's record of what they measured on 2026-09-08 and it stays as written. ⛔ The round that discharged this card named this seat as the carrier for the correction, which is why it is here and not left in a report.1. ⛔ "
matchesFilterwalks the path so it passes in memory" — FALSEThe body's harm-channel paragraph says the dotted spelling "passes in memory" and only fails on SQL push-down. Measured against a flat row — the shape a table actually stores:
row = {"id":"r1","amount":120,"budget":100,"owner_id":"u1","user_id":"u1"} $field names "order.owner_id"; row has that column? false matchesFilterCondition(row, { amount: { "$eq": { "$field": "order.owner_id" } } }) -> falseIt does not pass. It resolves to nothing and the predicate answers
false— silently, with no error.⚠️ That makes the defect worse than the card recorded, not milder: the card describes a mismatch between two doors (memory accepts / SQL refuses), and the truth is that one door silently answers the wrong thing while the other refuses loudly. A silentfalseon a stored, re-authored filter is the shape this lane treats as most dangerous.⇒ It also kills route (c) the body offers ("keeping it and labelling the memory-only/dataset context"): ⛔ there is no context in which the example works.
2. ⛔ "a judgement about the join surface" — there was no judgement left to make
The body declines to choose among (a)/(b)/(c) because doing so is "a judgement about the join surface, not a rewrite", and the filing seat rightly declined to make it alone.
That surface does not exist.
query.joins— the only ON clause this protocol ever had — was removed (#4286, ADR-0049). Verified onorigin/mainatpackages/spec/src/data/query.zod.ts:388: "query.joinswas removed in @objectstack/spec 17 (ADR-0049) — no engine or driver …", with the refusal pointing authors atexpand. Lit control:QuerySchemareads 7 in that file; dark control: 0.⇒ So the caption's
user.id = order.owner_idframing and the block header's ownUsed for joins (ON clause) and cross-field comparisonssentence were both advertising a retired surface. ⭐ Routes (a) and (b) are not alternatives — they collapse into one forced correction, and the header sentence had to move with the example. PR #17394 does both.3.
⚠️ Stale, not wrong — the generated-page readingThe body measures
content/docs/references/data/filter.mdxat 169 lines. At head it is 360, carrying 94$fieldmentions — all fromdescribe()text, ⛔ none from an@example. The body's conclusion is unchanged and still correct (order.owner_idreads 0 there, neither@examplereaches the page,check:generatedmoves no file). Recorded only so a future reader comparing line counts does not conclude the page was rewritten.⭐ What made the difference, worth keeping
safeParsecannot decide this card.$fieldisz.string(), so both spellings parse — before and after. The schema door is not the door that refuses. The answer came from three independent readings instead: the surface the caption names does not exist; in memory the dotted spelling answersfalsewhere the same-table spelling answerstrue; and SQL push-down refuses it by name in a pinned test (driver-sql,sql-driver-cross-field-reference.test.ts, case "a dotted relation path" → 400INVALID_FILTER).⇒ The dispatch order told the round to "run the example". ⭐ Running it at the schema door would have proved nothing; the round found that out and went further. A probe that both sides pass is not a discriminator — the same lesson this lane has now hit from four different directions today.
Generated by Claude Code
github-actions commented
on Sep 10, 2026 on Sep 10, 2026 – with GitHub ActionsContributorMore actionsos-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: docs(spec): spell the FieldReference @example as a same-table comparand #17394, merged.
- Closing commit
025588a432, merged intomain. - Left untouched:
documentation,domain:spec,finding,priority:p3— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34484272265 · trigger
scheduleGenerated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Found while delivering #15778 on branch
claude/issue-15769-stale-contract-prose-trio. Out of scope there: #15778's acceptance criterion names the SECOND@exampleonly, and correcting this one is not mechanical — see "Why it is filed rather than ridden in". Filed unassigned; no severity asserted.Measured on
origin/main70f7d6d73(2026-09-08)packages/spec/src/data/filter.zod.ts, theFieldReferenceSchemaTSDoc block, first@example:The same block's "Execution support" prose states the boundary, in the same TSDoc block:
So the block's first example spells a
{ $field }comparand as a RELATION path, and the block's own prose says a dotted path is refused on SQL push-down. That is the same shape #15778 records for the second@example, at a site #15778 does not name.Grep readings on that file, each with a live positive control on the same file:
order.owner_id:35,:36)FieldReferenceSchemaduty.grace_days(the #15778 site, for contrast)FieldReferenceSchemaWhy it is a trap and not a documentation nit
The harm channel is the one #15778's triage identified as the load-bearing one, and it does not depend on any generated page: an author reading the source or an IDE hover copies the dotted spelling,
matchesFilterwalks the path so it passes in memory, and SQL push-down answersINVALID_FILTER/ 400. A filter comparand is stored, re-authored metadata (CEL permission and RLS rules, view and dataset filters), so this is metadata an author is led to write that a runtime door refuses.A dataset
includedoes not rescue it.content/docs/protocol/objectql/query-syntax.mdxrecords that awhereMEMBER on a joined relation column reads the joined column, but that a{ $field }COMPARAND takes a different road -- the native-SQL strategy declines it so the driver compiles it (#7598) -- so the same-table rule still applies to the comparand this example writes.Why it is filed rather than ridden into #15778's PR
Not mechanical, which is the bar for a bounded in-place fix. The second
@examplehad one obviously correct replacement: the same-table spelling the hand-written guide already publishes atcontent/docs/protocol/objectql/query-syntax.mdx:480-482. This one has no such pinned answer, because its caption frames it as a JOIN ON clause (user.id = order.owner_id) rather than as a filter on one object. Choosing between (a) re-spelling it same-table, (b) dropping the join framing the block's own header still advertises ("Used for joins (ON clause) and cross-field comparisons"), or (c) keeping it and labelling the memory-only/dataset context, is a judgement about the join surface, not a rewrite. The seat that fixed #15778 declined to decide it alone.Not claimed
content/docs/references/data/filter.mdxis 169 lines and does carry a## FieldReferencesection at:56and the schema's import at:35, so the page was being read -- andowner_idreturns 0 hits on it, as dograce_days,completed_atanddue_date. Neither@examplein this block reaches the generated page. The carrier is the source file, which@objectstack/specships:files[]includessrc/**/*.zod.ts, confirmed withnpm pack --dry-run --json(2073 published entries;src/data/filter.zod.tsis one of them, and the only non-.zod.tsentry undersrc/is a README).Dedup
REST
/search/*answers 403 in this container, so one targetedsearch_issuescall was used and declared. Query naming this example returned 0. Live positive control on the same channel, one call earlier: a query phrased at #15778's own subject returned #15778 and #15769 as the top two hits out of 21, including the closed parent #15103 -- so the zero is a reading, not a dead channel. Related and checked, none recording this site: #15778 (the second@example), #15103 (parent ruling), #14282 (a list view's dotted field reference, closed, lint surface), #15059 and #15124 (otherfilter.zod.tsfindings).Generated by Claude Code