Skip to content

Commit 2973f7f

Browse files
os-zhuangclaude
andauthored
feat(spec,cli): enroll view in the liveness ledger (#2998 Track B) (#3046)
Adds 'view' to the GOVERNED set of the spec property-liveness gate and authors packages/spec/liveness/view.json — closing the rollout gap that let the objectui#1763/#2545 renderer/spec key-drift class survive undetected (the systemic cause named in #2998). - view.json classifies all 83 walkable properties: `list`/`form` drilled one level via children; `listViews`/`formViews` covered at top level (same inner shapes as list/form). Tally: 76 live, 2 experimental (form.buttons/form.defaults — #2998 Track A awaiting objectui#2545), 5 dead (list.responsive, list.performance, form.data, form.defaultSort, form.aria), misleading dead props authorWarn'd with corrective hints. - Seeded from docs/audits/2026-06-viewschema-property-liveness.md and re-verified against objectui HEAD (fb35e48): four audit-era DEAD findings had since gone live and are classified from current reads — form.submitBehavior, list.sharing.lockedBy, list-path ViewData providers, and the post-ADR-0021 list.chart dataset shape (the audit's "chart renderers never migrated" headline is resolved; the legacy-key reads are only the back-compat fallback). - CLI compile-time liveness lint gains view coverage: TYPE_COLLECTIONS maps the stack `views` collection, containers labelled by `object` (views have no `name`). +3 tests (17/17 green). - liveness/README.md: 13 governed types, view row, form-surface proof class note updated; minor changeset for @objectstack/spec + cli. Verification: check:liveness green (0 UNCLASSIFIED, no new stale-evidence warnings); lint-liveness-properties tests 17/17. Claude-Session: https://claude.ai/code/session_013Ue47V8zZ5QhcYMPiRQDA7 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6613ad0 commit 2973f7f

6 files changed

Lines changed: 196 additions & 5 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
---
5+
6+
feat(spec,cli): enroll `view` in the liveness ledger (#2998 Track B)
7+
8+
`view` joins the `GOVERNED` set of the spec property-liveness gate — the
9+
rollout gap that let the objectui#1763/#2545 class of renderer/spec key drift
10+
survive undetected. New `packages/spec/liveness/view.json` classifies all 83
11+
walkable properties (75 ledger entries + framework overlay fields): the `list`
12+
and `form` containers are drilled one level via `children`.
13+
14+
Seeded from the 2026-06 viewschema audit and **re-verified against objectui
15+
HEAD** — four audit-era DEAD findings had since gone live and are classified
16+
from current reads (`form.submitBehavior`, `list.sharing.lockedBy`, list-path
17+
`ViewData` providers, and the post-ADR-0021 `list.chart` dataset shape — the
18+
audit's "chart renderers never migrated" headline is resolved). Final tally:
19+
68 live, 2 experimental (`form.buttons`/`form.defaults`, #2998 Track A
20+
awaiting objectui#2545), 5 dead (`list.responsive`, `list.performance`,
21+
`form.data`, `form.defaultSort`, `form.aria`). All misleading dead props
22+
carry `authorWarn` + `authorHint`.
23+
24+
The CLI's compile-time liveness lint gains `view` coverage
25+
(`TYPE_COLLECTIONS` + view containers labelled by `object`), so authoring a
26+
dead prop — e.g. a spec-valid `chart` list view that renders empty — now warns
27+
at `os build` with a corrective hint.

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,41 @@ describe('lintLivenessProperties', () => {
146146
});
147147
expect(findings).toEqual([]);
148148
});
149+
150+
// ── view (#2998 Track B) ──────────────────────────────────────────────────
151+
152+
it('warns on dead list-level responsive config (view ledger, list.responsive)', () => {
153+
const findings = lintLivenessProperties({
154+
views: [{ object: 'task', list: { type: 'grid', responsive: { breakpoint: 'md' } } }],
155+
});
156+
const f = findings.find((x) => x.message.includes('list.responsive'));
157+
expect(f).toBeDefined();
158+
expect(f!.where).toBe("view 'task'"); // container binds via `object`, not `name`
159+
expect(f!.hint.length).toBeGreaterThan(0);
160+
});
161+
162+
it('warns on form.data and form.defaultSort but not on live form config', () => {
163+
const findings = lintLivenessProperties({
164+
views: [{
165+
object: 'task',
166+
form: {
167+
type: 'wizard',
168+
sections: [{ fields: ['title'] }],
169+
data: { provider: 'object', object: 'task' },
170+
defaultSort: [{ field: 'created_at' }],
171+
},
172+
}],
173+
});
174+
const msgs = paths(findings);
175+
expect(msgs.some((m) => m.includes('form.data'))).toBe(true);
176+
expect(msgs.some((m) => m.includes('form.defaultSort'))).toBe(true);
177+
expect(msgs.some((m) => m.includes('form.sections') || m.includes('form.type'))).toBe(false);
178+
});
179+
180+
it('stays silent on a clean grid view', () => {
181+
const findings = lintLivenessProperties({
182+
views: [{ object: 'task', list: { type: 'grid', columns: ['title'] } }],
183+
});
184+
expect(findings).toEqual([]);
185+
});
149186
});

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ const TYPE_COLLECTIONS: Array<{ type: string; key: string }> = [
180180
{ type: 'permission', key: 'permissions' },
181181
{ type: 'hook', key: 'hooks' },
182182
{ type: 'page', key: 'pages' },
183+
{ type: 'view', key: 'views' },
183184
];
184185

