Repository navigation
fix(lint)!: a flow condition's text-vs-number comparison gets a true verdict on every flow, record.x read and decision branch - #30015
Conversation
…y flow, record.x and decision branch WIP: formula flattened-scope warning arm and record-root typing, lint decision branches through the edge's schema-hint path, MCP validate_expression severity and triggerKind inputs. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzf4UmgvS2cusNbWnvy
…ate parity Formula pins for type-mismatch-run-may-fail and the record-root pass, lint pins for record.x / decision on record-triggered and other flows, MCP validate_expression parity against the gate for each severity and trigger kind, publish-door cases, the ADR-0087 semantic entry and the changeset. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzf4UmgvS2cusNbWnvy
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzf4UmgvS2cusNbWnvy
📓 Docs Drift CheckThis PR changes 4 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 175 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 a97e0a37db08b2050ddd313983c60ddec11be57e && git checkout a97e0a37db08b2050ddd313983c60ddec11be57e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 63b86b385dd2cbfeba131438112c49be2c74df91 6a3a1e2c4365a20865484c491e7d040cf39da969 && git checkout -B drift-repro 63b86b385dd2cbfeba131438112c49be2c74df91 && git merge --no-ff 6a3a1e2c4365a20865484c491e7d040cf39da969
node scripts/docs-audit/affected-docs.mjs --json 63b86b385dd2cbfeba131438112c49be2c74df91
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: REWORK REWORK items:
|
… a time-relative sweep is graded like a record trigger A flow run seeds its declared variables and node outputs before it flattens the record, and the flattening skips a bound name, so a bare read of such a name holds the variable: the type check no longer types it as the field it shadows (new optional ExprSchemaHint.flowVariables), while record.NAME and previous.NAME stay typed. A time-relative sweep launches each run with the swept row as its record, so a sweep of the object the conditions are typed against gets run-fails at the start, edge and decision sites, in the lint ledger and the MCP flow_condition map alike. The run-may-fail warning no longer asserts the flow's kind. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzf4UmgvS2cusNbWnvy
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier:
The replacement body Cited code read at the head:
The head's 65 check-runs. Face: delta review of the fix-up after REWORK 6624166953 (head ① Derived judgments
② Semver levelThe level is unchanged and still right: ③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
Requested by Yibo Wang · project thread
Fixes #29959
维护者速读
name > 5)现在每个位置都拿到真实判词。记录触发流程里写成record.name > 5、或写在decision分支里,发布时和裸字段名一样被拒(error,文案说运行在该条件处失败、触发写入照常落库);非记录触发流程(定时、API、手动/动作启动)仍是 warning,但文案从「静默得到 null」改为如实说明:条件出错时运行失败。MCPvalidate_expression新增可选输入severity(校验规则)和triggerKind(流程触发方式),答案与发布门一致。evaluateCondition在每种流程上都抛错、运行失败);record.x从不做类型判断;decision分支没有字段类型,name > 5运行必失败却无任何提示。MCP 工具也无法表达规则严重级别和流程触发方式,答案与发布门不一致。record.x读取或 decision 分支里有这种比较,下次发布会被拒(它们每次运行本来就会失败)。流程条件上的 warning 代码由type-mismatch改为type-mismatch-run-may-fail(18.3.0 的 formula / mcp 在流程条件上发的是type-mismatch,按代码识别的使用方需改认)。MCP 新输入均可选,不传时答案与之前相同。回滚即 revert 本 PR,无数据迁移。packages/formula/src/cel-engine.ts(firstTypeMismatch的 record 根第二遍)、packages/formula/src/validate.ts(type-mismatch-run-may-fail分支)、packages/lint/src/validate-expressions.ts(checkDeclaredPredicate的 decision 分支走check())。pnpm --filter @objectstack/objectql exec vitest run src/publish-type-mismatch-fail-closed.test.ts,预期 9 条全过:记录触发流程record.name > 5与 decision 分支name > 5被拒且文案说运行失败;定时流程同样写法发布成功、warning 文案说运行失败而不说 null;类型正确的对照发布无判词。Clause-②: yes (narrowing) — the author-time gate (
os build,os validate, the package publish and the flow publish door) refuses, on a record-triggered flow, atype-mismatchin adecisionbranch and in arecord.FIELD/previous.FIELDread of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition'stype-mismatchwarning is served under its own codetype-mismatch-run-may-failinstead oftype-mismatch. Added beside them (the widening half): the optionalseverityandtriggerKindinputs of thevalidate_expressionMCP tool, the codetype-mismatch-run-may-failin@objectstack/formula'sExpressionRefusalCode, the optionalflowVariableskey of@objectstack/formula'sExprSchemaHint, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served atype-mismatchwarning).Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/data-modeling/formulas.mdx(the type-soundness row names the decision branch, therecord.FIELDread and the warning on other flows) and the MCP skill text inpackages/mcp/src/skill-md.ts(the two newvalidate_expressioninputs);skills/**teaches no text-vs-number comparison in a flow condition (grep fortype mismatch,evaluates to null,validate_expressionand the decision examples), so no skill or eval moves.What changes
Re-measured on
mainat73a3ef9f(after #299539037b13d). The four sites, and what each now gets:record.FIELD/previous.FIELDin a start or edge conditionseedRunVariablesbinds the trigger record asrecordAND flattened to bare names, sorecord.nameis the barename;evaluateConditionthrows, the run fails, the write landsrecordadynmap)type-mismatch-run-fails, the bare name's verdictdecisionbranchconfig.conditions[].expressionbuiltin/logic-nodes.tsevaluates it over the same run variables, through the sameevaluateConditioncheckDeclaredPredicatepassed no schema hint); the null-guard twin alone refusedtype-mismatch-run-failstriggerType,schedule, time-relative,api), all three slots; an inert nodeconditionevaluateConditionthrows on every flow, so "silently evaluates to null" was false; but the run holds what its entrance hands ittype-mismatch, "silently evaluates to null" (start/edge only)type-mismatch-run-may-fail, worded for the failed run (start, edge, decision,record.FIELD)validate_expressionvalidationalways fail-closed,flow_conditionalways run-failsseverity/triggerKindinputs; each combination answers the gate's verdictSite 1, grading (seat's ruling: grade as #29953 did if the same runtime fact holds; keep the warning with true text if a reason is measured). I kept the warning, and the reason is measured on the entrances, not assumed. The run does fail on a fault in every flow (
dag-traversal.tsevaluateCondition, ADR-0032 §1c). What differs is whether the condition faults at all: on a record-triggered flow the bare name andrecord.FIELDare the materialized trigger record (record-change-trigger.tsbuildContext→seedRunVariables), so a text field holds text andname > 5faults unless the text is numeric. On any other flowseedRunVariablesbinds whatevercontext.recordthe entrance hands over (engine.tscelScope docblock, #22642): the inbound hook's request body (trigger-api, a JSON body whosenamemay be a number), atype: 'flow'action's loaded row or an empty record carrying onlyid, a parent flow's record (subflow/map), or nothing for a cron schedule or the REST trigger route. So the object's field type is not proven to be what the name holds, and refusing there would refuse conditions that can run. This is the same measurement that keeps the nullable-comparison twin off those flows (validate-null-guards.tsledger, "sparse or unknown"). The warning now says what happens when it does fault: "a flow condition that faults when it is evaluated fails the run: the flow stops at this condition and the work it exists to do is not done (it does not read null)", and why it is a warning.Where the text is decided (
@objectstack/formula). A'flattened'scope is a flow condition, the only caller that passes one with field types (lint's flow walk, the engine's registration advisory pass, MCPflow_condition). SotypeSoundnessIssueservestype-mismatch-run-may-failfor a flattened site with no declared outcome, and therecord-scopedtype-mismatch(formula fields, display predicates: there a fault reads null or never refuses) keeps its code and text. The engine's registration log line reads the new text too (it passes no outcome).Site 2,
record.FIELD(cel-engine.tsfirstTypeMismatch). The flattened typing binds fields bare and leavesrecord/previousdyn. When that first pass finds nothing and the source readsrecord.orprevious., a second pass types those two roots with therecord-scope struct (buildTypedEnv(…, flattenedRecordRoots)), for the text/boolean mismatch only, andoffendingRecordReadnames the read as written (record.title). Same judge, same schema hint, same outcome; no second judge. The date verdicts stay the bare name's alone (see Acceptance notes).Site 3,
decisionbranches (@objectstack/lint).checkDeclaredPredicatetakes an optionalflowConditionhint; adecisionnode passes{ objectName, faultOutcome: flowFaultOutcome }and the slot goes through the samecheck()an edge goes through. So the branch gets the object's fields and types and the flow's ledger outcome (run-failson a record-triggered flow, none elsewhere). The other declaredpredicateslots (a screen field'svisibleWhen) bind the screen's collected values and keep the hint-less call. ThefaultOutcomeOfledger docblock now carries the decision row, therecord.FIELDreading and the "any other flow" row with its reason.Site 4, MCP (
mcp-http-tools.ts). I took the "add the inputs" option.severity(error|warning|info, defaulterror) andtriggerKind(FLOW_TRIGGER_KINDSfrom@objectstack/spec/automationplusnone, defaultrecord_change).VALIDATE_SITE_MAP[site].faultOutcomeis now a function of the two, mirroring lint'sfaultOutcomeOf(validation:warning/info→fail-open, elsefail-closed; flow_condition:record_change→run-fails, else none). A call that passes neither answers exactly as before. An input on a site it does not describe is refused as a tool error ("severitydescribes a validation rule; it applies only with site "validation" …"). The tool description and the served skill text name both inputs. This is a widening of a published tool schema, declared in theClause-②line.@objectstack/spec. New semantic entry18.flow-record-read-and-decision-type-mismatch-refused, the changeset's ADR-0087 disposition (registered). The #29953 entryflow-condition-type-mismatch-refusedis left as is: its text (start and edge, bare name; other flows keep a warning) is still true.Tests
packages/formula/src/expression-refusal-codes.test.ts:PINSfortype-mismatch-run-may-fail(bare text,record.activeboolean: exact params and message).SWEEP_SCHEMASgains a flattened deal schema with no outcome, so the sweep reaches the new code and holds its params to its message.packages/formula/src/validate.test.ts:record.name,previous.name,record.is_activein a flattenedrun-failssite →type-mismatch-run-failsnaming the read; controlssize(record.name) > 5,double(record.name) > 5,record.amount > 5,record.name == 'x',record.is_active == trueclean. Flattened with no outcome →type-mismatch-run-may-fail, not "evaluates to null"; the record-scope warning keepstype-mismatch.packages/lint/src/validate-expressions.test.ts:triggerType,schedule,type: 'api'), start + decision +record.titleedge allwarning, every message "fails the run", none "evaluates to null" or "still lands";record.titlestart, two decision branches (bare text,record.wonboolean),previous.titleedge → fourerrors with their key paths and the run-fails text;??别名读法(#5009 建议 3 的核对结果) #5017 receiver meta-test tables theflowConditionplumbing receiver.packages/mcp/src/mcp-validate-expression.test.ts: gate parity. For eachseverity(omitted,error,warning,info) and eachtriggerKind(omitted,record_change,time_relative,schedule,api,none), the tool's(severity, message)list equalsvalidateStackExpressions's on the same rule or flow. An input on the wrong site is refused; well-typed conditions are clean at every input.packages/objectql/src/publish-type-mismatch-fail-closed.test.ts, new top-leveldescribein the existing harness: through the realsaveMetaItem(draft) +publishPackageDraftsdoor, a record-triggeredrecord.name > 5start condition and aname > 5decision branch are refused with the run-fails text; the same comparisons on ascheduleflow publish with two warnings saying the run fails; a well-typed control publishes clean.Suites (head 5a5fd5b, under os-verify-lock, shared box)
Ablations (each through scripts/ablation-replace.mjs; each restored: blob == HEAD, git diff HEAD empty)
Acceptance notes
date-arithmetic/date-text-arithmeticon a bare date field in a flow condition are errors whose text says the field reads null; in a flow the run fails instead. Pre-existing, and outside this card's type-mismatch scope, so therecord.FIELDpass here deliberately does not extend the date verdicts torecord.d + 1(it would serve that false sentence at a new site). Reported to the seat as a finding.variables: [{ name: 'title', type: 'number', defaultValue: 3 }]over a text fieldtitlegetstitle > 5refused astype-mismatch-run-fails("titleholds text"), though the run seeds the declared variable first and the flattening skips a bound name, so the condition reads the number (measured through the built lint dist on this head; the shadowing warning fires beside it). Present onmainsince fix(lint)!: a type mismatch in a record-triggered flow condition or an advisory validation rule is served a verdict that says what really happens #29953, the same on this branch; the type judge reads the object's field types without the declared-variable exclusion the null-guard twin applies (nullGuardBare). Fixing it needs the hint to exclude names from the bare typing but not fromrecord.FIELD, a new schema-hint key; reported to the seat as a finding, not fixed here.conditionkeeps a warning (now the run-may-fail text, which is true: it fails the run if it is ever evaluated, andflow-inert-node-conditionsays it is not).packages/formula/src/{validate,cel-engine,expression-refusal}.ts,packages/lint/src/validate-expressions.tsorpackages/mcp/src/mcp-http-tools.ts(the nearest: refactor(metadata-protocol): generate the protocol.ts section split with a deterministic codemod #29998 metadata-protocol split, refactor(spec): codemod split of conversions/registry.ts into per-entry modules (#29987) #29993 spec conversions registry split; chore: version packages #29963 is version packages).Rework after the contract review (head 6a3a1e2)
seedRunVariablesseeds the flow's declared variables and node outputs before it flattens the record, and the flattening skips a bound name. New optionalExprSchemaHint.flowVariables(a widening): lint passes the flow'scollectFlowVariableNamesset plus the engine-bound names at the start, edge, decision and inert node sites, and the type judge leaves those bare namesdyn(date verdicts too), whilerecord.NAME/previous.NAMEstay typed. This also closes the start/edge instance on main from fix(lint)!: a type mismatch in a record-triggered flow condition or an advisory validation rule is served a verdict that says what really happens #29953.run-fails.time-relative-trigger.tslaunches each run with{ record: row }, a stored row of the swept object, so on a flow whosetimeRelative.objectis the start node'sobjectName(or is omitted) the ledger,faultOutcomeOfand the MCPflow_conditionmap answerrun-fails. A sweep of another object keeps the warning. The null-guard twin stays on record-triggered flows.check()andExprSchemaHint.fieldTypescomments are refreshed.titleas a number variable: baretitle > 5at start, edge and decision gets no type verdict (shadow warning only),record.title > 5stays refused; the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017 table gains thesweepreceiver againstTimeRelativeTriggerSchema. formula:flowVariablesleaves bare names untyped and keepsrecord./previous.typed. mcp: each trigger kind's severity pinned, not only parity.bareShadowedignored: 1 failed. B4 mcp time-relative off: 1 failed. Each restored, blob == HEAD.Verification
All at head
5a5fd5b8(git rev-parse --short HEAD), the commit this PR opens on.node scripts/pm/dispatch-gates.mjs --commands: 107 derived families, each run after the full build, all exit 0. Re-derived just before reconciling (same 107).--ran: "107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED". The clone was unshallowed first. Among them:check-adr-0087-registration --base origin/main(registeredflow-record-read-and-decision-type-mismatch-refused, new here),check-changeset-no-major,check-empty-changeset,spec check:spec-changes/check:upgrade-guide,check:test-source-alias,check:cross-package-test-inputs,check:objectql-double-limit,check:engine-double-contract.pnpm lint(eslint . --no-inline-config): exit 0.pnpm --filter @objectstack/dogfood testunder the lock; narrowing arm):VERDICT command-exit 0, Test Files 416 passed | 3 skipped (419), Tests 3456 passed | 28 skipped (3484).🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzf4UmgvS2cusNbWnvy