Repository navigation
fix(lint)!: judge a roll-up's summaryOperations.filter keys against its child object - #31337
Conversation
…ts child object The engine runs `summaryOperations.filter` as the where of an aggregate on the child object, ANDed with the parent-FK match. A key the child lacks makes every child write answer SummaryRecomputeError while the summary keeps a stale value (measured on a real SqlDriver), and no authoring door judged the keys: one field's parse cannot see another object's field map. validateObjectFieldRefs gains the position: every filter key, walked by walkFilterFieldKeys ($and / $or / $not groups, operator objects, relationship paths), is resolved against the child that summaryOperations.object names, with the family's skips and its "Did you mean" verdict. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6f464897f4fd1f8d66e441c076b9ca061be45c9e && git checkout 6f464897f4fd1f8d66e441c076b9ca061be45c9e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 30e0bd160ee2afc1557746df0b6806625f37a408 8fc74827539ea8075a67076d2f22786467681747 && git checkout -B drift-repro 30e0bd160ee2afc1557746df0b6806625f37a408 && git merge --no-ff 8fc74827539ea8075a67076d2f22786467681747
node scripts/docs-audit/affected-docs.mjs --json 30e0bd160ee2afc1557746df0b6806625f37a408 |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract-review-claim: session_01FEkQ5mw7HFYn9o4LEGMQdK · head 834abfd · 2026-11-08T02:42Z |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Review of PR #31337 (card #31224, item D) on the narrowing face: the PR body and the committed changeset both carry ① Derived judgmentsWhat the hunk does, read against the code it calls — right as far as it refuses. The new block in Doors. Census. Showcase: MUST-FIX: the tree pins and documents, as accepted, a relationship spelling the engine refuses at every recompute. The new test "passes child fields, operators, and a relationship path through the child" asserts Should-fix: a Should-fix (minor): the block fires on a Observations, not findings. A dotted path into a ② Semver level
③ Boundary flags
④ Design judgmentAgainst the published spec on Implemented-by: VERDICT: FAIL |
…mpute query serves them The recompute's where serves only the nested one-level relation form; it refuses a dotted key, a field computed on read and a second relation level, and each refusal fails every child write's recompute (measured on a real SqlDriver). walkFilterFieldKeys flattens a dotted key and a nested condition into one path, so the position now walks the authored keys itself: an unknown key keeps the family's verdict; a dotted key is refused with the nested spelling (relation head) or the denormalise cure, a formula key with the denormalise cure, and beneath a relation an unknown, dotted, second-level or empty condition is refused. The block is guarded on a `summary` field. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Delta review of PR #31337 (card #31224, item D) on the narrowing face, following the FAIL record 6716423527 on ① Derived judgmentsThe must-fix landed — right. The position no longer imports Both should-fixes landed — right. A Parity with the engine, verdict by verdict. The engine's reading of the recompute
Observations, not findings. (1) A no-operator object beneath a non-relation child field ( ② Semver level
③ Boundary flags
④ Design judgmentAgainst the published spec on No must-fix open on this head. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK 维护者速读
席位核对Verdict: ACCEPT · head
Ruling-ref: the maintainer, 2026-11-07T16:00Z: 「你有 fable ,车道契约复审可以自己做」; 2026-11-07T16:59Z: 「你应该使用 pm-dispatch 技能审核和落地pr」; standing landing authorisation 2026-10-25T11:35Z: 「按照 pm-dispatch 规则复核过的都授权」. |
Requested by Leehom · project thread
Refs #31224
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none —
skills/**andcontent/docs/**teachsummaryOperations.filterkeys that are fields of the child object, which this rule accepts. The reasoning ships in theos explain object-field-ref-unknownentry.维护者速读
@objectstack/lint的object-field-ref-unknown规则新增一个位置:汇总字段summaryOperations.filter里的每个键(含$and/$or/$not分组),都按summaryOperations.object指向的子对象来判定,子对象没有的键报 error,并给出最接近的字段名;重算查询不能执行的写法也报 error:点号路径(改写为一层嵌套{ account: { status } })、读时计算的字段(如 formula)、关系下的第二层条件。另加os explain段落、测试和一个 lint minor changeset。where(与父外键匹配取 AND)。在真实 SqlDriver(better-sqlite3)上实测:键写错时,每次插入子记录都报SummaryRecomputeError(记录其实已写入),汇总值一直停在旧值。之前没有任何门检查这些键:单个字段的 Zod 解析看不到另一个对象的字段表。os lint/validate/build与对象保存/发布门会拒绝此前接受的汇总字段。普查为零:showcase 4 个带 filter 的汇总(status、billable、amount,都是子对象真实字段),hotcrm 0 个。回滚即 revert。packages/lint/src/validate-object-field-refs.ts里summaryOperations.filter那一段,以及模块头新增的一条位置说明。pnpm --filter @objectstack/lint exec vitest run src/validate-object-field-refs.test.ts,预期全部通过,其中新 describe 的 10 条为本卡新增。What changes
packages/lint/src/validate-object-field-refs.ts: a new field-level position invalidateObjectFieldRefs. For a field whosesummaryOperationsnames anobjectand carries afilter, the position reads the authored keys itself (judgeSummaryFilter): it descends$and/$or/$notgroups and operator objects; a dotted relation key is refused with the nested-form cure, a dotted computed or scalar head (classifyDottedFilterHead) or a formula key (isVirtualSearchField) with the denormalise cure; the nested one-level relation form is accepted, and beneath it an empty condition, a dotted key, an undeclared key or a second level is refused. Each key is resolved against the CHILD object, never the object that owns the summary. The rule's sharedjudgedoes the resolution, so the verdict, the "Did you mean", severityerrorand the three family skips are the same as at every other position: a child the stack does not define, a child with no readable field map, and a registry-injected system column. Path:objects[i].fields.FIELD.summaryOperations.filter.KEY(group arms keep their index, e.g.…filter.$and[1].$or[1].stauts). Hint: "Fix the key —summaryOperations.filterruns on the child objectsummaryOperations.objectnames, so each key is a field of that child, not of the object that owns this summary." The module header gains the position with its reader and the measurement.packages/lint/src/rule-explanations.ts: anos explain object-field-ref-unknownparagraph for the position, saying which object, what is walked, and what a miss costs..changeset/31224-summary-filter-key-unknown.md:'@objectstack/lint': minor, BREAKING banner, ADR-0087not-required (no-migration-prescription).Why lint, not parse. The verdict needs the child object's field map, which one field's Zod parse cannot see. The rule runs where the object graph is:
validateObjectFieldRefsis in the reference-integrity suite withruntimeTypes: ['flow', 'object'], so the error reachesos lint/os validate/os build, the live object save and the per-item and package publish. A draft stores with the finding in_diagnostics.Measured consequence (one-off probe, not kept): a real
SqlDriver(better-sqlite3) plus a realObjectQL, with parentpr_report.total = sum(pr_line.amount).filter: { status: 'ok' }, a child insert givestotal = 5.filter: { stauts: 'ok' }, the same insert answersSummaryRecomputeError("the triggering records WERE written") andtotalstays0.No
packages/spec, metadata-protocol source, runtime orui/change.Tests
packages/lint/src/validate-object-field-refs.test.ts, new describe "summaryOperations.filter (addresses the CHILD object, Enforce-or-remove: four metadata shapes cloud's authoring rules warn about still parse clean (boolean required, open flow start config, orphan action nodes, roll-up filter keys) #31224)":$and/$or/$not(paths pinned).{ account: { status, industry: { $in } } }.account.status, whose hint is the nested spelling;formulakey, at the top level and one level down;summaryfield..summaryOperations.filter..packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts: anobject-field-ref-unknownrow inCASES(a self-referential roll-up withfilter: { stauts: 'open' }). The draft saves and readsvalid: false; publish refuses with the same words; the package publish refuses and rolls back.expense-report.object.ts:status,billable,amount, all fields ofshowcase_expense_line), 0 unknown keys; hotcrmmainhas 0 filtered roll-ups. Control: 9 roll-ups in examples carry the live keysummaryOperations.field.Acceptance notes
Refs, notFixes.no (narrowing)(no new export, since the position joins an existing rule) withnarrowed.Verification
Final head
8fc74827: the branch was restarted fromorigin/main11c1326d(#31287 merged).8fc74827follows the contract review (6716423527): the position reads the authored keys itself instead ofwalkFilterFieldKeys, which flattens a dotted key and a nested condition into one path. The recompute'swhereserves only the nested one-level form, so a dotted key, a computed field and a second relation level are refused, and the three texts no longer teach the dotted spelling. Measured on a realSqlDriver+ObjectQL(one-off probe, not kept):{ report: { name: 'r' } }sums (total 5);{ 'report.name': 'r' }and aformulakey each answerSummaryRecomputeErroron the child insert, and the total stays 0.8fc74827: 67 derived bydispatch-gates --commands, all exit 0. They ran over the full build of834abfdb, with@objectstack/lintrebuilt for8fc74827and the teardown fixture commit fetched.--ranreconciles 67 accounted, 0 NOT-MEASURED, 0 unrun.check-changeset-no-majorwith apull_requestevent carrying this body exits 0.pnpm build(119 tasks), thenpnpm --filter @objectstack/dogfood testunder the verify lock (rerun on8fc74827after the lint rebuild): 435 files passed, 3 skipped; 3610 tests passed, 41 skipped; VERDICT command-exit 0.@objectstack/lint197 files / 8363 tests passed.@objectstack/metadata-protocol324 files passed, 4 skipped (22538 tests), including the newCASESrow (51/51 in that file).CI runs the full set.
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV