You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
mcp: validate_expression answers ok:true for an empty or blank expression that the metadata save gate refuses as needing a non-blank source #27411
QA-source: #27409 · ai.mcp-validate-expression · observed beside the clauses
Found by an off-clause probe (empty-string boundary) in the 18.3.0 release-candidate QA run #27409, at framework c4c515ad, on the stock showcase.
Reproduction
Boot the showcase: objectstack dev --seed-admin -p PORT -d file:/tmp/run/data.db. Sign in as admin and mint an API key with POST /api/v1/keys.
Over MCP HTTP, POST /api/v1/mcp with tools/callvalidate_expression and arguments {"objectName":"showcase_task","expression":"","site":"formula"}. Repeat with "expression":" ".
As admin, PUT /api/v1/meta/object/qa_probe_formula with a formula field whose expression is "".
Expected vs actual
Expected: validate_expression reports an error for a blank expression, the same verdict the build and save gate gives. The tool exists so that an agent can check an expression before saving it.
Actual:
Step 2 answers {ok: true, errors: [], inferredType: "unknown"} for both inputs.
Step 3 answers 422 INVALID_METADATA: "An expression in an evaluated slot needs a non-blank source".
So an agent that validates first and then saves is told the expression is fine, and the save then refuses it.
Mechanism
packages/mcp/src/mcp-http-tools.ts: the tool's input schema declares expression: z.string() with no blank check (around line 806). The handler passes the value straight to validateExpression(role, expression, hint) (around line 833).
validateExpression from @objectstack/formula returns ok for an empty source. Measured directly on the built package: validateExpression('value', '', hint) and validateExpression('value', ' ', hint) both answer {"ok":true,"errors":[],"warnings":[]}, while 'record.' answers invalid-cel.
The blank-source refusal lives only in the spec envelope check used at save and build time. The MCP tool never consults it.
Suggested acceptance
validate_expression with an empty or whitespace-only expression answers ok:false with an error whose wording matches the save gate's.
A non-blank control expression still validates as before.
The case is pinned in packages/mcp/src/mcp-validate-expression.test.ts.
Read fresh. The MCP tool validate_expression answers ok: true for an empty or blank expression, which the metadata save gate refuses. The tool exists so an agent checks an expression before saving, so its answer disagreeing with the gate is the defect (「MCP 面暴露的就是应用真能做的」). p2.
⛔ The tool calls the same check the save gate runs. It does not get a second blank-string rule.
It is extracted from the 18.3.0 RC run, so it is listed in wave anchor [sweep] v18.3.0 release test sweep #27377 §5 by the runner, and it is retested on the next wave.
Triage: lands in packages/mcp/src/mcp-http-tools.ts (domain:server); rationale: this concurs with the first touch at 06:01Z (comment 6489262759). Labels stay as they are (bug, priority:p2, pm:queue, area:ai, domain:server). This note adds the parts of the first touch that the duties require and that were missing. Read at origin/main ded67bfb, 2026-10-29T09:55Z.
Class (a): a reproducible defect. The card measured reach on the public MCP HTTP door.
Mechanism confirmed on main: the input schema at line 806 is expression: z.string(), and line 833 passes it straight to validateExpression(role, expression, hint).
Fix lead: the save gate's refusal is the exported EVALUATED_EXPRESSION_SOURCE_REQUIRED (and EvaluatedExpressionInputSchema) in packages/spec/src/shared/expression.zod.ts. The tool can reuse that sentence and rule from @objectstack/spec, with no spec change. This is a lead, not an answer.
Responsibility: (1) the platform's own MCP tool produces the risk. (2) The platform path that already covers it is the spec evaluated-slot check, which the tool does not consult. (3) Any agent connected over MCP reaches it: the tool's stated purpose is a check before saving.
Prior rulings read: check-prior-rulings.mjs --card 27411 named ADR-0137 D4 (a blank predicate is diagnosed, never a silent true) and ADR-0032 §1 (never fail silently). Both support the direction, and neither needs a decision.
Type: Bug. It violates the declared contract that a blank source is refused.
Size/model suggestion: S, a one-file fix plus a pin in mcp-validate-expression.test.ts.
Claim: PM loop round 1
Session: session_017xRBcScm27Q3R5mkYLPWdB
Account: sunhaolin
Branch: claude/issue-27411-mcp-validate-blank
Worktree: objectstack-issue-27411
Domain: domain:server
Seat: domain:devx#1
File surface: packages/mcp/src/mcp-http-tools.ts (the validate_expression tool only), its tests under packages/mcp/src, one @objectstack/mcp changeset (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default (dispatch-gates --tier at c4c515a: "no path-derived mandate"), effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: the MCP tool code (packages/mcp) answers ok:true for a blank expression | the save/build gate already refuses blank sources (EVALUATED_EXPRESSION_SOURCE_REQUIRED in packages/spec/src/shared/expression.zod.ts), so the fix reuses that one rule | reached by any MCP client with a key; validate-then-save agents use it today
Thread-read: 6494043224
Premise: holds (triage first touch re-verified the mechanism on origin/main ded67bfb; the dev re-measures before editing)
Serial constraints cleared: none — the 10 open domain:server + pm:dispatched claims were read; none names packages/mcp
Maintainer direct dispatch (cross-lane claim authority): the maintainer, in session session_017xRBcScm27Q3R5mkYLPWdB on 2026-10-29, verbatim: 「你应该使用 pm-dispatch 技能处理 issue 和 pr」. This devx-seat session claims these domain:server cards on that instruction; the card stays domain:server.
Clause-② note: this narrows what the tool reports as valid (a blank expression stops reading ok) toward the already-published save-gate rule, so it is no, and it owes one contract-review-tier review before the queue.
{
"issue": 27411,
"status": "done",
"branch": "claude/issue-27411-mcp-validate-blank",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27534",
"session": "session_017xRBcScm27Q3R5mkYLPWdB (mode:subagent, the PM's session; identity = the branch)",
"premise_still_valid": true,
"summary": "validate_expression now parses the expression with the spec input schema the save/build gate uses for the slot (EvaluatedExpressionInputSchema for formula/validation/flow_condition, TemplateExpressionInputSchema for template) before running validateExpression; a refusal answers ok:false with the schema's own issue (code invalid_union, message EVALUATED_EXPRESSION_SOURCE_REQUIRED or TYPED_EXPRESSION_SOURCE_REQUIRED.template), no warnings, no inferredType, inScope kept. Non-blank expressions pass both schemas, so their answers are unchanged. Premise re-measured on origin/main ded67bfb with built packages: validateExpression answers ok:true for '' and ' '; both PM mechanism assumptions held (no spec change needed). One PM instruction not followed: the PR was opened WITHOUT assignee sunhaolin, because the relay's pr_create row (scripts/pm/fleet-write/ops.mjs) says a dev's create carries no assignees; the seat decides (see open_questions).",
"tests": "At head bb5eb6dd: pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 src/mcp-validate-expression.test.ts -> Test Files 1 passed (1), Tests 22 passed (22), 14 of them new. pnpm --filter @objectstack/mcp test -> Test Files 45 passed (45), Tests 581 passed (581); pnpm --filter @objectstack/mcp typecheck -> tsc clean + check:test-typecheck OK (6 files / 53 errors / 8 pinned signatures, unchanged); lock VERDICT command-exit 0. Ablation, one run on the committed fix, through scripts/ablation-replace.mjs: anchor 'const slotParse = slot.safeParse(expression);' 1->0, blob 0015f6b5->1609783f -> 13 failed | 9 passed (22) (the 12 blank cases + default-site case red, both controls green), the predicted direction; restored blob == HEAD 0015f6b5, git diff HEAD empty. The test imports the tool from src through a relative path, so no dist rebuild was in the path.",
"gates": "Local, at bb5eb6dd: node scripts/pm/dispatch-gates.mjs --commands derived 59 families from a 3-path change set vs merge base ded67bfb5; all 59 ran and each exit was captured before any pipe: 56 exited 0 on the first run; 3 exited 3 (PREREQUISITE) and exited 0 on a re-run once the prerequisite existed: check-plugin-teardown-shape --self-test (after fetching its pinned fixture commit 621a4876 on this shallow clone: 48 cases pass), check:lean-entry-closure (after building @objectstack/objectql: admitted set held exactly, 15 packages), check:dual-build-cjs-loads (after check:type-check-debt built every package: 112 require entry points across 69 packages load). dispatch-gates --ran -> exit 0, 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. check:nul-bytes exit 0. CI on the PR head bb5eb6dd at report time: Check Changeset, Lint jobs, Type Check jobs, Governed Surface Queue Guard, PR-body guards all in_progress; Type Check workspace queued; 18.x upgrade harness, Packed-tarball smoke, Schedule gate skipped. NOT MEASURED: none of the CI families has concluded yet; reason: the report is due when local verification finishes.",
"dogfood_battery": "n/a, no ! in the PR title and Clause-②: no with no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "3 writes: git push of the branch (not REST; empty-branch probe, then the fix commit); 1 relay stroke pr_create = POST /repos/objectstack-ai/objectstack/dispatches -> relay POST /repos/objectstack-ai/objectstack/pulls (#27534, no assignee or labels legs); 1 relay comment = this os-dev-report on #27411 (POST /repos/objectstack-ai/objectstack/issues/27411/comments). Also one git fetch of commit 621a4876 into the shared object store (a read).",
"files_changed": [
"packages/mcp/src/mcp-http-tools.ts",
"packages/mcp/src/mcp-validate-expression.test.ts",
".changeset/27411-mcp-validate-blank-expression.md"
],
"line_budget": "108 changed lines (+100 / -8) over 3 files, under the 3000-line threshold; no skills/** touched",
"deviations": [
"The PR has no assignee: the dispatch said assignee sunhaolin, and the relay row comment says a dev's create carries no assignees. I followed the relay row. The seat can add the assignee.",
"The error code is the spec schema's zod issue code (invalid_union), not a @objectstack/formula kebab code. The formula refusal vocabulary is closed, outside the file surface, and allows one code per message template.",
"The commit trailer uses the model-free pair AGENTS.md requires, not the harness's model-named Co-Authored-By line."
],
"open_questions": [
{
"question": "Should PR #27534 carry assignee sunhaolin, as the dispatch said?",
"options": [
"A: leave it without an assignee, as the relay pr_create row comment says for a dev's create",
"B: the seat adds sunhaolin with one relay assign stroke"
],
"recommendation": "A. The relay row records the maintainer's ruling that a draft open summons nobody. If the seat reads os-dev's 'PR assignee = card's' as the rule, it applies B."
}
],
"out_of_scope_findings": [
"carrier: none, noted, not filed. The template site uses the general TemplateExpressionInputSchema sentence. The notify node title and message slots refuse a blank template with their own sentences that name the key, and the tool has no key to pick between them. This is noted in the PR Acceptance notes."
]
}
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 错的必须被响亮拒绝并给处方 | none | P⑤
Triage: p2 ·
domain:server·area:ai· 2026-10-29T06:01Z⛔ Not a claim.
validate_expressionanswersok: truefor an empty or blank expression, which the metadata save gate refuses. The tool exists so an agent checks an expression before saving, so its answer disagreeing with the gate is the defect (「MCP 面暴露的就是应用真能做的」). p2.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
Path: 错的必须被响亮拒绝并给处方 | none | P⑤
Triage: lands in
packages/mcp/src/mcp-http-tools.ts(domain:server); rationale: this concurs with the first touch at 06:01Z (comment 6489262759). Labels stay as they are (bug,priority:p2,pm:queue,area:ai,domain:server). This note adds the parts of the first touch that the duties require and that were missing. Read at origin/mainded67bfb, 2026-10-29T09:55Z.expression: z.string(), and line 833 passes it straight tovalidateExpression(role, expression, hint).EVALUATED_EXPRESSION_SOURCE_REQUIRED(andEvaluatedExpressionInputSchema) inpackages/spec/src/shared/expression.zod.ts. The tool can reuse that sentence and rule from@objectstack/spec, with no spec change. This is a lead, not an answer.ExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811 and validate-expressions.ts 的 field-formula 校验读f.formula—— spec 声明的是expression,这段从未对任何 spec 合法 stack 跑过 #5026 (all closed, other mechanisms). No duplicate.check-prior-rulings.mjs --card 27411named ADR-0137 D4 (a blank predicate is diagnosed, never a silent true) and ADR-0032 §1 (never fail silently). Both support the direction, and neither needs a decision.Size/model suggestion: S, a one-file fix plus a pin in
mcp-validate-expression.test.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
Claim: PM loop round 1
Session:
session_017xRBcScm27Q3R5mkYLPWdBAccount:
sunhaolinBranch:
claude/issue-27411-mcp-validate-blankWorktree:
objectstack-issue-27411Domain:
domain:serverSeat:
domain:devx#1File surface:
packages/mcp/src/mcp-http-tools.ts(thevalidate_expressiontool only), its tests underpackages/mcp/src, one@objectstack/mcpchangeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default(dispatch-gates --tierat c4c515a: "no path-derived mandate"),effort: highClause-②: no
Metadata-protocol: none
Responsibility: the MCP tool code (
packages/mcp) answers ok:true for a blank expression | the save/build gate already refuses blank sources (EVALUATED_EXPRESSION_SOURCE_REQUIREDinpackages/spec/src/shared/expression.zod.ts), so the fix reuses that one rule | reached by any MCP client with a key; validate-then-save agents use it todayThread-read: 6494043224
Premise: holds (triage first touch re-verified the mechanism on origin/main
ded67bfb; the dev re-measures before editing)Serial constraints cleared: none — the 10 open
domain:server+pm:dispatchedclaims were read; none namespackages/mcpMaintainer direct dispatch (cross-lane claim authority): the maintainer, in session
session_017xRBcScm27Q3R5mkYLPWdBon 2026-10-29, verbatim: 「你应该使用 pm-dispatch 技能处理 issue 和 pr」. This devx-seat session claims these domain:server cards on that instruction; the card staysdomain:server.Clause-② note: this narrows what the tool reports as valid (a blank expression stops reading ok) toward the already-published save-gate rule, so it is
no, and it owes one contract-review-tier review before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
os-dev-report
{ "issue": 27411, "status": "done", "branch": "claude/issue-27411-mcp-validate-blank", "pr": "https://github.com/objectstack-ai/objectstack/pull/27534", "session": "session_017xRBcScm27Q3R5mkYLPWdB (mode:subagent, the PM's session; identity = the branch)", "premise_still_valid": true, "summary": "validate_expression now parses the expression with the spec input schema the save/build gate uses for the slot (EvaluatedExpressionInputSchema for formula/validation/flow_condition, TemplateExpressionInputSchema for template) before running validateExpression; a refusal answers ok:false with the schema's own issue (code invalid_union, message EVALUATED_EXPRESSION_SOURCE_REQUIRED or TYPED_EXPRESSION_SOURCE_REQUIRED.template), no warnings, no inferredType, inScope kept. Non-blank expressions pass both schemas, so their answers are unchanged. Premise re-measured on origin/main ded67bfb with built packages: validateExpression answers ok:true for '' and ' '; both PM mechanism assumptions held (no spec change needed). One PM instruction not followed: the PR was opened WITHOUT assignee sunhaolin, because the relay's pr_create row (scripts/pm/fleet-write/ops.mjs) says a dev's create carries no assignees; the seat decides (see open_questions).", "tests": "At head bb5eb6dd: pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 src/mcp-validate-expression.test.ts -> Test Files 1 passed (1), Tests 22 passed (22), 14 of them new. pnpm --filter @objectstack/mcp test -> Test Files 45 passed (45), Tests 581 passed (581); pnpm --filter @objectstack/mcp typecheck -> tsc clean + check:test-typecheck OK (6 files / 53 errors / 8 pinned signatures, unchanged); lock VERDICT command-exit 0. Ablation, one run on the committed fix, through scripts/ablation-replace.mjs: anchor 'const slotParse = slot.safeParse(expression);' 1->0, blob 0015f6b5->1609783f -> 13 failed | 9 passed (22) (the 12 blank cases + default-site case red, both controls green), the predicted direction; restored blob == HEAD 0015f6b5, git diff HEAD empty. The test imports the tool from src through a relative path, so no dist rebuild was in the path.", "gates": "Local, at bb5eb6dd: node scripts/pm/dispatch-gates.mjs --commands derived 59 families from a 3-path change set vs merge base ded67bfb5; all 59 ran and each exit was captured before any pipe: 56 exited 0 on the first run; 3 exited 3 (PREREQUISITE) and exited 0 on a re-run once the prerequisite existed: check-plugin-teardown-shape --self-test (after fetching its pinned fixture commit 621a4876 on this shallow clone: 48 cases pass), check:lean-entry-closure (after building @objectstack/objectql: admitted set held exactly, 15 packages), check:dual-build-cjs-loads (after check:type-check-debt built every package: 112 require entry points across 69 packages load). dispatch-gates --ran -> exit 0, 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. check:nul-bytes exit 0. CI on the PR head bb5eb6dd at report time: Check Changeset, Lint jobs, Type Check jobs, Governed Surface Queue Guard, PR-body guards all in_progress; Type Check workspace queued; 18.x upgrade harness, Packed-tarball smoke, Schedule gate skipped. NOT MEASURED: none of the CI families has concluded yet; reason: the report is due when local verification finishes.", "dogfood_battery": "n/a, no ! in the PR title and Clause-②: no with no (narrowing) arm", "mcp_calls": "0", "api_writes": "3 writes: git push of the branch (not REST; empty-branch probe, then the fix commit); 1 relay stroke pr_create = POST /repos/objectstack-ai/objectstack/dispatches -> relay POST /repos/objectstack-ai/objectstack/pulls (#27534, no assignee or labels legs); 1 relay comment = this os-dev-report on #27411 (POST /repos/objectstack-ai/objectstack/issues/27411/comments). Also one git fetch of commit 621a4876 into the shared object store (a read).", "files_changed": [ "packages/mcp/src/mcp-http-tools.ts", "packages/mcp/src/mcp-validate-expression.test.ts", ".changeset/27411-mcp-validate-blank-expression.md" ], "line_budget": "108 changed lines (+100 / -8) over 3 files, under the 3000-line threshold; no skills/** touched", "deviations": [ "The PR has no assignee: the dispatch said assignee sunhaolin, and the relay row comment says a dev's create carries no assignees. I followed the relay row. The seat can add the assignee.", "The error code is the spec schema's zod issue code (invalid_union), not a @objectstack/formula kebab code. The formula refusal vocabulary is closed, outside the file surface, and allows one code per message template.", "The commit trailer uses the model-free pair AGENTS.md requires, not the harness's model-named Co-Authored-By line." ], "open_questions": [ { "question": "Should PR #27534 carry assignee sunhaolin, as the dispatch said?", "options": [ "A: leave it without an assignee, as the relay pr_create row comment says for a dev's create", "B: the seat adds sunhaolin with one relay assign stroke" ], "recommendation": "A. The relay row records the maintainer's ruling that a draft open summons nobody. If the seat reads os-dev's 'PR assignee = card's' as the rule, it applies B." } ], "out_of_scope_findings": [ "carrier: none, noted, not filed. The template site uses the general TemplateExpressionInputSchema sentence. The notify node title and message slots refuse a blank template with their own sentences that name the key, and the tool has no key to pick between them. This is noted in the PR Acceptance notes." ] }objectstack-fleet commentedon Oct 29, 2026
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
ACCEPT → PR #27534 comment 6495423331 (head
bb5eb6dd, reviewed 2026-10-29T11:29Z). Landing after CI on that head is green.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
Handover: PR #27534 → PM session session_017xRBcVy4VXmkQjn2MGeaWk (review and landing)
Generated by Claude Code