Repository navigation
fix(lint)!: publish refuses a text-vs-number comparison in a validation rule (or another fail-closed predicate) instead of warning, then refusing every write - #29923
Conversation
…sed predicate A validation rule (and a field requiredWhen / readonlyWhen, an option visibleWhen on the write path, a hook condition) fails closed: a predicate that cannot be evaluated refuses the write. `record.name > 5` on a text field faults on every value that is not numeric text, so it published with a warning that it "evaluates to null" and then refused every write. The shared validator now takes the site's fault outcome (`ExprSchemaHint.faultOutcome`), the lint gate passes it at exactly the fail-closed sites, and there the type-mismatch verdict is an error worded for a refused write. Formula fields keep the warning. The MCP validate_expression tool's validation site answers the same. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhUVCv2vcJJepWyHQQ
…de entry The semantic migration the narrowing owes: the surface, the like-with-like rewrite and why there is no mechanical one. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhUVCv2vcJJepWyHQQ
…ter its filter check:objectql-double-limit refuses a new limit-blind find double. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhUVCv2vcJJepWyHQQ
📓 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:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 174 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 8bc61d3c4441a47e00f16cd7618b793393878bdb && git checkout 8bc61d3c4441a47e00f16cd7618b793393878bdb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a04e9d76c13363280bdd86950603f0e0e6deb6dd 1dfad411f3605ce8d35b358524e1ec6393bc5d67 && git checkout -B drift-repro a04e9d76c13363280bdd86950603f0e0e6deb6dd && git merge --no-ff 1dfad411f3605ce8d35b358524e1ec6393bc5d67
node scripts/docs-audit/affected-docs.mjs --json a04e9d76c13363280bdd86950603f0e0e6deb6dd
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentCard scenario. One decision, one place. Role→severity is decided only in lint's Per file: FROM → TO in this CEL dialect: False positives probed: number/currency/percent/select/lookup/autonumber are Findings:
No must-fix. The narrowing takes away no declared capability: everything it refuses already refused every write at runtime (or, for numeric text, is named with its exact rewrite), and it makes the spec's own fail-closed refusal real at author time. Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
Requested by Yibo Wang · project thread
Fixes #29904
Clause-②: no (narrowing) — the author-time gate (
os build,os validate, the object save door and the package publish) refuses atype-mismatchit used to pass with a warning, at the predicate sites that fail closed. Nothing is widened.Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/data-modeling/formulas.mdx(the build-time validation table's type-soundness row saidwarningfor every site) and the MCP skill text inpackages/mcp/src/skill-md.ts;skills/**teaches no text-vs-number comparison and no type-soundness severity (grep for the verdict words andrecord.X > Non text fields: 0 hits), so no skill or eval moves.维护者速读
record.name > 5)现在发布时直接拒绝(error),公式字段仍只给 warning。packages/formula/src/validate.ts(fail-closed 分支与文案)、packages/lint/src/validate-expressions.ts(FAIL_CLOSED站点表及各调用点)、packages/mcp/src/mcp-http-tools.ts(validation 站点)。pnpm --filter @objectstack/objectql exec vitest run src/publish-type-mismatch-fail-closed.test.ts,预期 3 条全过:校验规则被拒、公式仍发布带 warning、可空孪生照旧被拒。What changes
@objectstack/formula.ExprSchemaHintgainsfaultOutcome?: 'fail-closed'. The validator cannot tell a validation rule'spredicatefrom a flow condition's, so the caller that knows the site declares it. With it,type-mismatchis graded an error whose message says what really happens (the write, or the operation a hook guards, is refused) and how to compare like with like; the refusal'sparamscarryoutcome: 'fail-closed'. Without it, the warning and its "evaluates to null" text are unchanged.@objectstack/lint. One constant,FAIL_CLOSED, with the site ledger in its docblock, passed bycheck()at exactly the fail-closed sites: validation rulecondition/whenand nestedthen/otherwiserules, fieldrequiredWhen/readonlyWhen, optionvisibleWhen, hookcondition. These are the sites where the nullable-comparison twin is already an error with the'fail-closed'outcome (plusreadonlyWhenand optionvisibleWhen, see below).@objectstack/mcp.validate_expressionwithsite: 'validation'passes the same outcome, so the tool an agent self-checks with answers what publish will. Its description and the served skill text say so.@objectstack/spec. Upgrade guide entryvalidation-type-mismatch-refused(semantic: no mechanical rewrite). ADR-0087 dispositionregistered validation-type-mismatch-refused.content/docs/data-modeling/formulas.mdx: the type-soundness row's severity and fix.Which roles fail closed (measured by reading each evaluator)
condition/when, nested rulesrule-validator.tscheckPredicate/checkConditional: "write rejected" (#4649)requiredWhenrule-validator.ts: refuses the write (ADR-0137 D2)readonlyWhenisReadonlyWhenLocked: throwsValidationErroron every fault but the unbound-root carve-out (ADR-0137 D2)visibleWhenevaluateOptionVisibility: refuses a write that picks the option (ADR-0137 D2, #22402)conditionhook-wrappers.ts: aborts the operation (#4775)expressiondecisionconditionrun-fails)visibleWhen/settledWhen,conditionalFormatting, field groupvisibleWhen, actionvisible/disabled/pressed, app landingvisiblecondition; RLS / permission predicatescompileCelToFilter/isSupportedRlsExpression), not CEL per rowThe evaluator's fail-closed behaviour is untouched.
The conversion the message names, measured on the evaluator (
ExpressionEngine.evaluate, values"abc"/"7"/ null):record.name > 5→ FAULT / true / FAULT;size(record.name) > 5→ false / false / FAULT;double(record.name) > 5→ FAULT / true / FAULT (validates clean, faults on non-numeric text, so the message says so);int(...)same asdouble.Tests
packages/lint/src/validate-expressions.test.ts: the old pin "warns on a validation predicate ordering a text field against a number" is the behaviour this card retires; it is replaced by five pins: the validationconditionrefusal (pathobjects[0].validations[0].condition, message), the formula warning, one refusal per other fail-closed site (when, nestedthen,requiredWhen,readonlyWhenon a boolean, optionvisibleWhen, hookcondition), the warning kept on fieldvisibleWhenand actionvisible, and the nullable twin unchanged.packages/formula/src/validate.test.ts:faultOutcome→ error withcode/params, boolean arm, and the two conversions validate clean.packages/mcp/src/mcp-validate-expression.test.ts:site: 'validation'→ error.packages/objectql/src/publish-type-mismatch-fail-closed.test.ts(new): the real protocolsaveMetaItem(draft) +publishPackageDraftsover a real ObjectQL: refused withexpression-invalid/errorat…validations[0].condition; formula publishes with the warning advisory; the nullable twin still refused.Package suites (at d5b798c, under os-verify-lock; the final commit 1dfad41 changes only the new objectql test's driver double)
Ablation (old severity put back in the fail-closed branch, formula rebuilt, marker proven in dist)
Mutation:
severity: 'error'→severity: ['warning', 'ablated-29904'][0] as 'warning'in the fail-closed arm;ablation-replaceanchor 1 → 0,ablation-dist-preflightmarker present indist/index.jsanddist/index.mjs.The nullable-twin pins stayed green under the mutation (they are the control). Restore: blob back to HEAD
6fcce907,git diff HEADempty, formula rebuilt, preflight--absentclean on all 6 built files; lint 5/5 and objectql 3/3 green again.Real boot (showcase `pnpm dev -- --fresh`, full build at d5b798c, as admin, writable package com.qa.tmm)
Before (card, main
589049b0): the same[1]published with awarning, and everyPOST /dataanswered 400 "could not be evaluated … write rejected". The draft save answers 200 for both this rule and the twin, as before (the save door keeps #4463 D1).Acceptance notes
severity: 'warning'/'info'. The runtime logs an unevaluable advisory rule instead of refusing; the gate refuses the comparison on every validation rule regardless, exactly as the nullable twin does. Kept for parity with the twin; the message's "refused" clause is exact forerrorrules.readonlyWhenand optionvisibleWhenare fail-closed sites the twin does not cover (itsreadonlyWhenexclusion is ruling #4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953 clause 3, about thehas()prescription, which this verdict does not give). Included because their evaluators refuse the write on a fault.packages/formula/src/validate.ts,packages/lint/src/validate-expressions.tsorpackages/mcp/src/mcp-http-tools.ts(per-PR file lists read).Verification
All at the final head 1dfad41.
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". The first round (at d5b798c) had one red,check:objectql-double-limit(the new test's copiedfinddouble ignoredlimit); fixed in 1dfad41, and the whole list was run again.pnpm lint(eslint . --no-inline-config): exit 0.pnpm --filter @objectstack/dogfood test, after a fullturbo run build, 119/119 tasks): Test Files 410 passed | 3 skipped (413), Tests 3379 passed | 28 skipped (3407); lock VERDICT command-exit 0.check-adr-0087-registration --base origin/main: "registered validation-type-mismatch-refused (new here)".🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzhUVCv2vcJJepWyHQQ