Skip to content

Commit c84cc23

Browse files
committed
docs(qa): formula-stdlib-matrix counts 29 functions and asserts the authoring door's unknown-function refusal
The registration chain gained isoDate and isoDatetime; the joinNonEmpty row goes through record fields because cel-js refuses the mixed list literal; the EXPECTED-FAIL clause flips to the 422 INVALID_METADATA refusal the object save door now gives a formula calling an unregistered function. Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC Co-authored-by: Claude <noreply@anthropic.com>
1 parent b6fbd0a commit c84cc23

1 file changed

Lines changed: 21 additions & 13 deletions

File tree

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,10 +1810,10 @@
18101810
},
18111811
{
18121812
"id": "api-backend.formula-stdlib-matrix",
1813-
"title": "CEL formula stdlib: all 27 registered functions give known answers against the BUILT package — calendar-day timezone contract, addMonths month-end clamp, floor(-1.2)==-2, null-dropping joins, mixed int/double arithmetic — and the unregistered-function silent-null fault is RECORDED, never ticked",
1813+
"title": "CEL formula stdlib: all 29 registered functions give known answers against the BUILT package — calendar-day timezone contract, addMonths month-end clamp, floor(-1.2)==-2, null-dropping joins, mixed int/double arithmetic, isoDate/isoDatetime text — and a formula calling an unregistered function is REFUSED at the authoring door (422), never stored to read null",
18141814
"since": "v16",
18151815
"status": "active",
1816-
"revision": 1,
1816+
"revision": 2,
18171817
"priority": "P1",
18181818
"surface": "mixed",
18191819
"personas": [
@@ -1827,24 +1827,24 @@
18271827
"runtime metadata authoring (PUT /meta/object on a scratch object) for the stdlib-through-the-live-door leg"
18281828
],
18291829
"knownGaps": [
1830-
"The stock formulas exercise only null-guard ternaries and arithmetic — NOT the stdlib catalog. The 27-function matrix therefore runs in a harness against the BUILT package (the same discipline as api-backend.formula-gates), and the live-door leg proves the same evaluator serves reads, not each function individually; a scratch formula field authored through the metadata channel carries two representative stdlib calls onto the live path. Record which oracle each function's verdict rests on."
1830+
"The stock formulas exercise only null-guard ternaries and arithmetic — NOT the stdlib catalog. The 29-function matrix therefore runs in a harness against the BUILT package (the same discipline as api-backend.formula-gates), and the live-door leg proves the same evaluator serves reads, not each function individually; a scratch formula field authored through the metadata channel carries two representative stdlib calls onto the live path. Record which oracle each function's verdict rests on."
18311831
]
18321832
},
18331833
"steps": [
1834-
"count the registration chain: packages/formula/src/stdlib.ts registerStdLib registers exactly 27 functions by registerFunction call — now, today, daysFromNow, daysAgo, isBlank, coalesce, trim, joinNonEmpty, daysBetween, addDays, addMonths, date, datetime, abs, round, floor, ceil, min, max, upper, lower, contains, startsWith, endsWith, matches, len, isEmpty. NOT enumSource-pinnable (a builder chain, not a named z.enum) — re-count the calls in source at run time and fail the matrix stale if the count moved; cross-check the docs catalog (content/docs/data-modeling/formulas.mdx) advertises the same set, no more, no fewer",
1834+
"count the registration chain: packages/formula/src/stdlib.ts registerStdLib registers exactly 29 bare-callable functions by chained registerFunction call — now, today, daysFromNow, daysAgo, isBlank, coalesce, trim, joinNonEmpty, daysBetween, addDays, addMonths, date, datetime, isoDate, isoDatetime, abs, round, floor, ceil, min, max, upper, lower, contains, startsWith, endsWith, matches, len, isEmpty (isoDate and isoDatetime since #22347). The receiver method `can` (current_user.can(object, verb)), which registerPermissionPredicate adds at the head of registerStdLib, is the permission predicate, not a bare-callable catalog function, and is outside this count — validate.ts's CEL_STDLIB_FUNCTIONS docblock tallies the same 29. NOT enumSource-pinnable (a builder chain, not a named z.enum) — re-count the calls in source at run time and fail the matrix stale if the count moved; cross-check the docs catalog (content/docs/data-modeling/formulas.mdx) advertises the same set, no more, no fewer",
18351835
"build the workspace (or confirm dist freshness — the stale-dist trap is the known false-negative here, same as api-backend.formula-gates); the harness must import the BUILT package and record the resolved module path",
1836-
"drive every one of the 27 with a pinned now and a known-answer expectation, at minimum: floor(-1.2)==-2 and ceil(-1.2)==-1 (round toward −∞/+∞, NOT toward zero); addMonths(date('2026-01-31'), 1) == 2026-02-28T00:00:00Z (month-end clamp, never a March overflow); daysBetween(a,b) sign convention (b−a, negative when b earlier); date()/datetime() alias parity on the same ISO input; coalesce(null,'x')=='x' but coalesce('', 'x')=='' (null/undefined only); joinNonEmpty(['a', null, ' ', 'b'], '-')=='a-b' (null AND blank-after-trim dropped); isBlank vs isEmpty on null/''/[]; trim(null)==''; upper/lower/contains/startsWith/endsWith on mixed input; matches regex truthiness; len over string/list/map; min/max returning the SMALLER/LARGER OPERAND VERBATIM (type preserved, numeric comparison); abs/round; now() === the pinned instant",
1836+
"drive every one of the 29 with a pinned now and a known-answer expectation, at minimum: floor(-1.2)==-2 and ceil(-1.2)==-1 (round toward −∞/+∞, NOT toward zero); addMonths(date('2026-01-31'), 1) == 2026-02-28T00:00:00Z (month-end clamp, never a March overflow); daysBetween(a,b) sign convention (b−a, negative when b earlier); date()/datetime() alias parity on the same ISO input; isoDate(date('2026-02-28T13:45:00Z'))=='2026-02-28' and isoDatetime(date('2026-02-28T13:45:00Z'))=='2026-02-28T13:45:00.000Z' (the UTC calendar), isoDate(today()) under the boundary zone below == that zone's calendar day, and text refused (isoDate('2026-01-01') faults no matching overload — write isoDate(date(s))); coalesce(null,'x')=='x' but coalesce('', 'x')=='' (null/undefined only); joinNonEmpty([record.a, record.b, record.c, record.d], '-')=='a-b' with record {a: 'a', b: null, c: ' ', d: 'b'} (null AND blank-after-trim dropped — written through record fields, because cel-js refuses a list LITERAL that mixes string and null at type-check, 'List elements must have the same type', so the literal ['a', null, ' ', 'b'] never evaluates); isBlank vs isEmpty on null/''/[]; trim(null)==''; upper/lower/contains/startsWith/endsWith on mixed input; matches regex truthiness; len over string/list/map; min/max returning the SMALLER/LARGER OPERAND VERBATIM (type preserved, numeric comparison); abs/round; now() === the pinned instant",
18371837
"calendar-day timezone contract (ADR-0053 Phase 2 D1): pin now to an instant whose calendar day DIFFERS between UTC and the reference zone (e.g. 2026-08-30T22:00:00Z with timezone Asia/Shanghai — locally already 08-31) and assert today() == 2026-08-31T00:00:00Z (the reference-tz calendar day expressed as UTC MIDNIGHT — never an instant carrying wall-clock time), daysFromNow/daysAgo offset from that same day; an unknown zone falls back to the UTC calendar day, never throws",
18381838
"mixed-arithmetic overloads (registerNumericCoercions, stdlib.ts): a double-typed record field divided/multiplied by an int literal (record.amount / 100) evaluates to the promoted double instead of faulting 'no such overload' to silent null (#1928), while pure int 7/2 == 3 keeps integer-division semantics untouched",
18391839
"live-door parity: GET a showcase_project row and confirm budget_remaining materializes (read-path evaluation); author a scratch object through the metadata channel whose formula field composes two stdlib calls (e.g. joinNonEmpty + upper) and read back the known answer — the stdlib reaching the LIVE read path, not just the harness",
1840-
"the expected-fail probe: author (or evaluate in-harness) a formula calling an UNregistered function (e.g. sqrt(4)); capture what actually happens at each door driven. At head the runtime contract is: a formula that compiles but does not EVALUATE yields null — applyFormulaPlan's own `r.ok ? … : null` (packages/objectql/src/engine.ts) — so the read answers 200 with the field null and no error surfaced (#3306 class; stdlib.ts documents this exact failure mode as the reason floor/ceil got registered). Record the observed shape verbatim; if an authoring-time gate refuses the formula before it is stored, record THAT as the (better) behavior instead"
1840+
"the unregistered-function probe: on a scratch object, PUT /api/v1/meta/object/NAME at the ACTIVE door (no ?mode=draft) with a formula field calling an UNregistered function (e.g. bad_f = sqrt(4)). Capture the 422 INVALID_METADATA and its expression-invalid issue, then GET the object and confirm the field was not stored. A ?mode=draft save of the same body is admitted (draft saves defer the gate to promotion, #4463 D1), and its draft→active promotion is refused the same way. What the door cannot see is unchanged underneath: a formula that compiles but faults at EVALUATE time, or a row stored before the gate, still reads null (applyFormulaPlan's `r.ok ? … : null`, packages/objectql/src/engine.ts), now with one warn line per (object, field) (#22031)"
18411841
],
18421842
"acceptance": [
18431843
{
1844-
"clause": "all 27 advertised functions evaluate to their known answers against the BUILT package — the matrix is exhaustive over the hand-counted registration chain, and the docs catalog matches it in both directions (an advertised-but-unregistered function or a registered-but-undocumented one each fail this clause)",
1844+
"clause": "all 29 advertised functions evaluate to their known answers against the BUILT package — the matrix is exhaustive over the hand-counted registration chain (registerStdLib's 29 bare-callable registerFunction calls; the receiver method `can` is the permission predicate, not a catalog function), and the docs catalog matches it in both directions (an advertised-but-unregistered function or a registered-but-undocumented one each fail this clause)",
18451845
"oracle": "test",
18461846
"verify": "harness output: one known-answer row per function, resolved module path pointing into dist/node_modules; the source count and the docs-table cross-check recorded",
1847-
"evidence": "the 27-row harness table + the resolved path + the two counts"
1847+
"evidence": "the 29-row harness table + the resolved path + the two counts"
18481848
},
18491849
{
18501850
"clause": "the calendar-day trio (today/daysFromNow/daysAgo) resolves to the REFERENCE-TIMEZONE calendar day expressed as UTC midnight (ADR-0053 D1) — proven with an instant whose calendar day differs between UTC and the zone, so a UTC-only implementation cannot pass by accident; unknown zone falls back to UTC without throwing",
@@ -1871,24 +1871,26 @@
18711871
"evidence": "the responses vs the expectation"
18721872
},
18731873
{
1874-
"clause": "EXPECTED-FAIL — the unregistered-function fault is silent-null at head, and this clause exists to RECORD that, not to bless it: the probe's actual behavior (200, field null, no surfaced error — applyFormulaPlan's r.ok?…:null; or a louder authoring-time refusal if one has since landed) is captured verbatim. A run must NOT tick this clause green: silent-null is the standing #3306-class hazard, and the clause flips to a positive assertion only when the platform grows a loud refusal",
1874+
"clause": "a formula calling an UNREGISTERED function is refused at the authoring door, before it is stored: an active-state object save (PUT /api/v1/meta/object/NAME, no ?mode=draft) whose formula field calls sqrt(...) answers 422 INVALID_METADATA carrying the build's located finding (rule expression-invalid, at that field's expression, naming `sqrt` as a NAME fault), and nothing lands; a ?mode=draft save is admitted (#4463 D1) and its promotion is refused the same way. This is the loud refusal the earlier EXPECTED-FAIL wording waited for (#22031, fixing #22019)",
18751875
"oracle": "api",
1876-
"verify": "the probe's captured behavior, compared against the engine.ts contract; any run recording 'pass' here without a platform change is a false green",
1877-
"evidence": "the captured probe trace + the run-record note naming which behavior was observed"
1876+
"verify": "the PUT answers 422 with code INVALID_METADATA and an issue whose rule is expression-invalid, located at the field's expression and naming the function; a follow-up GET /api/v1/meta/object/NAME shows the field was not stored. Pinned through the real saveMetaItem / publishMetaItem by packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (the 'runtime authoring gate on OBJECT writes — the formula field verdict (#22019)' block: sqrt(record.amount) refused 422 on an active save and on a draft's promotion, nothing lands, floor(record.amount) still saves), and at the rule layer by packages/lint/src/runtime-gate.object-formula-writes.test.ts. A row stored BEFORE the gate still reads null on the read path, now with one warn line per (object, field) (packages/objectql/src/engine-formula-fault-log.test.ts). That is the residual, not this clause",
1877+
"evidence": "the 422 response (code + the expression-invalid issue) + the post-refusal metadata read"
18781878
}
18791879
],
18801880
"negative": [
18811881
"a harness that silently resolved src/ instead of dist is not-run, not pass — the resolved module path is part of the evidence (the api-backend.formula-gates discipline)",
18821882
"today() returning an instant with wall-clock time (or the UTC calendar day under a non-UTC zone) breaks the one representation `record.date == today()` equality depends on — the exact drift ADR-0053 D1 exists to prevent",
1883-
"the silent-null probe ticked as a pass is a false green (the clause is expected-fail); equally, filing it as a NEW defect is wrong — it is a known standing class (#3306), recorded on the item"
1883+
"a formula calling an unregistered function that SAVES 2xx at the active door, and then reads null, is a FAIL: it is the #3306-class silent null this door now refuses (#22019); extract it. A row stored before the gate reading null, with its warn line, is the documented residual and not a new defect"
18841884
],
18851885
"traps": [
18861886
"stale-dist",
18871887
"clock-skew",
18881888
"timezone-boundary"
18891889
],
18901890
"source": [
1891-
"packages/formula/src/stdlib.ts#registerStdLib (registerStdLib — the 27 registerFunction calls; calendarDayUtc ADR-0053 D1; addMonthsUtc clamp; the floor/ceil #3306 note), (registerNumericCoercions #1928)",
1891+
"packages/formula/src/stdlib.ts#registerStdLib (registerStdLib — the 29 chained registerFunction calls, isoDate / isoDatetime since #22347; the receiver method `can`, from registerPermissionPredicate, is outside the count; calendarDayUtc ADR-0053 D1; addMonthsUtc clamp; the floor/ceil #3306 note), (registerNumericCoercions #1928)",
1892+
"packages/formula/src/validate.ts#CEL_STDLIB_FUNCTIONS (the advertised bare-callable list; its docblock tallies the 29 registerStdLib adds and why a receiver-only name such as `can` is never in it)",
1893+
"packages/lint/src/authoring-rules.ts#EXPRESSION_INVALID (the expression-invalid rule the object save door now dispatches on a formula field, #22031) + packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (the #22019 block: sqrt(record.amount) refused 422 INVALID_METADATA on an active save and on a draft's promotion, nothing lands) + packages/lint/src/runtime-gate.object-formula-writes.test.ts + packages/objectql/src/engine-formula-fault-log.test.ts (the read-path residual: a pre-gate row still reads null, one warn line per object and field)",
18921894
"packages/objectql/src/engine.ts#applyFormulaPlan (applyFormulaPlan — evaluate-fault → null, the silent-null contract; the evaluateFormulaField docblock states the compile-throws vs evaluate-nulls split)",
18931895
"content/docs/data-modeling/formulas.mdx (the advertised catalog the matrix cross-checks)",
18941896
"examples/app-showcase/src/data/objects/field-zoo.object.ts + project.object.ts (the stock live-leg formulas)",
@@ -1902,6 +1904,12 @@
19021904
"date": "2026-08-30",
19031905
"change": "new — the stdlib catalog (27 registered CEL functions) had no matrix: api-backend.formula-gates proves three runtime shapes and the build gate, and nothing asserted the advertised function set evaluates to its documented answers. Count verified by reading stdlib.ts (exactly 27 registerFunction calls — the register's '~27' resolved); the docs table (formulas.mdx) groups them into 18 rows covering the same 27. Not enum-pinnable (a registration chain, not a z.enum) — the count pin is a step-1 source re-count, stated on the item per the enumSource rules. The unregistered-function negative is authored as an expected-fail probe: engine.ts's applyFormulaPlan demonstrably nulls evaluate-faults, so a run records that behavior and must not tick it",
19041906
"ref": "#sweep-2026-08-30"
1907+
},
1908+
{
1909+
"revision": 2,
1910+
"date": "2026-10-13",
1911+
"change": "clause 1 (acceptance[0], #23104 C1) and clause 6 (acceptance[5], #23104 C6) revised, both stale spec. C1: registerStdLib now chains 29 bare-callable registerFunction calls, the 27 plus isoDate and isoDatetime (345d3f3d8, #22347), and formulas.mdx lists the same 29 in 19 rows. The title, step 1's count and list, step 3, the 27-function knownGap, clause 1's text and evidence, and the registerStdLib source note now say 29. Step 3 gains the isoDate / isoDatetime known answers. Its joinNonEmpty row was an assertion defect inside the item: cel-js refuses the literal ['a', null, ' ', 'b'] at type-check ('List elements must have the same type'), so the row now feeds the same values through record fields and expects 'a-b'. The receiver method `can` (registerPermissionPredicate, #18781) is registered at the head of registerStdLib but is not a bare-callable catalog function; step 1 and clause 1 now name it as outside the count, as validate.ts's CEL_STDLIB_FUNCTIONS docblock already does. C6: the EXPECTED-FAIL clause anticipated a louder authoring-time refusal, and that refusal has landed (f85a83b83, #22031, fixing #22019). The object save door dispatches the build's expression rule, so a formula calling sqrt(...) is refused 422 INVALID_METADATA (rule expression-invalid, a NAME fault) at the active door and on a draft's promotion, and nothing is stored. Clause 6, step 7, negative 3 and the title now assert that refusal. They cite protocol.runtime-authoring-gate.test.ts's #22019 block and the lint pin, and name the read-path residual (a pre-gate row still reads null, now with a warn line). Measured against the built packages/formula/dist at f931e765b: the literal joinNonEmpty row refused, the record-field row 'a-b', isoDate / isoDatetime as stated, sqrt(4) refused no matching overload",
1912+
"ref": "#23121"
19051913
}
19061914
]
19071915
},

0 commit comments

Comments
 (0)