Skip to content

Commit e601c04

Browse files
os-litantclaude
andauthored
test(qa): the ADR-0058 D7 expression ledger discovers the cron and template dialects (#15526)
* test(qa): the expression ledger discovers the cron and template dialects ADR-0058 D7's conformance ratchet re-discovers expression surfaces by SCHEMA NAME, and `EXPRESSION_INPUT_SCHEMAS` listed only `ExpressionInputSchema` and `SettingsVisibilityInputSchema`. `DECLARES_EXPRESSION` requires a listed name to start immediately after the colon, so the 12 positions typed `CronExpressionInputSchema` / `TemplateExpressionInputSchema` could never match: the ledger reported a complete classification while carrying zero `cron` and zero `template` rows. Structurally blind, not merely un-updated. Widen the roster and classify what it finds, on one commit — adding the names alone turns the ratchet red with no rows to point at. Discovery: 26 surfaces -> 36 (+10). The delta is 10 and not 12 because a ratchet key is `file:field`: `api/export.zod.ts:cronExpression` (`:576`, `:706`) and `system/disaster-recovery.zod.ts:schedule` (`:57`, `:238`) each collapse two positions onto one key. Recorded in the `discoverSurfaces` docblock, with the 8 pre-existing collapses that are not all the same surface (filed as #15500). Five new rows, each naming the reader actually found by walking out from the declaration — and saying so when none was found: - `cron-job-schedule` (enforced/throw) — the one cron slot with an evaluator: `toBoundaryJobSchedule` lowers the envelope and refuses by name, croner runs it, AppPlugin contains the throw as an ERROR log plus a counter. - `cron-knowledge-refresh` (experimental) — surfaced, deliberately unscheduled. - `cron-declared-unwired` (experimental) — five slots on subsystems that were declared and never built; no evaluator found for any of them. - `template-prompt` (experimental) — `PromptTemplateSchema` has no consumer. - `template-title-format` (experimental) — the KEY has a build-time reader that never evaluates the template; the interpolation site is in objectui and was not measured from this checkout, so it is not written in as if it had been. `@objectstack/formula` cronEngine has zero consumers outside its own package, so no cron slot is syntax-checked anywhere; the rows say that rather than implying it. Per #15028 the envelope arm accepts any dialect, so the parse does not pin these to `cron`/`template` either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * test(qa): move the ledger rows' tracker ids out of their runtime strings `check:doc-authoring` flagged 6 (file,id) pairs the new rows introduced: the `enforcement` and `note` fields are runtime strings that reach authors, operators and generated surfaces, and none of those readers can resolve a bare tracker id. The ids move to adjacent `//` comments, where the reader who CAN resolve them is already looking, and the strings say the same thing self-containedly. No baseline entry added — that file is maintainer-only and shrink-only, and adding one to get green is the path its own failure text refuses. The pinned population is unchanged at 831 sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b8229e2 commit e601c04

2 files changed

Lines changed: 121 additions & 3 deletions

File tree

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

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ import type { ConformanceRow } from '@objectstack/verify';
2121
// "declared-but-unwired predicate" — breaks the build. Discovery is by SCHEMA
2222
// NAME (`EXPRESSION_INPUT_SCHEMAS` in that file), so a slot narrowed onto its
2323
// own schema must register that schema there or it drops out of the scan.
24+
//
25+
// That promise covers all three dialects only as of #15027. The roster listed
26+
// `ExpressionInputSchema` and `SettingsVisibilityInputSchema` and nothing else,
27+
// so the 12 positions typed `CronExpressionInputSchema` /
28+
// `TemplateExpressionInputSchema` could never match — this ledger reported a
29+
// complete classification while carrying zero `cron` and zero `template` rows.
30+
// The claim above was FALSE for two whole dialects, and read as true, which is
31+
// the failure a ratchet is supposed to make impossible. Two limits survive and
32+
// are worth knowing before trusting a green run: discovery still cannot see a
33+
// slot typed with a schema nobody registered (the hazard is structural, not
34+
// spent), and a ratchet key is `file:field`, so several declarations of one key
35+
// name in one file share a single row — see the `discoverSurfaces` docblock and
36+
// #15500.
2437

2538
export type ExprMode = 'compile' | 'interpret';
2639
/**
@@ -241,7 +254,10 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
241254
// the L1 "Simple Sync" DataSyncConfig) left with the whole file in
242255
// #4738 — the L1 layer was narrative-only, so no engine ever evaluated
243256
// that predicate. Connector-attached sync (`ConnectorSchema.syncConfig`)
244-
// declares no expression surface; nothing to re-point at.
257+
// declares no CEL surface to re-point this cover at. It does declare a
258+
// cron one — `syncConfig.schedule` — which was invisible to discovery
259+
// when that was written and is classified by `cron-declared-unwired`
260+
// since #15027; nothing evaluates it either.
245261
// `kernel/metadata-loader.zod.ts:filter` (on MetadataLoadOptions and
246262
// MetadataExportOptions) was removed with the rest of that file's
247263
// zero-consumer duplicate envelope family in #4411. The surviving
@@ -250,6 +266,76 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
250266
// through this surface, and there is nothing to re-point at.
251267
],
252268
},
269+
270+
// ── CRON dialect (#15027) ─────────────────────────────────────────────────
271+
// Discovery was blind to `CronExpressionInputSchema` until the roster in the
272+
// companion test grew, so every row below is a FIRST classification, not an
273+
// update. Each `enforcement` names what was actually found by walking out
274+
// from the declaration; where the walk found no evaluator, the row says so
275+
// and takes a non-`enforced` state rather than borrowing a neighbour's.
276+
{
277+
id: 'cron-job-schedule',
278+
summary: 'declarative background-job cron schedule (CronSchedule.expression)',
279+
dialect: 'cron', mode: 'interpret', state: 'enforced', failPolicy: 'throw',
280+
// The lowering seam and its containment are #4567; croner, not cron-parser,
281+
// is the library behind the adapter.
282+
enforcement:
283+
'runtime/job-schedule.ts `toBoundaryJobSchedule` — the authoring→boundary seam: it lowers the parsed `{dialect:"cron",source}` envelope to the bare cron string the adapter takes, and THROWS naming the job on a non-cron dialect, an AST-only envelope, or a missing/blank source. Called from runtime/app-plugin.ts `start`; the boundary value reaches service-job/cron-job-adapter.ts `CronJobAdapter.schedule` → **croner** `Cron` (db-job-adapter.ts routes the cron variant there and persists the shape onto sys_job). The throw is CONTAINED at the call site, deliberately and visibly: AppPlugin catches per job, logs `Background job FAILED TO SCHEDULE — it will never run` at ERROR with the `jobScheduleFailuresTotal` counter, then reports the failed count — boot continues and the job does not run. Cron SYNTAX is not judged on this path at all: `toBoundaryJobSchedule` only checks dialect/source shape, and a syntactically invalid pattern throws later inside croner, into the same catch',
284+
covers: ['system/job.zod.ts:expression'],
285+
proof: 'packages/runtime/src/job-schedule.test.ts',
286+
note: 'The ONE cron slot in the spec with a measured evaluator. `@objectstack/formula` cronEngine is NOT on this path — see `cron-declared-unwired` for what that means for the rest.',
287+
},
288+
{
289+
// The key and its documented hand-off arrived with #14825.
290+
id: 'cron-knowledge-refresh',
291+
summary: 'knowledge-source periodic reindex cron (KnowledgeRefreshPolicy.cron) — surfaced, deliberately not scheduled',
292+
dialect: 'cron', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
293+
enforcement:
294+
'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)',
295+
covers: ['ai/knowledge-source.zod.ts:cron'],
296+
note: 'EXPERIMENTAL by DESIGN, and separated from `cron-declared-unwired` for that reason: the key documents its own hand-off — service-knowledge surfaces the value so an automation flow / external scheduler can call `reindexSource`, and the field docblock says so. Nothing in this repo schedules it, which is the intended state rather than an undelivered one. It still has no evaluator, so it is not `enforced`.',
297+
},
298+
{
299+
// Sibling cards named in this row's note: #15500 (ratchet-key granularity)
300+
// and #15028 (the envelope arm accepts any dialect).
301+
id: 'cron-declared-unwired',
302+
summary: 'cron slots on subsystems that were declared but never built — export schedules, flow schedule state, connector sync, cache warmup, DR backup/test',
303+
dialect: 'cron', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
304+
enforcement:
305+
'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',
306+
covers: [
307+
'api/export.zod.ts:cronExpression',
308+
'automation/execution.zod.ts:cronExpression',
309+
'integration/connector.zod.ts:schedule',
310+
'system/cache.zod.ts:schedule',
311+
'system/disaster-recovery.zod.ts:schedule',
312+
],
313+
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 keys each cover TWO declaring positions, because a ratchet key is `file:field`: `api/export.zod.ts:cronExpression` is `:576` (ScheduledExport) and `:706` (ScheduleExportRequest), and `system/disaster-recovery.zod.ts:schedule` is `:57` (BackupConfig) and `:238` (the DR `testing` block). Both pairs are genuinely the same surface twice, so one row is honest here — but see the sibling card on ratchet-key GRANULARITY, where the same collapse hides surfaces that are NOT the same. ⚠️ 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. And per the sibling finding on the dialect union, the envelope arm of `CronExpressionInputSchema` accepts any declared dialect, so even the parse does not pin these to `cron`.',
314+
},
315+
316+
// ── TEMPLATE dialect (#15027) ─────────────────────────────────────────────
317+
{
318+
// The apparent owner was #14797 (closed completed), delivered by PR #14819.
319+
id: 'template-prompt',
320+
summary: 'AI prompt-template system/user prompts (PromptTemplate.system, .user) — `{{var}}` interpolation',
321+
dialect: 'template', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
322+
enforcement:
323+
'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',
324+
covers: [
325+
'ai/model-registry.zod.ts:system',
326+
'ai/model-registry.zod.ts:user',
327+
],
328+
note: 'EXPERIMENTAL — declared prompt templates with no runtime evaluator (ADR-0049). Ownership was checked before classifying rather than assumed: the card that appeared to own these keys is closed as completed, and its delivered diff (`d355c361157`) touched exactly one file, `skills/objectstack-ai/SKILL.md` — it corrected a prose clause that called these keys CEL, and never owned a ledger row. No open card owns them.',
329+
},
330+
{
331+
id: 'template-title-format',
332+
summary: 'object record-title template (Object.titleFormat, deprecated → nameField per ADR-0079)',
333+
dialect: 'template', mode: 'interpret', state: 'experimental', failPolicy: 'compile-error',
334+
enforcement:
335+
'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)',
336+
covers: ['data/object.zod.ts:titleFormat'],
337+
note: 'EXPERIMENTAL, and the state is a deliberate split between two questions. `packages/spec/liveness/object.json` classifies the KEY `live` with the note "objectui ({{record.field}} interpolation)" — that ledger asks whether anything READS the key, and the answer is yes. THIS ledger asks what EVALUATES the expression and under which fail policy, and the only interpolation site named is in the sibling repo objectui, which is not in this checkout: ⛔ NOT measured here, so it is not written into `enforcement` as if it had been. Marking the row `enforced` on a second-hand reading is exactly the invented cell this ledger exists to prevent; marking it `removed` would contradict a governed ledger that measured more than I could. Re-state as `enforced` when someone measures the objectui site — or as `removed` when ADR-0079 retires the key.',
338+
},
253339
{
254340
id: 'cel-advanced-policy',
255341
summary: 'advanced security / versioning policy conditions',

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,45 @@ const DIALECTS = new Set(['cel', 'cron', 'template', 'js', 'settings-visibility'
3838
* #7327 hit it: narrowing the settings `visible` slots off `ExpressionInputSchema`
3939
* dropped them out of discovery and turned their ledger entry stale. A new
4040
* narrowed alias belongs in this list on the same commit that introduces it.
41+
*
42+
* The two DIALECT-typed inputs were missing for as long as they have existed
43+
* (#15027). `CronExpressionInputSchema` and `TemplateExpressionInputSchema` are
44+
* siblings of `ExpressionInputSchema` — same envelope, a different default
45+
* dialect on the bare-string arm — so every slot typed with one of them was a
46+
* declared expression surface that this scan could NEVER match: the pattern
47+
* requires a listed name to start immediately after the colon, and neither was
48+
* listed. The ledger therefore reported a complete classification over a
49+
* population with zero `cron` and zero `template` rows in it, while the spec
50+
* declared 12 such positions. Structurally blind, not merely un-updated — which
51+
* is why the roster and the rows classifying them landed on one commit.
4152
*/
42-
const EXPRESSION_INPUT_SCHEMAS = ['ExpressionInputSchema', 'SettingsVisibilityInputSchema'];
53+
const EXPRESSION_INPUT_SCHEMAS = [
54+
'ExpressionInputSchema',
55+
'SettingsVisibilityInputSchema',
56+
'CronExpressionInputSchema',
57+
'TemplateExpressionInputSchema',
58+
];
4359
const DECLARES_EXPRESSION = new RegExp(
4460
String.raw`^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?:${EXPRESSION_INPUT_SCHEMAS.join('|')})\b`,
4561
);
4662

47-
/** Re-discover every expression surface in the spec — the SAME scan the ledger encodes. */
63+
/**
64+
* Re-discover every expression surface in the spec — the SAME scan the ledger encodes.
65+
*
66+
* ⚠️ A ratchet key is `file:field`, NOT `file:line`, so N declarations of one
67+
* key name in one file are ONE key and one ledger row classifies all of them.
68+
* Measured at `61821e54cf5` while widening the roster above: 44 declaring
69+
* positions reduce to 34 keys (+2 hard-added RLS = 36 surfaces), and 10 keys
70+
* carry two positions each. Two of those collapses are the cron ones this
71+
* commit classifies (`api/export.zod.ts:cronExpression` at `:576`/`:706`,
72+
* `system/disaster-recovery.zod.ts:schedule` at `:57`/`:238`) — genuinely the
73+
* same surface twice, so one row is right for them. The other 8 predate this
74+
* commit and at least three of them collapse surfaces that are NOT the same
75+
* (#15500): `data/field.zod.ts:requiredWhen` covers both the server-enforced
76+
* `FieldSchema` gate and the `InlineGridColumnSchema` cell whose own describe
77+
* says nothing on the write path reads it. Do not read a green ratchet as
78+
* "every declaration is classified" — it means every KEY is.
79+
*/
4880
function discoverSurfaces(): Set<string> {
4981
const found = new Set<string>();
5082
const walk = (dir: string) => {

0 commit comments

Comments
 (0)