Repository navigation
Commit e5c5035
fix(mcp): validate_expression refuses a blank expression with the save gate's verdict (#27534)
Fixes #27411
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:MCP 工具 `validate_expression` 收到空串或纯空白表达式时,不再回 `ok: true`,改回 `ok:
false`,错误文案与保存闸门(`422 INVALID_METADATA`)同一句。
- 为什么改:agent 先用这个工具校验、再保存;以前工具说"没问题",保存却被拒,两边口径不一致。
- 风险与回滚:只收窄了工具对"空白表达式"的判定,非空表达式的结果不变;回滚即 revert 本 PR(一个文件的工具逻辑加一个测试文件)。
- 先看:`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_expression` now parses the expression with the same spec input
schema the save/build gate parses that slot with, before it runs the
formula checks:
| `site` | schema (from `@objectstack/spec/shared`) | refusal sentence |
|:---|:---|:---|
| `formula`, `validation`, `flow_condition` |
`EvaluatedExpressionInputSchema` |
`EVALUATED_EXPRESSION_SOURCE_REQUIRED` |
| `template` | `TemplateExpressionInputSchema` |
`TYPED_EXPRESSION_SOURCE_REQUIRED.template` |
When the schema refuses, the tool answers `ok: false` and each
`errors[]` entry is that schema's own issue: `code` is the issue code
(`invalid_union` today, the shape the spec docblock documents for a
blank bare string at an evaluated slot) and `message` is the published
sentence. No blank rule and no sentence is written in `packages/mcp`.
`warnings` is empty, `inferredType` is omitted, and `inScope` still
comes back so the agent can correct itself. When the schema accepts, the
tool runs `validateExpression` exactly 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) read `ok: true` at every site, because `validateExpression`
treats a blank source as "not authored" and returns ok. Saving the same
expression is refused: the formula `expression`, validation and
flow-edge conditions are declared with `EvaluatedExpressionInputSchema`,
which refuses a source that is blank after trimming
(`packages/spec/src/shared/expression.zod.ts`), and template slots with
`TemplateExpressionInputSchema`, 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.ts` adds 14 cases:
- `''`, `' '` and a tab and newline mix, at `formula`, `validation` and
`flow_condition`: `ok: false`, one error, `code` `invalid_union`,
`message` equal to the imported `EVALUATED_EXPRESSION_SOURCE_REQUIRED`,
no warnings, no `inferredType`, `inScope` still present.
- The same three inputs at `template`: `message` equal to
`TYPED_EXPRESSION_SOURCE_REQUIRED.template`.
- No `site` given defaults to `formula` and refuses.
- Controls: `' record.amount / 100 '` (formula) and `'Hi {{ record.title
}}'` (template) still answer `ok: true` with no errors.
## Acceptance notes
- The error `code` is 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 by
`packages/formula`, and its contract is one code per message template,
so reusing `empty-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.
- The `template` site uses the general `TemplateExpressionInputSchema`
sentence. A few template slots (the notify node's `title` and `message`)
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 the
`template` site's default slots use.
- PR assignee: the dispatch asked for assignee `sunhaolin` on
`pr_create`, but the relay's own row comment
(`scripts/pm/fleet-write/ops.mjs`, `pr_create`) says a dev's create
carries no `assignees`. 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.
<details><summary>Premise, measured before the edit on the built
packages (origin/main ded67bf)</summary>
`validateExpression('value', '', hint)` and `validateExpression('value',
' ', hint)` both answered `{"ok":true,"errors":[],"warnings":[]}`.
`EvaluatedExpressionInputSchema.safeParse('')` answered one issue, code
`invalid_union`, message `EVALUATED_EXPRESSION_SOURCE_REQUIRED`.
`TemplateExpressionInputSchema.safeParse('')` answered one issue, code
`invalid_union`, message `TYPED_EXPRESSION_SOURCE_REQUIRED.template`.
`'record.amount'` passed both.
</details>
<details><summary>Ablation (one run, on the committed fix)</summary>
`node scripts/ablation-replace.mjs` replaced `const slotParse =
slot.safeParse(expression);` with a constant `{ success: true }`: anchor
1 to 0, blob `0015f6b5` to `1609783f`.
`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 HEAD `0015f6b5`, `git diff HEAD` empty.
The test imports the tool from `src` through a relative path, so no
`dist` rebuild was part of the path.
</details>
<details><summary>Package test and typecheck</summary>
`pnpm --filter @objectstack/mcp test`: Test Files 45 passed (45), Tests
581 passed (581). `pnpm --filter @objectstack/mcp typecheck`: `tsc
--noEmit` clean, and `check:test-typecheck` OK (6 files, 53 errors, 8
pinned signatures held in the ledger, unchanged). Lock VERDICT
command-exit 0.
</details>
<details><summary>Derived gate families</summary>
`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 `--ran` reconciled 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), and
`check:dual-build-cjs-loads` (exit 0 once `check:type-check-debt` had
built every package: 112 require entry points across 69 packages load).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 21e5979 commit e5c5035
3 files changed
Lines changed: 100 additions & 8 deletions
File tree
- .changeset
- packages/mcp/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
72 | 77 | | |
73 | 78 | | |
74 | 79 | | |
| |||
481 | 486 | | |
482 | 487 | | |
483 | 488 | | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
484 | 496 | | |
485 | | - | |
486 | | - | |
487 | | - | |
488 | | - | |
489 | | - | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
490 | 505 | | |
491 | 506 | | |
492 | 507 | | |
| |||
828 | 843 | | |
829 | 844 | | |
830 | 845 | | |
831 | | - | |
| 846 | + | |
832 | 847 | | |
833 | | - | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
834 | 864 | | |
835 | | - | |
| 865 | + | |
| 866 | + | |
836 | 867 | | |
837 | 868 | | |
838 | 869 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
5 | 10 | | |
6 | 11 | | |
7 | 12 | | |
| |||
117 | 122 | | |
118 | 123 | | |
119 | 124 | | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
120 | 169 | | |
121 | 170 | | |
122 | 171 | | |
| |||
0 commit comments