Repository navigation
fix(mcp): validate_expression gives the publish gate's verdict at validation and formula sites - #33134
Conversation
…alidation and formula sites Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
📓 Docs Drift CheckThis PR changes 3 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 38 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 b9f7a7b462cf4974e169e887662daf226dffeb04 && git checkout b9f7a7b462cf4974e169e887662daf226dffeb04
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1bed8536bba1bd59e432e7b9d8ebc3c526efc637 fbdba0708e50558dd62890d05508f4132ae9b0c3 && git checkout -B drift-repro 1bed8536bba1bd59e432e7b9d8ebc3c526efc637 && git merge --no-ff fbdba0708e50558dd62890d05508f4132ae9b0c3
node scripts/docs-audit/affected-docs.mjs --json 1bed8536bba1bd59e432e7b9d8ebc3c526efc637
|
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx Contract reviewServed-tier: Read: card #32872 body and all 5 comments (claim 6855829217, amendments 6856483394 and 6857864931, os-dev-report 6858983583), PR #33134 body, the net diff ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentF1 — must-fix. Hop targets bypass the tool's system-object fence. F2 — should-fix. "A flow condition is judged by the shared validator alone, as at the gate" is false. At a flow condition the gate's F3 — should-fix. The enumeration pin fails for a narrower set of additions than the card asks. F4 — should-fix. Static import of lint's root barrel at MCP module load. F5 — nit. Clause-② sentence wording (②). F6 — nit. F7 — nit. Context the one-object probe stack does not share with the gate. Each is a corner to write down; the first two are the only ones where the tool refuses and the gate does not:
F8 — nit. "No gate caller passes Questions asked, answered:
Implemented-by: VERDICT: FAIL |
… load lint lazily Hop targets the tool's object-name guard refuses are neither read nor judged. The lint root entry is imported on first use. The enumeration pin also reads member calls at each site and every pass the whole walk pushes, and a source pin holds the validator-skip option to its one passer. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx Contract reviewServed-tier: Read: fix commit ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentRound-1 findings:
New in round 2:
No new defect found in the delta. The mirror is byte-identical at the head (the Implemented-by: VERDICT: PASS |
…object exposure census Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzhu4Yeph4D3KQ9Ao3
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx Contract reviewServed-tier: Read: the delta, the census pin whole at the head, the two tests its new row and the dev's reasoning cite, and the check-runs. Check-runs on the head at 19:59Z: 22 registered, 3 success, 3 skipped, 16 in progress, 0 failed. Still running: every Type Check job (workspace, source gates, consumer gates, debt ledger), every Lint job listed (ESLint, repo & tooling, docs & code guards, CI/release/engine, the three scoped ones, PM dispatch-gates), Check Changeset, Validate Package Dependencies, Auto Label, Flag docs. No Build or Test job had registered yet, so the census pin's own verdict on this head (Test Core, where it was red) does not exist. This PASS is the contract judgment on the classification; the seat reads Test Core 5/8 green on this sha before landing. My round-2 PASS did not catch this pin; CI did. ① Derived judgments
② Semver levelNo change. The delta is two test files; nothing published moves. Changeset, ③ Boundary flags
④ Design judgment
Findings:
Round-1 findings F1–F8: status as recorded in round 2; nothing in this delta moves them. Implemented-by: VERDICT: PASS |
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx 维护者速读 · PR #33134 @
|
Requested by Tesla · project thread
Fixes #32872
Clause-②: no (narrowing) — the MCP
validate_expressiontool now answersok: false(or a warning) at avalidationorformulasite for an expression the publish gate already refuses (or warns on). The gate's acceptance set does not change, and no existing exported type or signature changes.Metadata-protocol: none
Skills-docs: none — the published skills (
objectstack-formula,objectstack-ai) say only that the tool runs the build's validator, which stays true; what it refuses is taught by the servedSKILL.mdline this diff corrects.维护者速读
validate_expression在validation/formula位点除共享校验器外,还跑发布门禁自己的那几道检查(经 lint 新导出的judgeAuthoredSiteExpression,对调用者被服务的对象定义跑门禁本身的遍历),门禁拒的它也拒;跳转目标与describe_object守同一道系统对象围栏;lint 在首次需要时才懒加载;服务端SKILL.md与 Console 镜像同步改那一行。flow_condition位点不在本 PR(席位另开卡)。parent这几类答ok: true,到发布时才第一次被拒。@objectstack/mcp把@objectstack/lint从 devDependency 挪到 dependency(无环)。回滚即 revert 本 PR。packages/lint/src/authored-site-expression.ts(入口:构造门禁要判的 stack 再跑门禁自己的 walk)与packages/lint/src/validate-expressions.ts新增的sharedValidatorRunByCaller(仅跳过共享校验器那一次调用)。pnpm --filter @objectstack/lint exec vitest run src/authored-site-expression.test.ts与pnpm --filter @objectstack/mcp exec vitest run src/mcp-validate-expression.test.ts。What changes
This closes #32872 at the two sites the card names,
validationandformula. The gate's own passes at aflow_condition(the unprovisioned-anchor warning on an object-bound flow predicate, and the null-guard gate on a record-triggered flow's start,decisionand edge conditions) are not run by the tool, and this PR does not change its verdict there. They are a follow-up card.The tool promises the verdict the publish gate gives the rule, but it ran the shared expression validator (
@objectstack/formula'svalidateExpression) and nothing else. Measured onorigin/mainbefore the change, it answeredok: trueat these sites for rules the gate refuses:record.balance.nope(hop to a field the target does not declare)validationrecord.days > 3,daysneither required nor defaulted (null guard)validationparentrecord.owner_idon an ADR-0015externalobject@objectstack/lintNew
judgeAuthoredSiteExpression(packages/lint/src/authored-site-expression.ts). ⛔ It re-implements no pass. It builds the stack the gate would judge (the subject object carrying the one probe rule or probe formula field, plus the hop targets it reads through) and runs the gate's own walkrunStackExpressionPassesover it, as the object save door does (runtimeWriteType: 'object'). It returns exactly the findings the gate locates on that rule or field: by key pathobjects[0].validations[0].at a validation site, and by the field'swhereat a formula site. Any pass later added to the gate at either site is carried by construction.New
authoredExpressionHopTargets: the reference targets the expression reads through, read with the analysis and target accessor the gate's hop passes use.The gate gains one option,
sharedValidatorRunByCaller, which skips only thevalidateExpressioncall the caller has already made. Every other pass runs unchanged. No gate caller passes it. Its one passer is the new entry, and a source pin inauthored-site-expression.test.tsnow holds that by test (grep at8a5f028f):Served definitions carry the system columns the registry injects as declared fields. The gate judges the body as authored, so the entry reads each one through
stripInjectedSystemColumns(@objectstack/metadata-core, the write path's own strip). Without it, the null guard would refusecreated_at, and an external object's anchors would read as columns its author vouches for.@objectstack/mcpMcpDataBridgegains the optionalservedObjectDefinitionOf(name): the object definition as the caller is served it (the readdescribeObjectprojects). It is handed to the gate and rendered to nobody. A bridge without it answers the shared validator's verdict alone, as before. The stdio bridge serves it from its served view.validate_expressionatformulaandvalidationmerges the gate's findings intoerrors/warnings, as{ source, message }in the gate's words at the gate's severity. Aflow_conditionand atemplateare not judged by this change, and no definition is read for them.describe_object: a target the tool's object-name guard refuses is neither read nor judged, so a hop into a system object is judged only where the server setsallowSystemObjects.SKILL.mdline name what the tool now refuses, give the unprovisioned-column warning its two sites, and state the fence.@objectstack/lintmoves fromdevDependenciestodependencies(packages/mcp/package.json, with the matching six-linepnpm-lock.yamlmove). No cycle:@objectstack/lintdepends only on@objectstack/formula,@objectstack/metadata-core,@objectstack/sdui-parserand@objectstack/spec, and none of those depends on@objectstack/mcp.@objectstack/runtimeand@objectstack/clialready depend on@objectstack/lintat runtime. Loaded lazily:gatePassFindingsimports lint's root entry withawait import('@objectstack/lint')on the first call that judges a rule or formula field, and the module imports only its types, so the boot path still reaches lint only through@objectstack/lint/runtime, as every other server-side consumer does. The full workspace build orders it cleanly (119/119 tasks).@objectstack/runtime: the HTTP MCP bridge servesservedObjectDefinitionOfthroughreadObjectForCaller, the same caller-scoped readdescribeObjectuses (ADR-0106 D1), so a field masked from the caller is masked from the gate's input too.@object-ui/console: the downloadableSKILL.mdmirror carries the same line, byte for byte..changeset/32872-validate-expression-gate-parity.md: patch for the four packages, with the BREAKING verdict-narrowing banner and an ADR-0087not-required (no-migration-prescription)disposition. Every expression the tool now refuses, the gate already refused, so neither stored metadata nor consumer code has anything to rewrite.scripts/cross-package-test-inputs.mjs(the@objectstack/lintentry) andturbo.json(@objectstack/lint#testinputs) declarescripts/js-comment-mask.mjsand its.d.mts, which the new lint test imports to read the gate's source without its comments.Tests
packages/lint/src/authored-site-expression.test.ts(new, 26 tests)checkplus the validation-rule loop forvalidation;judgeFieldFormulaplus the span after its call site forformula), every call by bare name is classified asvalidator,plumbing,pass(with a refusal fixture) orpass-silent-here, and every member call by its method name is classified as plumbing.runStackExpressionPasses), every pass handed to the walk asissues.push(fn(orissues.push(...fn(, member names included, is classified. That is the shape of the walk's whole-stack passes at its tail (bulkConfirmTextIssues,actionCopyTemplateIssues).formulaCycleIssues(from main) is classifiedpass-silent-hereat the formula site, with a pin that it fires on the object's own cycle and locates nothing on the probe.judgeFieldFormula, aformulaCycleIssues-shaped spread after the fields loop, a new member call there, and a newissues.push(...)besidebulkConfirmTextIssues.issues.push({ … }), a loop over a helper's result, a helper that pushes intoissuesitself), and a pass handed by reference (.map(fn)). Such a pass is still carried by construction when it locates its finding on the judged rule's key path or the judged field'swhere. What no pin catches is a pass located elsewhere, or one that needs an input the tool is not handed.sharedValidatorRunByCallerto its declaration, the walk's one read, and the one passer inauthored-site-expression.ts. A second scan holdsrunStackExpressionPassesandStackExpressionOptionsoff all three package entries.runStackExpressionPassesminus the shared validator's).[].[]; hop-target reads.packages/mcp/src/mcp-validate-expression.test.ts: a new describe block with one tool-level pin per member (8). It also pins that an injectedcreated_atis not refused, that only hop targets are read, a CONTROL equal to the definition-less bridge, that a bridge without the member answers as before, and thatflow_condition/templateread no definition. Fence pin: a hop into a servedsys_useris refused withallowSystemObjectson. By default the target is not read, and the answer equals the definition-less bridge's.mcp-write-response-internal-fields.tripwire.test.ts,stdio-data-bridge.stored-metadata-body.test.ts) classify the new member. Both went red without the entry, as their ledgers intend.packages/runtime/src/domains/mcp-object-schema-mask.test.tsaddsservedObjectDefinitionOfto the ADR-0106 case table, with a masked field-set assertion.mcp-option-fields-read.test.tspins that it equals the served object, that its keys equaldescribeObject's fields, and that it answers null when the object is absent.Acceptance notes
code. The gate'sExprIssuehas none, so the tool's added findings are{ source, message }with the gate's sentence.this rule. The tool is handed the condition, not the rule it will sit on, so a gate sentence that names the rule (the null guard,parent) names itthis rule. The probe formula field is spelled__validate_expression__, and no gate sentence at the formula site names it.pass-silent-hereat the formula site. It landed on main and this branch merged it. Its call sits after the fields loop, inside the formula region, and both the formula enumeration pin and the whole-walk push pin went red on the merge (+ "formulaCycleIssues") until it was classified. It locates each finding on a cycle member's ownexpression. The probe is named__validate_expression__, which no authored formula can read, so the probe is never on a cycle. A pin shows the pass does fire on the object's own cycle, and that neither the gate nor the entry locates a finding on the probe. Limitation: the tool cannot give the cycle verdict for the formula being authored, because it is not handed that field's own name. That belongs with the follow-up card.storedExpressionInputIssuesispass-silent-here. That holds for aformulaprobe. A stored (non-formula) field'sexpressionauthored throughsite: 'formula'gets the gate's stored-input warnings and none from the tool. This goes with the follow-up card.objectsto the written package's closure, so a target outside it is not judged there. The tool judges any target the caller is served. The tool's refusal is true (the field does not exist), and the door's silence is the gap.okwhere the gate refuses. This residual runs in the pre-existing direction.options. A default option makes it non-nullable for the tool, while the authored body the gate reads names only the picklist.name === objectName. On the bridge's no-protocol fallback, a differently spelled name answers[].describe_objectgives for that target.fields/ stdlib hints still come from the tool's own index. Only the verdict reads the gate.validateExpressioncall, which carriescodes and the site options. The gate's copy is skipped, so a validator finding is never reported twice.Verification
Head:
8a5f028f. Round 2 is the review fixes ine306a89d, thenorigin/mainmerged for #33108 with its pass classified (bd2b25ff), then merged again for #33142, which touchedmcp-http-tools.ts(8a5f028f, clean). Round 1 was95b340e2.Measurement on origin/main (before)
A throwaway vitest file (not committed) ran the gate's walk and the tool's handler on the same fixtures at origin/main:
The formula rows for the null guard and
parentshow no gate finding: the gate runs those two passes only at a validation rule, and the entry follows it.Ablations (each red, each restored, `git diff HEAD` empty after)
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_01L9qibzhu4Yeph4D3KQ9Ao3