Skip to content

fix(mcp): validate_expression takes the flow's declared variables at a flow condition - #33255

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-33176-validate-expression-flow-variables
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-33176-validate-expression-flow-variables

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #33176

Clause-②: yes (widening) — validate_expression gains an optional input, variables, carrying the flow's declared variables at site: 'flow_condition', so the tool accepts a call it refuses today (strictToolInput); with no variables the answer is unchanged.
Metadata-protocol: none
Skills-docs: follow-up #33272

维护者速读

  • 改了什么:MCP validate_expression 在 flow_condition 位点新增可选入参 variables(流程声明的变量,形状即 spec 的 FlowVariableSchema)。给了它,探针流程声明这些变量,运行时流程门禁自己的"变量遮蔽同名字段"告警会回来;共享校验器把这些裸名当变量,不再按字段判类型。
  • 为什么改:流程运行时先绑定声明的变量、再平铺记录字段,所以与字段同名的裸名读到的是变量;工具此前拿不到变量,会按字段给出类型判定,也给不出门禁的遮蔽告警,代理保存流程时才看到不同的答案。
  • 风险与回滚:只加一个可选入参;不传时每个调用的答案与之前逐字相同(既有 72 个工具级用例不改动即为对照);门禁接受集不变。回滚即 revert 本 PR。
  • 先看:packages/lint/src/authored-site-expression.ts(authoredFlowProbe 声明变量)、packages/mcp/src/mcp-http-tools.ts(variables 入参、非流程位点拒收、flowVariables 提示、gatePassFindings 传参)。
  • 两分钟验证:pnpm --filter @objectstack/mcp exec vitest run src/mcp-validate-expression.test.ts,预期 77 个测试全过,其中 title * 2 > 10 带 variables 时无类型错误且得到门禁的遮蔽告警,不带时报 type-mismatch-run-fails。
  • 需要你做的:无,席位落地。

What changes

Input shape: an optional variables input, not "read from a named served flow". I chose the input. At a flow condition the tool reads only the subject object's served definition. Reading a named flow would need a new bridge member on both doors (the runtime bridge and the stdio bridge), a permission fence for flow metadata, and an answer for a flow that is not saved yet, which is the usual case while an agent authors one. That is outside this claim's file surface. The input takes the flow's own shape, z.array(FlowVariableSchema) from @objectstack/spec/automation, so an agent can hand over a flow's variables array as it stands. A malformed variable gets the spec's own refusal (a missing type, a $ name, an alias key such as input). No new shape is written by hand, which follows the AggregationNodeSchema precedent on aggregate_records.

With variables at site: 'flow_condition':

  • Gate half. gatePassFindings hands the variables to judgeAuthoredSiteExpression, and authoredFlowProbe declares them on the one-flow probe. The gate's own walk then runs warnShadowedFieldReads on the probe's edge and gives its warning for a variable named like a field of objectName that is read bare. ⛔ Nothing is re-implemented: the probe only declares what the flow declares.
  • Validator half. The shared validator gets the names through its existing option ExprSchemaHint.flowVariables, the key the gate hands it on the same flow. A bare read of a declared variable is therefore not typed as the field. "Typed as the variable" means what it means at the gate: the bare name stays dyn. Neither the gate nor the validator types a variable by its declared type, so the tool does not either.
  • record.FIELD still reads the field and is judged as before.

Without variables nothing changes. The probe carries no variables key and the hint carries no flowVariables key, so every call answers byte for byte as before. The tool description and the input description say that the shadowed-read warning needs the variables.

variables on any other site is refused, as startCondition is: `variables` describes a flow; it applies only with site "flow_condition".

stdio. Measured: both transports register one tool definition through wireBridgeTools, and the stdio bridge already serves servedObjectDefinitionOf. So stdio-data-bridge.ts needs no change. A parity pin drives a real StdioServerTransport and the HTTP door with the same arguments.

