Skip to content

fix(lint): a validation rule's expression finding carries the slot's key path - #27675

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kkb2g0
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-kkb2g0

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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, 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.

维护者速读

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):

{ 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 ==):

{ 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):

{ 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)

Tests

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):

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:

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.
  • Lock VERDICT command-exit 0 (5m23s).
Derived gates: 59 derived, 59 run, all exit 0
  • 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 621a4876 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).

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 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
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 33ab543913125b53feec6c116f13d616261a908c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b76e7d0ff08e90d96c9368cc885f77d08121ed05 — the merge of head 9d9987f555e1a52d3b52bc8670e20376cd5c61af into base 33ab543913125b53feec6c116f13d616261a908c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 9d9987f · 2026-10-29T17:40Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9d9987f555e1a52d3b52bc8670e20376cd5c61af
Local-runs: none
Scope: packages/lint/src/runtime-gate.object-field-existence-spellings.test.ts, packages/lint/src/runtime-gate.object-validation-writes.test.ts, packages/lint/src/validate-expressions.ts, packages/lint/src/validate-expressions.validation-rule-parent.test.ts, packages/lint/src/validate-expressions.validation-rule-path.test.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts + packages/lint/src/authoring-rules.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/object-graph.ts, packages/metadata-protocol/src/metadata-diagnostics.ts, packages/spec/src/data/object.zod.ts, packages/cli/src/commands/lint.ts, packages/cli/src/utils/artifact-packages.ts, ui/packages/app-shell/src/views/metadata-admin/draftVerdict.ts

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 — .changeset/27643-validation-rule-finding-path.md — is the lane seat's ACCEPT at TIER_DEFAULT; it is read here only for ②.

① Derived judgments

  • Accept set unchanged — right. The only producer hunk is in runStackExpressionPasses (packages/lint/src/validate-expressions.ts 2967–3053 at head). atSlot rewrites issues[k] in place as { ...issues[k], path } over the half-open range each slot's call pushed; it pushes nothing and drops nothing, and every push site it covers (check() 2384–2386 and its hop refusals 2147/2197, the parent root verdict 3043, checkNullGuards 2303) still pushes the same where, message, source, severity. Clause-②: no is the right declaration: nothing saves, publishes or refuses differently.
  • ExprIssue.path set on validation-rule findings — right, no new member. path?: string has existed since 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 its docblock and its population change. RulePredicate.key is on a non-exported interface. authoring-rules.ts:797 (path: i.path ?? i.where) needs no edit and gets none, so runAuthoringRules findings for this rule now carry the key path as the flow-edge findings already do.
  • The runtime gate needs no change — right, read in runtime-gate.ts. buildRuntimeWriteSnapshotSet (675–735) appends the written item LAST and keeps siblings at the same index in baseline, candidate and stored; fingerprint (604) keys on the raw positional path, so a sibling's objects[I].validations[N].x cancels across passes exactly as its prose path did. isLocatedOnAnotherEntry (768–797) reads the new path through its positional branch (objects[k], k vs writtenSlot) with the same answer the prose branch (object 'NAME') gave before. nameKeyFindingPath (959–968) runs AFTER the differential and re-spells objects[S] as objects.NAME because objects is a name-keyed context key — which is why the 422 path reads objects.rule_probe.validations[1].condition while a flow's keeps flows[0].edges[1].condition (flows are not a context collection). Consistent with the existing wire shape, not a new one.
  • _diagnostics[].path item-relative spelling — right. itemRelativeIssuePath (metadata-diagnostics.ts 388–400) strips the objects.NAME prefix and dots every [n], so the draft read serves validations.1.condition / validations.2.when / validations.2.then.condition, the shape serverErrorsAt (draftVerdict.ts 99–106, equality match) resolves and the shape Studio: the Validations tab holds the autosave on a served CEL verdict at the rule's own path, as the RLS arm does (#27482 layer 2 of 2) #27644 will ask for. Metadata-protocol: none is right: no metadata key or value changes; the envelope member is the one 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 already populates for flows.
  • Indexing over the authored lists — right. recordsOf (object-graph.ts 215–221) returns the array's own members filtered, so stack.objects.indexOf(obj) / obj.validations.indexOf(rule) give raw indices, and a null member before the entry does not renumber it (pinned). validations is z.array(ValidationRuleSchema) in the spec (object.zod.ts 3018, 4482), so there is no map-form validations to leave behind; the map-form objects collection keeps where alone, as map-form flows do.
  • Downstream readers — right as listed. os lint (cli/src/commands/lint.ts 790–798, 1146) prints at ${f.path}: positional at the CLI (objects[0].validations[1].condition), as it already prints a flow edge's; findingKey (artifact-packages.ts 104–105) strips the top-level index, so the per-package dedupe is unaffected. withServerOnlyErrors (draftVerdict.ts 72–79) can now dedupe a client error raised at the same key path — the intended direction for layer 2.
  • In-flight intersection — holds. Open PR feat(spec,lint,app-shell)!: a field group's relatedList names body tables only #27592 (now at head f8ed3f6c) touches protocol.runtime-authoring-gate.test.ts in the related-list block; no open PR touches validate-expressions.ts or authoring-rules.ts.

