|
1810 | 1810 | }, |
1811 | 1811 | { |
1812 | 1812 | "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", |
1814 | 1814 | "since": "v16", |
1815 | 1815 | "status": "active", |
1816 | | - "revision": 1, |
| 1816 | + "revision": 2, |
1817 | 1817 | "priority": "P1", |
1818 | 1818 | "surface": "mixed", |
1819 | 1819 | "personas": [ |
|
1827 | 1827 | "runtime metadata authoring (PUT /meta/object on a scratch object) for the stdlib-through-the-live-door leg" |
1828 | 1828 | ], |
1829 | 1829 | "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." |
1831 | 1831 | ] |
1832 | 1832 | }, |
1833 | 1833 | "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", |
1835 | 1835 | "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", |
1837 | 1837 | "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", |
1838 | 1838 | "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", |
1839 | 1839 | "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)" |
1841 | 1841 | ], |
1842 | 1842 | "acceptance": [ |
1843 | 1843 | { |
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)", |
1845 | 1845 | "oracle": "test", |
1846 | 1846 | "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" |
1848 | 1848 | }, |
1849 | 1849 | { |
1850 | 1850 | "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 | 1871 | "evidence": "the responses vs the expectation" |
1872 | 1872 | }, |
1873 | 1873 | { |
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)", |
1875 | 1875 | "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" |
1878 | 1878 | } |
1879 | 1879 | ], |
1880 | 1880 | "negative": [ |
1881 | 1881 | "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)", |
1882 | 1882 | "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" |
1884 | 1884 | ], |
1885 | 1885 | "traps": [ |
1886 | 1886 | "stale-dist", |
1887 | 1887 | "clock-skew", |
1888 | 1888 | "timezone-boundary" |
1889 | 1889 | ], |
1890 | 1890 | "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)", |
1892 | 1894 | "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)", |
1893 | 1895 | "content/docs/data-modeling/formulas.mdx (the advertised catalog the matrix cross-checks)", |
1894 | 1896 | "examples/app-showcase/src/data/objects/field-zoo.object.ts + project.object.ts (the stock live-leg formulas)", |
|
1902 | 1904 | "date": "2026-08-30", |
1903 | 1905 | "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", |
1904 | 1906 | "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" |
1905 | 1913 | } |
1906 | 1914 | ] |
1907 | 1915 | }, |
|
0 commit comments