Skip to content

Commit 931c402

Browse files
committed
feat(spec,cli): govern the 9 remaining metadata types — liveness coverage complete (#4488)
Nine new ledgers (app, book, doc, email_template, job, mapping, seed, translation, validation; ~150 verdicts, all verifiedAt-stamped), emptying PENDING_GOVERNANCE: every registered metadata type is now governed. - check-liveness.mts: unwrap() sees through z.preprocess pipes (takes the OUT side when IN is a transform) — translation's registered schema was unwalkable before this. - CLI lint TYPE_COLLECTIONS registers the six new types carrying authorWarn entries, with pins in the test suite (21 tests). - liveness/README.md: the count-table method is decided and recorded (mirrors check-liveness --json byStatus); all rows regenerated, the stale webhook row rewritten to the post-#3489/#3494 state. Key findings, all recorded in ledger notes and tracked in #4509: - email_template: the ENTIRE authoring surface is disconnected from sendTemplate (false compliance on auth mail). - app: areas[].visible / areas[].requiredPermissions are fail-open dead gates; homePageId dead; selector includeAll deliberately ignored. - translation.validationMessages: read by nothing while #3778's own migration table steers authors into it. - job / validation: runtime-authoring doors disconnected from their execution points. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WsgTqRF58HsQYKLsrZ5pQY
1 parent ea90179 commit 931c402

14 files changed

Lines changed: 1095 additions & 60 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': patch
4+
---
5+
6+
Liveness coverage is complete: the nine remaining registered metadata types are
7+
governed (#4488) — `app`, `book`, `doc`, `email_template`, `job`, `mapping`,
8+
`seed`, `translation`, `validation` — and `PENDING_GOVERNANCE` is empty. Every
9+
type in the metadata-type registry now has a ledger with per-property verdicts,
10+
evidence, and a `verifiedAt` stamp.
11+
12+
Spec:
13+
14+
- Nine new ledgers under `packages/spec/liveness/` (≈150 verdicts). Highlights:
15+
the ENTIRE `email_template` authoring surface is dead (nothing materializes
16+
metadata items into the `sys_email_template` rows `sendTemplate` reads — an
17+
admin editing the password-reset mail in Studio changes nothing; #4509);
18+
`app.areas[].visible` / `areas[].requiredPermissions` are fail-open dead
19+
gates (item-level siblings ARE enforced); `translation.validationMessages`
20+
is read by nothing while #3778's own migration table steers authors into it;
21+
`job`/`validation` have runtime-authoring doors disconnected from their
22+
execution points (#4509). `doc` and `seed` are fully live.
23+
- `check-liveness.mts`: the walker now sees through `z.preprocess` pipes
24+
(takes the OUT side when the IN side is a transform) — `translation`'s
25+
registered schema was unwalkable before this.
26+
- `liveness/README.md`: the per-type count table's method is now decided and
27+
recorded (it mirrors `check-liveness.mts --json` `byStatus`, the number CI
28+
enforces); all rows regenerated from one run, and the two-generations-stale
29+
`webhook` row rewritten to the post-#3489/#3494 state.
30+
31+
CLI:
32+
33+
- `lint-liveness-properties` registers the six newly governed types that carry
34+
`authorWarn` entries (`apps`, `books`, `jobs`, `emailTemplates`, `mappings`,
35+
`translations`), so authors hear about the misleading keys at compile time.

packages/cli/src/utils/lint-liveness-properties.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,136 @@ describe('lintLivenessProperties', () => {
250250
});
251251
expect(findings).toEqual([]);
252252
});
253+
254+
// ── #4488 — the nine remaining types, governed. Pins run against the REAL
255+
// ledgers, one per finding class the audit surfaced.
256+
257+
// The app ledger's most important entries: area-level gating keys that FAIL
258+
// OPEN (nothing evaluates them, so a "hidden"/"gated" area shows for
259+
// everyone), on the surface whose item-level siblings ARE enforced.
260+
it('warns on the fail-open area gates and dead homePageId (#4488)', () => {
261+
const findings = lintLivenessProperties({
262+
apps: [{
263+
name: 'crm',
264+
label: 'CRM',
265+
homePageId: 'nav_pipeline',
266+
areas: [{
267+
id: 'area_sales',
268+
label: 'Sales',
269+
order: 2,
270+
visible: "'sales' in current_user.positions",
271+
requiredPermissions: ['crm.access'],
272+
navigation: [],
273+
}],
274+
}],
275+
});
276+
const msgs = paths(findings);
277+
expect(msgs.some((m) => m.includes('homePageId'))).toBe(true);
278+
expect(msgs.some((m) => m.includes('areas.order'))).toBe(true);
279+
expect(msgs.some((m) => m.includes('areas.visible'))).toBe(true);
280+
expect(msgs.some((m) => m.includes('areas.requiredPermissions'))).toBe(true);
281+
// The gating hints must point at the enforced alternative (per-item gates),
282+
// or the warning just relocates the author's confusion.
283+
const perms = findings.find((f) => f.message.includes('areas.requiredPermissions'));
284+
expect(perms!.hint).toMatch(/per item|Per-item/i);
285+
});
286+
287+
// email_template: the WHOLE authoring surface is disconnected from
288+
// sendTemplate (webhook shape) — one per-artifact warn carried on `name`.
289+
it('warns once per email_template artifact via name (#4488)', () => {
290+
const findings = lintLivenessProperties({
291+
emailTemplates: [{
292+
name: 'crm.welcome',
293+
label: 'Welcome',
294+
subject: 'Hi {{user.name}}',
295+
bodyHtml: '<p>Welcome</p>',
296+
}],
297+
});
298+
const hit = findings.find((f) => f.message.includes('`name`'));
299+
expect(hit).toBeDefined();
300+
expect(hit!.hint).toMatch(/sys_email_template/);
301+
});
302+
303+
// translation.validationMessages: pointed at by #3778's own migration table,
304+
// read by nothing — the hint must say what actually renders (rule.message).
305+
it('warns on translation.validationMessages (#4488)', () => {
306+
const findings = lintLivenessProperties({
307+
translations: [{
308+
name: 'zh_cn',
309+
locale: 'zh-CN',
310+
validationMessages: { discount_limit: '折扣不能超过40%' },
311+
}],
312+
});
313+
const hit = findings.find((f) => f.message.includes('validationMessages'));
314+
expect(hit).toBeDefined();
315+
expect(hit!.hint).toMatch(/message/);
316+
});
317+
318+
// book: both inline translations maps are dead (the doc-level map two files
319+
// over works, which is what makes these read alive); job.id and
320+
// mapping.extractQuery are the other flat dead keys.
321+
it('warns on book/job/mapping dead keys (#4488)', () => {
322+
const findings = lintLivenessProperties({
323+
books: [{
324+
name: 'crm_guide',
325+
label: 'CRM Guide',
326+
translations: { 'zh-CN': { label: 'CRM 指南' } },
327+
groups: [{ key: 'basics', label: 'Basics', translations: { 'zh-CN': { label: '基础' } } }],
328+
}],
329+
jobs: [{
330+
name: 'nightly_sync',
331+
id: 'job_nightly',
332+
schedule: { type: 'cron', expression: '0 0 * * *' },
333+
handler: 'syncAll',
334+
}],
335+
mappings: [{
336+
name: 'csv_import_contacts',
337+
targetObject: 'contact',
338+
fieldMapping: [],
339+
extractQuery: { object: 'contact', fields: ['name'] },
340+
}],
341+
});
342+
const msgs = paths(findings);
343+
expect(msgs.some((m) => m.includes('`translations`'))).toBe(true);
344+
expect(msgs.some((m) => m.includes('groups.translations'))).toBe(true);
345+
expect(msgs.some((m) => m.includes('`id`'))).toBe(true);
346+
expect(msgs.some((m) => m.includes('extractQuery'))).toBe(true);
347+
});
348+
349+
// The unwarnable-default rule, negative direction: errorPolicy/batchSize
350+
// (mapping) and includeAll/placement (app selectors) materialize from schema
351+
// defaults on every compiled artifact, so their dead entries carry
352+
// _authorWarnSkipped instead of authorWarn — a compiled stack that only has
353+
// defaults must stay silent.
354+
it('stays silent on schema-default values and live-only artifacts (#4488)', () => {
355+
const findings = lintLivenessProperties({
356+
mappings: [{
357+
name: 'api_sync_orders',
358+
targetObject: 'order',
359+
fieldMapping: [{ source: 'Total', target: 'total' }],
360+
mode: 'upsert',
361+
upsertKey: ['external_ref'],
362+
// materialized defaults — must NOT warn:
363+
sourceFormat: 'csv',
364+
errorPolicy: 'skip',
365+
batchSize: 1000,
366+
}],
367+
apps: [{
368+
name: 'sales',
369+
label: 'Sales',
370+
contextSelectors: [{
371+
id: 'active_region',
372+
label: 'Region',
373+
optionsSource: { endpoint: '/api/v1/regions', valueKey: 'id', labelKey: 'name' },
374+
// materialized defaults — must NOT warn:
375+
includeAll: true,
376+
allValue: '',
377+
persist: 'query',
378+
placement: 'sidebar_header',
379+
}],
380+
}],
381+
seeds: [],
382+
});
383+
expect(findings).toEqual([]);
384+
});
253385
});

packages/cli/src/utils/lint-liveness-properties.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,15 @@ const TYPE_COLLECTIONS: Array<{ type: string; key: string }> = [
188188
// collection registered or its ledger warns nobody, which would leave the
189189
// ledger correct and silent: the exact shape this lint exists to prevent.
190190
{ type: 'datasource', key: 'datasources' },
191+
// #4488 — the six newly governed types that carry `authorWarn` entries.
192+
// (doc / seed / validation are governed too but warn on nothing today, so
193+
// they are not listed; add them here the day one of their entries warns.)
194+
{ type: 'app', key: 'apps' },
195+
{ type: 'book', key: 'books' },
196+
{ type: 'job', key: 'jobs' },
197+
{ type: 'email_template', key: 'emailTemplates' },
198+
{ type: 'mapping', key: 'mappings' },
199+
{ type: 'translation', key: 'translations' },
191200
];
192201

193202
/**

0 commit comments

Comments
 (0)