Skip to content

Commit 4ca7ccf

Browse files
claude[bot]claude
andauthored
feat(sdui): wire the ADR-0080 manifest into the production JSX gate — artefact, freshness gate, witness tests, ratchet-to-zero ledger (#13446)
* feat(sdui): check in the public-tier SDUI manifest with Node-route generator and provenance record * feat(sdui): freshness gate, production-path witness, and first-wiring ratchet ledger * ci(sdui): wire the manifest freshness gate and per-PR declaration-parity ratchet; declare the witness test's cross-package inputs * ci(sdui): cover the repo-root manifest in ci.yml's crosspkg scheduling filter (check:ci-filter-parity) * fix(lint): read the ratchet ledger via readFileSync — a NodeNext JSON module import adds a frozen TEST_DEBT tsc error * fix(ci): record #12924's repo-root declaration in the ci-filter-parity rollback pin — count, prose, and by-name assertion --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86cbe37 commit 4ca7ccf

11 files changed

Lines changed: 3037 additions & 6 deletions

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ jobs:
180180
- 'skills/**'
181181
- '.github/workflows/scaffold-e2e.yml'
182182
- '.claude/skills/spec-property-retirement/SKILL.md'
183+
# @objectstack/lint's production-witness suite reads the repo-root
184+
# SDUI manifest (#12924) — verbatim per the declaration in
185+
# scripts/cross-package-test-inputs.mjs (check:ci-filter-parity
186+
# holds this list to the declarations; the examples/ page files
187+
# the same suite declares are covered by entries the core filter
188+
# already carries).
189+
- 'sdui.manifest.json'
183190
184191
test:
185192
# Sharded 6-way BY PACKAGE: a core-touching PR ran the affected suite

.github/workflows/lint.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,23 @@ jobs:
553553
- name: sdui-parser stays in lockstep with objectui's copy
554554
run: pnpm check:sdui-lockstep
555555

556+
# The checked-in SDUI public-tier manifest (#12924, ruled 2026-08-29: wire
557+
# it). `resolveSduiManifest()` degrades to parse-only SILENTLY when the
558+
# repo-root `sdui.manifest.json` is absent or unreadable, so nothing in
559+
# the production path ever says "the gate lost its whitelist" — this step
560+
# is the thing that notices. Offline record checks only (existence, shape,
561+
# sha256 vs the provenance record, record pin vs `.objectui-sha`): the
562+
# regenerate-and-compare half runs where a regeneration can (the Node
563+
# generator needs the published @object-ui set — see
564+
# scripts/gen-sdui-manifest-node.mjs), and a pin bump goes red HERE until
565+
# it has been re-run — the same moment the lockstep step above forces the
566+
# parser parity re-verification, with an objectui checkout guaranteed in
567+
# hand. Unfiltered for the same reason as its neighbour: filtering to the
568+
# artefact would go dormant on exactly the PR that moves the pin.
569+
# Direct invocation per the GATE INVOCATION IDIOM at the top of this file.
570+
- name: SDUI manifest is present, intact and fresh at the objectui pin
571+
run: node scripts/check-sdui-manifest.mjs --self-test && node scripts/check-sdui-manifest.mjs
572+
556573
# pm-dispatch SKILL.md line ratchet (#7341 / #5925 item 7): the PM skill
557574
# is read in full by every seat session and every Routine fire, so its
558575
# size is a per-round tax on the whole fleet. The #7341 extraction moved
@@ -4750,6 +4767,18 @@ jobs:
47504767
- name: Check docs YAML examples parse against live spec schemas
47514768
run: pnpm --filter @objectstack/spec run check:yaml-examples
47524769

4770+
# ADR-0082 D4's spec↔registry react-block declaration-parity ratchet, fed
4771+
# by the checked-in repo-root manifest (#12924 execution point 1: the same
4772+
# artefact resolves this gate's EXTERNAL_INPUT_REQUIRED standing — the
4773+
# input it always lacked now lives in the tree, held fresh by the
4774+
# check-sdui-manifest step in the lint job). Baseline-ratcheted: only a
4775+
# divergence NEW since packages/spec/react-declaration-parity.baseline.json
4776+
# reds, so wiring it per-PR promotes the cut-rc-only run without
4777+
# manufacturing a standing red. Reads spec src/ through tsx like its
4778+
# neighbours — no dist. ~5 s.
4779+
- name: Ratchet spec-vs-registry react declaration parity against the checked-in manifest
4780+
run: MANIFEST="$PWD/sdui.manifest.json" pnpm --filter @objectstack/spec check:react-declaration-parity --baseline react-declaration-parity.baseline.json --strict
4781+
47534782
# Same anti-drift class as the gates above, for the generated translation
47544783
# bundles in packages/platform-objects/src/apps/translations/. Nothing
47554784
# regenerated them either, so they rotted three ways at once (#3670):
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"//": [
3+
"RATCHET-TO-ZERO ledger for the first live run of the wired SDUI JSX gate (#12924,",
4+
"ruled 2026-08-29: wire it; first-wiring noise handled per ui#6779's ratchet-to-zero —",
5+
"itemized, dated, visible, and NOT an exemption). Every row is a diagnostic the wired",
6+
"`validateJsxPages` (real repo-root sdui.manifest.json, real shipped pages) emits TODAY.",
7+
"",
8+
"Direction 1 — a finding NOT covered by a row is a NEW violation: fix the page (or, if",
9+
"the vocabulary legitimately grew, regenerate the manifest); never grow this file.",
10+
"Direction 2 — a row the live run no longer produces is a STALE entry: delete it in the",
11+
"same PR that fixed the page (or that landed the vocabulary the tags were waiting for).",
12+
"The ledger only shrinks. Zero rows = delete the file and the census assertion inverts",
13+
"to 'wired run is clean'.",
14+
"",
15+
"Why these 232 exist at all: the public tier deliberately declares no intrinsic HTML",
16+
"tags (57 curated blocks; div/a/p absent; `html` is the tier-C escape hatch), while the",
17+
"three shipped html pages author with div/a — every error below is a plain HTML tag,",
18+
"none is a real authoring mistake (measured 2026-08-29, re-measured 2026-08-30). The",
19+
"not-a-container rows are objectui#6740 (flex registers without isContainer). What the",
20+
"html tier's legal vocabulary IS remains a producer-side question (objectui); this",
21+
"ledger keeps the gate live and honest while that question settles."
22+
],
23+
"recordedAt": "2026-08-30",
24+
"findings": [
25+
{ "page": "showcase_capability_map", "rule": "jsx-forbidden-tag", "severity": "error", "tag": "a", "count": 17 },
26+
{ "page": "showcase_capability_map", "rule": "jsx-forbidden-tag", "severity": "error", "tag": "div", "count": 23 },
27+
{ "page": "showcase_capability_map", "rule": "jsx-not-a-container", "severity": "warning", "tag": "flex", "count": 14 },
28+
{ "page": "showcase_capability_map", "rule": "jsx-unknown-component", "severity": "error", "tag": "a", "count": 17 },
29+
{ "page": "showcase_capability_map", "rule": "jsx-unknown-component", "severity": "error", "tag": "div", "count": 23 },
30+
{ "page": "showcase_command_center_jsx", "rule": "jsx-forbidden-tag", "severity": "error", "tag": "div", "count": 29 },
31+
{ "page": "showcase_command_center_jsx", "rule": "jsx-not-a-container", "severity": "warning", "tag": "flex", "count": 12 },
32+
{ "page": "showcase_command_center_jsx", "rule": "jsx-unknown-component", "severity": "error", "tag": "div", "count": 29 },
33+
{ "page": "showcase_start_here", "rule": "jsx-forbidden-tag", "severity": "error", "tag": "a", "count": 4 },
34+
{ "page": "showcase_start_here", "rule": "jsx-forbidden-tag", "severity": "error", "tag": "div", "count": 27 },
35+
{ "page": "showcase_start_here", "rule": "jsx-not-a-container", "severity": "warning", "tag": "flex", "count": 6 },
36+
{ "page": "showcase_start_here", "rule": "jsx-unknown-component", "severity": "error", "tag": "a", "count": 4 },
37+
{ "page": "showcase_start_here", "rule": "jsx-unknown-component", "severity": "error", "tag": "div", "count": 27 }
38+
]
39+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Production-path witness + first-wiring ratchet for the SDUI JSX gate
4+
// (#12924, maintainer ruling 2026-08-29: wire it; execution point 3 demands a
5+
// witness that REALLY PARSES the checked-in manifest into `validateTree`).
6+
//
7+
// ── Why this file exists, stated as the blind spot it closes ──────────────
8+
//
9+
// Every other guard on `validateTree` in this repo constructs its manifest
10+
// IN MEMORY, so a green suite was compatible with the production gate being
11+
// parse-only for the whole life of the code — no test resolved a real
12+
// artefact, because there was nothing to resolve (#12924's finding). These
13+
// tests read the REAL repo-root `sdui.manifest.json` from DISK, feed it
14+
// through the REAL production entry points, and pin the arming delta itself.
15+
//
16+
// Two cross-package inputs, both declared so the graph can see them
17+
// (`check:examples-live-imports`, `@objectstack/lint#test` inputs in
18+
// turbo.json): the repo-root artefact, and the three shipped html pages.
19+
20+
import { existsSync, readFileSync } from 'node:fs';
21+
import { dirname, join } from 'node:path';
22+
import { fileURLToPath } from 'node:url';
23+
import { describe, expect, it } from 'vitest';
24+
25+
import { validateJsxPages } from './validate-jsx-pages.js';
26+
import { runAuthoringRules } from './authoring-rules.js';
27+
28+
import { CapabilityMapPage } from '../../../examples/app-showcase/src/ui/pages/capability-map.page.js';
29+
import { CommandCenterJsxPage } from '../../../examples/app-showcase/src/ui/pages/command-center-jsx.page.js';
30+
import { StartHerePage } from '../../../examples/app-showcase/src/ui/pages/start-here.page.js';
31+
32+
const HERE = dirname(fileURLToPath(import.meta.url));
33+
34+
interface LedgerRow {
35+
page: string;
36+
rule: string;
37+
severity: string;
38+
tag: string;
39+
count: number;
40+
}
41+
// readFileSync rather than a JSON module import: under NodeNext the latter
42+
// needs an import attribute (TS1543), which would add to this package's frozen
43+
// TEST_DEBT tsc count for no behavioral gain.
44+
const ledger = JSON.parse(readFileSync(join(HERE, 'sdui-jsx-baseline.json'), 'utf8')) as {
45+
findings: LedgerRow[];
46+
};
47+
48+
/** Walk up to the workspace root — the directory holding pnpm-workspace.yaml. */
49+
function findUp(predicate: (dir: string) => boolean): string {
50+
let dir = HERE;
51+
for (;;) {
52+
if (predicate(dir)) return dir;
53+
const parent = dirname(dir);
54+
if (parent === dir) throw new Error('workspace root not found from ' + HERE);
55+
dir = parent;
56+
}
57+
}
58+
const REPO = findUp((dir) => existsSync(join(dir, 'pnpm-workspace.yaml')));
59+
60+
// The artefact, from DISK — the same bytes `resolveSduiManifest()` (packages/
61+
// cli, path 1: join(process.cwd(), 'sdui.manifest.json')) picks up when the
62+
// gate runs from the repo root. Loud absence: an absent artefact silently
63+
// reverts production to parse-only, so this read failing IS the regression.
64+
const ARTEFACT = join(REPO, 'sdui.manifest.json');
65+
const manifest = JSON.parse(readFileSync(ARTEFACT, 'utf8'));
66+
67+
describe('production witness: the checked-in manifest reaches validateTree', () => {
68+
it('is the real artefact (57-component public tier, no intrinsic HTML tags)', () => {
69+
const keys = Object.keys(manifest.components);
70+
expect(keys.length).toBeGreaterThan(0);
71+
// The vocabulary facts the ratchet below stands on. If a regeneration
72+
// legitimately changes them, the ledger is re-derived in the same PR.
73+
expect(keys).toContain('flex');
74+
expect(keys).toContain('html');
75+
expect(keys).not.toContain('div');
76+
});
77+
78+
it('arms full validation through validateJsxPages: manifest-only diagnostics fire', () => {
79+
const stack = {
80+
pages: [
81+
{
82+
name: 'witness_page',
83+
kind: 'html',
84+
// `flex` is a real public component; `no-such-block` is not. Only
85+
// validateTree (fed by the DISK manifest) can tell them apart —
86+
// parse-only cannot emit unknown-component at all.
87+
source: '<flex direction="col" notARealProp="x"><no-such-block /></flex>',
88+
},
89+
],
90+
};
91+
const wired = validateJsxPages(stack, { manifest });
92+
const rules = new Set(wired.map((f) => f.rule));
93+
expect(rules).toContain('jsx-unknown-component'); // no-such-block, judged by the manifest
94+
expect(rules).toContain('jsx-unknown-prop'); // notARealProp on flex, judged by flex's declared inputs
95+
96+
// The arming delta itself: the SAME stack, parse-only, emits neither.
97+
const parseOnly = validateJsxPages(stack);
98+
const parseOnlyRules = new Set(parseOnly.map((f) => f.rule));
99+
expect(parseOnlyRules).not.toContain('jsx-unknown-component');
100+
expect(parseOnlyRules).not.toContain('jsx-unknown-prop');
101+
});
102+
103+
it('threads through the production registry entry (runAuthoringRules ctx.sduiManifest)', () => {
104+
// The same seam `os validate`/`os build`/`os lint` drive: authoring-rules'
105+
// validateJsxPages entry reads ctx.sduiManifest — never a lookalike call.
106+
const stack = {
107+
pages: [{ name: 'witness_page', kind: 'html', source: '<no-such-block />' }],
108+
};
109+
const withManifest = runAuthoringRules('validate', {
110+
normalized: stack,
111+
sduiManifest: manifest,
112+
});
113+
expect(withManifest.some((f) => f.rule === 'jsx-unknown-component')).toBe(true);
114+
115+
const without = runAuthoringRules('validate', { normalized: stack });
116+
expect(without.some((f) => f.rule === 'jsx-unknown-component')).toBe(false);
117+
});
118+
});
119+
120+
describe('first-wiring ratchet: the shipped pages against the wired gate (ui#6779 ratchet-to-zero)', () => {
121+
it('wired census over the three shipped html pages equals the ledger — both directions', () => {
122+
const stack = { pages: [CapabilityMapPage, CommandCenterJsxPage, StartHerePage] };
123+
const findings = validateJsxPages(stack as never, { manifest });
124+
125+
const census = new Map<string, number>();
126+
for (const f of findings) {
127+
const page = /page "([^"]+)"/.exec(f.where)?.[1] ?? '(unknown page)';
128+
const tag = /<([a-zA-Z0-9:_-]+)>/.exec(f.where)?.[1] ?? '(no tag)';
129+
const key = `${page}|${f.rule}|${f.severity}|${tag}`;
130+
census.set(key, (census.get(key) ?? 0) + 1);
131+
}
132+
133+
const recorded = new Map<string, number>(
134+
ledger.findings.map((r): [string, number] => [`${r.page}|${r.rule}|${r.severity}|${r.tag}`, r.count]),
135+
);
136+
137+
const newViolations: string[] = [];
138+
for (const [key, count] of census) {
139+
const allowed = recorded.get(key) ?? 0;
140+
if (count > allowed) newViolations.push(`${key} — live ${count} vs ledger ${allowed}`);
141+
}
142+
const stale: string[] = [];
143+
for (const [key, count] of recorded) {
144+
const live = census.get(key) ?? 0;
145+
if (live < count) stale.push(`${key} — ledger ${count} vs live ${live}`);
146+
}
147+
148+
expect(
149+
newViolations,
150+
'NEW wired-gate findings beyond the ratchet ledger. Fix the page (or regenerate the manifest ' +
151+
'if the vocabulary legitimately grew) — never grow packages/lint/src/sdui-jsx-baseline.json.',
152+
).toEqual([]);
153+
expect(
154+
stale,
155+
'STALE ledger rows — the live run no longer produces them. Ratchet-to-zero: delete these rows ' +
156+
'from packages/lint/src/sdui-jsx-baseline.json in this same PR.',
157+
).toEqual([]);
158+
});
159+
160+
it('parse-only over the same pages stays clean (today\'s pre-wiring behavior, pinned)', () => {
161+
const stack = { pages: [CapabilityMapPage, CommandCenterJsxPage, StartHerePage] };
162+
expect(validateJsxPages(stack as never)).toEqual([]);
163+
});
164+
});