② Semver level

.changeset/27643-validation-rule-finding-path.md: @objectstack/lint: patch — matches what the diff publishes. One package's source changes (packages/lint); the metadata-protocol diff is one test file and publishes nothing, so it rightly has no changeset line. The level is the one PR #26999 (#26858) set for the same kind of change — a locator value moving from prose to the key path, type shape and accept set unchanged, prose where still served. The Clause-②: no and Metadata-protocol: none lines are present and identical on the PR body and the changeset; no (narrowing) arm. Check Changeset, PR-body guards and Governed Surface Queue Guard are all success on the head.

Clause-②: no — judged right.

③ Boundary flags

④ Design judgment

Must-fix: none. Should-fix: none.

Implemented-by: claude/project-thread-kkb2g0
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

维护者速读 (席位终稿)

席位意见:接受。分诊契约复核 PASS(6504243774,就在当前 head 上),没有 must-fix,也没有 should-fix。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-29T20:21Z

Head: 9d9987f555e1a52d3b52bc8670e20376cd5c61af · card #27643 · claim 6502420877

  1. 形态: a draft PR to main carrying Fixes #27643 and Part of #27482 (layer 1, the producer): 7 paths (+300 / −26), inside the claim's surface. Each validation-rule expression finding that validate-expressions.ts pushes now carries the judged slot's key path (validations.N.condition, .when, .then.condition, and the nested then/otherwise slots) in the flow-edge spelling of 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; where and every message are byte-identical. Clause-②: no / Metadata-protocol: none, @objectstack/lint patch, matching fix(app-shell): an expression field shows the draft's server verdict at its own path #26999. check-governed-merges reports NOT governed, so the ordinary queue landing applies.
  2. 前提实测: before and after are measured in the body: on main the draft read's _diagnostics[].path for a rule is prose; on the head it is the key path, and the publish 422 path is objects.NAME.validations[N].condition. The contract review (6504243774, PASS, this head) confirmed that the metadata-protocol pin (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, plus the lint: a conditional validation rule's nested then / otherwise predicate is never validated, so os build passes and the object save door stores a predicate the top-level rule would refuse #22042 and lint 18.x: validationRuleParentIssue moves from warning to error, so a validations[] rule that reads parent is refused at os build and the object save door (the refusal half of #24720) #24957 blocks in the same file) is moved, not weakened, and that the value is the one layer 2 consumes.
  3. 门禁 check-runs (on this head): 56 runs: 40 success, 16 skipped, none red or pending. Dev-side: lint and metadata-protocol test and typecheck exit 0, dispatch-gates --ran 59/59, ablation recorded.
  4. 接受的偏差: a probe test committed in 7363e13f and deleted in 9d9987f5; the net diff is clean, and the squash merge carries none of it.
  5. 范围外去向: layer 2 (the Studio reader) stays on 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. Skills-docs: no skill teaches a validation-rule finding's path.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (r0) and the changeset. The seat readies the PR and arms auto-merge in this act, follows it to MERGED, and lands lint: a validation rule's expression finding carries the key path validations.N.condition, not prose, so the draft read can address it (#27482 layer 1 of 2) #27643.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 20:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 20:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit e793e9d Oct 29, 2026
57 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-kkb2g0 branch October 29, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant