Repository navigation
Commit 38fc529
fix(lint)!: judge a roll-up's summaryOperations.filter keys against its child object (#31337)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XF9CLMiiKFscnGZpZTirGFK)_
Refs #31224
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — `skills/**` and `content/docs/**` teach
`summaryOperations.filter` keys that are fields of the child object,
which this rule accepts. The reasoning ships in the `os explain
object-field-ref-unknown` entry.
## 维护者速读
- **改了什么**:`@objectstack/lint` 的 `object-field-ref-unknown`
规则新增一个位置:汇总字段 `summaryOperations.filter` 里的每个键(含 `$and` / `$or` / `$not`
分组),都按 `summaryOperations.object` 指向的子对象来判定,子对象没有的键报
error,并给出最接近的字段名;重算查询不能执行的写法也报 error:点号路径(改写为一层嵌套 `{ account: { status }
}`)、读时计算的字段(如 formula)、关系下的第二层条件。另加 `os explain` 段落、测试和一个 lint minor
changeset。
- **为什么改**:#31224 第 D 项。引擎把这个 filter 当作子对象聚合查询的 `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
1. `packages/lint/src/validate-object-field-refs.ts`: a new field-level
position in `validateObjectFieldRefs`. For a field whose
`summaryOperations` names an `object` and carries a `filter`, the
position reads the authored keys itself (`judgeSummaryFilter`): it
descends `$and` / `$or` / `$not` groups 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 shared `judge` does the resolution, so the verdict,
the "Did you mean", severity `error` and 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.filter` runs on the child object
`summaryOperations.object` names, 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.
2. `packages/lint/src/rule-explanations.ts`: an `os explain
object-field-ref-unknown` paragraph for the position, saying which
object, what is walked, and what a miss costs.
3. `.changeset/31224-summary-filter-key-unknown.md`:
`'@objectstack/lint': minor`, BREAKING banner, ADR-0087 `not-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: `validateObjectFieldRefs` is in the reference-integrity suite
with `runtimeTypes: ['flow', 'object']`, so the error reaches `os 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 real `ObjectQL`, with parent `pr_report.total =
sum(pr_line.amount)`.
- With `filter: { status: 'ok' }`, a child insert gives `total = 5`.
- With `filter: { stauts: 'ok' }`, the same insert answers
`SummaryRecomputeError` ("the triggering records WERE written") and
`total` stays `0`.
No `packages/spec`, metadata-protocol source, runtime or `ui/` change.
## Tests
- `packages/lint/src/validate-object-field-refs.test.ts`, new describe
"summaryOperations.filter (addresses the CHILD object, #31224)":
- **Refused**: a misspelt key (error, path, "Did you mean", hint); a key
only the parent has; misses inside `$and` / `$or` / `$not` (paths
pinned).
- **Accepted**: child fields with operators, and the nested one-level
relation form `{ account: { status, industry: { $in } } }`.
- **Refused, the shapes the recompute query refuses**:
- the dotted spelling `account.status`, whose hint is the nested
spelling;
- a dotted path into a scalar field, with the denormalise cure;
- a dotted key with an unknown head, which gets the unknown verdict;
- a `formula` key, at the top level and one level down;
- beneath a relation: an unknown related key, a dotted key, a second
relation holding a condition, and an empty condition.
- **Not judged**: a non-`summary` field.
- **Not judged**: a child the stack does not define; a roll-up with no
filter.
- The file's two coverage lists, which require every position to have
fired, gain `.summaryOperations.filter.`.
-
`packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts`:
an `object-field-ref-unknown` row in `CASES` (a self-referential roll-up
with `filter: { stauts: 'open' }`). The draft saves and reads `valid:
false`; publish refuses with the same words; the package publish refuses
and rolls back.
- **Reverse check** (one-off, not kept): with the new block guarded off,
5 red (the three refusals and both coverage lists); restored, green.
- **Census**: showcase has 4 filtered roll-ups
(`expense-report.object.ts`: `status`, `billable`, `amount`, all fields
of `showcase_expense_line`), 0 unknown keys; hotcrm `main` has 0
filtered roll-ups. Control: 9 roll-ups in examples carry the live key
`summaryOperations.field`.
## Acceptance notes
- Item D of #31224. Item C landed as #31287. Item B1 follows in its own
PR. Item A is not built (seat ruling). So `Refs`, not `Fixes`.
- The declaration follows the seat ruling: `no (narrowing)` (no new
export, since the position joins an existing rule) with `narrowed`.
## Verification
Final head `8fc74827`: the branch was restarted from `origin/main`
`11c1326d` (#31287 merged). `8fc74827` follows the contract review
(6716423527): the position reads the authored keys itself instead of
`walkFilterFieldKeys`, which flattens a dotted key and a nested
condition into one path. The recompute's `where` serves 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 real `SqlDriver` + `ObjectQL` (one-off probe,
not kept): `{ report: { name: 'r' } }` sums (total 5); `{ 'report.name':
'r' }` and a `formula` key each answer `SummaryRecomputeError` on the
child insert, and the total stays 0.
- **Gates on `8fc74827`**: 67 derived by `dispatch-gates --commands`,
all exit 0. They ran over the full build of `834abfdb`, with
`@objectstack/lint` rebuilt for `8fc74827` and the teardown fixture
commit fetched. `--ran` reconciles 67 accounted, 0 NOT-MEASURED, 0
unrun.
- **Changeset level**: `check-changeset-no-major` with a `pull_request`
event carrying this body exits 0.
- **Build and dogfood**: full `pnpm build` (119 tasks), then `pnpm
--filter @objectstack/dogfood test` under the verify lock (rerun on
`8fc74827` after the lint rebuild): 435 files passed, 3 skipped; 3610
tests passed, 41 skipped; VERDICT command-exit 0.
- **Suites**: `@objectstack/lint` 197 files / 8363 tests passed.
`@objectstack/metadata-protocol` 324 files passed, 4 skipped (22538
tests), including the new `CASES` row (51/51 in that file).
CI runs the full set.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
<!-- signed: domain:spec#1 · os-project-manager ·
session_01FEkQ5mw7HFYn9o4LEGMQdK -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent da7fce5 commit 38fc529
5 files changed
Lines changed: 382 additions & 1 deletion
File tree
- .changeset
- packages
- lint/src
- metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4249 | 4249 | | |
4250 | 4250 | | |
4251 | 4251 | | |
| 4252 | + | |
| 4253 | + | |
| 4254 | + | |
| 4255 | + | |
| 4256 | + | |
| 4257 | + | |
| 4258 | + | |
| 4259 | + | |
| 4260 | + | |
4252 | 4261 | | |
4253 | 4262 | | |
4254 | 4263 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
532 | 532 | | |
533 | 533 | | |
534 | 534 | | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
535 | 659 | | |
536 | 660 | | |
537 | 661 | | |
| |||
755 | 879 | | |
756 | 880 | | |
757 | 881 | | |
| 882 | + | |
758 | 883 | | |
759 | 884 | | |
760 | 885 | | |
| |||
773 | 898 | | |
774 | 899 | | |
775 | 900 | | |
776 | | - | |
| 901 | + | |
777 | 902 | | |
778 | 903 | | |
779 | 904 | | |
| |||
0 commit comments