diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8d0e2f2553..e03f0eb97c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -144,6 +144,29 @@ jobs: - name: Check generated reference docs are in sync with the spec run: pnpm --filter @objectstack/spec check:docs + # Same class, same reasoning, different surface: skills/*/references/_index.md + # and the objectstack-ui react-blocks contract are generated from + # packages/spec/src and committed, and nothing regenerated them either. These + # ship to third parties via `npx skills add objectstack-ai/framework`, so the + # drift is served straight to consumers' agents — 6 of 113 schema pointers named + # files the spec had already deleted or renamed. + # + # Not in ci.yml's `check-generated` job for the reason above: its `generated` + # filter lists specific spec paths (migrations/, conversions/, protocol-version) + # but no schema dirs, so a PR touching src/data/** or src/ui/** — exactly what + # drives these two artifacts — never triggers it. It is not required, either. + # + # Both read packages/spec/src via tsx and need no build (verified with every + # workspace dist/ removed), so they run before the workspace build. check:skill-refs + # additionally fails on a SKILL_MAP entry naming a file the spec no longer has: + # that silent skip is what let the map keep pointing at data/dataset.zod.ts for a + # year after #1620 renamed it to data/seed.zod.ts. + - name: Check generated skill references are in sync with the spec + run: pnpm --filter @objectstack/spec check:skill-refs + + - name: Check the react-blocks contract is in sync with the spec + run: pnpm --filter @objectstack/spec check:react-blocks + # Example apps are AI-authoring reference templates; a red typecheck is a # bad signal to copy from. tsup transpiles them without a full typecheck, # so build alone will not catch type drift — typecheck them explicitly. diff --git a/packages/spec/package.json b/packages/spec/package.json index 95b931fe29..a6c5ed2ba0 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -190,6 +190,7 @@ "gen:docs": "tsx scripts/build-docs.ts", "check:docs": "pnpm gen:schema && tsx scripts/build-docs.ts --check", "gen:skill-refs": "tsx scripts/build-skill-references.ts", + "check:skill-refs": "tsx scripts/build-skill-references.ts --check", "gen:skill-docs": "tsx scripts/build-skill-docs.ts", "check:skill-docs": "tsx scripts/build-skill-docs.ts --check", "analyze": "tsx scripts/analyze-bundle-size.ts", @@ -205,6 +206,7 @@ "test:coverage": "vitest run --coverage", "check:liveness": "tsx scripts/liveness/check-liveness.mts", "gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts", + "check:react-blocks": "tsx scripts/build-react-blocks-contract.ts --check", "check:react-conformance": "tsx scripts/check-react-blocks-conformance.ts" }, "keywords": [ diff --git a/packages/spec/scripts/build-react-blocks-contract.ts b/packages/spec/scripts/build-react-blocks-contract.ts index 6779cfa1e1..fd92e18496 100644 --- a/packages/spec/scripts/build-react-blocks-contract.ts +++ b/packages/spec/scripts/build-react-blocks-contract.ts @@ -8,19 +8,24 @@ // - skills/objectstack-ui/contracts/react-blocks.contract.json (machine) // - skills/objectstack-ui/references/react-blocks.md (AI-facing) // -// Run: pnpm --filter @objectstack/spec gen:react-blocks +// Run: +// pnpm --filter @objectstack/spec gen:react-blocks # write +// pnpm --filter @objectstack/spec check:react-blocks # verify in sync (CI) process.env.OS_EAGER_SCHEMAS = '1'; -import fs from 'fs'; import path from 'path'; import { z } from 'zod'; 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'); +const { emit, flush } = createSink({ check: CHECK, repoRoot: REPO }); + // ---- JSON-schema prop extraction ----------------------------------------- function resolveRoot(js: any): any { // zod v4 may wrap the root in { $ref: '#/$defs/X', $defs: { X: {...} } }. @@ -100,7 +105,7 @@ const contract = { 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 (a pre-styled Sheet/Dialog) — never hand-roll a fixed inset-0 backdrop.", blocks, }; -fs.writeFileSync(OUT_JSON, JSON.stringify(contract, null, 2) + '\n'); +emit(OUT_JSON, JSON.stringify(contract, null, 2) + '\n'); // markdown const esc = (s: string) => String(s).replace(/\|/g, '\\|'); @@ -134,7 +139,30 @@ L.push('## Injected scope (closure variables, reference directly — not props)' L.push(''); L.push('`React` · `useAdapter` · `data` · `variables` · `page`. Kanban/calendar/gantt/timeline/map of an object = `` with the matching visualization, or ``.'); L.push(''); -fs.writeFileSync(OUT_MD, L.join('\n')); +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_JSON)} + ${path.relative(REPO, OUT_MD)}`); for (const b of blocks) console.log(` <${b.tag}> ${b.props.length} props`); + +flush({ + surface: 'skills/objectstack-ui/ react-blocks contract', + regenerate: ' pnpm --filter @objectstack/spec gen:react-blocks\n git add skills/objectstack-ui', + guard: () => { + // Guard the two ways this generator can emit a plausible-but-empty contract + // and have --check compare it against nothing meaningful. + if (blocks.length === 0) return 'REACT_BLOCKS is empty — nothing to generate from packages/spec/src/ui/react-blocks.ts.'; + // `dataProps()` swallows a z.toJSONSchema failure and returns [], so schemas + // that fail to load degrade to a contract carrying only the hand-authored + // overlay props rather than crashing. Checking `props.length` would miss it + // (the overlay is still there) — the data props are what vanish. + const specBacked = blocks.filter((b) => b.specSchema); + const withData = specBacked.filter((b) => b.props.some((p) => p.kind === 'data')); + if (specBacked.length > 0 && withData.length === 0) { + return ( + `All ${specBacked.length} spec-backed blocks resolved 0 data props — the spec schemas failed to load\n` + + ` (z.toJSONSchema errors are swallowed in dataProps()), so this is the overlay alone, not a real contract.` + ); + } + return null; + }, +}); diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index af5c3f3131..658b6b02ab 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -18,11 +18,14 @@ * 3. Writes `skills/{name}/references/_index.md` with pointers + one-line * descriptions extracted from each file's leading JSDoc comment * - * Usage: tsx scripts/build-skill-references.ts + * Usage: + * tsx scripts/build-skill-references.ts # write + * tsx scripts/build-skill-references.ts --check # verify in sync (CI); exit 1 on drift */ import fs from 'fs'; import path from 'path'; +import { createSink, type Owns } from './lib/generated-output'; // ── Paths ──────────────────────────────────────────────────────────────────── @@ -31,6 +34,9 @@ const SPEC_SRC = path.resolve(__dirname, '../src'); const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills'); const SPEC_PKG = '@objectstack/spec'; +const CHECK = process.argv.includes('--check'); +const { emit, manageDir, flush } = createSink({ check: CHECK, repoRoot: REPO_ROOT }); + // ── Skill → Zod file mapping ──────────────────────────────────────────────── // Paths are relative to packages/spec/src/ (category/file.zod.ts) @@ -41,7 +47,7 @@ const SKILL_MAP: Record = { 'data/validation.zod.ts', 'data/hook.zod.ts', 'data/datasource.zod.ts', - 'data/dataset.zod.ts', + 'data/seed.zod.ts', 'security/permission.zod.ts', ], 'objectstack-query': [ @@ -94,7 +100,7 @@ const SKILL_MAP: Record = { // project setup (was objectstack-quickstart) 'kernel/manifest.zod.ts', 'data/datasource.zod.ts', - 'data/dataset.zod.ts', + 'data/seed.zod.ts', // plugin development (was objectstack-plugin) 'kernel/plugin.zod.ts', 'kernel/context.zod.ts', @@ -130,15 +136,26 @@ function extractLocalImports(filePath: string): string[] { return imports; } -function resolveAll(entryFiles: string[]): string[] { +/** + * Transitive closure of a skill's core files. + * + * Only SKILL_MAP entries can be `missing` — `extractLocalImports` resolves + * against disk, so a dep that doesn't exist is never queued. A dangling entry + * is therefore an authoring bug in SKILL_MAP, not a spec change to absorb, and + * the caller fails on it: silently dropping the pointer is how the skill ends + * up advertising a schema it no longer references (`data/dataset.zod.ts` + * lingered here for a year after #1620 renamed it to `data/seed.zod.ts`). + */ +function resolveAll(entryFiles: string[]): { files: string[]; missing: string[] } { const visited = new Set(); + const missing: string[] = []; const queue = [...entryFiles]; while (queue.length > 0) { const rel = queue.shift()!; if (visited.has(rel)) continue; const abs = path.resolve(SPEC_SRC, rel); if (!fs.existsSync(abs)) { - console.warn(` ⚠ File not found: ${rel} (skipped)`); + missing.push(rel); continue; } visited.add(rel); @@ -146,7 +163,7 @@ function resolveAll(entryFiles: string[]): string[] { if (!visited.has(dep)) queue.push(dep); } } - return [...visited].sort(); + return { files: [...visited].sort(), missing }; } // ── JSDoc description extractor ────────────────────────────────────────────── @@ -226,57 +243,73 @@ function generateIndex(skillName: string, coreFiles: string[], allFiles: string[ return lines.join('\n'); } -// ── Cleanup helper ─────────────────────────────────────────────────────────── +// ── Managed scope ──────────────────────────────────────────────────────────── -function cleanReferencesDir(refsDir: string) { - if (!fs.existsSync(refsDir)) { - fs.mkdirSync(refsDir, { recursive: true }); - return; - } - for (const entry of fs.readdirSync(refsDir)) { - // Keep hand-written markdown other than _index.md untouched. - if (entry === '_index.md') { - fs.rmSync(path.resolve(refsDir, entry)); - continue; - } - const entryPath = path.resolve(refsDir, entry); - const stat = fs.statSync(entryPath); - if (stat.isDirectory()) { - fs.rmSync(entryPath, { recursive: true }); - } else if (entry.endsWith('.zod.ts')) { - fs.rmSync(entryPath); - } - } +/** + * Which paths under a skill's `references/` folder this generator owns — i.e. + * would delete and rewrite on a real run, and must therefore flag as stale + * under `--check`. + * + * The folder is only *partially* ours: `react-blocks.md` is written by + * build-react-blocks-contract.ts and hand-written notes are allowed, so both + * are left alone. Subfolders and loose `*.zod.ts` are leftovers from the + * retired bundled-schema layout (skills used to carry copies of the schemas); + * we still sweep them so an old checkout converges. + */ +function ownsReferenceEntry(refsDir: string): Owns { + return (abs) => { + const rel = path.relative(refsDir, abs); + if (!rel || rel.startsWith('..')) return false; + if (rel.includes(path.sep)) return true; // inside a subfolder → wiped wholesale + return rel === '_index.md' || rel.endsWith('.zod.ts'); + }; } // ── Main ───────────────────────────────────────────────────────────────────── function main() { console.log('🔗 Building skill schema reference indexes...\n'); + const problems: string[] = []; let totalSkills = 0; for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) { const skillDir = path.resolve(SKILLS_DIR, skillName); if (!fs.existsSync(skillDir)) { - console.warn(`⚠ Skill directory not found: ${skillName}, skipping`); + problems.push(`${skillName} → no such skill directory under skills/`); continue; } console.log(`📦 ${skillName}`); - const allFiles = resolveAll(coreFiles); + const { files: allFiles, missing } = resolveAll(coreFiles); + for (const m of missing) problems.push(`${skillName} → ${m} (no such file under packages/spec/src)`); console.log(` ${coreFiles.length} core + ${allFiles.length - coreFiles.length} deps`); const refsDir = path.resolve(skillDir, 'references'); - cleanReferencesDir(refsDir); - - fs.writeFileSync( - path.resolve(refsDir, '_index.md'), - generateIndex(skillName, coreFiles, allFiles), - ); + manageDir(refsDir, ownsReferenceEntry(refsDir)); + emit(path.resolve(refsDir, '_index.md'), generateIndex(skillName, coreFiles, allFiles)); totalSkills += 1; } - console.log(`\n✅ Done — ${totalSkills} skill index files written`); + flush({ + surface: 'skills/*/references/_index.md', + regenerate: ' pnpm --filter @objectstack/spec gen:skill-refs\n git add skills', + guard: () => { + // A dangling SKILL_MAP entry drops a schema pointer from a shipped skill. + // Dropping it quietly is what this generator used to do; fail instead, in + // both modes — the map is authored config, so this is always a bug in it. + if (problems.length) { + return ( + `SKILL_MAP is out of sync with packages/spec/src:\n\n` + + problems.map((p) => ` - ${p}`).join('\n') + + `\n\n Fix the mapping in ${path.relative(REPO_ROOT, __filename)} — point it at the\n` + + ` schema's current path, or drop the entry if the concept is gone.` + ); + } + // Nothing emitted means nothing compared; "no drift" would read as green. + if (totalSkills === 0) return `No skills found under ${path.relative(REPO_ROOT, SKILLS_DIR)} — nothing to generate.`; + return null; + }, + }); } main(); diff --git a/packages/spec/scripts/lib/generated-output.ts b/packages/spec/scripts/lib/generated-output.ts new file mode 100644 index 0000000000..019908611b --- /dev/null +++ b/packages/spec/scripts/lib/generated-output.ts @@ -0,0 +1,142 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Shared output sink for the spec's generators. + * + * Every generated file goes through `emit()`, every wholesale-regenerated + * folder through `manageDir()`. Nothing touches the output tree until + * `flush()`, so the write and `--check` modes run byte-for-byte identical + * generation logic and differ only in the final disposition — write to disk, + * or compare against it. That shared path is what makes `--check` trustworthy: + * it cannot pass on output a real run wouldn't produce, because it *is* the + * real run minus the writes. + * + * `--check` reports three kinds of drift: + * + emitted but absent on disk (spec added it) + * ~ emitted and different on disk (spec changed it) + * - on disk, owned, and not emitted (spec no longer defines it) + * + * The third kind is why `manageDir()` exists. A generator that wipes its + * output folder before rewriting will delete such a file on the next real run, + * so the check must fail on it too. `git diff --exit-code` cannot see this + * class at all — a stale *untracked* leftover is invisible to it. + */ + +import fs from 'fs'; +import path from 'path'; + +/** Decides which on-disk paths a managed dir's regeneration owns. */ +export type Owns = (absPath: string) => boolean; + +export interface FlushOptions { + /** Human name of the generated surface, for drift messages. */ + surface: string; + /** Shell line that regenerates + stages the surface. */ + regenerate: string; + /** + * Guard against a vacuously-green check. A run whose inputs failed to load + * emits nothing, and "nothing differs" would read as success — the gate + * would pass while checking nothing at all. Return a message to fail with, + * or null when the inputs look sane. + */ + guard?: () => string | null; +} + +export function createSink(options: { check: boolean; repoRoot: string }) { + const { check, repoRoot } = options; + + /** Absolute path → intended content. */ + const emitted = new Map(); + /** Absolute dir → predicate for the paths its regeneration owns. */ + const managed = new Map(); + + const rel = (p: string) => path.relative(repoRoot, p); + + function emit(filePath: string, content: string): void { + emitted.set(path.resolve(filePath), content); + } + + /** + * Mark `dir` as regenerated wholesale. `owns` narrows that to part of the + * folder when the generator only replaces some entries and leaves the rest + * (hand-written files, another generator's output) alone; it defaults to the + * whole tree. + */ + function manageDir(dir: string, owns: Owns = () => true): void { + managed.set(path.resolve(dir), owns); + } + + function walk(dir: string): string[] { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { + const full = path.join(dir, e.name); + return e.isDirectory() ? walk(full) : [full]; + }); + } + + /** Every on-disk path a real run would delete before rewriting. */ + function ownedOnDisk(): string[] { + return [...managed].flatMap(([dir, owns]) => walk(dir).filter(owns)); + } + + /** Remove now-empty dirs left behind after deleting owned files. */ + function pruneEmptyDirs(root: string): void { + if (!fs.existsSync(root)) return; + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const child = path.join(root, entry.name); + pruneEmptyDirs(child); + if (fs.readdirSync(child).length === 0) fs.rmdirSync(child); + } + } + + function flush(opts: FlushOptions): void { + const failure = opts.guard?.(); + if (failure) { + console.error(`\n✗ ${failure}\n`); + process.exit(1); + } + + if (!check) { + for (const file of ownedOnDisk()) fs.rmSync(file, { force: true }); + for (const dir of managed.keys()) pruneEmptyDirs(dir); + for (const [file, content] of emitted) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); + } + console.log(`\n✅ Generated ${emitted.size} files`); + return; + } + + const added: string[] = []; + const changed: string[] = []; + for (const [file, content] of emitted) { + if (!fs.existsSync(file)) added.push(rel(file)); + else if (fs.readFileSync(file, 'utf-8') !== content) changed.push(rel(file)); + } + const stale = ownedOnDisk() + .filter((f) => !emitted.has(path.resolve(f))) + .map(rel); + + const drift = [ + ...added.map((f) => ` + ${f} (missing — spec adds it)`), + ...changed.map((f) => ` ~ ${f} (out of date)`), + ...stale.map((f) => ` - ${f} (stale — spec no longer defines it)`), + ]; + + if (drift.length === 0) { + console.log(`✅ ${emitted.size} generated files in sync with packages/spec`); + return; + } + + console.error( + `\n✗ ${opts.surface} is out of date with packages/spec:\n\n` + + drift.join('\n') + + `\n\nThese files are GENERATED — do not hand-edit them. Regenerate and commit:\n\n` + + `${opts.regenerate}\n`, + ); + process.exit(1); + } + + return { emit, manageDir, flush }; +} diff --git a/skills/objectstack-ai/references/_index.md b/skills/objectstack-ai/references/_index.md index ee0022a628..ac511bf4da 100644 --- a/skills/objectstack-ai/references/_index.md +++ b/skills/objectstack-ai/references/_index.md @@ -20,9 +20,11 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies - `node_modules/@objectstack/spec/src/automation/state-machine.zod.ts` — XState-inspired State Machine Protocol +- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/lazy-schema.ts` — Wrap a Zod schema constructor so its body is only evaluated on first use. +- `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) ## How to read these diff --git a/skills/objectstack-api/references/_index.md b/skills/objectstack-api/references/_index.md index 1ed954518b..f49bb97771 100644 --- a/skills/objectstack-api/references/_index.md +++ b/skills/objectstack-api/references/_index.md @@ -29,8 +29,6 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/http.zod.ts` — Shared HTTP Schemas - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/lazy-schema.ts` — Wrap a Zod schema constructor so its body is only evaluated on first use. -- `node_modules/@objectstack/spec/src/system/encryption.zod.ts` — Field-level encryption protocol -- `node_modules/@objectstack/spec/src/system/masking.zod.ts` — Data masking protocol for PII protection ## How to read these diff --git a/skills/objectstack-automation/references/_index.md b/skills/objectstack-automation/references/_index.md index 79a4322d80..8a914ac250 100644 --- a/skills/objectstack-automation/references/_index.md +++ b/skills/objectstack-automation/references/_index.md @@ -10,18 +10,19 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/automation/approval.zod.ts` — Approval Step Approver Type - `node_modules/@objectstack/spec/src/automation/execution.zod.ts` — Automation Execution Protocol -- `node_modules/@objectstack/spec/src/automation/flow.zod.ts` — Flow Node Types +- `node_modules/@objectstack/spec/src/automation/flow.zod.ts` — Flow Node Types — **built-in seed set** (ADR-0018). - `node_modules/@objectstack/spec/src/automation/node-executor.zod.ts` — Node Executor Plugin Protocol — Wait Node Pause/Resume - `node_modules/@objectstack/spec/src/automation/state-machine.zod.ts` — XState-inspired State Machine Protocol - `node_modules/@objectstack/spec/src/automation/trigger-registry.zod.ts` — Trigger Registry Protocol - `node_modules/@objectstack/spec/src/automation/webhook.zod.ts` — Webhook Trigger Event -- `node_modules/@objectstack/spec/src/automation/workflow.zod.ts` — Trigger events for workflow automation ## Transitive dependencies +- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/lazy-schema.ts` — Wrap a Zod schema constructor so its body is only evaluated on first use. +- `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) ## How to read these diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index 5934be89b6..6a9ebc7417 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -8,25 +8,26 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas -- `node_modules/@objectstack/spec/src/data/dataset.zod.ts` — Data Import Strategy - `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Driver Identifier - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum - `node_modules/@objectstack/spec/src/data/hook.zod.ts` — Hook Lifecycle Events - `node_modules/@objectstack/spec/src/data/object.zod.ts` — API Operations Enum +- `node_modules/@objectstack/spec/src/data/seed.zod.ts` — Seed Import Strategy - `node_modules/@objectstack/spec/src/data/validation.zod.ts` — ObjectStack Validation Protocol - `node_modules/@objectstack/spec/src/security/permission.zod.ts` — Entity (Object) Level Permissions ## Transitive dependencies -- `node_modules/@objectstack/spec/src/automation/state-machine.zod.ts` — XState-inspired State Machine Protocol - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. +- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/public-auth-features.ts` — Public auth feature-flag registry (#2874) - `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/http.zod.ts` — Shared HTTP Schemas - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/lazy-schema.ts` — Wrap a Zod schema constructor so its body is only evaluated on first use. -- `node_modules/@objectstack/spec/src/system/encryption.zod.ts` — Field-level encryption protocol -- `node_modules/@objectstack/spec/src/system/masking.zod.ts` — Data masking protocol for PII protection +- `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) +- `node_modules/@objectstack/spec/src/shared/visibility.ts` — Conditional-visibility predicate normalization (ADR-0089) - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Object Schema - `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md index d9799b473f..14bdbb941a 100644 --- a/skills/objectstack-platform/references/_index.md +++ b/skills/objectstack-platform/references/_index.md @@ -8,11 +8,11 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas -- `node_modules/@objectstack/spec/src/data/dataset.zod.ts` — Data Import Strategy - `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Driver Identifier +- `node_modules/@objectstack/spec/src/data/seed.zod.ts` — Seed Import Strategy - `node_modules/@objectstack/spec/src/kernel/context.zod.ts` — Runtime Mode Enum - `node_modules/@objectstack/spec/src/kernel/feature.zod.ts` — Feature Rollout Strategy -- `node_modules/@objectstack/spec/src/kernel/manifest.zod.ts` — Schema for the ObjectStack Manifest. +- `node_modules/@objectstack/spec/src/kernel/manifest.zod.ts` — Structured permission grants requested by a plugin (ADR-0025 §3.2). - `node_modules/@objectstack/spec/src/kernel/metadata-plugin.zod.ts` — Metadata Plugin Protocol - `node_modules/@objectstack/spec/src/kernel/plugin-capability.zod.ts` — Plugin Capability Protocol - `node_modules/@objectstack/spec/src/kernel/plugin-lifecycle-events.zod.ts` — Plugin Lifecycle Events Protocol @@ -22,12 +22,21 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies +- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum +- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. - `node_modules/@objectstack/spec/src/kernel/cluster.zod.ts` — Cluster Protocol - `node_modules/@objectstack/spec/src/kernel/metadata-customization.zod.ts` — Metadata Customization Layer Protocol - `node_modules/@objectstack/spec/src/kernel/metadata-loader.zod.ts` — Metadata Loader Protocol +- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/public-auth-features.ts` — Public auth feature-flag registry (#2874) - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/lazy-schema.ts` — Wrap a Zod schema constructor so its body is only evaluated on first use. +- `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) +- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema +- `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Base Navigation Item Schema +- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Object Schema +- `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol ## How to read these diff --git a/skills/objectstack-ui/contracts/react-blocks.contract.json b/skills/objectstack-ui/contracts/react-blocks.contract.json index f94ddc3b72..16776edc42 100644 --- a/skills/objectstack-ui/contracts/react-blocks.contract.json +++ b/skills/objectstack-ui/contracts/react-blocks.contract.json @@ -113,7 +113,7 @@ "type": "string", "kind": "data", "required": false, - "description": "Drawer width, e.g. \"480px\" (drawer forms)" + "description": "[DEPRECATED → size buckets] Drawer width, e.g. \"480px\". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it." }, { "name": "modalSize", @@ -131,7 +131,7 @@ }, { "name": "sections", - "type": "object[]", + "type": "any[]", "kind": "data", "required": false, "description": "" @@ -478,11 +478,11 @@ "description": "Field on related object that points to this record (e.g., \"account_id\")" }, { - "name": "columns", - "type": "string[]", + "name": "relationshipValueField", + "type": "string", "kind": "data", "required": true, - "description": "Fields to display in the related list" + "description": "Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions)." }, { "name": "limit", @@ -498,6 +498,13 @@ "required": true, "description": "Show \"View All\" link to see all related records" }, + { + "name": "columns", + "type": "string[]", + "kind": "data", + "required": false, + "description": "Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list …" + }, { "name": "sort", "type": "string | object[]", diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 6c89f333f0..415d5256b8 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -25,13 +25,15 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Field Type Enum - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. +- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/public-auth-features.ts` — Public auth feature-flag registry (#2874) - `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: AggregationFunctionEnum, SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/http.zod.ts` — Shared HTTP Schemas - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema - `node_modules/@objectstack/spec/src/shared/lazy-schema.ts` — Wrap a Zod schema constructor so its body is only evaluated on first use. -- `node_modules/@objectstack/spec/src/system/encryption.zod.ts` — Field-level encryption protocol -- `node_modules/@objectstack/spec/src/system/masking.zod.ts` — Data masking protocol for PII protection +- `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) +- `node_modules/@objectstack/spec/src/shared/visibility.ts` — Conditional-visibility predicate normalization (ADR-0089) - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Object Schema - `node_modules/@objectstack/spec/src/ui/keyboard.zod.ts` — Focus Trap Configuration Schema - `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index 0325419b84..2812d73e6b 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -31,10 +31,10 @@ Server-connected create/edit/view form for one object. Config props come from th | `columns` | `integer` | data | | Number of columns for the form body | | `tabPosition` | `'top' \| 'bottom' \| 'left' \| 'right'` | data | | Tab strip position (tabbed forms) | | `drawerSide` | `'top' \| 'bottom' \| 'left' \| 'right'` | data | | Drawer side (drawer forms) | -| `drawerWidth` | `string` | data | | Drawer width, e.g. "480px" (drawer forms) | +| `drawerWidth` | `string` | data | | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | | `modalSize` | `'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'` | data | | Modal size (modal forms) | | `splitDirection` | `'horizontal' \| 'vertical'` | data | | Split orientation (split forms) | -| `sections` | `object[]` | data | | | +| `sections` | `any[]` | data | | | | `subforms` | `object[]` | data | | Inline master-detail child collections | | `submitBehavior` | `object` | data | | Post-submit behavior | @@ -113,9 +113,10 @@ Related child records via a lookup. Config props from the spec RecordRelatedList | `objectName` | `string` | binding | | The parent object. | | `recordId` | `string \| number` | controlled | | The parent record. | | `relationshipField` | `string` | data | ✓ | Field on related object that points to this record (e.g., "account_id") | -| `columns` | `string[]` | data | ✓ | Fields to display in the related list | +| `relationshipValueField` | `string` | data | ✓ | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). | | `limit` | `integer` | data | ✓ | Number of records to display initially | | `showViewAll` | `boolean` | data | ✓ | Show "View All" link to see all related records | +| `columns` | `string[]` | data | | Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list … | | `sort` | `string \| object[]` | data | | Sort order for related records | | `filter` | `object[]` | data | | Additional filter criteria for related records | | `title` | `string` | data | | Custom title for the related list |