Repository navigation
formula: the metadata save door stores a formula that calls an unregistered function (sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019
Description
Activity
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsPath: ② the capabilities an end user meets in the app — a formula field's value | 缺项 (
api-backend.formula-stdlib-matrixA6 is an expected-fail) | P2Triage: first grade,
bug·priority:p2·domain:engine·area:records·pm:queue. The save door gives the build's verdict, and the read path stops swallowing the faultTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T17:02Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in the metadata save path for objects (
packages/metadata-protocol/src) andpackages/objectql/src/engine.ts(applyFormulaPlan, the planning-stage compile) ⇒domain:engine; rationale: the lane table putspackages/objectql,packages/formulaandpackages/metadata*indomain:engine.- Verified at filing:
engine.ts:2255maps a failed evaluation tonullwith no log; the planning compile at about:1562discards its result; novalidateExpressioncall site exists undermetadata-protocol/srcorobjectql/src. - Why p2: an author gets a 200 and a field that is null on every row, with nothing anywhere to say why.
formulas.mdxpromises the opposite, at both the save door and the call site (ADR-0032). - Not a new gate:
os buildalready refuses this expression, and the docs already say metadata registration does too. This card makes the second door give the verdict the first one gives. ⛔ No new rule, no new refusal code: reuse the build's located message. - Shape, and what it owes:
- The save door now refuses an input it used to accept, so it owes a changeset that says so, and a contract review.
- ⛔ Stored rows are not migrated or rejected on read. A row saved before the gate keeps reading (with the new log line) until it is next edited.
- Stop and report if the claimant finds stored formula fields in this repo's examples or templates that the validator refuses: that is a migration question, not this card's.
- The read-path half is a log line only. ⛔ The value stays
null, because changing the read answer is a protocol change. - QA loop: on landing, the accepting seat appends
api-backend.formula-stdlib-matrixto qa(checklist): park the blocked partial-coverage gaps of the 17.7 pre-release runs (#21720, #21721, #21782, #21784) — each gets a fixture recipe, a pin, a re-pointed clause or a recorded knownGap #22017's retest list.
- Verified at filing:
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 6, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsClaim: PM loop round 50 · 2026-10-06T17:29Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user, asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22019-formula-save-door-validate
Worktree:objectstack-issue-22019
Domain:domain:engine
Seat:domain:engine#1
Provenance:- Triage graded this card
pm:queue(6021332805). It is the only eligiblepm:queuecard in this lane. - The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」), and no dev slot is in use: finding(metadata-protocol): a stored copy of a view container a package ships on another package's object expands under its own name (#21334's arm), so a form withdrawn in that copy does not reach the package's shipped form of that name #21980's PR fix(metadata-protocol)!: a package's stored copy of a container it ships overlays its shipped views, so a withdrawal saved in the copy holds at the anonymous form doors #22023 waits on a confirmation.
File surface (atorigin/main1fb274e61c), per triage's grade 6021332805: - The save door. In
packages/metadata-protocol/src, the object save path runsvalidateExpression('value', …)(packages/formula/src/validate.ts:808, already a dependency ofmetadata-protocol) over each formula field's expression. It refuses what the validator refuses, with the same located messageos buildgives.- The expected landing is the runtime authoring gate (
runtime-authoring-gate.ts), or whereversaveMetaItemjudges an object body. The dev measures where. - ⛔ No new rule, and ⛔ no new refusal code.
- The expected landing is the runtime authoring gate (
- The read path. In
packages/objectql/src/engine.ts'sapplyFormulaPlan(about:2240–:2255) and the planning-stage compile (about:1559), a failed evaluation logs one attributed line per (object, field) under ADR-0032.- ⛔ The value stays
null. Changing the read answer is a protocol change.
- ⛔ The value stays
- ⛔ Stored rows are not migrated or rejected on read.
- Stop condition (triage): if stored formula fields in this repository's examples or templates fail the validator, stop and report. That is a migration question.
- Pins:
- the save door refuses
sqrt(record.amount); - a registered call,
floor(record.amount), still saves; - a row stored before the gate still reads, and the read-path log names it.
- If a door-level pin is needed, one public dogfood case under
packages/qa/dogfood/test/(declared on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024).
- the save door refuses
.changeset/22019-*.md:@objectstack/metadata-protocol(and@objectstack/objectqlfor the log line), stating the refusal.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no (narrowing)- Provisional. The save door refuses a body it accepted, but the refusal is the verdict
os buildand the docs already state (formulas.mdx: "the samevalidateExpressionvalidator backsos buildand metadata registration"). - BREAKING: at least
minor, a!subject, an ADR-0087 marker and a remedy, unless the dev measures that no published grade applies. - Triage says a contract review is owed, and the seat runs it.
- The dev measures the built entry declarations and reports any exported signature that moves.
Thread-read: 6021332805
Serial constraints cleared: at 2026-10-06T17:29Z: - Open PRs (fix(service-messaging, service-storage, service-settings, metadata-protocol): the remaining principal-less producers take the explicit system opt-in #22025, fix(metadata-protocol)!: a package's stored copy of a container it ships overlays its shipped views, so a withdrawal saved in the copy holds at the anonymous form doors #22023, fix(ci): accumulate shard-timings runs until every package has three executions, provisional below that #22022, feat(spec,analytics): a dataset answer's measure column states its aggregate, labelled or not (fields[].aggregate) #22021, fix(spec): EvalUser.isPlatformAdmin is the ADR-0095 D3 PLATFORM_ADMIN standing, not a deprecated alias #22018, fix(deps): take the fixes for sharp and shell-quote that turn main's OSV scan red #22016, chore(objectui): bump the console pin to a58626c88dc8 (carries objectui#11670 and #11669) #22015, chore(pm): delete report-only check-widening-tells.mjs and its wiring (ruling 208) #22002, chore: version packages #21988, feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974), each file list read by
filename:- none touches
objectql/src/engine.ts,packages/formula/srcorformulas.mdx; - only this lane's PR fix(metadata-protocol)!: a package's stored copy of a container it ships overlays its shipped views, so a withdrawal saved in the copy holds at the anonymous form doors #22023 (finding(metadata-protocol): a stored copy of a view container a package ships on another package's object expands under its own name (#21334's arm), so a form withdrawn in that copy does not reach the package's shipped form of that name #21980) touches
protocol.ts, in the view-container expansion and hydration regions, not the object save path.
- none touches
- [finding]
sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 (blocked) and finding(metadata-protocol): a stored copy of a view container a package ships on another package's object expands under its own name (#21334's arm), so a form withdrawn in that copy does not reach the package's shipped form of that name #21980 (awaiting confirmation) share no region with this card.
- Triage graded this card
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22019, "status": "done", "branch": "claude/issue-22019-formula-save-door-validate", "pr": "https://github.com/objectstack-ai/objectstack/pull/22031", "session": "session_017ErfyP2Rx7XWHJA27QjyUi (mode:subagent, the PM's session; identity is the branch). Before any edit: the order file's sha256 prefix a9f0dfe338157d58 was verified, and the newest Claim on #22019 (6021803960) names this branch.", "premise_still_valid": true, "summary": "The object save door now gives the build's formula verdict. The cause was one registry declaration in @objectstack/lint: the build's expression rule validateStackExpressions declared runtimeTypes flow/action/hook and never object, so the runtime authoring gate, which saveMetaItem (publish) and the draft promotion already run, never dispatched it on an object write. The entry now declares object and passes the gate's write type to the rule. On an object write the rule runs the field-formula pass alone: the build's own validateExpression('value', ...) call, with its warnings. Every other object-borne pass is fenced off by name. The door's 422 issue is the build's finding: same rule, where, path, message and hint. The published validateStackExpressions(stack) signature is unchanged. On the read path, applyFormulaPlan now hands a failed evaluation to a sink that the engine binds per object, at find, findOne and the write response. ObjectQL.reportFormulaFault logs one warn per (object, field) per engine instance through the engine's logger. The value stays null, and stored rows are neither migrated nor refused. No source change was needed in packages/metadata-protocol: the landing moved to the producer, @objectstack/lint, as the file-surface clause allows. The reason is in the PR body.", "tests": "All at HEAD fca16e0688. Package tests: @objectstack/lint 120 files, 5638 tests passed. @objectstack/metadata-protocol 219 files passed and 3 skipped, 28049 tests passed and 19 skipped. @objectstack/objectql 379 files, 7513 tests passed. typecheck Done for lint, metadata-protocol and objectql, test-typecheck included. Pins: (a) and (d) are the #22019 block in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the real saveMetaItem and publishMetaItem. (b) is in the same block. (c) is packages/objectql/src/engine-formula-fault-log.test.ts, 6 tests. The lint door and its fence are packages/lint/src/runtime-gate.object-formula-writes.test.ts, 9 tests, plus the object roster pin in runtime-gate.object-writes.test.ts. Reverse verification, from committed HEAD fca16e0688: scripts/ablation-replace.mjs removed 'object' from the entry's runtimeTypes (anchor hit 1 time, 1 to 0, blob 5dd250340913 to d1ae5ae5f889), and @objectstack/lint was rebuilt. ablation-dist-preflight found the marker present in 4 built files on the pristine build and absent from all 14 on the mutated build (--absent --source-marker). Result: metadata-protocol #22019 block 3 failed (a x2, d), 1 passed (b); lint door test 6 failed, 3 passed. Restore: blob 5dd250340913 equals HEAD, git diff HEAD 0 bytes; lint rebuilt, marker present in 4 built files again; block 4 passed, lint 9 passed. Second ablation: the sink call onFault?.(fp.name, r.error) deleted, objectql pin 5 failed / 1 passed (the null-answer case stays green, as designed), restored blob-equal with git diff HEAD empty. H4 corpus measurement: 29 formula fields on 28 objects gave 0 build errors, 0 build warnings, 0 door errors and 0 door advisories, against 1 build error and 1 door error for the sqrt control in the same harness.", "mcp_calls": "0", "api_writes": "3 relay strokes. Each is one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]. (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls: PR #22031, draft, read back 14264 bytes sent = 14264 stored. (2) label-write --assign os-project-manager, POST /repos/objectstack-ai/objectstack/issues/22031/assignees, read back matching. (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/22019/comments. Not REST: 10 git pushes to the branch (the empty-branch probe, the WIP commits, the merge of origin/main, and the final head fca16e0688).", "gates": "dispatch-gates.mjs --commands, re-derived at fca16e0688 with no paths: 68 commands, the same as at 2850ecbec6. These ran together with the artifact-roster block (54) and the four symbol-anchor sweeps (check:adr-anchors, check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors): 125 unique commands, all exit 0 at fca16e0688. dispatch-gates --ran: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. Prerequisites: check:dual-build-cjs-loads and check:published-readme-exports first answered PREREQUISITE NOT MET (exit 3), and were re-run green after a full workspace build. PR-context gates: check-closing-target-claim and check-single-claim-paths were run as reads against PR #22031, both exit 0. check-partof-closing-keyword on the body, check-changeset-no-major --event (level axis: no (narrowing) graded minor) and check-adr-0087-registration also exit 0. Fixed on the way: check:engine-double-contract (a new file's fake engine was not in the ledger) and check:objectql-double-limit (two fake finds did not hold the bound). The door pins moved into the gate test's already-pinned double, and the objectql fake driver applies the bound after its filter, so no ledger was touched. CI: not awaited, in_progress at report time.", "line_budget": "n/a", "deviations": [ "Landing: the save door's half lives in packages/lint (authoring-rules.ts, validate-expressions.ts), not packages/metadata-protocol. The door already runs the registry; the gap was the registry's object declaration. The file-surface clause allows producer-side landing, and the reason is in the PR body.", "Narrowing mechanism: the gate's write type reaches the rule through AuthoringRuleContext.runtimeWriteType. Its doc said only the reference-integrity suite reads it; the doc now names this second reader and the same argument (one entry over several passes).", "The metadata-protocol changeset entry names a package with no source change, as the order asked, because the door users meet is there. The refusal ships from @objectstack/lint.", "The door pins (a), (b), (d) are a new block in the existing protocol.runtime-authoring-gate.test.ts, not a new file, to reuse an already-pinned engine double. A first draft in a new file reddened check:engine-double-contract and check:objectql-double-limit.", "Extra reverse verification for (c), the read-path sink, beyond the order's (a) and (d).", "One errant invocation: pnpm build -- --concurrency=2 forwarded the flag into the cli and client-react build scripts, which failed on argv. Re-run as turbo run build --concurrency=2, all 72 tasks green. No code effect.", "Attribution: commits end with AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The PR body ends with AGENTS.md's session-URL footer, not the harness reminder's lines, by the reminder's own precedence clause." ], "files_changed": [ ".changeset/22019-object-save-door-formula-verdict.md", ".changeset/22019-objectql-formula-fault-log.md", "packages/lint/src/authoring-rules.ts", "packages/lint/src/validate-expressions.ts", "packages/lint/src/runtime-gate.object-formula-writes.test.ts", "packages/lint/src/runtime-gate.object-writes.test.ts", "packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts", "packages/objectql/src/engine.ts", "packages/objectql/src/engine-formula-fault-log.test.ts" ], "clause_2": "no (narrowing), measured. Accept set: an object write in publish mode (PUT /api/v1/meta/object/NAME, saveMetaItem), the draft promotion (publishMetaItem) and publishPackageDrafts used to answer 200 for a formula field whose expression the shared validator refuses. They now answer 422 INVALID_METADATA expression-invalid. Built entry declarations, base vs head, each package rebuilt from base sources and then restored blob-equal: @objectstack/lint has one doc comment added (AuthoringRuleContext.runtimeWriteType) plus chunk-hash renames, and no exported signature moves; @objectstack/objectql has three private member names on ObjectQL (formulaFaultReported, formulaFaultSink, reportFormulaFault) plus chunk-hash renames; @objectstack/metadata-protocol has no source change. Nothing widens. Changeset: minor, BREAKING banner, bang title, remedy, ADR-0087 not-required (no-migration-prescription). check-adr-0087-registration and check-changeset-no-major --event are green. A contract review is owed and the seat runs it.", "hypotheses": { "H1": "Confirmed in part, falsified in part. saveMetaItem judges an object body at assertRuntimeAuthoringRules (packages/metadata-protocol/src/protocol.ts:20410), and the draft promotion does too (:21973). That reaches evaluateRuntimeAuthoringGate (runtime-authoring-gate.ts:897), then runRuntimeAuthoringRules (packages/lint/src/runtime-gate.ts:917), filtered by runtimeAuthoringRulesFor (:550). So save and publish share one gate already. Falsified: the check does not go in the door, which holds no rules by design and whose wiring guard refuses one. The build's entry is the registry rule validateStackExpressions (packages/lint/src/validate-expressions.ts). Its formula call is at :2043 on base (:1957 on head), and the gap was authoring-rules.ts:574 on base, runtimeTypes flow/action/hook. The call is reused through the registry, not copied.", "H2": "The object-borne expression sites the build judges (validate-expressions.ts) are: validations[].condition and .when, with null guards over then/otherwise; fields[].requiredWhen, readonlyWhen, conditionalRequired and visibleWhen, with the root verdict, the parent gate, the requiredWhen null guard and the traversal refusal; fields[].options[].visibleWhen; fields[].expression, with the unprovisioned-anchor warning; and actions[].visible and actions[].disabled. Falsified: default values are not judged by the build. The door mirrors only the formula pass. The rest is the out-of-scope finding.", "H3": "Confirmed. On base, engine.ts:2255 maps a fault to null with no log, and the planning compile at :1562 discards its result (ExpressionEngine.compile never throws; the CEL engine catches). The existing per-key log-once shapes are the module-global warnOnce (validation/record-validator.ts:1755, console.warn, per process) and the engine's per-instance sets (transactionUnsupportedReported :3683, cascadeNotAtomicReported :3692 with warnCascadeNotAtomic :15911, this.logger.warn). Neither is a callable helper. The new report follows the per-instance engine shape: formulaFaultReported (engine.ts:5107 on head) and reportFormulaFault (:5144), once per (object, field) per engine instance, at warn.", "H4": "Confirmed; the stop condition was not met. All 29 stored formula fields on 28 objects were judged by the build pass and by the door function at its snapshot shape: examples 7 on 6 (app-crm 4 on 3, app-showcase 2 on 2, app-todo 1 on 1, app-multi-package 0) and platform display_title formulas 22 on 22. The result was 0 errors and 0 warnings at both. No templates carry formula fields." }, "dogfood_pin": "Not needed. (a) runs the protocol door itself, and the REST mapping of its 422 INVALID_METADATA on PUT /meta/object is already pinned (packages/rest/src/meta-object-owd-gate.test.ts). Nothing to declare to domain:cli.", "docs": "No docs line became false. formulas.mdx's promise (the validator backs os build and metadata registration) now holds for formula fields. An optional addition, not a correction: the Build-time validation section could name the object save door beside registerFlow. If wanted, it is for the seat to declare to domain:devx.", "cleanup": "The branch is pushed at fca16e0688 and nothing is uncommitted. The worktree's node_modules and the worktree itself are removed as the step after this post.", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: through the real saveMetaItem in publish mode, an object with validations[].condition 'sqrt(record.amount) > 1' and a bare-reference requiredWhen 'amount > 1' saved with success, while os build's entry (runAuthoringRules('build')) refused both at error ('found no matching overload for sqrt(dyn)', 'bare reference amount'). · evidence: the object save door still gives no build verdict on validation-rule predicates, the field-rule slots (requiredWhen/readonlyWhen/conditionalRequired/visibleWhen), option visibleWhen, or object action predicates. Contract: formulas.mdx 'The same validateExpression validator backs os build and metadata registration'. Seam: spec:ObjectSchema.validations[].condition / FieldSchema.requiredWhen -> runtime:runtimeAuthoringRulesFor('object') (packages/lint/src/runtime-gate.ts). This PR fences those passes off the object door by name (StackExpressionOptions) and pins the fence, so a crossing is a deliberate edit measured over the stored corpus. · dedupe words: object save door validation rule predicate os build verdict; requiredWhen bare reference saves through meta object; runtime gate object write fenced expression passes", "carrier: none · noted, not filed (PR Acceptance notes). planFormulaProjection's planning ExpressionEngine.compile (engine.ts:1562) is a no-op: it never throws and its result is unread. Its comment and evaluateFormulaField's docblock describe a throw that never happens.", "carrier: none · noted, not filed (PR Acceptance notes). evaluateFormulaField, the exported hook-side helper with no engine, still answers null for a fault without a log line.", "carrier: none · noted, not filed. protocol.runtime-authoring-gate.test.ts:360 says 'ALL SEVEN object-gated rules'; it was nine before this PR and is ten after it." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsLanded: PR #22031 →
f85a83b83bonmain. It merged through the merge queue at 2026-10-06T20:49Z, after entering the queue at 2026-10-06T20:10Z. Verified at 2026-10-06T20:50Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash. It is on
origin/mainas a single-parent commit (parent099a94da4b). Its diffstat is the reviewed one: 9 files, +772/-50. - What is on
main.- The
validateStackExpressionsregistry entry declaresobject. On an object write it runs the build's field-formula pass alone. Every other pass is fenced by name, and the fence is pinned. - A formula field that calls an unregistered function, or reads a field the object has not got, is refused at the object save door with the build's located finding (
422·INVALID_METADATA). Save, draft promotion and package publish share the one gate. - On the read path,
applyFormulaPlanlogs a formula fault through the engine's logger atwarn, once per (object, field) per engine. The value staysnull. - ⛔ Stored rows are not migrated or rejected on read. A row saved before the gate keeps reading
null, now with the log line, until it is next edited.
- The
- The card.
Fixes #22019closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body. - From this release.
@objectstack/metadata-protocolminor, BREAKING,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription): an object save whose formula field the build refuses now answers422where it used to answer200.@objectstack/objectqlpatch: the formula-fault log line.
- QA loop.
api-backend.formula-stdlib-matrixA6 is appended to qa(checklist): park the blocked partial-coverage gaps of the 17.7 pre-release runs (#21720, #21721, #21782, #21784) — each gets a fixture recipe, a pin, a re-pointed clause or a recorded knownGap #22017's retest list in this act, as triage asked (6021332805). - Filed from this card: finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032. The object door still gives no build verdict on validation conditions, the field-rule slots, option
visibleWhenor action predicates.
Generated by Claude Code
- The squash. It is on
- added 2 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a reproducible product defect, class (b): a door that contradicts its own stated contract.
content/docs/data-modeling/formulas.mdxsays "the samevalidateExpressionvalidator backsos buildand metadata registration". At the runtime save door it does not, and the fault that validator exists to prevent reaches users as a silentnull.Reader: triage's first touch (grade and route), then the execution seat that claims it.
QA-source: #21784 · api-backend.formula-stdlib-matrix · acceptance[5]
Filed by the triage seat (objectstack-wide, seat post #6015,
session_01AavokzJ5DndAwitDXvKy4U). It is theExtracted:disposition for the last fail on qa-run #21784 without a card. The record called it "known expected-fail #3306". But #3306 closedcompletedon 2026-07-20 with a different fix (floor/ceilregistered, and date arithmetic made a build error), so the row had no live carrier. ⛔ Not graded here. ⛔ Not a claim.Dedupe: MCP
search_issueson objectstack, open and closed, for "formula calling an unregistered function saves and reads null silently, no authoring-time refusal": 0 hits. The nearest closed card is #3306, a different mechanism (above).What happens
From the record, confirmed by verifier VF4 at
316be321:PUT /api/v1/meta/object/fx_sqrt?package=…, with a formula fieldsqrt(record.amount)→ 200.POST /api/v1/data/fx_sqrt {amount: 16}→ the formula field reads null. Nothing is logged.sqrtis not one of the 27 registered stdlib functions.os buildrefuses the same expression: "Unknown function", severity error (formulas.mdxabout:228).Where (read on
mainat this filing)packages/objectql/src/engine.ts:2255, inapplyFormulaPlan:rec[fp.name] = r.ok ? … : nulldrops the fault with no log line;ExpressionEngine.compile(expr)(about:1562, "to surface syntax errors at planning stage") discards its result;formulas.mdx(about:640) states ADR-0032's rule for call sites: "must not silently swallow that".validateExpressioncall reaches an object's formula fields on the metadata save path.git grep -n validateExpression -- packages/metadata-protocol/src packages/objectql/srcfinds no call site.registerFlowinservice-automationdoes call it. That is the "same verdict" the docs promise for metadata registration.Done when
validateExpression('value', …)refuses, with the same located messageos buildgives. This is the docs' existing promise, ⛔ not a new rule: the build's verdict, at a second door.sqrt(record.amount);floor(record.amount)) still saves;api-backend.formula-stdlib-matrixA6 then scores the new behaviour, as its own step says: "if an authoring-time gate refuses the formula before it is stored, record THAT as the (better) behavior". The seat that accepts this card appends the item to the wave anchor's retest list, qa(checklist): park the blocked partial-coverage gaps of the 17.7 pre-release runs (#21720, #21721, #21782, #21784) — each gets a fixture recipe, a pin, a re-pointed clause or a recorded knownGap #22017 (§5, trigger 2).