Repository navigation
fix(mcp): validate_expression refuses a blank expression with the save gate's verdict - #27534
Conversation
…e gate's verdict The tool parses the expression with the spec input schema the save/build gate uses for the slot before running the formula checks, so a blank or whitespace-only source answers ok:false with the gate's own issue code and published sentence instead of ok:true. Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 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 — 12 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 d7bc3f0c4d5575c7dfb6e099d414596c37b665e4 && git checkout d7bc3f0c4d5575c7dfb6e099d414596c37b665e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc bb5eb6dd02918a08862500c787d71f6f3f797256 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff bb5eb6dd02918a08862500c787d71f6f3f797256
node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc |
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB 席位核对Verdict: ACCEPT at head
Open question from the dev (PR assignee): option A. The PR stays without an assignee, as the relay Accepted deviations: the error Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB Ready to land at 2026-10-29T11:41Z, held. CI on head The landing step (relay Generated by Claude Code |
Fixes #27411
Clause-②: no
Metadata-protocol: none
维护者速读
validate_expression收到空串或纯空白表达式时,不再回ok: true,改回ok: false,错误文案与保存闸门(422 INVALID_METADATA)同一句。packages/mcp/src/mcp-http-tools.ts中validate_expression处理函数里的slot.safeParse(expression)分支;其次VALIDATE_SITE_MAP新增的slot列。pnpm --filter @objectstack/mcp exec vitest run src/mcp-validate-expression.test.ts,预期 22 个用例全部通过。What changes
validate_expressionnow parses the expression with the same spec input schema the save/build gate parses that slot with, before it runs the formula checks:site@objectstack/spec/shared)formula,validation,flow_conditionEvaluatedExpressionInputSchemaEVALUATED_EXPRESSION_SOURCE_REQUIREDtemplateTemplateExpressionInputSchemaTYPED_EXPRESSION_SOURCE_REQUIRED.templateWhen the schema refuses, the tool answers
ok: falseand eacherrors[]entry is that schema's own issue:codeis the issue code (invalid_uniontoday, the shape the spec docblock documents for a blank bare string at an evaluated slot) andmessageis the published sentence. No blank rule and no sentence is written inpackages/mcp.warningsis empty,inferredTypeis omitted, andinScopestill comes back so the agent can correct itself. When the schema accepts, the tool runsvalidateExpressionexactly as before.What was narrowed, and why it is the published rule. Before this change an empty or whitespace-only expression (
'',' ', a tab or newline mix) readok: trueat every site, becausevalidateExpressiontreats a blank source as "not authored" and returns ok. Saving the same expression is refused: the formulaexpression, validation and flow-edge conditions are declared withEvaluatedExpressionInputSchema, which refuses a source that is blank after trimming (packages/spec/src/shared/expression.zod.ts), and template slots withTemplateExpressionInputSchema, which refuses a blank template. The tool now gives that verdict. Nothing else is narrowed: every non-blank string passes both schemas, so its answer is byte-for-byte the old one.Tests
packages/mcp/src/mcp-validate-expression.test.tsadds 14 cases:'',' 'and a tab and newline mix, atformula,validationandflow_condition:ok: false, one error,codeinvalid_union,messageequal to the importedEVALUATED_EXPRESSION_SOURCE_REQUIRED, no warnings, noinferredType,inScopestill present.template:messageequal toTYPED_EXPRESSION_SOURCE_REQUIRED.template.sitegiven defaults toformulaand refuses.' record.amount / 100 '(formula) and'Hi {{ record.title }}'(template) still answerok: truewith no errors.Acceptance notes
codeis the spec schema's zod issue code (invalid_union), not a kebab code from@objectstack/formula's refusal vocabulary. That vocabulary is closed and owned bypackages/formula, and its contract is one code per message template, so reusingempty-expression(whose message is different) would break it. Adding a new formula code is outside this card's file surface. If a stable kebab code is wanted for this refusal, that is a formula-package change.templatesite uses the generalTemplateExpressionInputSchemasentence. A few template slots (the notify node'stitleandmessage) refuse with their own sentence that names the key. The tool has no key, so it cannot pick between them; the general sentence is the one thetemplatesite's default slots use.sunhaolinonpr_create, but the relay's own row comment (scripts/pm/fleet-write/ops.mjs,pr_create) says a dev's create carries noassignees. This PR was opened without one. The seat decides.Verification
All readings below were taken at head
bb5eb6dd(the final commit), except the premise row, which was taken on origin/main before the edit.Premise, measured before the edit on the built packages (origin/main ded67bf)
validateExpression('value', '', hint)andvalidateExpression('value', ' ', hint)both answered{"ok":true,"errors":[],"warnings":[]}.EvaluatedExpressionInputSchema.safeParse('')answered one issue, codeinvalid_union, messageEVALUATED_EXPRESSION_SOURCE_REQUIRED.TemplateExpressionInputSchema.safeParse('')answered one issue, codeinvalid_union, messageTYPED_EXPRESSION_SOURCE_REQUIRED.template.'record.amount'passed both.Ablation (one run, on the committed fix)
node scripts/ablation-replace.mjsreplacedconst slotParse = slot.safeParse(expression);with a constant{ success: true }: anchor 1 to 0, blob0015f6b5to1609783f.src/mcp-validate-expression.test.ts: 13 failed, 9 passed (22), meaning the 12 blank cases plus the default-site case failed and both controls passed. Restored: blob equals HEAD0015f6b5,git diff HEADempty. The test imports the tool fromsrcthrough a relative path, so nodistrebuild was part of the path.Package test and typecheck
pnpm --filter @objectstack/mcp test: Test Files 45 passed (45), Tests 581 passed (581).pnpm --filter @objectstack/mcp typecheck:tsc --noEmitclean, andcheck:test-typecheckOK (6 files, 53 errors, 8 pinned signatures held in the ledger, unchanged). Lock VERDICT command-exit 0.Derived gate families
node scripts/pm/dispatch-gates.mjs --commands(change set of 3 paths against merge base ded67bf) named 59 families; all 59 ran and exited 0, and--ranreconciled 59 derived, 59 run, 0 NOT-MEASURED. Three needed a prerequisite first and were re-run after it:check:lean-entry-closure(exit 0 after building@objectstack/objectql)check-plugin-teardown-shape --self-test(exit 0 after fetching its pinned fixture commit on this shallow clone), andcheck:dual-build-cjs-loads(exit 0 oncecheck:type-check-debthad built every package: 112 require entry points across 69 packages load).Generated by Claude Code