Skip to content

Commit 9a33cdd

Browse files
os-justinclaude
andauthored
refactor(spec,skills): gen:react-blocks emits one artifact — the markdown is the single AI-facing rendering (#15257)
The generator wrote the same table twice: a machine-readable JSON rendering (5,352 tokens) and a markdown one (3,153 tokens) — same four blocks, same prop counts, byte-identical `note`. The JSON had zero consumers in this repo beyond the register rows that named it, and zero in objectui. It is retired: the writer is dropped, the file is deleted, `--check` verifies one artifact, and every reference that named the path moves with it — the merge-driver row and its comment, the regeneration orchestrator entry, the governed-merge register's candidate and its self-test fixtures, the skills token ratchet's boundary fixture and pinned exclusion set, ADR-0082, and the one skill-body link. Measured: the published skill bundle drops from 156,787 to 151,426 tokens (-5,361, -3.4%); the generator-owned population goes 11 files to 10 and the published-file population 47 to 46. Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent c0d32cd commit 9a33cdd

8 files changed

Lines changed: 39 additions & 594 deletions

File tree

‎.gitattributes‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@
117117
# artifact that was in NEITHER list, which is how #13646 and #13335 were each found
118118
# by hand, during a merge. Eleven generators were unaccounted; each now has one.
119119
#
120-
# The three rows added here are the ones whose answer was "route": the per-skill
121-
# reference indexes (#13335) and both halves of the react-blocks contract. Their
120+
# The two rows added here are the ones whose answer was "route": the per-skill
121+
# reference indexes (#13335) and the react-blocks contract (#14296 item 3 retired
122+
# its second, JSON rendering — one artifact now, so one row). Their
122123
# neighbours got the other answer for reasons recorded per path — the skill docs and
123124
# the AI skills guide are MIXED (a spliced block in hand-written prose, so a deferral
124125
# would launder the prose; #14064 added a THIRD answer for the mixed file whose
@@ -152,5 +153,4 @@ docs/audits/2026-07-unknown-key-strictness-ledger.counts.md merge=os-regen
152153
content/docs/references/** merge=os-regen
153154
content/docs/permissions/system-context.mdx merge=os-regen
154155
skills/*/references/_index.md merge=os-regen
155-
skills/objectstack-ui/contracts/react-blocks.contract.json merge=os-regen
156156
skills/objectstack-ui/references/react-blocks.md merge=os-regen

‎docs/adr/0082-react-component-contract-governance.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ They drift silently: a component can accept a prop the spec never declared (an u
3636

3737
`packages/spec/src/ui/react-blocks.ts` is a **block→schema index**: each curated public block (`<ObjectForm>` → `FormViewSchema`, `<ListView>` → `ListViewSchema`, `<RecordDetails>` → `RecordDetailsProps`, `<ObjectChart>` → `ChartConfigSchema`, …) names its spec schema, plus a per-block `dataProps` allowlist that curates *which* schema props to surface (ADR-0080: capability ≠ contract — `ListView` has 45 schema props; the contract surfaces ~10 high-signal ones).
3838

39-
`scripts/build-react-blocks-contract.ts` generates the AI-facing contract (`skills/objectstack-ui/contracts/react-blocks.contract.json` + `references/react-blocks.md`) by reading the spec schemas (`z.toJSONSchema`, with `OS_EAGER_SCHEMAS=1` to resolve lazy schemas), taking each prop's spec-authored `.describe()`, and merging the React overlay. **Hand-authoring is rejected** — a hand-written contract drifts into fiction; a generated one is zero-drift by construction.
39+
`scripts/build-react-blocks-contract.ts` generates the AI-facing contract (`skills/objectstack-ui/references/react-blocks.md`) by reading the spec schemas (`z.toJSONSchema`, with `OS_EAGER_SCHEMAS=1` to resolve lazy schemas), taking each prop's spec-authored `.describe()`, and merging the React overlay. **Hand-authoring is rejected** — a hand-written contract drifts into fiction; a generated one is zero-drift by construction. *(#14296 item 3 = A, 2026-09-02: this generator also emitted a second, machine-readable rendering of the same table at `contracts/react-blocks.contract.json` — same four blocks, same prop counts, byte-identical `note`. It had zero consumers in either repo and charged the published skill bundle for a second copy of one table, so it is retired: the markdown is the single AI-facing output, and `check:react-blocks` verifies one artifact.)*
4040

4141
### 2. Registry `inputs` are a projection, not the protocol
4242

‎packages/spec/scripts/build-react-blocks-contract.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44
// react-blocks.ts: the `data` (config) props are read from each block's SPEC
55
// zod schema via z.toJSONSchema (single source — no re-authoring); the
66
// binding/controlled/callback props come from the hand-authored interaction
7-
// overlay. Emits:
8-
// - skills/objectstack-ui/contracts/react-blocks.contract.json (machine)
7+
// overlay. Emits ONE artifact:
98
// - skills/objectstack-ui/references/react-blocks.md (AI-facing)
109
//
10+
// It used to emit a second, machine-readable rendering of the same table at
11+
// skills/objectstack-ui/contracts/react-blocks.contract.json. That rendering
12+
// had zero consumers in either repo and cost the published skill bundle a
13+
// second copy of one table, so it was retired (#14296 item 3 = A): one table,
14+
// one rendering. Do not reinstate a second output here without a ruling.
15+
//
1116
// Run:
1217
// pnpm --filter @objectstack/spec gen:react-blocks # write
1318
// pnpm --filter @objectstack/spec check:react-blocks # verify in sync (CI)
@@ -20,7 +25,6 @@ import { REACT_BLOCKS, type ReactInteractionProp } from '../src/ui/react-blocks'
2025
import { createSink } from './lib/generated-output';
2126

2227
const REPO = path.resolve(__dirname, '../../..');
23-
const OUT_JSON = path.join(REPO, 'skills/objectstack-ui/contracts/react-blocks.contract.json');
2428
const OUT_MD = path.join(REPO, 'skills/objectstack-ui/references/react-blocks.md');
2529

2630
const CHECK = process.argv.includes('--check');
@@ -126,14 +130,13 @@ const blocks = REACT_BLOCKS.map((b) => {
126130
return { tag: b.tag, schemaType: b.schemaType, summary: b.summary, specSchema: b.schema ? true : false, props };
127131
});
128132

133+
// The contract, as the ONE rendering below prints it. `version` and `source`
134+
// were the retired JSON envelope's own fields and went with it: the markdown
135+
// states its provenance in the frontmatter `description` it emits instead.
129136
const contract = {
130-
version: 2,
131137
adr: 'ADR-0081',
132-
source: "GENERATED from the REACT_BLOCKS definition in the '@objectstack/spec/ui' module — data props from the spec zod schemas, binding/controlled/callback from the React overlay.",
133138
note: "Props each component accepts in kind:'react' page source. Reference blocks by their PascalCase tag. kind: data=declarative config (from the spec schema) · binding=connects to data · controlled=React state · callback=React function. These blocks are for DATA. Live data: const adapter = useAdapter(); adapter.find/findOne/create/update. STYLING (ADR-0065) — a page's source is runtime metadata, so the console's build-time Tailwind NEVER scans it: utility classNAMES silently produce no CSS. Do NOT use Tailwind className in page source. (a) Layout/chrome: inline style={} with hsl(var(--token)) theme colors — e.g. color:'hsl(var(--foreground))', background:'hsl(var(--card))', border:'1px solid hsl(var(--border))', and px/flex for layout. (b) Overlays: render <ObjectForm formType='drawer'|'modal' open onOpenChange> (a pre-styled Sheet/Dialog) — never hand-roll a fixed inset-0 backdrop.",
134-
blocks,
135139
};
136-
emit(OUT_JSON, JSON.stringify(contract, null, 2) + '\n');
137140

138141
// markdown
139142
const esc = (s: string) => String(s).replace(/\|/g, '\\|');
@@ -169,7 +172,7 @@ L.push('`React` · `useAdapter` · `data` · `variables` · `page`. Kanban/calen
169172
L.push('');
170173
emit(OUT_MD, L.join('\n'));
171174

172-
console.log(`react-blocks contract: ${blocks.length} blocks → ${path.relative(REPO, OUT_JSON)} + ${path.relative(REPO, OUT_MD)}`);
175+
console.log(`react-blocks contract: ${blocks.length} blocks → ${path.relative(REPO, OUT_MD)}`);
173176
for (const b of blocks) console.log(` <${b.tag}> ${b.props.length} props`);
174177

175178
flush({

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@
120120
* `evals/**` and hand-authored `references/**` are all this.
121121
*
122122
* 2. EXCLUDED — GENERATOR-OWNED outputs. `skills/<name>/references/_index.md`
123-
* is written by `gen:skill-refs` and `references/react-blocks.md` +
124-
* `contracts/react-blocks.contract.json` by `gen:react-blocks`. A ceiling
123+
* is written by `gen:skill-refs` and `references/react-blocks.md` by
124+
* `gen:react-blocks`. A ceiling
125125
* on a generated file reds on a legitimate REGENERATION — a spec author
126126
* moves an indexed headline, the generator faithfully rewrites the index,
127127
* and this gate stops them. That is the wrong actor: the generated bytes
@@ -454,8 +454,8 @@ export const CEILINGS = new Map([
454454
['skills/objectstack-pm-dispatch/rules/dev-template.md', 1838],
455455

456456
// objectstack-ui — the two authored eval files; its `references/react-blocks.md`
457-
// and `contracts/react-blocks.contract.json` are generator-owned and carry no
458-
// row here on purpose (see the boundary section).
457+
// is generator-owned and carries no row here on purpose (see the boundary
458+
// section). Its JSON twin was retired by #14296 item 3 and is gone entirely.
459459
['skills/objectstack-ui/evals/README.md', 289],
460460
['skills/objectstack-ui/evals/analytics-inline-vs-dataset.json', 1102],
461461

@@ -720,7 +720,7 @@ function selfTest() {
720720

721721
// The #12392 boundary fixtures, all real paths on this tree.
722722
const genIndex = 'skills/objectstack-ai/references/_index.md';
723-
const genContract = 'skills/objectstack-ui/contracts/react-blocks.contract.json';
723+
const genContract = 'skills/objectstack-ui/references/react-blocks.md';
724724
const authoredRef = 'skills/objectstack-data/references/data-hooks.md';
725725
const authoredRule = 'skills/objectstack-data/rules/relationships.md';
726726
const genGreen = verdict(genIndex, 754, undefined, generatorOwning(genIndex));
@@ -836,7 +836,6 @@ function selfTest() {
836836
'skills/objectstack-i18n/references/_index.md',
837837
'skills/objectstack-platform/references/_index.md',
838838
'skills/objectstack-query/references/_index.md',
839-
'skills/objectstack-ui/contracts/react-blocks.contract.json',
840839
'skills/objectstack-ui/references/_index.md',
841840
'skills/objectstack-ui/references/react-blocks.md',
842841
]],

‎scripts/pm/check-governed-merges.mjs‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ export const GENERATED_SURFACE_EXCEPTIONS = Object.freeze([
789789
Object.freeze({
790790
id: 'spec-react-blocks',
791791
ruling: '#11705',
792-
candidate: /^skills\/[^/]+\/(references\/react-blocks\.md|contracts\/react-blocks\.contract\.json)$/,
792+
candidate: /^skills\/[^/]+\/references\/react-blocks\.md$/,
793793
surfaceId: 'skills-catalog',
794794
generator: 'pnpm --filter @objectstack/spec gen:react-blocks',
795795
verify: Object.freeze({ pkg: '@objectstack/spec', script: 'scripts/build-react-blocks-contract.ts', gate: 'check:react-blocks' }),
@@ -2369,7 +2369,7 @@ const REPLAYS = [
23692369
*/
23702370
const REGISTER_SAMPLES = {
23712371
'spec-skill-refs': 'skills/objectstack-ui/references/_index.md',
2372-
'spec-react-blocks': 'skills/objectstack-ui/contracts/react-blocks.contract.json',
2372+
'spec-react-blocks': 'skills/objectstack-ui/references/react-blocks.md',
23732373
};
23742374

23752375
// Returned by `selfTest()` only after its verdict is printed. The dispatch
@@ -3425,9 +3425,14 @@ async function selfTest() {
34253425
assert('every-sample-is-matched-by-the-row-it-illustrates',
34263426
Object.entries(REGISTER_SAMPLES).every(([id, p]) => generatedExceptionFor(p)?.id === id), JSON.stringify(REGISTER_SAMPLES));
34273427
assert('a-generated-index-routes-to-the-skill-refs-generator', generatedExceptionFor(genIndex) === skillRefs);
3428-
assert('both-react-blocks-outputs-route-to-their-own-generator',
3429-
generatedExceptionFor('skills/objectstack-ui/references/react-blocks.md') === reactBlocks &&
3430-
generatedExceptionFor('skills/objectstack-ui/contracts/react-blocks.contract.json') === reactBlocks);
3428+
assert('the-react-blocks-output-routes-to-its-own-generator',
3429+
generatedExceptionFor('skills/objectstack-ui/references/react-blocks.md') === reactBlocks);
3430+
// The retirement, asserted rather than remembered (#14296 item 3 = A): the
3431+
// generator emits ONE rendering, so the JSON path it used to write is not a
3432+
// register member any more. A candidate that still matched it would hand a
3433+
// deleted path a recompute nothing can satisfy.
3434+
assert('the-retired-json-rendering-is-no-longer-a-register-member',
3435+
generatedExceptionFor('skills/objectstack-ui/contracts/react-blocks.contract.json') === null);
34313436
// ⭐ The ruled limit, stated as a membership question: hand-authored skill
34323437
// content is not on the register at all, so it never reaches a generator.
34333438
assert('a-hand-authored-skills-file-is-not-registered-and-is-never-consulted-against-a-generator',

‎scripts/regen-artifacts.mjs‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -324,21 +324,18 @@ export const REGEN_ARTIFACTS = Object.freeze([
324324
//
325325
// No `readsDist`/`readsSchemaTree`: the generator walks `src/` directly.
326326
{ path: 'skills/*/references/_index.md', gen: 'gen:skill-refs', check: 'check:skill-refs' },
327-
// #13731. The ADR-0081 react-tier contract, both halves. Generated WHOLE — the
328-
// markdown's frontmatter and its "do not edit by hand" banner are emitted by the
329-
// generator too, so there is no hand-written region for a deferral to launder,
330-
// which is the question this table exists to ask. A projection of the
331-
// `REACT_BLOCKS` definition in `@objectstack/spec/ui`, one section per block: two
332-
// PRs adding different blocks are a set union that git reports as a conflict.
327+
// #13731. The ADR-0081 react-tier contract — ONE artifact since #14296 item 3
328+
// retired its machine-readable JSON twin (the same table rendered twice, with
329+
// zero consumers). Generated WHOLE — the markdown's frontmatter and its "do not
330+
// edit by hand" banner are emitted by the generator too, so there is no
331+
// hand-written region for a deferral to launder, which is the question this
332+
// table exists to ask. A projection of the `REACT_BLOCKS` definition in
333+
// `@objectstack/spec/ui`, one section per block: two PRs adding different blocks
334+
// are a set union that git reports as a conflict.
333335
//
334336
// Same cheap-half caveat as its neighbour above, same answer: `check:react-blocks`
335337
// runs in `lint.yml` on `pull_request` and `merge_group`, re-deriving from the
336338
// definition, so the driver removes hand-merge rounds and is never the only signal.
337-
{
338-
path: 'skills/objectstack-ui/contracts/react-blocks.contract.json',
339-
gen: 'gen:react-blocks',
340-
check: 'check:react-blocks',
341-
},
342339
{
343340
path: 'skills/objectstack-ui/references/react-blocks.md',
344341
gen: 'gen:react-blocks',

0 commit comments

Comments
 (0)