Measured against the dispatch's mechanism notes (on origin/main 295ce127):

  • The input schema took objectName, expression, site, severity, triggerKind, sweepObject, startCondition, field. Holds.
  • The probe declared no variables. Holds; the module header said so.
  • Does the validator have an option for extra bindings? Yes: flowVariables, names only. It does not type a variable at all, so a bare variable is left untyped rather than typed by the variable's type.
  • Does stdio serve the same definition? Yes, as described above.

Tests

  • packages/lint/src/authored-site-expression.test.ts (50 → 57)
    • The flow-condition table. warnShadowedFieldReads moves from pass-silent-here to pass. Its fixture now carries variables, and the entry is pinned equal to the runtime gate at the start, decision and edge slots of a hand-written flow that declares the same variables (+2 slot cases).
    • New block with the flow's declared variables (#33176) (+5):
      • the probe declares the variables as handed, and has no variables key without them;
      • CONTROL: without variables the gate and the entry both say nothing;
      • a variable that shadows no field gets no warning;
      • only the bare read is warned, and record.title is not;
      • every trigger kind, and a condition after a start condition, answers as the gate.
  • packages/mcp/src/mcp-validate-expression.test.ts (72 → 77)
    • (a) Variable title shadows the text field title, read bare in title * 2 > 10. The tool's whole verdict, validator half included, equals the gate's verdict on a flow declaring it: one shadowed-read warning, no errors, ok: true. The finding names title and gp_req.
    • (b) CONTROL, the same call without variables: ok: false with type-mismatch-run-fails, the bare name typed as the field, equal to the definition-less bridge's answer.
    • The warning is the gate's after a startCondition too.
    • (d) A variable that shadows no field (threshold) gets [true, [], []], as at the gate.
    • The spec's own variable refusals: a missing type, $record, the alias input.
    • (c) The refusal table gains three rows: variables at validation, at template and at the default site.
  • packages/mcp/src/mcp-stdio-tools.test.ts (18 → 20): the same variables input schema is advertised on stdio and HTTP. The shadowed call, the CONTROL call and the off-site refusal each give the same tools/call result on both transports, and the three answers differ as the input says they should.

Acceptance notes

Verification

Head 4cae1f45ab (origin/main 295ce127 plus two commits). Every reading below was taken at that head.

Package suites, typecheck and lint
os-verify-lock: pnpm --filter @objectstack/lint --filter @objectstack/mcp run test
packages/lint test:  Test Files  208 passed (208)   Tests  8877 passed (8877)
packages/mcp test:   Test Files  64 passed (64)     Tests  1174 passed (1174)
VERDICT command-exit 0
os-verify-lock: pnpm --filter @objectstack/lint --filter @objectstack/mcp run typecheck
packages/lint typecheck$ tsc --noEmit && pnpm check:test-typecheck
packages/mcp typecheck$ tsc --noEmit && pnpm check:test-typecheck
VERDICT command-exit 0
pnpm lint (eslint . --no-inline-config) :: exit 0
Ablations (each through scripts/ablation-replace.mjs; each restored with blob == HEAD and git diff HEAD empty)
L1  lint: the probe declares no variables (the `variables` spread in authoredFlowProbe deleted)
      lint suite reads src: 6 failed | 51 passed (57) — the 3 warnShadowedFieldReads slot cases,
      the probe read-back, the bare-read-only case, the trigger-kind/start-condition case
A1  mcp: the validator hint loses `flowVariables`
      1 failed | 96 passed (97) — "given the flow's variables, a bare read of one is the variable"
A2  mcp: `variables` not handed to the gate
      3 failed | 94 passed (97) — the whole-verdict pin, the start-condition pin, the stdio parity pin
A3  first attempt void: deleting the `if` line alone left a syntax error, no tests ran. Re-done as A3b:
A3b mcp: the off-site refusal disabled (`if (false)`)
      2 failed | 95 passed (97) — the refusal table, the stdio parity pin