scripts/check-ci-filter-parity.mjs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -603,14 +603,18 @@ export async function selfTest() {
603603
// single-file `crosspkg` entry the way #10848's SKILL.md is. Plus, since
604604
// #12201, the one declaration under the `skills/**` root that card added
605605
// (the export-list corpus gate reads the published catalog from inside
606-
// @objectstack/spec). Ten plus one plus two plus one: the rollback now
607-
// uncovers fourteen. This pin is judged over the LIVE declaration table on
606+
// @objectstack/spec). Plus, since #12924, the one repo-root declaration that
607+
// card added (the checked-in SDUI manifest artefact @objectstack/lint's
608+
// production-witness suite reads from the workspace root), covered only
609+
// through its own single-file `crosspkg` entry the way #10848's SKILL.md is.
610+
// Ten plus one plus two plus one plus one: the rollback now uncovers
611+
// fifteen. This pin is judged over the LIVE declaration table on
608612
// purpose: a declaration added under a root the rollback keeps leaves the
609613
// count alone, one under a new root moves it and is recorded here by name.
610614
const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS);
611615
assert(
612-
new Set(uncoveredGlobs(preFix)).size === 14,
613-
`rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one -- got ${new Set(uncoveredGlobs(preFix)).size}`,
616+
new Set(uncoveredGlobs(preFix)).size === 15,
617+
`rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one -- got ${new Set(uncoveredGlobs(preFix)).size}`,
614618
);
615619
assert(
616620
uncoveredGlobs(preFix).includes('skills/**'),
@@ -628,6 +632,10 @@ export async function selfTest() {
628632
uncoveredGlobs(preFix).includes('skills/objectstack-automation/SKILL.md'),
629633
`-- and #10178 added the automation skill file, by name`,
630634
);
635+
assert(
636+
uncoveredGlobs(preFix).includes('sdui.manifest.json'),
637+
`-- and #12924 added the repo-root SDUI manifest artefact, by name`,
638+
);
631639

632640
// ── (7) WIRING: the gate and its self-test really run in CI ──────────────
633641
const SELF = 'scripts/check-ci-filter-parity.mjs';
@@ -652,7 +660,7 @@ export async function selfTest() {
652660
`same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` +
653661
`\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` +
654662
`stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` +
655-
`pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one, ` +
663+
`pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one, ` +
656664
`and the CI wiring read out of lint.yml.`,
657665
);
658666
return 0;

0 commit comments

Comments
 (0)