You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e793e9d
Browse filesBrowse the repository at this point in the historyBrowse files
fix(lint): a validation rule's expression finding carries the slot's key path (#27675)
domain:spec#3 · os-support-ai · session_01WGufzj2PNcHjESYvrviQf7
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBSqjVuhLMsVzHs6jpRpHNTh)_Fixes#27643
Part of #27482 (layer 1 of 2, the producer; the Studio reader, layer 2,
stays on #27482).
Clause-②: no
Metadata-protocol: none
Both declarations are the claim's, measured and kept: no accept set
moves (every body that saved or published before does so now, every
refusal still refuses with the same rule, `where` and message); only the
locator of an existing refusal changes, and no metadata key or value is
touched. They match what PR #26999 (#26858, the flow-edge precedent)
declared for the same kind of change: `Clause-②: no`,
`Metadata-protocol: none`, one `@objectstack/lint` patch changeset.
`@objectstack/metadata-protocol` publishes nothing new here (its diff is
one test file), so it has no changeset line.
## 维护者速读
- 改了什么:对象校验规则(`validations[]`)表达式被拒时,发现项的 `path` 从描述文字(`object 'X' ·
validation rule 'R'`)改为键路径(`objects[0].validations[1].condition`);草稿读取的
`_diagnostics[].path` 因此变成 `validations.1.condition`,发布 422 的
`issues[].path` 变成 `objects.X.validations[1].condition`。`where` 文字不变。
- 为什么改:Studio
按字段路径对号显示服务端结论;流程连线条件已经这样做(#26858),校验规则此前只有描述文字,界面对不上号,无法据此暂停自动保存(#27482
第 2 层要用)。
- 风险与回滚:拒绝与放行完全不变,只换定位字段;直接读 `path` 文字的调用方会看到新拼写(仓内已查遍,见 Acceptance
notes)。回滚即还原本 PR。
- 先看:`packages/lint/src/validate-expressions.ts`(对象校验规则循环里的
`atSlot`),`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`(#22032
块新增的草稿读取断言),`packages/lint/src/validate-expressions.validation-rule-path.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run
src/validate-expressions.validation-rule-path.test.ts`,应 9/9 全绿。
- 需要你做的:无,席位落地(契约复核后)。
## What changes
### Measured first, on `origin/main` `b761968d`
A draft of an object whose `amount` field is nullable, with three rules
(`ok_rule` clean; `amount_rule` `record.amount > 100`; a `conditional`
`cond_rule` with `when: record.amount > 5` and `then` → `inner`
`record.amount < 1000`), saved through the real `saveMetaItem` (draft),
read with `getMetaItem({ state: 'draft' })`, then published:
Before (`_diagnostics.errors[]`, then the publish 422 `issues[]` path /
where):
```text
{ path: "object 'rule_probe' · validation rule 'amount_rule'", code: "expression-invalid" }
{ path: "object 'rule_probe' · validation rule 'cond_rule' when-predicate", code: "expression-invalid" }
{ path: "object 'rule_probe' · validation rule 'cond_rule' then → 'inner'", code: "expression-invalid" }
422 path = where = "object 'rule_probe' · validation rule 'amount_rule'" (and likewise for the other two)
```
The flow-edge precedent on the same harness (the #26707 `edge_cel` flow,
edge `e2` condition `record.name ==`):
```text
{ path: "edges.1.condition", code: "expression-invalid" }
422 path = "flows[0].edges[1].condition", where = "flow 'edge_cel' · edge 'e2' (decide→end) condition"
```
After, at this PR's head (same probe):
```text
{ path: "validations.1.condition", code: "expression-invalid" }
{ path: "validations.2.when", code: "expression-invalid" }
{ path: "validations.2.then.condition", code: "expression-invalid" }
422 path = "objects.rule_probe.validations[1].condition", where = "object 'rule_probe' · validation rule 'amount_rule'"
422 path = "objects.rule_probe.validations[2].when", where = "object 'rule_probe' · validation rule 'cond_rule' when-predicate"
422 path = "objects.rule_probe.validations[2].then.condition", where = "object 'rule_probe' · validation rule 'cond_rule' then → 'inner'"
```
Messages are byte-identical before and after.
### The producer (`packages/lint/src/validate-expressions.ts`)
- The object validation-rule loop computes the rule's key path over the
lists as authored (`objects[I].validations[N]`, `indexOf` on the raw
arrays, the #26858 spelling), and stamps `${rulePath}.${slot}` onto
every issue each slot's call pushes. That covers all three verdicts the
pass gives: `check()` (syntax, bare refs, unknown fields, hop checks),
the `parent` root verdict (#24720) and `checkNullGuards` (#4763).
- The expression slots a rule has, measured from `rulePredicates` and
the loop: `condition` and `when` on the rule itself, and the same two on
every `conditional` rule nested under `then` / `otherwise`, at any
depth. `RulePredicate` gains `key` (`condition`, `when`,
`then.condition`, `otherwise.then.when`, …). The nested slots are
included: they are slots of the same top-level rule and have an
unambiguous key path (`validations.0.then.condition`); a flow's nested
region edge was left out in #26858 only because it has none.
- A map-form `objects` collection has no index to spell, so its findings
keep `where` alone, as a map-form `flows` collection's do.
- No new member: `ExprIssue.path` (from #26858) carries it, and
`authoring-rules.ts`'s `path: i.path ?? i.where` needs no change. The
publish rule is not copied anywhere.
- The runtime gate is untouched: its differential compares the raw
positional paths (siblings sit at the same index in every snapshot),
`nameKeyFindingPath` re-spells `objects[I]` as `objects.NAME` after it,
and `withPublishVerdict`'s `itemRelativeIssuePath` strips that prefix
for `_diagnostics`.
## Tests
- New
`packages/lint/src/validate-expressions.validation-rule-path.test.ts` (9
cases), one per slot and verdict, each asserting the key path AND the
unchanged `where`: the null-guard gate on `condition`; `check()` on
`condition`; `check()` and the null-guard gate on `when`; nested
`then.condition`, `otherwise.when`, `otherwise.then.condition`; the
`parent` root verdict; raw-list indexing past `null` members (object and
rule); map-form `objects` keeps no path; a field formula finding gains
none; and the publish gate's by-name spelling
`objects.rule_probe.validations[1].condition`.
-
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`:
the #22032 block's pin moves from the prose to
`objects.fx_rule.validations[0].condition` (the 422 on save and on
promotion), with `where` still asserted as the prose; a new case pins
the draft read's `_diagnostics[].path` at `validations.0.condition` for
both refused bodies; `(d)` door-vs-build parity keeps
`rule`/`where`/`message`/`hint` exact and compares `path` once the
door's by-name top-level spelling is applied. The #22042 (nested) and
#24957 (`parent`) blocks of the same file pinned the same prose path and
move the same way. Nothing is skipped.
- Lint suites that pinned `path: where` for a validation-rule finding
move the same way: `runtime-gate.object-validation-writes.test.ts` (8
cases; a local `asAtDoor` applies the door's by-name spelling for the
door-vs-build `toEqual`s),
`validate-expressions.validation-rule-parent.test.ts` (2),
`runtime-gate.object-field-existence-spellings.test.ts` (the `validation
condition` slot's door path).
- `validate-expressions.test.ts`'s #5017 receiver meta-test reads the
new index through the tabled `obj` receiver (`obj.validations.indexOf`),
so no excuse is added.
### Ablation (path stamping removed)
At head `7363e13f` (the fix and the moved pins committed; the later
changeset commit touches no code), through
`scripts/ablation-replace.mjs` with a trap restore, under the lock:
- Mutation: `if (!rulePath) return;` → `if (!rulePath ||
'ablated-27643') return;` in `atSlot` (no finding gains a path). Landed:
anchor 1 → 0, blob `a4aad9da` → `fbc41d77`. `pnpm --filter
@objectstack/lint build` exit 0; `ablation-dist-preflight.mjs
@objectstack/lint 'ablated-27643'`: marker present in 4 built files,
exit 0.
- Red: `validate-expressions.validation-rule-path.test.ts` 7 failed / 2
passed (the map-form and off-rule controls stay green, as they should);
`protocol.runtime-authoring-gate.test.ts -t '22032|22042|24957'` 11
failed / 30 passed (the draft-read `_diagnostics[].path` pin among
them).
- Restore: blob back to `a4aad9da` = HEAD, `git diff HEAD` empty,
rebuild exit 0, preflight `--absent` exit 0 (marker absent from all 20
built files, tree clean). Green again: 9/9 and 41/41.
- A first attempt used a comment marker (`/* ablated-27643 */`), which
the bundler strips: the preflight answered "marker ABSENT from dist/"
(exit 1), so that run's reading is void even though the same suites went
red; it was redone with the string marker above.
## Acceptance notes
Readers that match on a validation-rule finding's path, grepped
repo-wide at this head (`git grep` for `· validation '`, `· validation
rule '` and `.path` beside them):
- `packages/lint` tests: the four files above, updated. Every other lint
test matches on `where`, which is unchanged.
- `packages/metadata-protocol`: the #22032, #22042 and #24957 blocks,
updated. `withPublishVerdict` needs no change (it already strips an
`objects.NAME` prefix).
- `packages/cli`: `os lint` prints `at ${issue.path}`
(`authoringFindingToLintIssue`), so for these findings it now prints the
key path where it printed the `where` text a second time; the message
still leads with `where`. `artifact-packages.ts`'s `findingKey`
normalises the top-level index (`objects[3]` → `objects[]`), so its
per-package dedupe is unaffected. `os validate` / `os build` / `doctor`
print `where`.
-
`packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts`
matches `[rule, where]`, unchanged.
- `ui/packages/app-shell/src/views/metadata-admin/draftVerdict.ts`:
`serverErrorsAt` matches a path by equality, and no Studio field asks
for `validations.N.condition` yet, so nothing changes on screen until
layer 2 (#27482). `withServerOnlyErrors` dedupes a client error and a
server error with the same path and message; with the prose path no
client error ever matched, with the key path a client error raised at
`validations.N.condition` with the same message is shown once, which is
the intended direction.
- `objectql`'s runtime refusal text (`validation rule 'X' on OBJ could
not be evaluated`) is a different message on a different surface and is
untouched.
In-flight intersection (open PRs at the time of the claim, files listed
per PR): only #27592 (head `e2368298`) touches
`protocol.runtime-authoring-gate.test.ts`, in the #27069 related-list
block near the end of the file (an added `it` and a changed fixture); no
hunk overlaps the #22032 / #22042 / #24957 blocks. No open PR touches
`validate-expressions.ts` or `authoring-rules.ts`.
## Verification
All at head `9d9987f5` (the PR head), under
`scripts/pm/os-verify-lock.sh` on the shared box:
<details><summary>Suites: lint and metadata-protocol test + typecheck,
all exit 0</summary>
- `pnpm --filter @objectstack/lint test`: exit 0, Test Files 178 passed
(178), Tests 7613 passed (7613).
- `pnpm --filter @objectstack/lint typecheck`: exit 0.
- `pnpm --filter @objectstack/metadata-protocol test`: exit 0, Test
Files 279 passed, 4 skipped (283), Tests 21782 passed, 26 skipped
(21808).
- `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0.
- Lock VERDICT command-exit 0 (5m23s).
</details>
<details><summary>Derived gates: 59 derived, 59 run, all exit
0</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 59 families derived at `9d9987f5`.
- Serial pass: 56 exit 0 and 3 exit 3 (prerequisites, nothing measured):
`check-plugin-teardown-shape --self-test` (shallow clone),
`check:dual-build-cjs-loads` and `check:lean-entry-closure` (no full
build).
- After `git fetch origin 621a487` and a full `turbo run build
--concurrency=2` (exit 0, lock VERDICT command-exit 0): all three rerun
exit 0.
- `dispatch-gates --ran`: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN,
exit 0.
- Dogfood: not run (no `!` in the title and no `(narrowing)` arm).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzj2PNcHjESYvrviQf7)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(lint): a validation rule's `expression-invalid` finding carries the slot's key path (`objects[0].validations[1].condition`) beside its `where` text
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
An `expression-invalid` finding on an object validation rule's predicate used to be located by its `where` prose alone (`object 'invoice' · validation rule 'amount_rule'`), which is also what its `path` carried, so a reader keyed on paths could not address it. Its `path` is now the judged slot's key path in the spelling a flow edge's findings use, so a draft's `_diagnostics[].path` reports it item-relative (`validations.1.condition`, `validations.2.when`, `validations.2.then.condition`) and the publish refusal's `issues[].path` reads `objects.invoice.validations[1].condition`.
11
+
12
+
-`where`, the message and the verdict are unchanged: nothing is accepted or refused differently.
13
+
- Every finding of the validation-rule pass moves: the expression check, the `parent` root verdict and the null-guard gate, on a rule's `condition` and `when` and on a `conditional` rule's nested `then` / `otherwise` branches.
14
+
- The object and the rule are indexed in the lists as authored (a `null` member before them does not renumber them).
15
+
- An object in a map-form `objects` collection keeps `where` as its `path`, as before.
16
+
-`os lint` prints the key path after `at` for these findings, where it printed the `where` text a second time.
0 commit comments