Repository navigation
fix(lint): a validation rule's expression finding carries the slot's key path - #27675
Conversation
…key path Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzj2PNcHjESYvrviQf7
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzj2PNcHjESYvrviQf7
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzj2PNcHjESYvrviQf7
📓 Docs Drift Check4 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 b76e7d0ff08e90d96c9368cc885f77d08121ed05 && git checkout b76e7d0ff08e90d96c9368cc885f77d08121ed05
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 33ab543913125b53feec6c116f13d616261a908c 9d9987f555e1a52d3b52bc8670e20376cd5c61af && git checkout -B drift-repro 33ab543913125b53feec6c116f13d616261a908c && git merge --no-ff 9d9987f555e1a52d3b52bc8670e20376cd5c61af
node scripts/docs-audit/affected-docs.mjs --json 33ab543913125b53feec6c116f13d616261a908c |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 9d9987f · 2026-10-29T17:40Z Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs read: card #27643 (body, 3 comments: triage, claim, dev report), parent card #27482 (body, 8 comments), PR #27675 (body, 7-file list, net diff from the files endpoint, 2 comments), the 55 check-runs on the head. The rest of the diff — ① Derived judgments
② Semver level
Clause-②: no — judged right. ③ Boundary flags
④ Design judgment
Must-fix: none. Should-fix: none. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP 维护者速读 (席位终稿)
席位意见:接受。分诊契约复核 PASS(6504243774,就在当前 head 上),没有 must-fix,也没有 should-fix。 席位核对 · ACCEPT ·
|
domain:spec#3 · os-support-ai · session_01WGufzj2PNcHjESYvrviQf7
Requested by Yibo Wang · project thread
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,
whereand 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/lintpatch changeset.@objectstack/metadata-protocolpublishes 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文字不变。path文字的调用方会看到新拼写(仓内已查遍,见 Acceptance notes)。回滚即还原本 PR。packages/lint/src/validate-expressions.ts(对象校验规则循环里的atSlot),packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts(finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #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/mainb761968dA draft of an object whose
amountfield is nullable, with three rules (ok_ruleclean;amount_rulerecord.amount > 100; aconditionalcond_rulewithwhen: record.amount > 5andthen→innerrecord.amount < 1000), saved through the realsaveMetaItem(draft), read withgetMetaItem({ state: 'draft' }), then published:Before (
_diagnostics.errors[], then the publish 422issues[]path / where):The flow-edge precedent on the same harness (the #26707
edge_celflow, edgee2conditionrecord.name ==):After, at this PR's head (same probe):
Messages are byte-identical before and after.
The producer (
packages/lint/src/validate-expressions.ts)objects[I].validations[N],indexOfon the raw arrays, the Studio: an expression field that says "Valid CEL" should read the draft's server verdict at its own path (RLS predicate, flow edge condition) #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), theparentroot verdict (lint: avalidations[]rule whose condition readsparentpassesos validate, but novalidations[]rule bindsparent, so every write under it is refused as unevaluable #24720) andcheckNullGuards(has(x)reads as a null guard and is not one — a publish-time lint should reject un-guarded nullable comparisons in CEL predicates #4763).rulePredicatesand the loop:conditionandwhenon the rule itself, and the same two on everyconditionalrule nested underthen/otherwise, at any depth.RulePredicategainskey(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 Studio: an expression field that says "Valid CEL" should read the draft's server verdict at its own path (RLS predicate, flow edge condition) #26858 only because it has none.objectscollection has no index to spell, so its findings keepwherealone, as a map-formflowscollection's do.ExprIssue.path(from Studio: an expression field that says "Valid CEL" should read the draft's server verdict at its own path (RLS predicate, flow edge condition) #26858) carries it, andauthoring-rules.ts'spath: i.path ?? i.whereneeds no change. The publish rule is not copied anywhere.nameKeyFindingPathre-spellsobjects[I]asobjects.NAMEafter it, andwithPublishVerdict'sitemRelativeIssuePathstrips that prefix for_diagnostics.Tests
packages/lint/src/validate-expressions.validation-rule-path.test.ts(9 cases), one per slot and verdict, each asserting the key path AND the unchangedwhere: the null-guard gate oncondition;check()oncondition;check()and the null-guard gate onwhen; nestedthen.condition,otherwise.when,otherwise.then.condition; theparentroot verdict; raw-list indexing pastnullmembers (object and rule); map-formobjectskeeps no path; a field formula finding gains none; and the publish gate's by-name spellingobjects.rule_probe.validations[1].condition.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts: the finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 block's pin moves from the prose toobjects.fx_rule.validations[0].condition(the 422 on save and on promotion), withwherestill asserted as the prose; a new case pins the draft read's_diagnostics[].pathatvalidations.0.conditionfor both refused bodies;(d)door-vs-build parity keepsrule/where/message/hintexact and comparespathonce the door's by-name top-level spelling is applied. The lint: a conditional validation rule's nestedthen/otherwisepredicate is never validated, soos buildpasses and the object save door stores a predicate the top-level rule would refuse #22042 (nested) and lint 18.x:validationRuleParentIssuemoves fromwarningtoerror, so avalidations[]rule that readsparentis refused atos buildand the object save door (the refusal half of #24720) #24957 (parent) blocks of the same file pinned the same prose path and move the same way. Nothing is skipped.path: wherefor a validation-rule finding move the same way:runtime-gate.object-validation-writes.test.ts(8 cases; a localasAtDoorapplies the door's by-name spelling for the door-vs-buildtoEquals),validate-expressions.validation-rule-parent.test.ts(2),runtime-gate.object-field-existence-spellings.test.ts(thevalidation conditionslot's door path).validate-expressions.test.ts's validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017 receiver meta-test reads the new index through the tabledobjreceiver (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), throughscripts/ablation-replace.mjswith a trap restore, under the lock:if (!rulePath) return;→if (!rulePath || 'ablated-27643') return;inatSlot(no finding gains a path). Landed: anchor 1 → 0, bloba4aad9da→fbc41d77.pnpm --filter @objectstack/lint buildexit 0;ablation-dist-preflight.mjs @objectstack/lint 'ablated-27643': marker present in 4 built files, exit 0.validate-expressions.validation-rule-path.test.ts7 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[].pathpin among them).a4aad9da= HEAD,git diff HEADempty, rebuild exit 0, preflight--absentexit 0 (marker absent from all 20 built files, tree clean). Green again: 9/9 and 41/41./* 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 grepfor· validation ',· validation rule 'and.pathbeside them):packages/linttests: the four files above, updated. Every other lint test matches onwhere, which is unchanged.packages/metadata-protocol: the finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032, lint: a conditional validation rule's nestedthen/otherwisepredicate is never validated, soos buildpasses and the object save door stores a predicate the top-level rule would refuse #22042 and lint 18.x:validationRuleParentIssuemoves fromwarningtoerror, so avalidations[]rule that readsparentis refused atos buildand the object save door (the refusal half of #24720) #24957 blocks, updated.withPublishVerdictneeds no change (it already strips anobjects.NAMEprefix).packages/cli:os lintprintsat ${issue.path}(authoringFindingToLintIssue), so for these findings it now prints the key path where it printed thewheretext a second time; the message still leads withwhere.artifact-packages.ts'sfindingKeynormalises the top-level index (objects[3]→objects[]), so its per-package dedupe is unaffected.os validate/os build/doctorprintwhere.packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.tsmatches[rule, where], unchanged.ui/packages/app-shell/src/views/metadata-admin/draftVerdict.ts:serverErrorsAtmatches a path by equality, and no Studio field asks forvalidations.N.conditionyet, so nothing changes on screen until layer 2 (Studio: an expression the engine refuses (RLS USING on a formula field, a null-unsafe validation predicate) keeps autosaving — the server verdict is shown on the field but never holds the save #27482).withServerOnlyErrorsdedupes 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 atvalidations.N.conditionwith 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) touchesprotocol.runtime-authoring-gate.test.ts, in the #27069 related-list block near the end of the file (an addeditand a changed fixture); no hunk overlaps the #22032 / #22042 / #24957 blocks. No open PR touchesvalidate-expressions.tsorauthoring-rules.ts.Verification
All at head
9d9987f5(the PR head), underscripts/pm/os-verify-lock.shon the shared box:Suites: lint and metadata-protocol test + typecheck, all exit 0
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.Derived gates: 59 derived, 59 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 59 families derived at9d9987f5.check-plugin-teardown-shape --self-test(shallow clone),check:dual-build-cjs-loadsandcheck:lean-entry-closure(no full build).git fetch origin 621a4876and a fullturbo 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.!in the title and no(narrowing)arm).Generated by Claude Code