Repository navigation
Commit 46d2b65
fix(lint)!: a report whose runtimeFilter names a field its dataset's object lacks is refused at publish (report-filter-field-unknown) (#30003)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBQ8wXxPd6sXS6s1vP1qX6L8)_
Fixes #29900
Clause-②: yes (narrowing) — the report publish door, `os validate` and
`os build` refuse a report `runtimeFilter` key they accepted before;
`@objectstack/lint` also exports `validateReportRuntimeFilters`,
`REPORT_FILTER_FIELD_UNKNOWN` and the `ReportRuntimeFilterFinding` type.
Metadata-protocol: narrowed
Skills-docs: none — `skills/**` and `content/docs/**` teach
`runtimeFilter` keys that are fields of the dataset's object, which this
rule accepts; no page teaches a key the object lacks. The reasoning
ships as the `os explain report-filter-field-unknown` entry.
## 维护者速读
- 改了什么:报表的 `runtimeFilter` 里写了数据集对象上不存在的字段时,发布(以及 `os validate` / `os
build`)直接 422 拒收,报出 `reports[N].runtimeFilter.KEY`;和仪表盘组件 `filter`
早已有的拦截是同一套判定。
- 为什么改:以前这种报表发布返回 200,之后每个打开报表的人看到的都是 "Dataset query failed"
报错面板,而同样的键写在仪表盘上会被拒;报表是唯一把坏过滤键放进线上的出口。
- 风险与回滚:这是对作者输入的收窄(BREAKING)。示例应用的 9 个报表(5 个带 `runtimeFilter`)测得 0
条误报;接受范围和仪表盘孪生规则一致,另外多接受数据集维度名。回滚就是 revert 本 PR。
-
先看:`packages/lint/src/validate-report-runtime-filter.ts`(新规则)、`packages/lint/src/validate-widget-bindings.ts`
的
`judgeDatasetFilterKeys`(从孪生规则抽出的共用判定)、`packages/lint/src/reference-integrity-suite.ts`
新增的套件成员。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/protocol.dashboard-dataset-publish-gate.test.ts -t 29900`,预期 6
个用例全过(坏键 422、字段 / 维度 / 系统列放行、仪表盘对照不变)。
- 需要你做的:无,席位落地。
## What changes
- **New rule `report-filter-field-unknown`** (`error`,
`packages/lint/src/validate-report-runtime-filter.ts`). Each key of a
report's `runtimeFilter` is judged on the bound dataset's object graph.
On a `joined` report, each block's own filter is judged against the
block's dataset, and the report-level `runtimeFilter` against every
block's dataset, because the renderer merges it into every block's query
(`DatasetReportRenderer`: `mergeFilters(outerFilter,
block.runtimeFilter)`).
- **One judgement, shared with the dashboard twin.** The twin's inline
key judgement in `validateWidgetBindings` moved into
`judgeDatasetFilterKeys` (existence, then the `include` clause), and its
dataset index into `indexResolvableDatasets`. The widget arm now calls
both, with its rule ids, messages, hints and paths unchanged (the twin's
suite is green unchanged). The report arm reads only the existence
verdict. Neither helper is exported from the package entry.
- **Accepted keys.** Everything the twin accepts: object fields,
registry-injected system columns, dotted relationship paths, all `$`
operators and combinators, and the three skips (an object the stack does
not define, one with no readable field map, an injected column). One
more: a bare key naming a **dimension** of the bound dataset, because
the dataset door binds a bare `where` key through the cube's members
first (`dataset-filter-field-binding.ts`: «a key naming only a dimension
still resolves through that dimension»).
- **Where it runs.** A reference-integrity suite member with
`runtimeTypes: ['flow', 'report']`, beside `validateReportDateRange`. So
it runs on `os validate`, `os build` and `os lint`, and at the runtime
report publish door (draft promotion, active save, package draft
publish). `defineStack` does not run lint rules, so it does not reach
`defineStack` (the twin does not either).
- **`os explain report-filter-field-unknown`**: a new
`RULE_EXPLANATIONS` entry.
- **Changeset:** `@objectstack/lint` minor, BREAKING, ADR-0087
`not-required (no-migration-prescription)`.
## Why
The card's reproduction: a summary report over `orch_ds2` with
`runtimeFilter: { no_such_field: 'x' }` published 200, and the Console
showed every reader "Dataset query failed: 400 Bad Request — Filter
member no_such_field …". The same key on a dashboard widget's `filter`
was refused 422 (`widget-filter-field-unknown`), so the report was the
one consumer of the same dataset query that let it through.
## Tests
- `packages/lint/src/validate-report-runtime-filter.test.ts` (new, 12):
the card's repro at `reports[0].runtimeFilter.no_such_field` with the
twin's verdict and "Did you mean". It also covers a field key, a
dimension key, a system column under `$or` / `$not`, dotted paths, a
joined report (block filter, plus the container filter per block), the
skips, and that the include clause is not this rule's. A **parity pin**
runs six keys through both rules on one dataset and gets the same
verdict from each. The `validate` and `build` commands are pinned too.
- `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: at the
door's own snapshot shape, the card's repro is refused at its path, a
joined block's own key and a container key are refused, and field,
dimension, system-column and `$` keys publish clean. The report member
pin moves from two members to three.
- `packages/lint/src/reference-integrity-suite.test.ts`: order pin gains
the member after `validateReportDateRange`.
-
`packages/metadata-protocol/src/protocol.dashboard-dataset-publish-gate.test.ts`,
a new `#29900` block on the existing harness (no new engine double, so
no ledger moves). These pins run through the real `saveMetaItem` draft
then `publishMetaItem`: the card's repro is refused `422
INVALID_METADATA` at `reports[0].runtimeFilter.no_such_field`, and the
active save is refused the same way. A field key, a dimension key and a
system column under `$and` each publish. The dashboard control on the
same dataset is still refused `widget-filter-field-unknown` at
`dashboards[0].widgets[0].filter.no_such_field`.
## Acceptance notes
- **Ruling premise measured false, intent kept.** The ruling lists
"dataset dimensions" among keys the twin already accepts. Measured: the
twin **refuses** a bare dimension name that differs from its field. A
probe of a dimension `state` over field `status` with `filter: { state:
1 }` answered `widget-filter-field-unknown` at
`dashboards[0].widgets[0].filter.state`. The report rule accepts it, as
the ruling asks; that refuses less than the twin, never more. Whether
the twin should accept it too is reported to the seat, not changed here.
- **Cross-package case (measured, not changed).** At the door, with
`packageScope: { packageId: 'P', dependencies: [] }` and the dataset's
object carrying `_packageId: 'showcase'`, both
`widget-filter-field-unknown` and `report-filter-field-unknown` are
silent. With `dependencies: ['showcase']`, or with no scope, both
refuse. The mechanism is `buildRuntimeWriteSnapshotSet`, which narrows
`objects` to the written package's closure (#9612) but not `datasets`. A
dataset outside the closure therefore still resolves, while its object
is absent, and skip 1 fires. This is a gap and not a stated exemption,
since no comment rules that an out-of-closure dataset's keys go
unjudged. Reported to the seat to file.
- **Not this rule's:** a dotted key that resolves but whose prefix the
dataset's `include` omits (the twin's
`widget-filter-field-not-included`). The report arm ignores that
verdict, and a pin holds it silent.
- **Declarations, measured against the twin.**
`widget-filter-field-unknown` landed in `fa1eca3` as a minor "narrows
the accept set" change, before Clause-② existed. This PR declares the
narrowing and also exports a new validator, rule id and type from the
`@objectstack/lint` entry, so the line is `yes (narrowing)`; the claim's
provisional line was `no (narrowing)`. The changeset follows `ae7f49e`
(a refusal of input that never worked): BREAKING, Before/Now, ADR-0087
`not-required (no-migration-prescription)`. A literal `FROM → TO.` label
is refused by `check-adr-0087-registration` as contradicting
`no-migration-prescription`. A refused key has no rewrite that keeps the
author's meaning, so the body says what the author does instead.
- **Shipped corpus:** the rule over `examples/app-showcase` (4 reports,
1 with `runtimeFilter`), `app-todo` (5, 4 with it), `app-crm` (0) and
`app-multi-package` (0) gives **0 findings**. A positive control on a
showcase report with `no_such_field_probe` fires at
`reports[0].runtimeFilter.no_such_field_probe`.
## Verification
Every reading below is at head `29690d3da`.
**Fix-up after contract review 6620744276 (head `c238f6f96`).**
`e88d432c9` changes the changeset's `Skills-docs:` value to `none — …`,
matching this body. `c238f6f96` merges `origin/main` at `0b99c2b16`
(#29992) with `scripts/pm/os-regen-merge.sh`; the merge is clean, and
the branch diff against main is still the same 10 files. On the merged
head, after a full `turbo run build` (120/120 tasks):
- `pnpm --filter @objectstack/lint test`: 193 files and 7969 tests pass.
- The `#29900` route pins: 13/13 pass.
- The three dogfood files main moved, `signup-organization-census`,
`signup-organization-census-walled` and `batch-row-hook-aborted`: 3
files and 29 tests pass. That clears the two signup reds from the
battery below.
- Derived gates: 58 derived, 58 run, all exit 0 (`--ran`: 58 run, 0
NOT-MEASURED).
<details><summary>Ablation (one, of the rule)</summary>
Mutation: `if (verdict.kind !== 'unknown') continue;` → `if
(verdict.kind !== 'unknown' || 'ABLATE29900'.length > 0) continue;` in
`validate-report-runtime-filter.ts`, via `scripts/ablation-replace.mjs`
(anchor 1 → 0, blob `3b984fd59a88` → `c25f0755128f`). Then `pnpm
--filter @objectstack/lint build` ran, and `ablation-dist-preflight.mjs
@objectstack/lint ABLATE29900` exited 0 (the marker is in `dist/`).
- Red: lint `validate-report-runtime-filter.test.ts` +
`runtime-gate.inert-type-writes.test.ts` gave 10 failed / 52 passed,
including the card's repro, the door LIT pins and the `validate` /
`build` pins. metadata-protocol `-t 29900` gave 2 failed / 4 passed: the
draft→publish 422 and the active-save 422. The controls stayed green.
- Restore: blob back to `3b984fd59a88` (== HEAD), `git diff HEAD` empty,
rebuilt, `--absent` preflight exit 0; 62/62 and 6/6 green.
</details>
<details><summary>Package tests and typecheck</summary>
- `pnpm --filter @objectstack/lint test`: Test Files 193 passed, Tests
7969 passed; `typecheck` (tsc + test-typecheck) exit 0.
- `pnpm --filter @objectstack/metadata-protocol test`: Test Files 313
passed | 4 skipped, Tests 22345 passed | 26 skipped; `typecheck` exit 0.
- `eslint --no-inline-config` over the 9 changed source/test files: exit
0.
- `pnpm lint` (repo-wide `eslint . --no-inline-config`): exit 0.
</details>
<details><summary>Derived gates (dispatch-gates --commands)</summary>
`node scripts/pm/dispatch-gates.mjs --commands` on head `29690d3da`
derives 58 commands; all 58 ran after the full build and exited 0.
`--ran` reconciliation: "58 derived famil(ies) accounted for — 58 run, 0
NOT-MEASURED".
- In the first pass, three commands exited 3 for environment reasons,
and reran green: `check-plugin-teardown-shape --self-test` (shallow
clone; fixture commit fetched, 48 cases), `check:dual-build-cjs-loads`
(needed the full build), and `check-adr-0087-registration` (exit 1, a
`FROM → TO.` label beside `no-migration-prescription`, fixed in the
changeset).
- `check:engine-double-contract` and `check:objectql-double-limit` were
red on a copied stub engine in a new test file. The route pins moved
into the existing harness, so no ledger moves, and both are green.
</details>
<details><summary>Dogfood battery (narrowing arm)</summary>
Head `29690d3da`, one locked command: `pnpm exec turbo run build
--concurrency=2 && pnpm --filter @objectstack/dogfood test` (no shard,
no path). `os-verify-lock: VERDICT command-exit 1 · held the lock
2841s`. Test Files 1 failed | 414 passed | 3 skipped (418), Tests 2
failed | 3431 passed | 28 skipped (3461).
- The 2 failures are both in
`test/signup-organization-census.dogfood.test.ts`: "guard: the boot is
single, and the sign-up reached all three writers" ("the verification
mail persisted, in the identity-mail log: expected 0 to be greater than
0") and "sys_identity_email carries no organization column, and the
sign-up's mail row names none".
- **Not this change.** The file alone on this head gives 2 failed / 1
passed. As a control, the file alone with `packages/lint/src` checked
out at `origin/main` and lint rebuilt (the dist carries 0
`report-filter-field-unknown`) gives the same 2 failed / 1 passed.
Restored afterwards: tree clean, lint rebuilt (2 hits). The test
exercises self-registration mail, which no lint rule reads. Reported to
the seat as out of scope.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
repo's required Chinese summary).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzhd49bgGPqyqFfVu83
<!-- signed: domain:server#6 · os-support-ai ·
session_01WGufzhd49bgGPqyqFfVu83 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f19fbb8 commit 46d2b65
10 files changed
Lines changed: 666 additions & 62 deletions
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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
721 | 721 | | |
722 | 722 | | |
723 | 723 | | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
724 | 732 | | |
725 | 733 | | |
726 | 734 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
56 | 59 | | |
57 | 60 | | |
58 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
| 114 | + | |
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
| |||
514 | 515 | | |
515 | 516 | | |
516 | 517 | | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
517 | 535 | | |
518 | 536 | | |
519 | 537 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5490 | 5490 | | |
5491 | 5491 | | |
5492 | 5492 | | |
| 5493 | + | |
| 5494 | + | |
| 5495 | + | |
| 5496 | + | |
| 5497 | + | |
| 5498 | + | |
| 5499 | + | |
| 5500 | + | |
| 5501 | + | |
| 5502 | + | |
| 5503 | + | |
| 5504 | + | |
| 5505 | + | |
| 5506 | + | |
| 5507 | + | |
| 5508 | + | |
| 5509 | + | |
| 5510 | + | |
| 5511 | + | |
| 5512 | + | |
| 5513 | + | |
| 5514 | + | |
| 5515 | + | |
| 5516 | + | |
5493 | 5517 | | |
5494 | 5518 | | |
5495 | 5519 | | |
| |||
5762 | 5786 | | |
5763 | 5787 | | |
5764 | 5788 | | |
| 5789 | + | |
5765 | 5790 | | |
5766 | 5791 | | |
5767 | 5792 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
527 | 527 | | |
528 | 528 | | |
529 | 529 | | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 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 | + | |
530 | 597 | | |
531 | 598 | | |
532 | 599 | | |
| |||
928 | 995 | | |
929 | 996 | | |
930 | 997 | | |
931 | | - | |
| 998 | + | |
932 | 999 | | |
933 | 1000 | | |
934 | 1001 | | |
935 | 1002 | | |
936 | 1003 | | |
937 | 1004 | | |
938 | 1005 | | |
939 | | - | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
940 | 1011 | | |
941 | 1012 | | |
942 | 1013 | | |
| |||
0 commit comments