Skip to content

Commit d2d1197

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16581-public-picker-filter-lowering
2 parents 029a172 + 2539f4a commit d2d1197

10 files changed

Lines changed: 1248 additions & 27 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"clean": "turbo run clean && rm -rf dist",
1717
"setup": "pnpm install && pnpm --filter @objectstack/spec build",
1818
"prepare": "node scripts/setup-git-hooks.mjs",
19-
"check:merge-driver": "node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test",
19+
"check:merge-driver": "node scripts/git-env.mjs --self-test && node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test",
2020
"version": "changeset version && node scripts/sync-protocol-version.mjs && node scripts/sync-template-versions.mjs && node scripts/sync-docs-image-tags.mjs && node scripts/sync-release-index-currency.mjs",
2121
"release": "pnpm run build && bash scripts/build-console.sh && bash scripts/release-publish.sh",
2222
"docs:dev": "pnpm --filter @objectstack/docs dev",

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)