The MCP suites read @objectstack/lint through its dist/ (no vitest alias). L1 was read on the lint suite, which imports src. The MCP leg of the probe is covered by A2.

Derived gate families (dispatch-gates --commands, 67 families; --ran reconciliation)
dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit 4cae1f45ab
65 families :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3   PREREQUISITE NOT MET (no full build here) — NOT MEASURED, CI's Lint & Repo Gates runs it
pnpm check:lean-entry-closure   :: exit 3   PREREQUISITE NOT MET (objectql dist absent) — NOT MEASURED, same
✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 65 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).

test:repo and the full farm are CI's. No export was renamed or removed, so check-platform-checklist is not owed. Full dogfood is not owed: the title has no ! and the Clause-② arm is (widening).


Generated by Claude Code

…a flow condition

The probe flow declares the variables the caller hands it, so the runtime
flow door's shadowed-read warning fires, and the shared validator is handed
their names as `flowVariables`, so a bare read of one is not typed as the
field. Without the input the answer is unchanged.

Claude-Session: https://claude.ai/code/session_017xRBcSZ51q2s3nMfakhdJ3
Co-authored-by: Claude <noreply@anthropic.com>
…t the runtime flow door

The shadowed-read warning is now a `pass` in the flow-condition table, read
through the entry at the start, decision and edge slots with the variables
declared; the tool-level pins cover the type verdict moving to the variable,
the unchanged CONTROL answer without the input, the off-site refusal, a
variable that shadows nothing, the spec's own variable refusals, and one
stdio/HTTP parity pin. Adds the changeset.

