Skip to content

Commit 2539f4a

Browse files
claude[bot]claude
andauthored
fix(qa): give the expression ledger a fail-policy for "nothing evaluates this" (#16660)
`FAIL_POLICIES` had four members and all four describe what an EVALUATOR does when the expression is bad. Five rows classify slots that have no evaluator at all, so each had to borrow a member claiming something stronger: four carried `compile-error` (the Zod parse is the refusal — true, and a property every row in the ledger shares) and `cel-advanced-policy` carried `fail-closed`, a RUNTIME refusal, on a slot whose own `enforcement` cell reads `(no runtime consumer yet)`. On a security-flavoured row that reads as a security guarantee. Adds `unevaluated` and re-states all five rows with it. Every other row's `failPolicy` was re-read against the new member and left where it was. Minting a word that could be borrowed as loosely would reproduce the defect one member wider, so the member arrives with a pin. "Non-empty runtime enforcement" cannot be checked as `enforcement !== ''` — `ExprSurface` makes the cell required, so every row has one. The checkable question is what the cell SAYS: an `unevaluated` row must state the absence it claims and must not name a runtime evaluator site, and it cannot be `state: 'enforced'`. The detector carries a positive control against the COMPILE rows another pin already requires to name the canonical compiler, so an emptied regex reds instead of passing vacuously. Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ Co-authored-by: Claude <noreply@anthropic.com>
1 parent cc238db commit 2539f4a

2 files changed

Lines changed: 119 additions & 10 deletions

File tree

packages/qa/dogfood/test/expression-conformance.ledger.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,35 @@ export type ExprMode = 'compile' | 'interpret';
5656
*/
5757
export type ExprDialect = 'cel' | 'cron' | 'template' | 'js' | 'settings-visibility';
5858
export type ExprState = 'enforced' | 'experimental' | 'removed';
59-
/** ADR-0058 D5 fail-policy tiers. */
60-
export type FailPolicy = 'compile-error' | 'fail-closed' | 'fail-soft-log' | 'throw';
59+
/**
60+
* ADR-0058 D5 fail-policy tiers, plus the one state D5 has no tier for.
61+
*
62+
* The four D5 tiers each describe what an EVALUATOR does when the expression is
63+
* bad. `unevaluated` is the fifth member because some slots have no evaluator at
64+
* all: nothing reads the value, so the honest answer to "what happens when the
65+
* expression is bad" is "nothing happens", and every one of the four would be a
66+
* stronger claim than the row can support.
67+
*
68+
* That is not a hypothetical shortfall — it is what the rows carrying it were
69+
* doing before this member existed. `compile-error` claimed the Zod parse was
70+
* the refusal (true as far as it goes, and weaker than it reads: the parse
71+
* judges the value's SHAPE and never its grammar, so it is a refusal every row
72+
* in this ledger shares), and `fail-closed` claimed a RUNTIME refusal on a slot
73+
* whose own `enforcement` cell said `(no runtime consumer yet)`. On a
74+
* security-flavoured row that second one reads as a security guarantee, which
75+
* is the borrowing with a consequence past legibility.
76+
*
77+
* ⛔ `unevaluated` is NOT a synonym for `experimental`, and NOT a tier for "not
78+
* measured here". `cel-inline-grid-cell` is `experimental` and stays
79+
* `fail-soft-log`: its enforcement cell names an evaluator it could not reach
80+
* (the objectui renderer, outside this checkout) and reports a MEASURED write
81+
* path on which nothing refuses. An absence this checkout cannot establish is
82+
* not an absence this ledger may assert — that is the invented cell the ledger
83+
* exists to prevent, in the other direction. The companion test pins the
84+
* distinction so the new word cannot be borrowed the way the old ones were:
85+
* see `NAMES_RUNTIME_EVALUATOR` / `DECLARES_NO_EVALUATOR` there.
86+
*/
87+
export type FailPolicy = 'compile-error' | 'fail-closed' | 'fail-soft-log' | 'throw' | 'unevaluated';
6188

6289
export interface ExprSurface extends ConformanceRow {
6390
dialect: ExprDialect;
@@ -346,7 +373,7 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
346373
// The key and its documented hand-off arrived with #14825.
347374
id: 'cron-knowledge-refresh',
348375
summary: 'knowledge-source periodic reindex cron (KnowledgeRefreshPolicy.cron) — surfaced, deliberately not scheduled',
349-
dialect: 'cron', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
376+
dialect: 'cron', mode: 'interpret', state: 'experimental', failPolicy: 'unevaluated',
350377
enforcement:
351378
'PARSE ONLY — `CronExpressionInputSchema` refuses a blank/non-string, non-envelope value and normalizes to `{dialect:"cron",source}`; nothing evaluates the result. service-knowledge/knowledge-service.ts reads `refresh.onRecordChange` and NEVER `refresh.cron` (measured: the only `refresh` reads in that package are the two `onRecordChange` sites)',
352379
covers: ['ai/knowledge-source.zod.ts:KnowledgeRefreshPolicySchema.cron'],
@@ -357,7 +384,7 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
357384
// and #15028 (the envelope arm now pins the dialect — the note's last sentence).
358385
id: 'cron-declared-unwired',
359386
summary: 'cron slots on subsystems that were declared but never built — export schedules, flow schedule state, connector sync, cache warmup, DR backup/test',
360-
dialect: 'cron', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
387+
dialect: 'cron', mode: 'interpret', state: 'experimental', failPolicy: 'unevaluated',
361388
enforcement:
362389
'PARSE ONLY — `CronExpressionInputSchema` refuses a blank/non-string, non-envelope value and normalizes to the envelope; NO EVALUATOR FOUND for any of these five keys. Reader hunt, per key, walking out from each declaration (2026-09-04, `61821e54cf5`): `api/export.zod.ts:cronExpression` — the whole `ExportJobApiContracts` family has zero consumers and rest-server serves no `/api/v1/data/export` route, so `POST /api/v1/data/export/schedules` is a declared contract nothing implements; `IExportService` has no provider binding, which its own source already records. `automation/execution.zod.ts:cronExpression` — `ScheduleStateSchema` has no consumer outside packages/spec; the schedule TRIGGER that does work reads a flow start node `config.schedule` through trigger-schedule/schedule-trigger.ts `normalizeSchedule`, a different shape this key never reaches. `integration/connector.zod.ts:schedule` — `syncConfig` has no reader outside packages/spec. `system/cache.zod.ts:schedule` (CacheWarmup) and `system/disaster-recovery.zod.ts:schedule` (BackupConfig + the DR `testing` block) — neither schema has any consumer outside packages/spec',
363390
covers: [
@@ -367,15 +394,15 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
367394
'system/cache.zod.ts:CacheWarmupSchema.schedule',
368395
'system/disaster-recovery.zod.ts:BackupConfigSchema.schedule', 'system/disaster-recovery.zod.ts:DisasterRecoveryPlanSchema.schedule',
369396
],
370-
note: 'EXPERIMENTAL — five declared cron slots with no runtime evaluator (ADR-0049 enforce-or-remove candidates; each wants its own look, and the card that surfaced them says so rather than proposing a sweep). ⚠️ TWO of these surfaces are declared TWICE: `api/export.zod.ts` `cronExpression` on `ScheduledExportSchema` and on `ScheduleExportRequestSchema`, and `system/disaster-recovery.zod.ts` `schedule` on `BackupConfigSchema` and on `DisasterRecoveryPlanSchema` (the DR `testing` block). Both pairs are genuinely the same surface twice, so one row is honest here — and now that each declaring position carries its OWN key, that judgement is written out as two `covers` entries instead of being assumed by a collapse. ⚠️ The `failPolicy` on this row is `compile-error` because the PARSE is the only thing that ever refuses one of these values; it is not a claim that cron SYNTAX is checked. It is not: `@objectstack/formula` cronEngine validates 5/6-field patterns and `@` aliases, and has ZERO consumers outside packages/formula — nothing routes these slots through it. The parse now DOES pin these slots to the cron dialect (the sibling finding on the dialect union is closed): the envelope arm of `CronExpressionInputSchema` accepts a `cron` envelope only and its bare-string arm refuses a blank string, each with one issue at the slot naming the fix — and it still judges no cron syntax, by position: no grammar is restated in spec; `croner` judges the pattern where a schedule is wired (`cron-job-schedule`).',
397+
note: 'EXPERIMENTAL — five declared cron slots with no runtime evaluator (ADR-0049 enforce-or-remove candidates; each wants its own look, and the card that surfaced them says so rather than proposing a sweep). ⚠️ TWO of these surfaces are declared TWICE: `api/export.zod.ts` `cronExpression` on `ScheduledExportSchema` and on `ScheduleExportRequestSchema`, and `system/disaster-recovery.zod.ts` `schedule` on `BackupConfigSchema` and on `DisasterRecoveryPlanSchema` (the DR `testing` block). Both pairs are genuinely the same surface twice, so one row is honest here — and now that each declaring position carries its OWN key, that judgement is written out as two `covers` entries instead of being assumed by a collapse. ⚠️ The `failPolicy` on this row is `unevaluated`. It read `compile-error` until the vocabulary gained a member for "nothing evaluates this slot", and that value was the closest available rather than a true one: the PARSE is the only thing that ever refuses one of these values, which is a property every row in this ledger shares and says nothing about this one. It was never a claim that cron SYNTAX is checked. It is not: `@objectstack/formula` cronEngine validates 5/6-field patterns and `@` aliases, and has ZERO consumers outside packages/formula — nothing routes these slots through it. The parse now DOES pin these slots to the cron dialect (the sibling finding on the dialect union is closed): the envelope arm of `CronExpressionInputSchema` accepts a `cron` envelope only and its bare-string arm refuses a blank string, each with one issue at the slot naming the fix — and it still judges no cron syntax, by position: no grammar is restated in spec; `croner` judges the pattern where a schedule is wired (`cron-job-schedule`).',
371398
},
372399

373400
// ── TEMPLATE dialect (#15027) ─────────────────────────────────────────────
374401
{
375402
// The apparent owner was #14797 (closed completed), delivered by PR #14819.
376403
id: 'template-prompt',
377404
summary: 'AI prompt-template system/user prompts (PromptTemplate.system, .user) — `{{var}}` interpolation',
378-
dialect: 'template', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
405+
dialect: 'template', mode: 'interpret', state: 'experimental', failPolicy: 'unevaluated',
379406
enforcement:
380407
'PARSE ONLY — `TemplateExpressionInputSchema` refuses a blank/non-string, non-envelope value and normalizes to `{dialect:"template",source}`; NO EVALUATOR FOUND. `PromptTemplateSchema` has no consumer outside packages/spec (measured 2026-09-04), so nothing interpolates the `{{var}}` holes and nothing checks that the declared `variables` match them',
381408
covers: [
@@ -389,7 +416,7 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
389416
{
390417
id: 'template-title-format',
391418
summary: 'object record-title template (Object.titleFormat, deprecated → nameField per ADR-0079)',
392-
dialect: 'template', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
419+
dialect: 'template', mode: 'interpret', state: 'experimental', failPolicy: 'unevaluated',
393420
enforcement:
394421
'PARSE ONLY in this repo — `TemplateExpressionInputSchema` refuses a blank/non-string, non-envelope value. The KEY has a build-time reader that is NOT an evaluator: lint/validate-record-title.ts `validateRecordTitle` (wired into authoring-rules.ts, run by `os build` / `os lint` / the MCP authoring surface) reports every declaration as `title-format-retired`, an advisory WARNING steering the author to `nameField` — it reads that the key is present and never looks at the template text. The server-side title resolver deliberately does NOT read it: spec/src/data/display-name.ts `objectTitleCompleteness` / `resolveRecordDisplayName` resolve `nameField` then the `displayNameField` alias then a derivation, and ADR-0079 states the reason (render-only; the server can neither return nor query it)',
395422
covers: ['data/object.zod.ts:ObjectSchemaBase.titleFormat'],
@@ -398,12 +425,12 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
398425
{
399426
id: 'cel-advanced-policy',
400427
summary: 'advanced security / versioning policy conditions',
401-
dialect: 'cel', mode: 'interpret', state: 'experimental', failPolicy: 'fail-closed',
428+
dialect: 'cel', mode: 'interpret', state: 'experimental', failPolicy: 'unevaluated',
402429
enforcement: '(no runtime consumer yet)',
403430
covers: [
404431
'kernel/plugin-security-advanced.zod.ts:PluginPermissionSchema.condition',
405432
'kernel/plugin-versioning.zod.ts:MultiVersionSupportSchema.condition',
406433
],
407-
note: 'EXPERIMENTAL — declared policy conditions with no runtime evaluator yet (ADR-0056 D8 / ADR-0049 tracking).',
434+
note: 'EXPERIMENTAL — declared policy conditions with no runtime evaluator yet (ADR-0056 D8 / ADR-0049 tracking). This row is why `unevaluated` was minted: it carried `fail-closed` — a RUNTIME refusal — while its own `enforcement` cell said `(no runtime consumer yet)`, so on a security-flavoured row the ledger read as a security guarantee over a slot nothing evaluates. `fail-closed` here was the one borrowing with a consequence past legibility, and spreading it to the other four unwired rows was refused for that reason.',
408435
},
409436
];

packages/qa/dogfood/test/expression-conformance.test.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,35 @@ const REPO_ROOT = join(HERE, '../../../..');
2424
const SPEC_SRC = join(REPO_ROOT, 'packages/spec/src');
2525

2626
const MODES = new Set(['compile', 'interpret']);
27-
const FAIL_POLICIES = new Set(['compile-error', 'fail-closed', 'fail-soft-log', 'throw']);
27+
const FAIL_POLICIES = new Set(['compile-error', 'fail-closed', 'fail-soft-log', 'throw', 'unevaluated']);
28+
29+
/**
30+
* Runtime evaluator/compiler SITES, as this ledger's own enforcement cells
31+
* name them. Used only as the negative half of the `unevaluated` pin below: a
32+
* row claiming nothing evaluates its slot must not, in the same breath, name
33+
* the thing that does.
34+
*
35+
* The vocabulary is drawn from the enforcement cells already in the ledger
36+
* rather than invented, and it is deliberately a DETECTOR, not an inventory —
37+
* a row may name an evaluator this list has never heard of (the objectui
38+
* renderers are named in prose, with no callable token to match), so a MISS
39+
* proves nothing on its own. That is why the pin's other half is a positive
40+
* requirement rather than this one alone.
41+
*/
42+
const NAMES_RUNTIME_EVALUATOR =
43+
/celEngine|cronEngine|ExpressionEngine\.evaluate|compileCelToFilter|celToFilter|matchesFilterCondition|evaluateVisibility|evaluateValidationRules|evalFieldPredicate|evalRowPredicate|useRowPredicate|resolveCascadingOptions|toBoundaryJobSchedule|croner/;
44+
45+
/**
46+
* The closed set of spellings that STATE the absence `unevaluated` claims.
47+
*
48+
* ⚠️ This does not make the claim true — no regex reads prose for honesty. What
49+
* it does is refuse the shape the four older members were borrowed in: a row
50+
* whose enforcement cell simply describes a site and leaves the reader to infer
51+
* what happens to a bad expression. An `unevaluated` row has to say, in the
52+
* cell itself, that this ledger looked and found no evaluator — which is the
53+
* sentence a reviewer can check and a future author can be held to.
54+
*/
55+
const DECLARES_NO_EVALUATOR = /NO EVALUATOR FOUND|PARSE ONLY|no runtime consumer/;
2856
// `settings-visibility` is not one of the spec's `ExpressionDialect` members on
2957
// purpose (#7327): it is a closed non-CEL grammar with its own evaluator, and
3058
// the ledger's job is to say what a surface IS, not what its schema used to
@@ -214,4 +242,58 @@ describe('ADR-0058 D7 — expression surface conformance ledger', () => {
214242
+ 'Give the colliding declarations distinguishable keys, then classify each on its own row.');
215243
expect(collisions, collisions.join('\n')).toEqual([]);
216244
});
245+
246+
// The pin that makes `unevaluated` worth minting. The card this member comes
247+
// from is about a vocabulary with no word for "nothing evaluates this slot",
248+
// which forced five rows to borrow a member claiming something stronger —
249+
// `compile-error` on four, and `fail-closed` on a security-flavoured row
250+
// whose own enforcement cell read `(no runtime consumer yet)`. A new word
251+
// that could be borrowed just as loosely would reproduce that defect one
252+
// member wider, so the word arrives with the assertions below.
253+
//
254+
// "Non-empty runtime enforcement" cannot be checked as `enforcement !== ''`:
255+
// `ExprSurface` makes the cell REQUIRED, so every row has a non-empty one,
256+
// the five honest `unevaluated` rows included. The checkable question is what
257+
// the cell SAYS — it must state the absence, and it must not name the runtime
258+
// site whose existence the row is denying.
259+
it('`unevaluated` states an absence, and cannot be borrowed the way the old members were', () => {
260+
// Positive control for the detector itself. An emptied or mistyped
261+
// NAMES_RUNTIME_EVALUATOR makes the negative assertion below vacuously
262+
// green — the "reports green because it never looked" failure the pin above
263+
// guards against with its own control. The COMPILE rows are exactly the
264+
// rows another pin in this file already requires to name the canonical
265+
// compiler, so they are rows this detector MUST fire on.
266+
const compileRows = EXPRESSION_SURFACE.filter((x) => x.mode === 'compile');
267+
expect(
268+
compileRows.length,
269+
'no COMPILE rows in the ledger — the runtime-evaluator detector has nothing to be controlled against, so the assertions below prove nothing',
270+
).toBeGreaterThan(0);
271+
for (const s of compileRows) {
272+
expect(
273+
NAMES_RUNTIME_EVALUATOR.test(s.enforcement),
274+
`${s.id}: the runtime-evaluator detector does not fire on a row that is required to name the canonical compiler — the DETECTOR is broken, not the row`,
275+
).toBe(true);
276+
}
277+
278+
for (const s of EXPRESSION_SURFACE.filter((x) => x.failPolicy === 'unevaluated')) {
279+
// `enforced` means the platform enforces the surface; `unevaluated` means
280+
// nothing reads it. Restricting this pin to `experimental` rows would
281+
// leave `state: 'enforced'` as the escape hatch, so the contradiction is
282+
// refused directly instead.
283+
expect(
284+
s.state,
285+
`${s.id}: state 'enforced' and failPolicy 'unevaluated' contradict each other — nothing evaluates the slot, so nothing enforces it`,
286+
).not.toBe('enforced');
287+
288+
expect(
289+
DECLARES_NO_EVALUATOR.test(s.enforcement),
290+
`${s.id}: failPolicy 'unevaluated' but the enforcement cell never states the absence it claims. Say it in the cell — 'NO EVALUATOR FOUND', 'PARSE ONLY', or 'no runtime consumer' — so the claim is reviewable rather than inferred from silence`,
291+
).toBe(true);
292+
293+
expect(
294+
NAMES_RUNTIME_EVALUATOR.test(s.enforcement),
295+
`${s.id}: failPolicy 'unevaluated' says nothing evaluates this slot, but the enforcement cell names a runtime evaluator/compiler site. One of the two is wrong: if something evaluates it, classify it under the ADR-0058 D5 tier that describes what happens to a bad expression there`,
296+
).toBe(false);
297+
}
298+
});
217299
});

0 commit comments

Comments
 (0)