From 68a6a5942113da134f6844c92955d43b9915c9c7 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:15:43 +0800 Subject: [PATCH] fix(spec): regenerate drifted skills output and gate it in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `skills/` is generated from the spec by `gen:skill-refs` and `gen:react-blocks` and committed, but neither had a `check:` sibling and nothing in CI regenerated them. Both existing gates miss this surface: `check:docs` (#3134) covers only content/docs/references/, and `check:skill-docs` only SKILL.md frontmatter → README + skills-reference.mdx. So it drifted — 8 committed files were stale on main. This matters more than the content/docs case: `skills/` is published to third parties via `npx skills add objectstack-ai/framework --all`, so stale output ships to users. Fix the drift's root cause, not the output ------------------------------------------ Two published skills pointed agents at `data/dataset.zod.ts`, which #1620 renamed to `data/seed.zod.ts`. The generator warned and skipped, staying green, so objectstack-data and objectstack-platform silently lost their seed schema pointer and told agents to Read a path that no longer exists. SKILL_MAP now names seed.zod.ts, and a missing mapped file is a hard error instead of a warning — that half-rename is exactly what warn-and-skip is good at hiding. Add --check to both generators ------------------------------ Following the convention #3134 established in build-docs.ts: every write goes through emit(), every regenerated folder through manageDir(), and nothing touches the tree until flush() — so check and write share byte-for-byte identical generation logic and differ only in final disposition. Not `git diff --exit-code`, which sees only tracked files and would miss a new or leftover page. The sink is extracted to scripts/lib/generated-output.ts since both generators need it; manageDir() takes a predicate because a skill's references/ folder is a selective wipe, not a wholesale one — hand-written siblings (data-hooks.md, plugin-hooks.md) and the other generator's react-blocks.md live there and must survive. Wire into "TypeScript Type Check", not ESLint --------------------------------------------- main requires exactly 4 status checks (TypeScript Type Check, Build Core, Test Core, Dogfood Regression Gate). ESLint is not one of them, so a gate placed there runs but cannot block a merge. Same job as check:docs, same reason: no paths filter, required, and both read src/ via tsx with no build (~2s). Proven able to go red before shipping: tampered content, deleted file, leftover file, leftover in a sub-folder, stale SKILL_MAP entry, and both vacuous cases (empty SKILL_MAP / empty REACT_BLOCKS) each exit 1; a hand-written .md dropped in references/ stays green. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/lint.yml | 20 +++ packages/spec/package.json | 2 + .../scripts/build-react-blocks-contract.ts | 46 ++++- .../spec/scripts/build-skill-references.ts | 112 ++++++++----- packages/spec/scripts/lib/generated-output.ts | 158 ++++++++++++++++++ skills/objectstack-ai/references/_index.md | 2 + skills/objectstack-api/references/_index.md | 2 - .../references/_index.md | 5 +- skills/objectstack-data/references/_index.md | 9 +- .../objectstack-platform/references/_index.md | 13 +- .../contracts/react-blocks.contract.json | 17 +- skills/objectstack-ui/references/_index.md | 6 +- .../objectstack-ui/references/react-blocks.md | 7 +- 13 files changed, 329 insertions(+), 70 deletions(-) create mode 100644 packages/spec/scripts/lib/generated-output.ts diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8d0e2f2553..e75346d874 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -144,6 +144,26 @@ jobs: - name: Check generated reference docs are in sync with the spec run: pnpm --filter @objectstack/spec check:docs + # Generated-skills gate: skills/ is generated from the spec by + # `gen:skill-refs` + `gen:react-blocks` and committed, and nothing checked + # it — both had a `gen:` script but no `check:` sibling, so `check:docs` + # above (which only covers content/docs/references/) and `check:skill-docs` + # (only SKILL.md frontmatter → README + skills-reference.mdx) both miss it. + # It drifted: 8 files were stale on main, and skills/objectstack-{data, + # platform}/references/_index.md still pointed agents at + # data/dataset.zod.ts — renamed to data/seed.zod.ts back in #1620. + # + # This bites harder than the docs case: skills/ is PUBLISHED to third + # parties via `npx skills add objectstack-ai/framework --all`, so stale + # output ships to users. Same job as check:docs and for the same reason — + # it is a required status check with no paths filter, so the gate cannot go + # dormant. Both read src/ via tsx and need no build, so they fail in ~2s. + - name: Check generated skill references are in sync with the spec + run: pnpm --filter @objectstack/spec check:skill-refs + + - name: Check generated 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..6aeab8ae33 100644 --- a/packages/spec/scripts/build-react-blocks-contract.ts +++ b/packages/spec/scripts/build-react-blocks-contract.ts @@ -8,19 +8,28 @@ // - 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 +// `skills/` is published to third parties (`npx skills add … --all`), so stale +// output here ships to users — `--check` gates it in CI. +// +// Run: +// pnpm --filter @objectstack/spec gen:react-blocks # write +// pnpm --filter @objectstack/spec check:react-blocks # verify in sync (CI); exit 1 on drift 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 { createGeneratedOutput } from './lib/generated-output'; const REPO = path.resolve(__dirname, '../../..'); -const OUT_JSON = path.join(REPO, 'skills/objectstack-ui/contracts/react-blocks.contract.json'); +const CONTRACTS_DIR = path.join(REPO, 'skills/objectstack-ui/contracts'); +const OUT_JSON = path.join(CONTRACTS_DIR, 'react-blocks.contract.json'); const OUT_MD = path.join(REPO, 'skills/objectstack-ui/references/react-blocks.md'); +const CHECK = process.argv.includes('--check'); +const out = createGeneratedOutput({ repoRoot: REPO, check: CHECK }); + // ---- JSON-schema prop extraction ----------------------------------------- function resolveRoot(js: any): any { // zod v4 may wrap the root in { $ref: '#/$defs/X', $defs: { X: {...} } }. @@ -93,6 +102,17 @@ const blocks = REACT_BLOCKS.map((b) => { return { tag: b.tag, schemaType: b.schemaType, summary: b.summary, specSchema: b.schema ? true : false, props }; }); +// No blocks means the overlay failed to load, not that the contract is empty. +// Writing that would wipe the published contract; checking it would compare +// nothing and pass. Fail loudly in both modes instead. +if (blocks.length === 0) { + console.error( + `\n✗ No React blocks found in packages/spec/src/ui/react-blocks.ts — nothing to ${CHECK ? 'check' : 'write'}.\n` + + ` REACT_BLOCKS is empty; the contract would be generated with zero blocks.\n`, + ); + process.exit(1); +} + const contract = { version: 2, adr: 'ADR-0081', @@ -100,7 +120,10 @@ 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'); +// contracts/ is wholly owned by this generator, so a file in there we didn't +// emit is one a real run deletes — e.g. a contract left behind by a rename. +out.manageDir(CONTRACTS_DIR); +out.emit(OUT_JSON, JSON.stringify(contract, null, 2) + '\n'); // markdown const esc = (s: string) => String(s).replace(/\|/g, '\\|'); @@ -134,7 +157,16 @@ 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')); +out.emit(OUT_MD, L.join('\n')); + +// Disposition: write the emitted tree, or report drift against it. +out.flush({ + what: 'skills/objectstack-ui react-blocks contract', + noun: 'react-blocks contract files', + fix: ['pnpm --filter @objectstack/spec gen:react-blocks', 'git add skills'], +}); -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`); +if (!CHECK) { + console.log(`\n✅ react-blocks contract: ${blocks.length} blocks`); + for (const b of blocks) console.log(` <${b.tag}> ${b.props.length} props`); +} diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index af5c3f3131..51b1221018 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -18,11 +18,17 @@ * 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 + * `skills/` is published to third parties (`npx skills add … --all`), so stale + * output here ships to users — `--check` gates it in CI. + * + * 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 { createGeneratedOutput } from './lib/generated-output'; // ── Paths ──────────────────────────────────────────────────────────────────── @@ -31,6 +37,8 @@ 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'); + // ── Skill → Zod file mapping ──────────────────────────────────────────────── // Paths are relative to packages/spec/src/ (category/file.zod.ts) @@ -41,7 +49,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 +102,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,18 +138,27 @@ function extractLocalImports(filePath: string): string[] { return imports; } -function resolveAll(entryFiles: string[]): string[] { +function resolveAll(skillName: string, entryFiles: string[]): string[] { const visited = new Set(); const queue = [...entryFiles]; while (queue.length > 0) { - const rel = queue.shift()!; - if (visited.has(rel)) continue; - const abs = path.resolve(SPEC_SRC, rel); + const relPath = queue.shift()!; + if (visited.has(relPath)) continue; + const abs = path.resolve(SPEC_SRC, relPath); + // Only SKILL_MAP entries can miss here — transitive deps are already + // existence-filtered in extractLocalImports — so this is always a stale + // hand-authored mapping. Fatal, not a warning: warn-and-skip silently + // dropped the seed schema from two published skills across the + // dataset.zod.ts → seed.zod.ts rename, and stayed green doing it. if (!fs.existsSync(abs)) { - console.warn(` ⚠ File not found: ${rel} (skipped)`); - continue; + console.error( + `\n✗ SKILL_MAP[${skillName}] points at a file that does not exist: ${relPath}\n` + + ` Expected ${rel(path.resolve(SPEC_SRC, relPath))} — it was probably renamed or removed.\n` + + ` Update SKILL_MAP in ${rel(__filename)}.\n`, + ); + process.exit(1); } - visited.add(rel); + visited.add(relPath); for (const dep of extractLocalImports(abs)) { if (!visited.has(dep)) queue.push(dep); } @@ -226,57 +243,66 @@ function generateIndex(skillName: string, coreFiles: string[], allFiles: string[ return lines.join('\n'); } -// ── Cleanup helper ─────────────────────────────────────────────────────────── +// ── Output sink ────────────────────────────────────────────────────────────── -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); - } - } +const out = createGeneratedOutput({ repoRoot: REPO_ROOT, check: CHECK }); +const rel = (p: string) => path.relative(REPO_ROOT, p); + +/** + * What a real run removes from a skill's `references/` folder. Hand-written + * markdown alongside `_index.md` is deliberately preserved — `data-hooks.md`, + * `plugin-hooks.md`, and `react-blocks.md` (which build-react-blocks-contract.ts + * owns) all live here — so this is a selective wipe, not a wholesale one. + */ +function refsDirDeletes(relPath: string): boolean { + // Nested → lives in a sub-folder a real run removes wholesale. + if (relPath.includes(path.sep)) return true; + return relPath === '_index.md' || relPath.endsWith('.zod.ts'); } // ── Main ───────────────────────────────────────────────────────────────────── function main() { - console.log('🔗 Building skill schema reference indexes...\n'); - let totalSkills = 0; + console.log(`🔗 ${CHECK ? 'Checking' : 'Building'} skill schema reference indexes...\n`); for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) { const skillDir = path.resolve(SKILLS_DIR, skillName); + // A mapped skill that isn't on disk means SKILL_MAP lies. Skipping it would + // silently ship a skill with no schema index (and leave --check green), so + // fail instead — same reasoning as the missing-file check in resolveAll. if (!fs.existsSync(skillDir)) { - console.warn(`⚠ Skill directory not found: ${skillName}, skipping`); - continue; + console.error( + `\n✗ SKILL_MAP names a skill with no directory: ${skillName}\n` + + ` Expected ${rel(skillDir)}. Update SKILL_MAP in ${rel(__filename)}.\n`, + ); + process.exit(1); } - console.log(`📦 ${skillName}`); - const allFiles = resolveAll(coreFiles); - console.log(` ${coreFiles.length} core + ${allFiles.length - coreFiles.length} deps`); + if (!CHECK) console.log(`📦 ${skillName}`); + const allFiles = resolveAll(skillName, coreFiles); + if (!CHECK) console.log(` ${coreFiles.length} core + ${allFiles.length - coreFiles.length} deps`); const refsDir = path.resolve(skillDir, 'references'); - cleanReferencesDir(refsDir); + out.manageDir(refsDir, refsDirDeletes); + out.emit(path.resolve(refsDir, '_index.md'), generateIndex(skillName, coreFiles, allFiles)); + } - fs.writeFileSync( - path.resolve(refsDir, '_index.md'), - generateIndex(skillName, coreFiles, allFiles), + // A run that mapped no skills emits nothing, and "nothing differs" would read + // as success — the gate would pass while checking no skills at all. Fail + // loudly instead of greenly. + if (out.size === 0) { + console.error( + `\n✗ No skill reference indexes generated — nothing to ${CHECK ? 'check' : 'write'}.\n` + + ` SKILL_MAP is empty, or ${rel(SKILLS_DIR)} is missing.\n`, ); - totalSkills += 1; + process.exit(1); } - console.log(`\n✅ Done — ${totalSkills} skill index files written`); + out.flush({ + what: 'skills/*/references/_index.md', + noun: 'skill reference indexes', + fix: [`pnpm --filter ${SPEC_PKG} gen:skill-refs`, 'git add skills'], + }); } 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..1d5c9ac7ff --- /dev/null +++ b/packages/spec/scripts/lib/generated-output.ts @@ -0,0 +1,158 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Shared sink for generators that own files in the repo, so each can offer a +// trustworthy `--check` gate (the convention #3134 established in +// scripts/build-docs.ts, generalized). +// +// Every generated file goes through emit(), every regenerated folder through +// manageDir(). Nothing touches the output tree until flush(), so `--check` and a +// real write run execute 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. +// +// Why not `git diff --exit-code`: that only sees tracked files, so a generator +// that should have deleted a page — or one that emits a brand-new untracked one +// — slips through. This compares the emitted tree against the disk directly. +// +// build-docs.ts still carries its own inline copy of this pattern; it predates +// the extraction and can migrate separately. + +import fs from 'fs'; +import path from 'path'; + +/** Would a real run delete this path? `relPath` is relative to the managed dir. */ +export type DeletePredicate = (relPath: string) => boolean; + +export interface FlushMessages { + /** What the tree is, for the drift header — e.g. `skills/*​/references/_index.md`. */ + what: string; + /** Plural noun for the success line — e.g. `skill reference indexes`. */ + noun: string; + /** Shell lines that regenerate and stage the tree. */ + fix: string[]; +} + +export interface GeneratedOutput { + /** Record intended content for a file. Nothing is written until flush(). */ + emit(filePath: string, content: string): void; + /** + * Declare a folder this generator regenerates. Anything on disk inside it that + * `deletes` claims and this run didn't emit is stale — a real run would remove + * it, so `--check` reports it. Defaults to a wholesale wipe. + */ + manageDir(dir: string, deletes?: DeletePredicate): void; + /** Did this run emit that file? Lets generation ask the sink, not the disk. */ + wasEmitted(filePath: string): boolean; + /** How many files this run emitted. */ + readonly size: number; + /** Write the emitted tree, or (in check mode) report how it differs. */ + flush(messages: FlushMessages): void; +} + +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]; + }); +} + +/** A managed sub-folder is removed wholesale by a real run; deleting its files + * leaves the husk behind, so drop any directory left empty. */ +function pruneEmptyDirs(dir: string): void { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const full = path.join(dir, entry.name); + pruneEmptyDirs(full); + if (fs.readdirSync(full).length === 0) fs.rmdirSync(full); + } +} + +export function createGeneratedOutput(opts: { + repoRoot: string; + check: boolean; +}): GeneratedOutput { + const { repoRoot, check } = opts; + + /** Absolute path → intended content. */ + const emitted = new Map(); + /** Absolute dir → predicate naming the paths a real run deletes. */ + const managedDirs = new Map(); + + const rel = (p: string) => path.relative(repoRoot, p); + const wasEmitted = (filePath: string) => emitted.has(path.resolve(filePath)); + + /** On-disk paths a real run deletes because this run didn't emit them. */ + const staleFiles = (): string[] => { + const out: string[] = []; + for (const [dir, deletes] of managedDirs) { + for (const file of walk(dir)) { + if (deletes(path.relative(dir, file)) && !wasEmitted(file)) out.push(file); + } + } + return out; + }; + + return { + emit(filePath, content) { + emitted.set(path.resolve(filePath), content); + }, + + manageDir(dir, deletes = () => true) { + managedDirs.set(path.resolve(dir), deletes); + }, + + wasEmitted, + + get size() { + return emitted.size; + }, + + flush(messages) { + if (!check) { + for (const file of staleFiles()) { + fs.rmSync(file); + console.log(`✗ Removed stale ${rel(file)}`); + } + for (const dir of managedDirs.keys()) { + if (fs.existsSync(dir)) pruneEmptyDirs(dir); + } + for (const [file, content] of emitted) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); + console.log(`✓ Generated ${rel(file)}`); + } + console.log(`\n✅ Generated ${emitted.size} files`); + return; + } + + const changed: string[] = []; + const added: 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 drift = [ + ...added.map((f) => ` + ${f} (missing — a real run creates it)`), + ...changed.map((f) => ` ~ ${f} (out of date)`), + ...staleFiles().map((f) => ` - ${rel(f)} (stale — a real run deletes it)`), + ]; + + if (drift.length === 0) { + console.log(`✅ ${emitted.size} ${messages.noun} in sync with packages/spec`); + return; + } + + console.error( + `\n✗ ${messages.what} 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` + + messages.fix.map((l) => ` ${l}`).join('\n') + + `\n`, + ); + process.exit(1); + }, + }; +} 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 |