185186
/**
@@ -214,7 +215,10 @@ export function lintLivenessProperties(stack: AnyRec): LivenessLintFinding[] {
214215
const warnMap = loadWarnMap(dir, type);
215216
if (warnMap.size === 0) continue;
216217
for (const item of asArray(stack[key])) {
217-
const name = typeof item.name === 'string' ? item.name : `(unnamed ${type})`;
218+
// view containers bind via `object`, not `name`
219+
const name = typeof item.name === 'string' ? item.name
220+
: typeof item.object === 'string' ? item.object
221+
: `(unnamed ${type})`;
218222
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
219223
}
220224
}

packages/spec/liveness/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ binding lands one class at a time (ADR-0054 §3), never as a big-bang backfill.
8080
| Master-detail controlled-by-parent | ✅ enforced | `object.sharingModel` | `controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent` |
8181
| Flow nodes | ✅ enforced | `flow.nodes.type` | `flow-node.dogfood.test.ts#flow-node-execution` |
8282
| Analytics dims/measures | ✅ enforced | `dataset.dimensions.dateGranularity` | `analytics-timezone.dogfood.test.ts#analytics-tz-bucketing` |
83-
| Form layout/section/widget | ⛔ pending || none yet (form surface not yet governed) |
83+
| Form layout/section/widget | ⛔ pending || none yet (`view` is governed as of #2998 Track B, so `view.form.*` can carry the binding — the dogfood proof is the missing half) |
8484

8585
To bind a pending class: add its dogfood proof + `@proof:` tag, set `bound: true` and
8686
its `ledgerBindings` in `proof-registry.mts`, add the `proof` to the ledger entry, and
@@ -166,7 +166,7 @@ The governed set is `GOVERNED` at the top of `check-liveness.mts`. To add a type
166166
RecordDetailView had been gating the History tab on it the whole time (#2707).
167167
4. Add the type to `GOVERNED`; confirm the gate is green.
168168

169-
## Current state — 12 governed types
169+
## Current state — 13 governed types
170170

171171
Counts include drilled `children` entries; regenerate with the snippet below rather
172172
than hand-editing (this table drifted badly once — field was listed 34/39 while the
@@ -201,8 +201,9 @@ EOF
201201
| skill | 10 |||| fully live |
202202
| dataset | 19 || 1 || `measures.certified` declared-but-unenforced governance flag |
203203
| page | 16 ||| 1 | fully live + one planned |
204+
| view | 68 | 2 | 5 || list/form drilled via `children` (#2998 Track B); dead = list.{responsive,performance} + form.{data,defaultSort,aria}, all but aria authorWarn'd; exp = form.{buttons,defaults} awaiting objectui#2545; audit-era DEAD lines superseded by re-verification (submitBehavior, sharing.lockedBy, list ViewData providers, and the ADR-0021 chart shape — all live now); level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only |
204205

205206
The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every
206207
misleading entry carries `authorWarn` so authors hear about it at compile time.
207-
Not yet governed (rollout): view, dashboard, app, report, job, datasource,
208+
Not yet governed (rollout): dashboard, app, report, job, datasource,
208209
translation, email_template, doc, book, validation, seed.

0 commit comments

Comments
 (0)