Repository navigation
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
Conversation
…n advisory validation rule is served a verdict that says what really happens - Flow start / edge conditions of a record-triggered flow: refused with the run-fails wording (the run fails, the triggering write lands), the outcome and severity the null-guard twin already has there. - Advisory (`warning` / `info`) validation rules: still refused, as the twin refuses them, but both verdicts now say the rule is skipped and the write proceeds instead of "the write is refused". - One refusal code per outcome: type-mismatch (warning), type-mismatch-fail-closed, type-mismatch-run-fails, type-mismatch-fail-open; the role -> outcome decision lives in one ledger, `faultOutcomeOf`, for both verdicts. - D3 entry flow-condition-type-mismatch-refused; the validation-type-mismatch-refused entry's create answers 201. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfu5kYQW45XjsF79Rp
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flags
④ Narrowing face
Rework
Implemented-by: VERDICT: REWORK |
…in 18.3.0; the non-record flow warning is pinned on a start and an edge - Changeset: replace the "Supersedes, in the same release" paragraph. `@objectstack/formula@18.3.0` / `@objectstack/mcp@18.3.0` already serve `type-mismatch` + `params.outcome: 'fail-closed'` (shipped without a changelog line); this release renames that arm and removes `outcome`. - Lint test: the non-record-triggered case now covers a flow with no `triggerType` and a `schedule` flow, start and edge condition each; the inert-node case is its own test. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfu5kYQW45XjsF79Rp
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
📓 Docs Drift CheckThis PR changes 4 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 9 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 f7c6d6773442690b53c23c3b02d1b8f1702b32d0 && git checkout f7c6d6773442690b53c23c3b02d1b8f1702b32d0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c92b88728cb533338759c3d700a683f29db1ee60 be72183fe8e6a02d5c699bbc52254fa93101700e && git checkout -B drift-repro c92b88728cb533338759c3d700a683f29db1ee60 && git merge --no-ff be72183fe8e6a02d5c699bbc52254fa93101700e
node scripts/docs-audit/affected-docs.mjs --json c92b88728cb533338759c3d700a683f29db1ee60
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Head moved · 2026-11-03T18:30Z ·
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Head moved · 2026-11-03T20:01Z ·
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Head moved · 2026-11-03T22:29Z ·
|
Requested by Yibo Wang · project thread
Fixes #29926
维护者速读
name > 5)写在「记录触发流程」的开始条件或连线条件里,现在发布时直接拒绝(error),文案说明真实后果:流程运行在该条件处失败,触发它的写入照常落库。写在建议性校验规则(severity: 'warning' | 'info')里仍被拒,但两条判词(类型不匹配和可空比较)都改成如实说明:规则被跳过、写入照常进行、建议永远不会出现。type-mismatch一个代码对应两句话,违反「一个代码一句模板」的契约,现在每种结局各有自己的代码。type-mismatch带outcome: 'fail-closed'的那一支改名为type-mismatch-fail-closed,并去掉outcome参数。这一支已随 18.3.0 发出(@objectstack/formula@18.3.0的类型和@objectstack/mcp@18.3.0的validate_expression都在用,但 18.3.0 的 changelog 里没有记录),所以按 18.3.0 的type-mismatch+outcome识别它的使用方需要改认新代码。回滚即 revert 本 PR,无数据迁移。packages/lint/src/validate-expressions.ts(faultOutcomeOf账本,两条判词共用)、packages/formula/src/validate.ts(TYPE_MISMATCH_FAULT_ARMS:每种结局的代码和文案)。pnpm --filter @objectstack/objectql exec vitest run src/publish-type-mismatch-fail-closed.test.ts,预期 5 条全过:校验规则被拒、公式仍发布带 warning、可空孪生照旧被拒、建议性规则被拒但文案不说写入被拒、记录触发流程条件被拒且文案说运行失败。Clause-②: no (narrowing) — the author-time gate refuses, in a record-triggered flow's start or edge condition, a
type-mismatchit used to pass with a warning; and the fail-closed arm of the verdict is served under its own codetype-mismatch-fail-closedinstead oftype-mismatch+params.outcome. Added beside them (not counted as the arm): the codestype-mismatch-run-failsandtype-mismatch-fail-open, and the exported typeExprFaultOutcomefrom@objectstack/formula.Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/data-modeling/formulas.mdx(the type-soundness row names each outcome) and the MCP skill text inpackages/mcp/src/skill-md.ts(flow_conditionnow errors too).skills/**teaches no text-vs-number comparison in a flow condition or an advisory rule (grep fortype mismatch,evaluates to nulland the flow condition examples), so no skill or eval moves.What changes
One ledger for both verdicts (
@objectstack/lint).FAIL_CLOSEDbecomesfaultOutcomeOf(site), the one function every call site asks what a site's runtime does on a fault. Its docblock carries the ledger, read off each evaluator. The answer goes to the shared validator asExprSchemaHint.faultOutcome(the type-mismatch verdict) and tocheckNullGuardsas itsNullGuardOutcome(the nullable-comparison verdict). The tokens are the same three, so one rule cannot get two different stories.condition/when/ nested branches,severity: 'error'fail-closedseverity: 'warning'/'info'(top-level rule's severity; branches inherit it)fail-openrequiredWhen/readonlyWhen, optionvisibleWhen, hookconditionfail-closedcondition, edgeconditionevaluateConditionthrows; the run fails, the write landsrun-failsconditionItem 1, flow conditions. The twin refuses there (
checkNullGuards(..., 'run-fails'), severityerror, #29026), so the type mismatch does too. Its message: "… here a flow condition that cannot be evaluated fails the run: the flow stops at this condition, the write that fired it still lands, and the work the flow exists to do is not done." It applies on exactly the twin's sites: the top-level start node and every edge of arecord-*flow. The twin'sdecisionbranch row has no type-mismatch counterpart: the branch slot is validated without the object's field types (checkDeclaredPredicate), so no type mismatch reaches it, before or after.Item 2, advisory validation rules. Measured on the twin: it refuses every validation rule regardless of
severity, with "the write is rejected fail-closed". So the type mismatch keeps the refusal (error) and both verdicts now word it asfail-open. The twin's'fail-open'outcome was unused (its clause still described the pre-ADR-0137requiredWhen), so its clause is reworded for the surface that really skips: "so the rule is SKIPPED fail-open: it is advisory, so the write proceeds, the rule is never applied (its advisory never fires), and the only trace is a log line saying it could not be evaluated". The type-mismatch arm says the same. Neither claims a refused write.Item 3, refusal codes (
@objectstack/formula). I took the "own code per arm" option.type-mismatch(warning, reads null),type-mismatch-fail-closed,type-mismatch-run-failsandtype-mismatch-fail-open(errors) each name one template, chosen from a table keyed by the outcome (TYPE_MISMATCH_FAULT_ARMS), so a new outcome without its own code does not compile. Theoutcomeparam is gone. The fail-closed sentence is byte-identical to #29923's (the new pin's message is that text). Pinned inPINS(four new cases), andSWEEP_SCHEMASgains the deal schema once per outcome, so the sweep reaches every code and holds its params to its message.Readers of the renamed code:
validate_expressionpasses the code through unchanged. Itssite: 'validation'now answerstype-mismatch-fail-closed, andsite: 'flow_condition'answerstype-mismatch-run-failsas an error. Fields bound bare is the record-triggered shape, and the gate refuses there. Its description and the served skill text say so.git grepfortype-mismatchandEXPRESSION_REFUSAL_CODESoutsidepackages/formulafinds only the MCP test and unrelatedsdui-parsercodes. A Studio catalogue in objectui keyed totype-mismatch+outcomewould need the new code.The[Edit, rework r1: false.type-mismatch+outcomearm has not shipped in a release: the publish gate: a validation rule comparing a text field with a number (record.name > 5) publishes with only a warning that says it "evaluates to null", then every write to the object is refused #29904 changeset is still pending in.changeset/.@objectstack/formula@18.3.0(npm, 2026-11-03T15:09Z) shipsoutcome: 'fail-closed'inExpressionRefusalParams, and@objectstack/mcp@18.3.0serves it on thevalidationsite. The publish gate: a validation rule comparing a text field with a number (record.name > 5) publishes with only a warning that says it "evaluates to null", then every write to the object is refused #29904 changeset is still pending only because version-packages chore: version packages #26517 branched before4c2cd29a, so 18.3.0's changelog has no line for it. This PR renames a served, released code: an 18.3.0 consumer keyed totype-mismatch+params.outcome === 'fail-closed'moves totype-mismatch-fail-closed(FROM → TO in the changeset).]formulas.mdxrow (above).Item 4.
18.validation-type-mismatch-refusedacceptanceCriteria: "answers 200" → "answers 201".@objectstack/spec. New semantic entry18.flow-condition-type-mismatch-refused, registered as the ADR-0087 disposition. The changeset says what 18.3.0 already serves (thetype-mismatch+outcomearm, shipped without a changelog line) and what this release changes about it, and that the #29904 changeset's "flow conditions keep the warning" no longer holds.Why
Measured on
mainat21510bba(after #299234c2cd29a), through the realsaveMetaItem(draft) +publishPackageDraftsdoor. This is the objectql test below, run with the ledger ablated back to #29923's behaviour (ablation A5):Both are false for that role. The runtime readings:
evaluateConditionthrows on a fault (ADR-0032 §1c), and the record-change trigger isolates the write.evaluateValidationRulesruns an advisory rule withadvisory: trueand logsunevaluableAdvisoryMessage("write proceeded: the rule is advisory, so it was not applied to this write").Tests
packages/formula/src/validate.test.ts: fail-closed now asserts codetype-mismatch-fail-closedwith exact params (nooutcome). Two new tests coverrun-fails(flattened, barename, conversions validate clean) andfail-open.packages/formula/src/expression-refusal-codes.test.ts:PINSfor the three codes (exact params and message, a boolean case on fail-closed).SWEEP_SCHEMASadds one schema per outcome.packages/lint/src/validate-expressions.test.ts:errorwith paths and the run-fails text;triggerTypeand ascheduleflow keep the warning on the start and the edge condition; an inert node condition of a record-triggered flow keeps it too (rework r1);warningandinfo, a nested branch included):errorwith the fail-open text;errorrule;??别名读法(#5009 建议 3 的核对结果) #5017 key meta-test tablesrule.severityand thesiteplumbing receiver.packages/mcp/src/mcp-validate-expression.test.ts:validation→type-mismatch-fail-closed;flow_condition→type-mismatch-run-failserror (was asserted a warning).packages/objectql/src/publish-type-mismatch-fail-closed.test.ts: two new door cases, the advisory rule and the record-triggered flow.Suites (at the final head, under os-verify-lock, shared box)
Ablations (each restored: blob == HEAD, git diff HEAD empty)
Acceptance notes
record.xin a flow condition is not judged for type soundness at all: the flattened-scope check types bare names only, sorecord.title > 5in an edge draws no verdict, before or after (the new tests use the bare spelling). Pre-existing, out of this card.packages/lint/src/validate-expressions.ts,packages/lint/src/validate-null-guards.ts,packages/formula/src/validate.tsorpackages/mcp/src/mcp-http-tools.ts: none of the 18 open PRs at 2026-11-03T17:12Z (the nearest, fix(objectql, metadata-protocol, runtime): a package's export carries the objectExtensions it was installed with, and the owner's export folds no other package's extension #29948, touches objectql / metadata-protocol / runtime only; docs(releases): 18.3.0 release notes and its upgrading row #29938 is the release notes).Verification
Rework r1 (head
06abf5ac, changeset text + one lint test):check-adr-0087-registration --base origin/mainand--self-test,check-changeset-no-major --base origin/main,check-empty-changeset --base origin/mainand--self-test,check:changeset-gate-self-tests: exit 0.validate-expressions.test.ts459/459, lintcheck:test-typecheckand ESLint on the file: exit 0.dispatch-gates --commandsderives the same 106-family list as before. Everything below was measured at61a4cdbf, which differs only in those two files.node scripts/pm/dispatch-gates.mjs --commands: 106 derived families, each run, all exit 0.--ranreconciliation: "106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN". Four exited 3 on the first pass for missing prerequisites (check-plugin-teardown-shape --self-teston a shallow clone;check:skill-examples,check:dual-build-cjs-loadsandcheck:lean-entry-closurebefore a full build). They were run again aftergit fetch --unshallowand a fullturbo run build(119/119 tasks): exit 0. The final head61a4cdbfdiffers from the swept commit only in the new objectql test's type annotation (and the commit message).check:objectql-double-limitandcheck:engine-double-contractwere run again on it: exit 0.check-adr-0087-registration --base origin/main: "registered flow-condition-type-mismatch-refused (new here)".pnpm lint(eslint . --no-inline-config): exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzfu5kYQW45XjsF79Rp