Claude-Session: https://claude.ai/code/session_017xRBcSZ51q2s3nMfakhdJ3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 2 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
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (literal, 58 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e540baf8873c22642ce191b99e7e5ce1fd3b4620 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 613c15ade235b26fde7bfb47408521b67e98b7ab — the merge of head 4cae1f45ab2246d254a51847b7bdd1e8a0752266 into base e540baf8873c22642ce191b99e7e5ce1fd3b4620, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 613c15ade235b26fde7bfb47408521b67e98b7ab && git checkout 613c15ade235b26fde7bfb47408521b67e98b7ab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e540baf8873c22642ce191b99e7e5ce1fd3b4620 4cae1f45ab2246d254a51847b7bdd1e8a0752266 && git checkout -B drift-repro e540baf8873c22642ce191b99e7e5ce1fd3b4620 && git merge --no-ff 4cae1f45ab2246d254a51847b7bdd1e8a0752266

node scripts/docs-audit/affected-docs.mjs --json e540baf8873c22642ce191b99e7e5ce1fd3b4620

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

review#2 · os-project-manager · session_01FEkQ5nhu4dRbKVQKDB4CDB

Contract-review-claim: session_01FEkQ5nhu4dRbKVQKDB4CDB · head 4cae1f4 · 2026-11-14T15:09Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

review#2 · os-project-manager · session_01FEkQ5nhu4dRbKVQKDB4CDB

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 4cae1f45ab2246d254a51847b7bdd1e8a0752266
Local-runs: none

① Derived judgments

Gate state on this head when it was read: 69 check-runs. 31 succeeded, 15 were skipped, 0 failed and 23 were still in progress (Test Core 8 of 8 shards, Test UI 14 shards, Type Check UI). Every Lint family had passed, and so had Check Changeset, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate, Build Core and Type Check · workspace. This verdict judges the diff; the in-progress runs still have to finish green before the PR lands.

Accept-set and public-surface changes the diff makes:

  • MCP validate_expression input schema. It gains an optional input, variables: z.array(FlowVariableSchema), which widens the accept set (under strictToolInput a call carrying this key was refused before). Right. The shape is the spec's own, imported from @objectstack/spec/automation, with no hand-written copy. A malformed variable is refused by the spec's strict object. One tool definition serves both doors through wireBridgeTools, and the stdio parity pin advertises the same items.required on both transports.
  • Off-site refusal. variables at any site other than flow_condition is refused, in the same wording as startCondition, including an empty array at the default site. Right.
  • Without variables, answers are unchanged. The probe, the hint and gatePassFindings each spread the key only when it is defined, so a call without the input builds the same probe and hint as on main; the CONTROL pins hold this. Right.
  • Validator half. It receives the declared names through the existing ExprSchemaHint.flowVariables, read only at scope: 'flattened' (the flow_condition row of VALIDATE_SITE_MAP), the same key the gate hands the validator in validate-expressions.ts. The card's own scenario (title * 2 > 10 with variable title) is pinned to the gate's verdict. Right.
  • Gate half. authoredFlowProbe declares the variables, so the gate's own warnShadowedFieldReads reads them; nothing is re-implemented, and findings on the declarations themselves are dropped by onSlot. Right.
  • @objectstack/lint public type. The exported AuthoredSiteExpression and authoredFlowProbe gain an optional variables: an additive type widening, named in the changeset. Right.
  • Description text. The tool and input descriptions say the shadowed-read warning and the variable binding of a bare name need variables. Right.

Findings:

  • nit: the spec-shape refusal test asserts only isError: true; the PR body and changeset say the refusal is the spec's own sentence, but no test checks the wording.
  • nit: FlowVariableSchema comes in through a second import from @objectstack/spec/automation, beside the one that already imports FLOW_TRIGGER_KINDS.

② Semver level

Clause-②: yes (widening) and Metadata-protocol: none are line-initial in both the PR body and the changeset. Widening is correct (a new optional input on a closed tool schema, plus an optional key on an exported lint type); packages/spec is untouched and FlowVariableSchema is reused unchanged, so none is correct. A non-protocol widening ships patch: the changeset gives @objectstack/mcp and @objectstack/lint a patch, the two published packages the diff moves. Matches. Check Changeset is green.

③ Boundary flags

  • open_questions: none (dev report 6879320769).
  • Input shape (the card left it open): an input rather than reading a named served flow. Accepted — reading a named flow needs a new bridge member on both doors and fails for an unsaved flow, the usual case while an agent authors one.
  • Serial constraint with fix(lint)!: os build and the object_extension publish door judge each validation rule an object extension adds by the object's own rule pass #33205: hunks in authored-site-expression.test.ts do not overlap, and this PR does not touch validate-expressions.ts. Cleared.
  • Skills-docs: follow-up owed for the served SKILL.md line in packages/mcp/src/skill-md.ts and its Console mirror in ui/apps/console/src/pages/developer/objectstack-skill.ts: both list the validate_expression signature without variables (confirmed on main). should-fix, outside this diff's claim surface, passed to the landing seat: file the follow-up card and rewrite the line to #N before ACCEPT. It does not block this diff.
  • Out-of-scope observation: given variables, the validator half receives only the declared names, while the gate on a real flow also leaves node ids, screen field names and the engine-bound names untyped. Pre-existing, disclosed in the Acceptance notes; the shadowed-read warning (declared variables only) is exact. Accepted as noted.
  • Check-runs in progress (see ①): the landing seat arms only once they are green.

Implemented-by: claude/issue-33176-validate-expression-flow-variables
Reviewed-by: session_01FEkQ5nhu4dRbKVQKDB4CDB

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · sunhaolin · session_017xRBcSZ51q2s3nMfakhdJ3

席位核对 · 2026-11-14T15:31Z · ACCEPT

Head 4cae1f45. Contract review: PASS 6879518229 (review#2, TIER_DEFAULT, same head). Dev report: 6879320769.

State change: ready and auto-merge armed (--land).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 15:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 9f736f2 Nov 14, 2026
78 of 79 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-33176-validate-expression-flow-variables branch November 14, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants