Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@
# artifact that was in NEITHER list, which is how #13646 and #13335 were each found
# by hand, during a merge. Eleven generators were unaccounted; each now has one.
#
# The three rows added here are the ones whose answer was "route": the per-skill
# reference indexes (#13335) and both halves of the react-blocks contract. Their
# The two rows added here are the ones whose answer was "route": the per-skill
# reference indexes (#13335) and the react-blocks contract (#14296 item 3 retired
# its second, JSON rendering — one artifact now, so one row). Their
# neighbours got the other answer for reasons recorded per path — the skill docs and
# the AI skills guide are MIXED (a spliced block in hand-written prose, so a deferral
# would launder the prose; #14064 added a THIRD answer for the mixed file whose
Expand Down Expand Up @@ -152,5 +153,4 @@ docs/audits/2026-07-unknown-key-strictness-ledger.counts.md merge=os-regen
content/docs/references/** merge=os-regen
content/docs/permissions/system-context.mdx merge=os-regen
skills/*/references/_index.md merge=os-regen
skills/objectstack-ui/contracts/react-blocks.contract.json merge=os-regen
skills/objectstack-ui/references/react-blocks.md merge=os-regen
2 changes: 1 addition & 1 deletion docs/adr/0082-react-component-contract-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ They drift silently: a component can accept a prop the spec never declared (an u

`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).

`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.
`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.)*

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

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

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

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

// The contract, as the ONE rendering below prints it. `version` and `source`
// were the retired JSON envelope's own fields and went with it: the markdown
// states its provenance in the frontmatter `description` it emits instead.
const contract = {
version: 2,
adr: 'ADR-0081',
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.",
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.",
blocks,
};
emit(OUT_JSON, JSON.stringify(contract, null, 2) + '\n');

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

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

flush({
Expand Down
11 changes: 5 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@
* `evals/**` and hand-authored `references/**` are all this.
*
* 2. EXCLUDED — GENERATOR-OWNED outputs. `skills/<name>/references/_index.md`
* is written by `gen:skill-refs` and `references/react-blocks.md` +
* `contracts/react-blocks.contract.json` by `gen:react-blocks`. A ceiling
* is written by `gen:skill-refs` and `references/react-blocks.md` by
* `gen:react-blocks`. A ceiling
* on a generated file reds on a legitimate REGENERATION — a spec author
* moves an indexed headline, the generator faithfully rewrites the index,
* and this gate stops them. That is the wrong actor: the generated bytes
Expand Down Expand Up @@ -454,8 +454,8 @@ export const CEILINGS = new Map([
['skills/objectstack-pm-dispatch/rules/dev-template.md', 1838],

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

Expand Down Expand Up @@ -720,7 +720,7 @@ function selfTest() {

// The #12392 boundary fixtures, all real paths on this tree.
const genIndex = 'skills/objectstack-ai/references/_index.md';
const genContract = 'skills/objectstack-ui/contracts/react-blocks.contract.json';
const genContract = 'skills/objectstack-ui/references/react-blocks.md';
const authoredRef = 'skills/objectstack-data/references/data-hooks.md';
const authoredRule = 'skills/objectstack-data/rules/relationships.md';
const genGreen = verdict(genIndex, 754, undefined, generatorOwning(genIndex));
Expand Down Expand Up @@ -836,7 +836,6 @@ function selfTest() {
'skills/objectstack-i18n/references/_index.md',
'skills/objectstack-platform/references/_index.md',
'skills/objectstack-query/references/_index.md',
'skills/objectstack-ui/contracts/react-blocks.contract.json',
'skills/objectstack-ui/references/_index.md',
'skills/objectstack-ui/references/react-blocks.md',
]],
Expand Down
15 changes: 10 additions & 5 deletions scripts/pm/check-governed-merges.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ export const GENERATED_SURFACE_EXCEPTIONS = Object.freeze([
Object.freeze({
id: 'spec-react-blocks',
ruling: '#11705',
candidate: /^skills\/[^/]+\/(references\/react-blocks\.md|contracts\/react-blocks\.contract\.json)$/,
candidate: /^skills\/[^/]+\/references\/react-blocks\.md$/,
surfaceId: 'skills-catalog',
generator: 'pnpm --filter @objectstack/spec gen:react-blocks',
verify: Object.freeze({ pkg: '@objectstack/spec', script: 'scripts/build-react-blocks-contract.ts', gate: 'check:react-blocks' }),
Expand Down Expand Up @@ -2369,7 +2369,7 @@ const REPLAYS = [
*/
const REGISTER_SAMPLES = {
'spec-skill-refs': 'skills/objectstack-ui/references/_index.md',
'spec-react-blocks': 'skills/objectstack-ui/contracts/react-blocks.contract.json',
'spec-react-blocks': 'skills/objectstack-ui/references/react-blocks.md',
};

// Returned by `selfTest()` only after its verdict is printed. The dispatch
Expand Down Expand Up @@ -3425,9 +3425,14 @@ async function selfTest() {
assert('every-sample-is-matched-by-the-row-it-illustrates',
Object.entries(REGISTER_SAMPLES).every(([id, p]) => generatedExceptionFor(p)?.id === id), JSON.stringify(REGISTER_SAMPLES));
assert('a-generated-index-routes-to-the-skill-refs-generator', generatedExceptionFor(genIndex) === skillRefs);
assert('both-react-blocks-outputs-route-to-their-own-generator',
generatedExceptionFor('skills/objectstack-ui/references/react-blocks.md') === reactBlocks &&
generatedExceptionFor('skills/objectstack-ui/contracts/react-blocks.contract.json') === reactBlocks);
assert('the-react-blocks-output-routes-to-its-own-generator',
generatedExceptionFor('skills/objectstack-ui/references/react-blocks.md') === reactBlocks);
// The retirement, asserted rather than remembered (#14296 item 3 = A): the
// generator emits ONE rendering, so the JSON path it used to write is not a
// register member any more. A candidate that still matched it would hand a
// deleted path a recompute nothing can satisfy.
assert('the-retired-json-rendering-is-no-longer-a-register-member',
generatedExceptionFor('skills/objectstack-ui/contracts/react-blocks.contract.json') === null);
// ⭐ The ruled limit, stated as a membership question: hand-authored skill
// content is not on the register at all, so it never reaches a generator.
assert('a-hand-authored-skills-file-is-not-registered-and-is-never-consulted-against-a-generator',
Expand Down
19 changes: 8 additions & 11 deletions scripts/regen-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -324,21 +324,18 @@ export const REGEN_ARTIFACTS = Object.freeze([
//
// No `readsDist`/`readsSchemaTree`: the generator walks `src/` directly.
{ path: 'skills/*/references/_index.md', gen: 'gen:skill-refs', check: 'check:skill-refs' },
// #13731. The ADR-0081 react-tier contract, both halves. Generated WHOLE — the
// markdown's frontmatter and its "do not edit by hand" banner are emitted by the
// generator too, so there is no hand-written region for a deferral to launder,
// which is the question this table exists to ask. A projection of the
// `REACT_BLOCKS` definition in `@objectstack/spec/ui`, one section per block: two
// PRs adding different blocks are a set union that git reports as a conflict.
// #13731. The ADR-0081 react-tier contract — ONE artifact since #14296 item 3
// retired its machine-readable JSON twin (the same table rendered twice, with
// zero consumers). Generated WHOLE — the markdown's frontmatter and its "do not
// edit by hand" banner are emitted by the generator too, so there is no
// hand-written region for a deferral to launder, which is the question this
// table exists to ask. A projection of the `REACT_BLOCKS` definition in
// `@objectstack/spec/ui`, one section per block: two PRs adding different blocks
// are a set union that git reports as a conflict.
//
// Same cheap-half caveat as its neighbour above, same answer: `check:react-blocks`
// runs in `lint.yml` on `pull_request` and `merge_group`, re-deriving from the
// definition, so the driver removes hand-merge rounds and is never the only signal.
{
path: 'skills/objectstack-ui/contracts/react-blocks.contract.json',
gen: 'gen:react-blocks',
check: 'check:react-blocks',
},
{
path: 'skills/objectstack-ui/references/react-blocks.md',
gen: 'gen:react-blocks',
Expand Down
Loading
Loading