Repository navigation
fix(mcp): validate_expression gives the runtime publish gate's verdict at a flow condition and judges the field being authored - #33168
Conversation
…t at a flow condition and judges a named field At site flow_condition the tool now runs the gate's own passes through judgeAuthoredSiteExpression, over a one-flow probe judged as the runtime publish gate judges a flow write: the null guard on a record-triggered flow's record./previous. reads, and the unprovisioned-anchor warning. Two optional inputs: startCondition (the start guard downstream conditions are presumed under) and field (the formula-site field the expression is authored on, so a stored field gets the stored-expression warnings and a formula the cycle verdict). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxiHaRMZVvPqb3cEac
📓 Docs Drift CheckThis PR changes 2 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 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 96a9957ae940f1e635f159074fb0574e94a55bb1 && git checkout 96a9957ae940f1e635f159074fb0574e94a55bb1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3e19223bf561694fdbb575dd2490847038916132 5eea9771e798ea2b4519a8781cc60df60103af10 && git checkout -B drift-repro 3e19223bf561694fdbb575dd2490847038916132 && git merge --no-ff 5eea9771e798ea2b4519a8781cc60df60103af10
node scripts/docs-audit/affected-docs.mjs --json 3e19223bf561694fdbb575dd2490847038916132
|
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx Contract reviewServed-tier: Face: narrowing (the paths decide no face; the Inputs: card #33147 body and its 3 comments (triage 6863163129, claim 6864566168, dev report 6867633023; no ruling and no earlier review record is on the thread), PR #33168 body, file list (8 files) and net diff against Check-runs on the head: 66 concluded, 56 success, 10 skipped, 0 failed, 0 pending (read 2026-11-14T00:02Z). Success includes Build Core, Type Check (workspace, source gates, consumer gates, debt ledger), every Lint family, Test Core 8/8, Test UI 16/16, Type Check UI, Lint UI, Build and E2E UI, Live E2E UI, Dogfood Regression Gate, Check Changeset, PR-body guards and Governed Surface Queue Guard. The 10 skipped are opt-in or queue-only jobs (Queue verdict, Borrowed queue verdict, Build Docs, packed-tarball smoke, 18.x upgrade harness, Temporal Conformance, UI Published Artifacts, Dogfood Verify CLI, the sharded dogfood matrix, the schedule gate). ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
② Semver level
③ Boundary flagsPR author login:
④ Design judgment
Observation, outside this diff: the validator half of the tool has never been handed a flow's variables ( Findings: F1 should-fix, F2 should-fix, ③ flag 2 should-fix (process), two nits. No must-fix. Implemented-by: VERDICT: PASS |
…see, and read fields as record.FIELD at every site The lint entry header, the site-map jsdoc and the changeset said the runtime flow publish door never gives the shadowed-field-read warning. It does; the tool just is never handed the flow's variables. The site input and the tool description still said flow conditions bind fields bare, against the record.FIELD advice. No answer changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxiHaRMZVvPqb3cEac
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx Contract reviewServed-tier: Delta round on record 6868101966 (head Check-runs on the head: 66 concluded, 56 success, 10 skipped, 0 failed, 0 pending (read 2026-11-14T01:07Z). The same 10 opt-in or queue-only jobs are skipped as on the previous head. ① Derived judgmentsThe delta moves no accept set. Its hunks are:
So the commit is prose only: comments, two description strings and the changeset. The two strings are served bytes (an agent reads them in ② Semver levelUnchanged and still right: ③ Boundary flagsPR author login:
④ Design judgmentF1, closed. Each of the three places now says what the tree does:
F2, closed. The tool description's parenthesis now reads "read fields as
One nit, new: "Every site reads fields as The two nits of the first record ( Findings on this head: no must-fix, no should-fix, three nits. Implemented-by: VERDICT: PASS |
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx 维护者速读 · PR #33168 @
|
Requested by Tesla · project thread
Fixes #33147
Clause-②: yes (narrowing) — the MCP
validate_expressiontool now answersok: false(or a warning) at aflow_condition, and for a named stored or formula field at aformulasite, where the publish gate already refuses (or warns). It also accepts two new optional inputs,startConditionandfield, which is the widening arm. The gate's acceptance set does not change.Metadata-protocol: none
Skills-docs: none — the published skills say only that the tool runs the build's validator, which stays true; what it judges at each site is taught by the served
SKILL.mdline this diff updates (and its Console mirror, byte for byte).维护者速读
validate_expression在flow_condition位点按运行时发布门禁对流程写入的判法,跑门禁自己的检查(记录触发流程上record./previous.未判空的可空字段报错,外部对象未落存储的系统列告警);新增两个可选入参:startCondition(下游条件继承起始条件已证的非空)和field(公式位点按该字段本身判:存储字段给存储表达式告警,公式字段给循环判定)。ok: true,代理到保存流程时才第一次被拒;存储字段的告警和公式循环它也答不出。packages/lint/src/authored-site-expression.ts(流程探针与field就地替换)、packages/mcp/src/mcp-http-tools.ts的validate_expression入参与gatePassFindings。pnpm --filter @objectstack/lint exec vitest run src/authored-site-expression.test.ts,预期 50 个测试全过。What changes
PR #33134 closed the gap at the
validationandformulasites. This PR closes the three gaps it left.1.
flow_condition: the gate's own passes. Measured at the #33134 head (fbdba070) before this change, on a record-triggered flow ontaskwithdaysnullable. Each condition was placed on the start node, adecisionbranch and an edge, and all three slots answered alike:os validate(build door)record.days > 3previous.days > 3record.owner_id == 'u1'on an external objectdays > 3(bare)user.id == 'u1',foo.bar == 1record.title == 'x'(CONTROL)The two doors disagree because the runtime publish gate's per-write snapshot shows no entrance, so its root scope is never provable (
perWriteSnapshotEntrance). That turns off the bare-name null guard and the unbound-root refusal there. The existing pin invalidate-expressions.flow-null-guards.test.tsalready records this disagreement.The seat chose the runtime flow publish door, the one a flow saved through MCP or Studio meets. The tool must never refuse what that door accepts. So
okat aflow_conditionis that door's verdict, notos validate's. The tool description, the servedSKILL.mdline and the changeset each say this in one sentence and tell an agent to writerecord.FIELD.@objectstack/lintjudgeAuthoredSiteExpressiontakessite: 'flow_condition'. ⛔ It still re-implements no pass. It builds a one-flow probe (authoredFlowProbe): a start node bound toobjectNamewith the trigger the caller names, its start condition when one is given, and one edge to an end node carrying the condition. It then runs the gate's own walk withruntimeWriteType: 'flow'and keeps the findings on that edge's key path (flows[0].edges[0].condition). Each trigger kind is spelled the wayresolveFlowTriggerKindreads it, and a pin reads every kind back.runStackExpressionPassescall. Only the stack, the write type and the slot filter differ by site.sharedValidatorRunByCallerstill has one passer, and the source pin from fix(mcp): validate_expression gives the publish gate's verdict at validation and formula sites #33134 still holds that.@objectstack/mcp:VALIDATE_SITE_MAP.flow_conditiongainsgatePasses: 'flow_condition'. At a flow condition the tool reads the subject's served definition and nothing else, because nothing hydrates a relationship hop there.2. New optional input
startCondition(siteflow_conditiononly). The gate presumes what a flow's start condition proves at every condition downstream. Without that presumption, the tool would refuserecord.days > 3on an edge after a start conditionrecord.days != null, which the gate accepts. GivenstartCondition, the probe carries it on the start node, and the gate's own presumption applies. That includes the door split for a bare start guard:days != nullproves nothing at the runtime door, so the edge is still refused there and here. Omitted, the condition is judged as the start condition itself. A pin shows that a start probe and an edge probe answer alike when no start condition is given, so no slot input is needed.3. New optional input
field(siteformulaonly, a snake_case field name): the field the expression is authored on. It resolves only through the caller's served definition (servedObjectDefinitionOf).formula) then gets the gate'sstoredExpressionInputIssueswarnings (card item 2), and a formula field is judged on the object's formula graph under its own name, soformulaCycleIssuesrefuses the cycle it would close (card item 3).Other surfaces:
SKILL.mdline, with its Console mirror byte for byte, name what is judged at aflow_condition, the two inputs, and the limits.startConditionon another site andfieldon another site are refused, like the existing site-scoped inputs..changeset/33147-validate-expression-flow-condition-parity.md: patch for@objectstack/mcp,@objectstack/lintand@object-ui/console, with the BREAKING verdict-narrowing banner and the ADR-0087not-required (no-migration-prescription)disposition, naming both inputs.Tests
packages/lint/src/authored-site-expression.test.ts(26 → 50 tests)decisionbranch block, the edge block, and thecheck/checkDeclaredPredicatehelpers they hand the source to. Bare calls and member calls are both covered. The classification:checkFlowNullGuardsandwarnUnprovisionedAnchorsarepass. Each is pinned at all three slots: the entry's findings equal the runtime gate's on a hand-written flow (start,decision, two ends, three edges).refuseUnboundRootsandwarnShadowedFieldReadsarepass-silent-here, with the reason: os validate's whole-flow judgments over variables, node ids and entrances. Each pin is non-vacuous:os validatespeaks on the same condition, given the bindings it reads.refuseStoredMetadataBodyHopandrefuseUndeclaredHopFieldarepass-silent-here: they run only where a hop is hydrated.os validaterefuses it, and the runtime gate and the entry do not.validateExpression,checkStructuralConditionandpredicateSlotRefusalare the validator and envelope checks the tool runs itself. The rest is plumbing.decisionpasses. A bare start guard still refuses, as at the runtime door. The start condition's own findings are not reported.resolveFlowTriggerKind, and each answers as the gate does. The null guard fires on a record-triggered flow only.[].field. A declared stored field gets the gate's stored-expression warnings, and the unnamed probe gets none. A declared formula gets the cycle refusal under its own name. Array-form fields read the same. An undeclared name answers like no name.formulaCycleIssuesandstoredExpressionInputIssuesclassifications keeppass-silent-herefor the unnamed probe, and their reasons now point at thefieldblock.packages/mcp/src/mcp-validate-expression.test.ts(60 → 72 tests)flow_condition: therecord.null guard, theprevious.null guard, and the anchor warning. Each is the gate's verdict and sentence, and the definition-less bridge answered ok before.days > 3answers exactly as before. A start guard is honoured. A schedule flow gets no null guard. CONTROL equals the definition-less bridge.flow_conditionreads nothing now coverstemplatealone.fieldblock with a masking bridge. A stored field's warnings, the cycle refusal, and a field masked from the caller answers exactly as a field the object does not declare (and as no field at all). A bad field name is refused.Acceptance notes
okat aflow_conditionis the runtime publish gate's verdict, notos validate's.os validatealso refuses a bare name's unguarded null read, a root the run does not bind, and warns on a read a flow variable shadows. It judges those over the whole flow's variables, node ids and entrances, which the tool is not handed. The runtime door gives none of them, and the tool follows it so that it never refuses a condition that door accepts.flow 'this flow' · edge 'this condition' (start→end) condition, because the tool is handed the condition and not the flow it will sit in.fieldis judged as a new formula field, and there is nofieldTypeinput (seat ruling). Once the field is declared, or served in a draft, it is judged as stored.fieldresolves only throughservedObjectDefinitionOf, and the runtime bridge serves that throughreadObjectForCaller, which masks a field the caller cannot read.mcp-object-schema-mask.test.ts's case table already pins that mask from fix(mcp): validate_expression gives the publish gate's verdict at validation and formula sites #33134. This PR adds the tool-level pin that a masked name and an absent name answer alike.decisionbranch and an edge are the same slot to the gate, apart from the start-condition presumption, so there is no slot input. A non-start node'sconfig.conditionis inert and the gate does not judge it.ruling|Ruling-ref|maintainer|⛔) before editing, on the surface:authored-site-expression.ts1 (its own ⛔ no re-implementation),validate-expressions.ts28,lint/src/index.ts7,mcp-http-tools.ts39,skill-md.ts1. The ones that bear on this change are the root-scope stand-down at the runtime gate (rulings5791822697item 3 and6104584601, cited inflow-cel-root-scope.ts). This PR follows them: the tool judges no root where that door judges none.os-dev-reportcomment.Verification
Head:
c21d260c, which is529ccc6d(the change) withorigin/mainmerged once before opening, cleanly. The merge brought #33117, which also editsmcp-http-tools.tsin another tool, plus two metadata-protocol changes. Every suite and gate below that namesc21d260cwas re-run after that merge.Ablations (each through scripts/ablation-replace.mjs, each restored with git diff HEAD empty)
Suites at head
Derived gates (dispatch-gates --commands, reconciled with --ran)
Dogfood battery (owed by the narrowing arm)
🤖 Generated with Claude Code
https://claude.ai/code/session_01L9qibxiHaRMZVvPqb3cEac