@estiva-app/ui 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/registry/cli.ts", "../../src/registry/siblings.ts", "../../src/registry/skill.ts"],
4
- "sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `estiva-ui` \u2014 the catalogue as a command (UIG-12, UIG-13; docs/GATES.md \u00A723\n * for why it ships in the package rather than as a script pasted into each repo).\n *\n * estiva-ui find <words\u2026> [--here | --also [name=]<folder>\u2026] [--json] [--limit n]\n * estiva-ui build [--out <path>]\n * estiva-ui check\n * estiva-ui skill [--out <folder>]\n *\n * Every command takes `--root <folder>` (the library it reads; default: here)\n * and `--repo <name>` (what an app's entries are called; default: its package name).\n *\n * **In the package** (`@estiva-app/ui` itself): `build` writes the committed\n * `registry.json`, and `check` rebuilds it and fails on a single byte of drift.\n *\n * **In an app**: nothing is committed (Katerina, 18 September 2026 \u2014 the\n * package is public, the apps are private). `check` builds the app's catalogue\n * and fails when a part has no one-line description or cannot be sorted, or\n * when a reusable part breaks the usage-page contract (UIG-19, `./contract`);\n * CI's `gate` job runs it. `build` writes the catalogue to a file only when asked.\n *\n * `find` searches the package's catalogue \u2014 the one shipped inside the installed\n * package, or this repo's own in the package \u2014 plus, in an app, the app's own,\n * built fresh, plus every app beside it (UIG-20, `./siblings`): found, not\n * named, so one command answers the same from any repository, or from the\n * folder that holds them. `--here` searches no neighbour; `--also` names them.\n *\n * `skill` writes the repository's Claude skill loader (UIG-20, `./skill`), or\n * the one for `--out <folder>`; `check` fails when the repository's is missing\n * or differs.\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, dirname, join, relative, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { CONTRACT_KINDS, contractProblems, linkProblems } from './contract'\nimport { findInRegistries, formatFindings } from './find'\nimport { findSiblings, workspaceOf } from './siblings'\nimport { loaderPath, loaderProblem, loaderText, repositoryOf, settingsPath, settingsWithSearch } from './skill'\nimport { CLASSES, validateRegistry, type Registry } from './schema'\n\n/**\n * The builders, loaded only when they are used.\n *\n * They read TypeScript with TypeScript, and `typescript` is a dev dependency of\n * the app that installs this package \u2014 a real one in Peek, Ship and this repo,\n * and possibly absent elsewhere. A static import made the bundler put the whole\n * builder in the same chunk as `find`, so `npx estiva-ui find \u2026` died with\n * ERR_MODULE_NOT_FOUND before opening the file it needed.\n */\nconst packageBuilder = () => import('./build')\nconst appBuilder = () => import('./app')\nconst gatedScan = () => import('./gated')\n\nconst [command, ...rest] = process.argv.slice(2)\nconst VALUED = ['--limit', '--out', '--file', '--root', '--repo', '--also']\nconst flag = (name: string) => rest.includes(`--${name}`)\nconst value = (name: string) => {\n const at = rest.indexOf(`--${name}`)\n return at === -1 ? undefined : rest[at + 1]\n}\nconst values = (name: string) => rest.flatMap((word, i) => (word === `--${name}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []))\n/** Everything that is not a flag or a flag's value: the words of the question. */\nconst plain = () => {\n const out: string[] = []\n for (let i = 0; i < rest.length; i++) {\n if (rest[i].startsWith('--')) {\n if (VALUED.includes(rest[i])) i += 1\n continue\n }\n out.push(rest[i])\n }\n return out\n}\n\nconst root = resolve(value('root') ?? '.')\nconst manifestAt = (folder: string) => JSON.parse(readFileSync(resolve(folder, 'package.json'), 'utf8')) as { name?: string }\n/** The package itself, or an app that installs it. */\nconst isPackage = existsSync(resolve(root, 'package.json')) && manifestAt(root).name === '@estiva-app/ui'\n\n/** The package's committed catalogue: `--file`, this repo's own in the package, else the copy inside the installed package. */\nfunction packageRegistryPath(): string {\n const named = value('file')\n if (named) return resolve(named)\n if (isPackage) return resolve(root, 'registry.json')\n // dist/registry/cli.js \u2192 the package's own root. Never an app's own folder:\n // a registry.json there is the app's, written by `estiva-ui build`.\n return fileURLToPath(new URL('../../registry.json', import.meta.url))\n}\n\nfunction readPackageRegistry(): Registry {\n const path = packageRegistryPath()\n if (!existsSync(path)) throw new Error(`no registry.json at ${path} \u2014 run \"estiva-ui build\" in the package first`)\n const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry\n const problems = validateRegistry(registry)\n // A malformed catalogue is worse than none: it would answer, wrongly.\n if (problems.length) throw new Error(`${path} does not match the schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/** The installed package whose skill text a loader reads: this repo in the package, else the app's install. */\nconst packageRoot = () => (isPackage ? root : resolve(root, 'node_modules', '@estiva-app', 'ui'))\n\n/** The Claude skill loader (UIG-20): reports, and says whether it passed. */\nfunction checkLoader(): boolean {\n const problem = loaderProblem(repositoryOf(root), packageRoot())\n if (problem) {\n process.stderr.write(`claude skill: ${problem}\\n`)\n return false\n }\n process.stdout.write('claude skill: the loader reads the package\u2019s text\\n')\n return true\n}\n\nasync function buildApp(folder: string, repo: string | undefined): Promise<Registry> {\n const { buildAppRegistry } = await appBuilder()\n const registry = buildAppRegistry({ root: folder, repo })\n const problems = validateRegistry(registry)\n if (problems.length) throw new Error(`the catalogue of ${registry.builtFrom.repo} does not match its own schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/**\n * Every story and docs link in a catalogue, against the Storybook beside it\n * (UIG-19). `storybook index` writes the ids Storybook itself would serve \u2014 no\n * server, no browser, seconds \u2014 so a renamed story, or a page that no longer\n * compiles, fails here rather than as a dead link somebody finds later.\n * Reports, and says whether it passed. A folder with no Storybook has nothing\n * to check.\n */\nfunction checkLinks(registry: Registry, folder: string): boolean {\n const bin = resolve(folder, 'node_modules', '.bin', process.platform === 'win32' ? 'storybook.cmd' : 'storybook')\n if (!existsSync(resolve(folder, '.storybook')) || !existsSync(bin)) {\n process.stdout.write('story links: no Storybook here, nothing to check\\n')\n return true\n }\n const out = join(mkdtempSync(join(tmpdir(), 'estiva-ui-index-')), 'index.json')\n // A `.cmd` needs a shell on Windows, and Node wants that as one quoted line\n // rather than an argument list. Both paths are ours: the app's folder and a temp file.\n const run =\n process.platform === 'win32'\n ? spawnSync(`\"${bin}\" index -o \"${out}\" --quiet`, { cwd: folder, encoding: 'utf8', shell: true })\n : spawnSync(bin, ['index', '-o', out, '--quiet'], { cwd: folder, encoding: 'utf8' })\n if (run.status !== 0 || !existsSync(out)) {\n process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:\\n${(run.stderr || run.stdout || '').trim()}\\n`)\n return false\n }\n const ids = new Set(Object.keys((JSON.parse(readFileSync(out, 'utf8')) as { entries: Record<string, unknown> }).entries))\n const dead = linkProblems(registry, ids)\n const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0)\n if (dead.length) {\n process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:\\n ${dead.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write(`story links: all ${count} open a story or docs page Storybook has\\n`)\n return true\n}\n\n/**\n * What a part draws only when a prop is passed, where no story passes it\n * (UIG-19, `./gated`). Strict, by Katerina's ruling: a part's own stories show\n * everything it can draw. In an app it reads the reusable parts; in the package,\n * every component (`kinds` is the package's one class, `component`).\n */\nasync function checkGated(registry: Registry, folder: string, kinds?: string[]): Promise<boolean> {\n const { gatedFindings } = await gatedScan()\n const found = gatedFindings(registry, folder, { ...(kinds ? { kinds } : {}), pictured: 'own' })\n if (found.length) {\n const lines = found.map((f) => `${f.part} (${f.file}) draws something only when given \\`${f.prop}\\`, and no story that draws it passes \\`${f.prop}\\`: pass it in a story`)\n process.stderr.write(`stories: ${found.length === 1 ? 'one thing a part draws is' : `${found.length} things parts draw are`} never shown:\\n ${lines.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write('stories: everything a part draws behind a prop is shown by a story\\n')\n return true\n}\n\n/** One line per kind, in a fixed order, so two runs read the same. */\nfunction summary(registry: Registry): string {\n const count = (cls: string) => registry.entries.filter((entry) => entry.app?.class === cls).length\n const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(', ')\n const files = registry.builtFrom.files ?? 0\n return `${registry.builtFrom.repo}: ${registry.entries.length} parts in ${files - registry.filesWithoutParts.length} files (${registry.filesWithoutParts.length} more hold none) \u2014 ${kinds}. Every part is described and sorted.`\n}\n\nasync function main(): Promise<number> {\n switch (command) {\n case 'find': {\n const query = plain().join(' ')\n if (!query.trim()) {\n process.stderr.write('estiva-ui find <words\u2026> \u2014 what the thing you need does, in words\\n')\n return 1\n }\n const registries: Registry[] = [readPackageRegistry()]\n // An app that cannot be read is said out loud, and the rest still answer.\n const add = async (folder: string, repo: string | undefined) => {\n try {\n registries.push(await buildApp(folder, repo))\n } catch (error) {\n const [first, ...more] = String(error instanceof Error ? error.message : error).split('\\n')\n process.stderr.write(`not searched: ${first}${more.length ? `\\n${more.slice(0, 3).join('\\n')}${more.length > 3 ? `\\n \u2026and ${more.length - 3} more \u2014 run \"estiva-ui check\" there` : ''}` : ''}\\n`)\n }\n }\n // The folder it runs in is searched when it is an app; the folder that\n // holds the repositories is not one, and only its neighbours answer.\n const here = !isPackage && existsSync(resolve(root, 'package.json')) ? (manifestAt(root).name ?? basename(root)) : null\n if (here) await add(root, value('repo'))\n if (!values('also').length && !flag('here')) {\n for (const sibling of findSiblings(workspaceOf(root), { skip: here ? [here] : [] })) await add(sibling.folder, sibling.name)\n }\n for (const also of values('also')) {\n const [name, folder] = also.includes('=') ? [also.slice(0, also.indexOf('=')), also.slice(also.indexOf('=') + 1)] : [undefined, also]\n const at = resolve(root, folder)\n if (!existsSync(resolve(at, 'package.json'))) {\n process.stderr.write(`not searched: ${name ?? basename(at)} \u2014 nothing at ${at}\\n`)\n continue\n }\n await add(at, name)\n }\n const limit = Number(value('limit') ?? 5)\n const findings = findInRegistries(registries, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })\n if (flag('json')) {\n process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\\n`)\n return 0\n }\n process.stdout.write(`${formatFindings(registries, findings, query)}\\n`)\n // Nothing found is not a failure \u2014 it is the answer that sends a session\n // to ask rather than to invent.\n return 0\n }\n\n case 'build': {\n const out = resolve(root, value('out') ?? 'registry.json')\n if (isPackage) {\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const registry = buildRegistry({ root })\n const problems = validateRegistry(registry)\n if (problems.length) {\n process.stderr.write(`the registry this build produced does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\\n`)\n return 0\n }\n const { serializeRegistry } = await packageBuilder()\n const registry = await buildApp(root, value('repo'))\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${summary(registry)}\\n`)\n return 0\n }\n\n case 'check': {\n if (!isPackage) {\n const registry = await buildApp(root, value('repo'))\n process.stdout.write(`${summary(registry)}\\n`)\n // The usage-page contract and the story links (UIG-19): the one copy, run by every app's `gate` job.\n const broken = contractProblems(registry, root)\n if (broken.length) {\n process.stderr.write(`${broken.length === 1 ? 'a part breaks' : `${broken.length} parts break`} the usage-page contract:\\n ${broken.join('\\n ')}\\n`)\n } else {\n const owed = registry.entries.filter((entry) => (CONTRACT_KINDS as readonly string[]).includes(entry.app?.class ?? '')).length\n process.stdout.write(`usage pages: all ${owed} reusable parts keep the contract and are drawn somewhere\\n`)\n }\n const linked = checkLinks(registry, root)\n const shown = await checkGated(registry, root)\n const loaded = checkLoader()\n return broken.length || !linked || !shown || !loaded ? 1 : 0\n }\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const path = packageRegistryPath()\n const built = buildRegistry({ root })\n const problems = validateRegistry(built)\n if (problems.length) {\n process.stderr.write(`the registry does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n if (!existsSync(path)) {\n process.stderr.write(`${path} is missing \u2014 run \"npm run registry\"\\n`)\n return 1\n }\n // Byte for byte. The point of the check is that the committed file cannot\n // drift from the code; \"near enough\" is drift.\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== serializeRegistry(built)) {\n process.stderr.write(`${path} is not what the code produces \u2014 run \"npm run registry\" and commit the result\\n`)\n return 1\n }\n process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\\n`)\n const linked = checkLinks(built, root)\n const shown = await checkGated(built, root, ['component'])\n const loaded = checkLoader()\n return linked && shown && loaded ? 0 : 1\n }\n\n case 'skill': {\n const out = value('out')\n const folder = out ? resolve(out) : repositoryOf(root)\n // The loader's path starts where the session does and cannot climb out:\n // Claude Code refuses a `..` in it.\n if (relative(folder, packageRoot()).startsWith('..')) {\n process.stderr.write(`${folder} does not hold ${packageRoot()}: a loader there could not reach the skill's text\\n`)\n return 1\n }\n const path = loaderPath(folder)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, loaderText(folder, packageRoot()), 'utf8')\n process.stdout.write(`${path}: the Claude skill, read from ${packageRoot()}\\n`)\n const settings = settingsWithSearch(folder)\n if (settings) {\n writeFileSync(settingsPath(folder), settings, 'utf8')\n process.stdout.write(`${settingsPath(folder)}: the search allowed without a prompt\\n`)\n }\n return 0\n }\n\n default:\n process.stderr.write('estiva-ui <find | build | check | skill>\\n')\n return 1\n }\n}\n\ntry {\n process.exitCode = await main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n", "/// <reference types=\"node\" />\n/**\n * The apps beside this one, found rather than named (UIG-20).\n *\n * `estiva-ui find` with nothing else typed searches the package, the app it is\n * run in, and every app that sits beside it. Katerina's condition for having no\n * shared Storybook is that one search, run unasked from any repository, covers\n * every catalogue; a list of names written into each repository's script would\n * be the drifting copy the gates exist to remove, and would miss the next app.\n *\n * An app is a folder, or one folder inside it, whose `package.json` depends on\n * `@estiva-app/ui` and which has a `src` to read. A service that does not\n * install the package is not an app of this catalogue until it does.\n *\n * Each app is searched once. A second checkout of the same app (a worktree, or\n * somebody's copy) carries the same package name; the app the search runs in\n * wins, then the one whose top folder has the shortest name.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'\nimport { basename, dirname, join, resolve } from 'node:path'\n\nexport const PACKAGE_NAME = '@estiva-app/ui'\n\nexport interface Sibling {\n /** What its entries are called: its top folder's name. */\n name: string\n /** The folder the catalogue is built from. */\n folder: string\n /** Its `package.json` name, which says two checkouts are one app. */\n packageName: string\n}\n\nfunction manifest(folder: string): { name?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> } | null {\n const file = join(folder, 'package.json')\n if (!existsSync(file)) return null\n try {\n return JSON.parse(readFileSync(file, 'utf8'))\n } catch {\n return null\n }\n}\n\n/** The name of the app in `folder`, or null when the folder is not one. */\nexport function appName(folder: string): string | null {\n const m = manifest(folder)\n if (!m || m.name === PACKAGE_NAME) return null\n const uses = Boolean(m.dependencies?.[PACKAGE_NAME] ?? m.devDependencies?.[PACKAGE_NAME])\n return uses && existsSync(join(folder, 'src')) ? (m.name ?? basename(folder)) : null\n}\n\nconst isDir = (path: string) => {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n\n/**\n * The folder that holds the repositories: the parent of the repository `from`\n * is in, or `from` itself when it is in none (a folder that only holds them).\n */\nexport function workspaceOf(from: string): string {\n let at = resolve(from)\n for (;;) {\n if (existsSync(join(at, '.git'))) return dirname(at)\n const up = dirname(at)\n if (up === at) return resolve(from)\n at = up\n }\n}\n\n/** Every app in `workspace`, one per package name, except those named in `skip`. */\nexport function findSiblings(workspace: string, { skip = [] }: { skip?: string[] } = {}): Sibling[] {\n let tops: string[]\n try {\n tops = readdirSync(workspace).filter((name) => !name.startsWith('.') && name !== 'node_modules' && isDir(join(workspace, name)))\n } catch {\n return []\n }\n const found: Sibling[] = []\n for (const top of tops) {\n const folder = join(workspace, top)\n const inside = readdirSync(folder).filter((name) => !name.startsWith('.') && name !== 'node_modules' && isDir(join(folder, name)))\n for (const candidate of [folder, ...inside.map((name) => join(folder, name))]) {\n const packageName = appName(candidate)\n if (packageName) found.push({ name: top, folder: candidate, packageName })\n }\n }\n const byPackage = new Map<string, Sibling>()\n for (const sibling of found.sort((a, b) => a.name.length - b.name.length || a.name.localeCompare(b.name))) {\n if (skip.includes(sibling.packageName) || byPackage.has(sibling.packageName)) continue\n byPackage.set(sibling.packageName, sibling)\n }\n return [...byPackage.values()].sort((a, b) => a.name.localeCompare(b.name))\n}\n", "/// <reference types=\"node\" />\n/**\n * The Claude skill (UIG-20): search the catalogue before building anything.\n *\n * Its text lives once, in this package, as `skill/estiva-ui.md` (Katerina,\n * 17 September 2026: the same gate text pasted into two repositories is a\n * defect). Claude Code only loads a skill from a `SKILL.md` under a folder's\n * `.claude/skills/`, and no plugin source can point into `node_modules`, so each\n * repository commits a loader: the trigger below, and one line that reads the\n * installed package's text into the skill when it opens (`` !`\u2026` `` runs before\n * Claude sees the skill). `estiva-ui skill` writes it; `estiva-ui check` fails\n * when a repository's loader is missing or is not what this writes.\n *\n * Measured with fresh sessions of Claude Code 2.1.215 (UIG-20), three things\n * shape that line and the settings beside it:\n *\n * - An injected command runs without a prompt only when Claude Code can read it\n * statically as read-only: a plain `cat` of a plain path. A path with `..`\n * (\"may follow a symlink outside the working directory\"), a `$(\u2026)`, or a\n * `node -e` are all refused, and a refused command stops the skill opening.\n * So the path starts at `${CLAUDE_PROJECT_DIR}`, the folder the session\n * started in, as the editor gate's hook does: a session started above or\n * below the repository gets neither.\n * - An `allowed-tools` line makes opening the skill itself ask for permission\n * (\"Execute skill\"). So the search is allowed in the repository's committed\n * `.claude/settings.json` instead, and the skill carries no `allowed-tools`.\n * - Claude reads a skill's description before it opens the skill, so the\n * trigger is written into the loader; it is the one part that cannot be\n * pulled in later.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve, sep } from 'node:path'\n\nexport const SKILL_NAME = 'estiva-ui'\n\n/** When Claude opens the skill by itself. Kept under Claude Code's 1,536 characters. */\nexport const SKILL_DESCRIPTION =\n 'Use before building, adding or changing anything on screen in an app that uses @estiva-app/ui, or in @estiva-app/ui itself: ' +\n 'a screen, panel, list, row, header, dialog, menu, empty state, a new component, or how something looks. ' +\n 'Also when someone asks for a component in plain words (\"I need a\u2026\", \"add a\u2026\", \"is there a\u2026\"). ' +\n \"It searches every catalogue at once, the package's and each app's Storybook, for a part that already does the job, \" +\n 'and lists the mistakes this codebase keeps making.'\n\n/** The search, allowed without a prompt in the folder's committed `.claude/settings.json`. */\nexport const SEARCH_RULES = ['Bash(npm run ui:find *)', 'Bash(npm --prefix * run ui:find *)', 'PowerShell(npm run ui:find *)', 'PowerShell(npm --prefix * run ui:find *)']\n\n/** The skill's text, inside a package folder. */\nexport const skillTextPath = (packageRoot: string) => join(packageRoot, 'skill', `${SKILL_NAME}.md`)\n\n/** Where a folder keeps its loader. */\nexport const loaderPath = (folder: string) => join(folder, '.claude', 'skills', SKILL_NAME, 'SKILL.md')\n\n/** Where a folder keeps the settings that allow the search. */\nexport const settingsPath = (folder: string) => join(folder, '.claude', 'settings.json')\n\n/** The loader for `folder`, reading the text from `packageRoot`, from where a session in `folder` starts. */\nexport function loaderText(folder: string, packageRoot: string): string {\n const fromTop = relative(folder, skillTextPath(packageRoot)).split(sep).join('/')\n return [\n '---',\n `name: ${SKILL_NAME}`,\n `description: ${JSON.stringify(SKILL_DESCRIPTION)}`,\n '---',\n '',\n '<!-- Written by `estiva-ui skill` from @estiva-app/ui; `estiva-ui check` fails when it differs. The text is the installed package\u2019s, read in below. -->',\n '',\n `!\\`cat \"\\${CLAUDE_PROJECT_DIR}/${fromTop}\"\\``,\n '',\n ].join('\\n')\n}\n\ntype Settings = { permissions?: { allow?: string[] } & Record<string, unknown> } & Record<string, unknown>\n\nconst readSettings = (folder: string): Settings | null => {\n const path = settingsPath(folder)\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Settings\n } catch {\n return null\n }\n}\n\n/** `folder`'s settings with the search allowed, everything else as it was; null when they cannot be read. */\nexport function settingsWithSearch(folder: string): string | null {\n const settings = readSettings(folder)\n if (!settings) return null\n const allow = settings.permissions?.allow ?? []\n const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule))\n if (!missing.length && existsSync(settingsPath(folder))) return null\n settings.permissions = { ...settings.permissions, allow: [...allow, ...missing] }\n return `${JSON.stringify(settings, null, 2)}\\n`\n}\n\n/** The repository `from` is in: the nearest folder with `.git`, or `from` when there is none. */\nexport function repositoryOf(from: string): string {\n let at = resolve(from)\n for (;;) {\n if (existsSync(join(at, '.git'))) return at\n const up = dirname(at)\n if (up === at) return resolve(from)\n at = up\n }\n}\n\n/** What is wrong with `folder`'s loader, or null when it is what `loaderText` writes. */\nexport function loaderProblem(folder: string, packageRoot: string): string | null {\n const path = loaderPath(folder)\n const shown = relative(process.cwd(), path).split(sep).join('/') || path\n if (!existsSync(path)) return `${shown} is missing: run \"estiva-ui skill\" and commit it`\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== loaderText(folder, packageRoot)) return `${shown} is not what the package writes: run \"estiva-ui skill\" and commit it`\n if (!existsSync(skillTextPath(packageRoot))) return `${shown} reads ${skillTextPath(packageRoot)}, which is not there`\n const allow = readSettings(folder)?.permissions?.allow ?? []\n const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule))\n if (missing.length) return `.claude/settings.json does not allow the search (${missing.join(', ')}): run \"estiva-ui skill\" and commit it`\n return null\n}\n"],
5
- "mappings": "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",
6
- "names": ["existsSync", "readFileSync", "basename", "dirname", "join", "relative", "resolve", "existsSync", "readFileSync", "dirname", "join", "resolve", "packageRoot", "resolve", "readFileSync", "existsSync", "join", "basename", "serializeRegistry", "registry", "linked", "shown", "loaded", "relative", "dirname"]
3
+ "sources": ["../../src/registry/cli.ts", "../../src/registry/siblings.ts", "../../src/registry/skill.ts", "../../src/registry/storymap.ts"],
4
+ "sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `estiva-ui` \u2014 the catalogue as a command (UIG-12, UIG-13; docs/GATES.md \u00A723\n * for why it ships in the package rather than as a script pasted into each repo).\n *\n * estiva-ui find <words\u2026> [--here | --also [name=]<folder>\u2026] [--json] [--limit n]\n * estiva-ui build [--out <path>]\n * estiva-ui check\n * estiva-ui skill [--out <folder>]\n *\n * Every command takes `--root <folder>` (the library it reads; default: here)\n * and `--repo <name>` (what an app's entries are called; default: its package name).\n *\n * **In the package** (`@estiva-app/ui` itself): `build` writes the committed\n * `registry.json`, and `check` rebuilds it and fails on a single byte of drift.\n *\n * **In an app**: nothing is committed (Katerina, 18 September 2026 \u2014 the\n * package is public, the apps are private). `check` builds the app's catalogue\n * and fails when a part has no one-line description or cannot be sorted, or\n * when a reusable part breaks the usage-page contract (UIG-19, `./contract`),\n * or when the app's hand-written map of its Storybook no longer matches it\n * (`./storymap`); CI's `gate` job runs it. `build` writes the catalogue to a file only when asked.\n *\n * `find` searches the package's catalogue \u2014 the one shipped inside the installed\n * package, or this repo's own in the package \u2014 plus, in an app, the app's own,\n * built fresh, plus every app beside it (UIG-20, `./siblings`): found, not\n * named, so one command answers the same from any repository, or from the\n * folder that holds them. `--here` searches no neighbour; `--also` names them.\n *\n * `skill` writes the repository's Claude skill loader (UIG-20, `./skill`), or\n * the one for `--out <folder>`; `check` fails when the repository's is missing\n * or differs.\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, dirname, join, relative, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { CONTRACT_KINDS, contractProblems, linkProblems } from './contract'\nimport { findInRegistries, formatFindings } from './find'\nimport { findSiblings, workspaceOf } from './siblings'\nimport { loaderPath, loaderProblem, loaderText, repositoryOf, settingsPath, settingsWithSearch } from './skill'\nimport { storyMapProblems } from './storymap'\nimport { CLASSES, validateRegistry, type Registry } from './schema'\n\n/**\n * The builders, loaded only when they are used.\n *\n * They read TypeScript with TypeScript, and `typescript` is a dev dependency of\n * the app that installs this package \u2014 a real one in Peek, Ship and this repo,\n * and possibly absent elsewhere. A static import made the bundler put the whole\n * builder in the same chunk as `find`, so `npx estiva-ui find \u2026` died with\n * ERR_MODULE_NOT_FOUND before opening the file it needed.\n */\nconst packageBuilder = () => import('./build')\nconst appBuilder = () => import('./app')\nconst gatedScan = () => import('./gated')\n\nconst [command, ...rest] = process.argv.slice(2)\nconst VALUED = ['--limit', '--out', '--file', '--root', '--repo', '--also']\nconst flag = (name: string) => rest.includes(`--${name}`)\nconst value = (name: string) => {\n const at = rest.indexOf(`--${name}`)\n return at === -1 ? undefined : rest[at + 1]\n}\nconst values = (name: string) => rest.flatMap((word, i) => (word === `--${name}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []))\n/** Everything that is not a flag or a flag's value: the words of the question. */\nconst plain = () => {\n const out: string[] = []\n for (let i = 0; i < rest.length; i++) {\n if (rest[i].startsWith('--')) {\n if (VALUED.includes(rest[i])) i += 1\n continue\n }\n out.push(rest[i])\n }\n return out\n}\n\nconst root = resolve(value('root') ?? '.')\nconst manifestAt = (folder: string) => JSON.parse(readFileSync(resolve(folder, 'package.json'), 'utf8')) as { name?: string }\n/** The package itself, or an app that installs it. */\nconst isPackage = existsSync(resolve(root, 'package.json')) && manifestAt(root).name === '@estiva-app/ui'\n\n/** The package's committed catalogue: `--file`, this repo's own in the package, else the copy inside the installed package. */\nfunction packageRegistryPath(): string {\n const named = value('file')\n if (named) return resolve(named)\n if (isPackage) return resolve(root, 'registry.json')\n // dist/registry/cli.js \u2192 the package's own root. Never an app's own folder:\n // a registry.json there is the app's, written by `estiva-ui build`.\n return fileURLToPath(new URL('../../registry.json', import.meta.url))\n}\n\nfunction readPackageRegistry(): Registry {\n const path = packageRegistryPath()\n if (!existsSync(path)) throw new Error(`no registry.json at ${path} \u2014 run \"estiva-ui build\" in the package first`)\n const registry = JSON.parse(readFileSync(path, 'utf8')) as Registry\n const problems = validateRegistry(registry)\n // A malformed catalogue is worse than none: it would answer, wrongly.\n if (problems.length) throw new Error(`${path} does not match the schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/** The installed package whose skill text a loader reads: this repo in the package, else the app's install. */\nconst packageRoot = () => (isPackage ? root : resolve(root, 'node_modules', '@estiva-app', 'ui'))\n\n/** The Claude skill loader (UIG-20): reports, and says whether it passed. */\nfunction checkLoader(): boolean {\n const problem = loaderProblem(repositoryOf(root), packageRoot())\n if (problem) {\n process.stderr.write(`claude skill: ${problem}\\n`)\n return false\n }\n process.stdout.write('claude skill: the loader reads the package\u2019s text\\n')\n return true\n}\n\nasync function buildApp(folder: string, repo: string | undefined): Promise<Registry> {\n const { buildAppRegistry } = await appBuilder()\n const registry = buildAppRegistry({ root: folder, repo })\n const problems = validateRegistry(registry)\n if (problems.length) throw new Error(`the catalogue of ${registry.builtFrom.repo} does not match its own schema:\\n ${problems.join('\\n ')}`)\n return registry\n}\n\n/**\n * Every story and docs link in a catalogue, against the Storybook beside it\n * (UIG-19). `storybook index` writes the ids Storybook itself would serve \u2014 no\n * server, no browser, seconds \u2014 so a renamed story, or a page that no longer\n * compiles, fails here rather than as a dead link somebody finds later.\n * Reports, and says whether it passed. A folder with no Storybook has nothing\n * to check.\n */\nfunction checkLinks(registry: Registry, folder: string): boolean {\n const bin = resolve(folder, 'node_modules', '.bin', process.platform === 'win32' ? 'storybook.cmd' : 'storybook')\n if (!existsSync(resolve(folder, '.storybook')) || !existsSync(bin)) {\n process.stdout.write('story links: no Storybook here, nothing to check\\n')\n return true\n }\n const out = join(mkdtempSync(join(tmpdir(), 'estiva-ui-index-')), 'index.json')\n // A `.cmd` needs a shell on Windows, and Node wants that as one quoted line\n // rather than an argument list. Both paths are ours: the app's folder and a temp file.\n const run =\n process.platform === 'win32'\n ? spawnSync(`\"${bin}\" index -o \"${out}\" --quiet`, { cwd: folder, encoding: 'utf8', shell: true })\n : spawnSync(bin, ['index', '-o', out, '--quiet'], { cwd: folder, encoding: 'utf8' })\n if (run.status !== 0 || !existsSync(out)) {\n process.stderr.write(`story links: Storybook could not index its stories, so no link can be trusted:\\n${(run.stderr || run.stdout || '').trim()}\\n`)\n return false\n }\n const ids = new Set(Object.keys((JSON.parse(readFileSync(out, 'utf8')) as { entries: Record<string, unknown> }).entries))\n const dead = linkProblems(registry, ids)\n const count = registry.entries.reduce((n, entry) => n + (entry.storyId ? 1 : 0) + (entry.docsId ? 1 : 0), 0)\n if (dead.length) {\n process.stderr.write(`story links: ${dead.length} of ${count} lead nowhere:\\n ${dead.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write(`story links: all ${count} open a story or docs page Storybook has\\n`)\n return true\n}\n\n/**\n * What a part draws only when a prop is passed, where no story passes it\n * (UIG-19, `./gated`). Strict, by Katerina's ruling: a part's own stories show\n * everything it can draw. In an app it reads the reusable parts; in the package,\n * every component (`kinds` is the package's one class, `component`).\n */\nasync function checkGated(registry: Registry, folder: string, kinds?: string[]): Promise<boolean> {\n const { gatedFindings } = await gatedScan()\n const found = gatedFindings(registry, folder, { ...(kinds ? { kinds } : {}), pictured: 'own' })\n if (found.length) {\n const lines = found.map((f) => `${f.part} (${f.file}) draws something only when given \\`${f.prop}\\`, and no story that draws it passes \\`${f.prop}\\`: pass it in a story`)\n process.stderr.write(`stories: ${found.length === 1 ? 'one thing a part draws is' : `${found.length} things parts draw are`} never shown:\\n ${lines.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write('stories: everything a part draws behind a prop is shown by a story\\n')\n return true\n}\n\n/**\n * The app's hand-written map of its Storybook \u2014 the heading order and the\n * Introduction \u2014 against the Storybook and the catalogue (`./storymap`).\n * Reports, and says whether it passed.\n */\nfunction checkStoryMap(registry: Registry, folder: string): boolean {\n const problems = storyMapProblems(registry, folder)\n if (problems.length) {\n process.stderr.write(`storybook map: ${problems.length === 1 ? 'one line no longer matches' : `${problems.length} lines no longer match`}:\\n ${problems.join('\\n ')}\\n`)\n return false\n }\n process.stdout.write('storybook map: the heading order and the Introduction match the Storybook and the catalogue\\n')\n return true\n}\n\n/** One line per kind, in a fixed order, so two runs read the same. */\nfunction summary(registry: Registry): string {\n const count = (cls: string) => registry.entries.filter((entry) => entry.app?.class === cls).length\n const kinds = CLASSES.map((cls) => `${count(cls)} ${cls}`).join(', ')\n const files = registry.builtFrom.files ?? 0\n return `${registry.builtFrom.repo}: ${registry.entries.length} parts in ${files - registry.filesWithoutParts.length} files (${registry.filesWithoutParts.length} more hold none) \u2014 ${kinds}. Every part is described and sorted.`\n}\n\nasync function main(): Promise<number> {\n switch (command) {\n case 'find': {\n const query = plain().join(' ')\n if (!query.trim()) {\n process.stderr.write('estiva-ui find <words\u2026> \u2014 what the thing you need does, in words\\n')\n return 1\n }\n const registries: Registry[] = [readPackageRegistry()]\n // An app that cannot be read is said out loud, and the rest still answer.\n const add = async (folder: string, repo: string | undefined) => {\n try {\n registries.push(await buildApp(folder, repo))\n } catch (error) {\n const [first, ...more] = String(error instanceof Error ? error.message : error).split('\\n')\n process.stderr.write(`not searched: ${first}${more.length ? `\\n${more.slice(0, 3).join('\\n')}${more.length > 3 ? `\\n \u2026and ${more.length - 3} more \u2014 run \"estiva-ui check\" there` : ''}` : ''}\\n`)\n }\n }\n // The folder it runs in is searched when it is an app; the folder that\n // holds the repositories is not one, and only its neighbours answer.\n const here = !isPackage && existsSync(resolve(root, 'package.json')) ? (manifestAt(root).name ?? basename(root)) : null\n if (here) await add(root, value('repo'))\n if (!values('also').length && !flag('here')) {\n for (const sibling of findSiblings(workspaceOf(root), { skip: here ? [here] : [] })) await add(sibling.folder, sibling.name)\n }\n for (const also of values('also')) {\n const [name, folder] = also.includes('=') ? [also.slice(0, also.indexOf('=')), also.slice(also.indexOf('=') + 1)] : [undefined, also]\n const at = resolve(root, folder)\n if (!existsSync(resolve(at, 'package.json'))) {\n process.stderr.write(`not searched: ${name ?? basename(at)} \u2014 nothing at ${at}\\n`)\n continue\n }\n await add(at, name)\n }\n const limit = Number(value('limit') ?? 5)\n const findings = findInRegistries(registries, query, { limit: Number.isFinite(limit) && limit > 0 ? limit : 5 })\n if (flag('json')) {\n process.stdout.write(`${JSON.stringify(findings.map((finding) => finding.entry), null, 2)}\\n`)\n return 0\n }\n process.stdout.write(`${formatFindings(registries, findings, query)}\\n`)\n // Nothing found is not a failure \u2014 it is the answer that sends a session\n // to ask rather than to invent.\n return 0\n }\n\n case 'build': {\n const out = resolve(root, value('out') ?? 'registry.json')\n if (isPackage) {\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const registry = buildRegistry({ root })\n const problems = validateRegistry(registry)\n if (problems.length) {\n process.stderr.write(`the registry this build produced does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${registry.entries.length} entries from ${registry.builtFrom.exports} exports\\n`)\n return 0\n }\n const { serializeRegistry } = await packageBuilder()\n const registry = await buildApp(root, value('repo'))\n writeFileSync(out, serializeRegistry(registry), 'utf8')\n process.stdout.write(`${out}: ${summary(registry)}\\n`)\n return 0\n }\n\n case 'check': {\n if (!isPackage) {\n const registry = await buildApp(root, value('repo'))\n process.stdout.write(`${summary(registry)}\\n`)\n // The usage-page contract and the story links (UIG-19): the one copy, run by every app's `gate` job.\n const broken = contractProblems(registry, root)\n if (broken.length) {\n process.stderr.write(`${broken.length === 1 ? 'a part breaks' : `${broken.length} parts break`} the usage-page contract:\\n ${broken.join('\\n ')}\\n`)\n } else {\n const owed = registry.entries.filter((entry) => (CONTRACT_KINDS as readonly string[]).includes(entry.app?.class ?? '')).length\n process.stdout.write(`usage pages: all ${owed} reusable parts keep the contract and are drawn somewhere\\n`)\n }\n const linked = checkLinks(registry, root)\n const shown = await checkGated(registry, root)\n const loaded = checkLoader()\n const mapped = checkStoryMap(registry, root)\n return broken.length || !linked || !shown || !loaded || !mapped ? 1 : 0\n }\n const { buildRegistry, serializeRegistry } = await packageBuilder()\n const path = packageRegistryPath()\n const built = buildRegistry({ root })\n const problems = validateRegistry(built)\n if (problems.length) {\n process.stderr.write(`the registry does not match its own schema:\\n ${problems.join('\\n ')}\\n`)\n return 1\n }\n if (!existsSync(path)) {\n process.stderr.write(`${path} is missing \u2014 run \"npm run registry\"\\n`)\n return 1\n }\n // Byte for byte. The point of the check is that the committed file cannot\n // drift from the code; \"near enough\" is drift.\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== serializeRegistry(built)) {\n process.stderr.write(`${path} is not what the code produces \u2014 run \"npm run registry\" and commit the result\\n`)\n return 1\n }\n process.stdout.write(`${path}: current, ${built.entries.length} entries from ${built.builtFrom.exports} exports\\n`)\n const linked = checkLinks(built, root)\n const shown = await checkGated(built, root, ['component'])\n const loaded = checkLoader()\n return linked && shown && loaded ? 0 : 1\n }\n\n case 'skill': {\n const out = value('out')\n const folder = out ? resolve(out) : repositoryOf(root)\n // The loader's path starts where the session does and cannot climb out:\n // Claude Code refuses a `..` in it.\n if (relative(folder, packageRoot()).startsWith('..')) {\n process.stderr.write(`${folder} does not hold ${packageRoot()}: a loader there could not reach the skill's text\\n`)\n return 1\n }\n const path = loaderPath(folder)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, loaderText(folder, packageRoot()), 'utf8')\n process.stdout.write(`${path}: the Claude skill, read from ${packageRoot()}\\n`)\n const settings = settingsWithSearch(folder)\n if (settings) {\n writeFileSync(settingsPath(folder), settings, 'utf8')\n process.stdout.write(`${settingsPath(folder)}: the search allowed without a prompt\\n`)\n }\n return 0\n }\n\n default:\n process.stderr.write('estiva-ui <find | build | check | skill>\\n')\n return 1\n }\n}\n\ntry {\n process.exitCode = await main()\n} catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n}\n", "/// <reference types=\"node\" />\n/**\n * The apps beside this one, found rather than named (UIG-20).\n *\n * `estiva-ui find` with nothing else typed searches the package, the app it is\n * run in, and every app that sits beside it. Katerina's condition for having no\n * shared Storybook is that one search, run unasked from any repository, covers\n * every catalogue; a list of names written into each repository's script would\n * be the drifting copy the gates exist to remove, and would miss the next app.\n *\n * An app is a folder, or one folder inside it, whose `package.json` depends on\n * `@estiva-app/ui` and which has a `src` to read. A service that does not\n * install the package is not an app of this catalogue until it does.\n *\n * Each app is searched once. A second checkout of the same app (a worktree, or\n * somebody's copy) carries the same package name; the app the search runs in\n * wins, then the one whose top folder has the shortest name.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'\nimport { basename, dirname, join, resolve } from 'node:path'\n\nexport const PACKAGE_NAME = '@estiva-app/ui'\n\nexport interface Sibling {\n /** What its entries are called: its top folder's name. */\n name: string\n /** The folder the catalogue is built from. */\n folder: string\n /** Its `package.json` name, which says two checkouts are one app. */\n packageName: string\n}\n\nfunction manifest(folder: string): { name?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> } | null {\n const file = join(folder, 'package.json')\n if (!existsSync(file)) return null\n try {\n return JSON.parse(readFileSync(file, 'utf8'))\n } catch {\n return null\n }\n}\n\n/** The name of the app in `folder`, or null when the folder is not one. */\nexport function appName(folder: string): string | null {\n const m = manifest(folder)\n if (!m || m.name === PACKAGE_NAME) return null\n const uses = Boolean(m.dependencies?.[PACKAGE_NAME] ?? m.devDependencies?.[PACKAGE_NAME])\n return uses && existsSync(join(folder, 'src')) ? (m.name ?? basename(folder)) : null\n}\n\nconst isDir = (path: string) => {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n\n/**\n * The folder that holds the repositories: the parent of the repository `from`\n * is in, or `from` itself when it is in none (a folder that only holds them).\n */\nexport function workspaceOf(from: string): string {\n let at = resolve(from)\n for (;;) {\n if (existsSync(join(at, '.git'))) return dirname(at)\n const up = dirname(at)\n if (up === at) return resolve(from)\n at = up\n }\n}\n\n/** Every app in `workspace`, one per package name, except those named in `skip`. */\nexport function findSiblings(workspace: string, { skip = [] }: { skip?: string[] } = {}): Sibling[] {\n let tops: string[]\n try {\n tops = readdirSync(workspace).filter((name) => !name.startsWith('.') && name !== 'node_modules' && isDir(join(workspace, name)))\n } catch {\n return []\n }\n const found: Sibling[] = []\n for (const top of tops) {\n const folder = join(workspace, top)\n const inside = readdirSync(folder).filter((name) => !name.startsWith('.') && name !== 'node_modules' && isDir(join(folder, name)))\n for (const candidate of [folder, ...inside.map((name) => join(folder, name))]) {\n const packageName = appName(candidate)\n if (packageName) found.push({ name: top, folder: candidate, packageName })\n }\n }\n const byPackage = new Map<string, Sibling>()\n for (const sibling of found.sort((a, b) => a.name.length - b.name.length || a.name.localeCompare(b.name))) {\n if (skip.includes(sibling.packageName) || byPackage.has(sibling.packageName)) continue\n byPackage.set(sibling.packageName, sibling)\n }\n return [...byPackage.values()].sort((a, b) => a.name.localeCompare(b.name))\n}\n", "/// <reference types=\"node\" />\n/**\n * The Claude skill (UIG-20): search the catalogue before building anything.\n *\n * Its text lives once, in this package, as `skill/estiva-ui.md` (Katerina,\n * 17 September 2026: the same gate text pasted into two repositories is a\n * defect). Claude Code only loads a skill from a `SKILL.md` under a folder's\n * `.claude/skills/`, and no plugin source can point into `node_modules`, so each\n * repository commits a loader: the trigger below, and one line that reads the\n * installed package's text into the skill when it opens (`` !`\u2026` `` runs before\n * Claude sees the skill). `estiva-ui skill` writes it; `estiva-ui check` fails\n * when a repository's loader is missing or is not what this writes.\n *\n * Measured with fresh sessions of Claude Code 2.1.215 (UIG-20), three things\n * shape that line and the settings beside it:\n *\n * - An injected command runs without a prompt only when Claude Code can read it\n * statically as read-only: a plain `cat` of a plain path. A path with `..`\n * (\"may follow a symlink outside the working directory\"), a `$(\u2026)`, or a\n * `node -e` are all refused, and a refused command stops the skill opening.\n * So the path starts at `${CLAUDE_PROJECT_DIR}`, the folder the session\n * started in, as the editor gate's hook does: a session started above or\n * below the repository gets neither.\n * - An `allowed-tools` line makes opening the skill itself ask for permission\n * (\"Execute skill\"). So the search is allowed in the repository's committed\n * `.claude/settings.json` instead, and the skill carries no `allowed-tools`.\n * - Claude reads a skill's description before it opens the skill, so the\n * trigger is written into the loader; it is the one part that cannot be\n * pulled in later.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve, sep } from 'node:path'\n\nexport const SKILL_NAME = 'estiva-ui'\n\n/** When Claude opens the skill by itself. Kept under Claude Code's 1,536 characters. */\nexport const SKILL_DESCRIPTION =\n 'Use before building, adding or changing anything on screen in an app that uses @estiva-app/ui, or in @estiva-app/ui itself: ' +\n 'a screen, panel, list, row, header, dialog, menu, empty state, a new component, or how something looks. ' +\n 'Also when someone asks for a component in plain words (\"I need a\u2026\", \"add a\u2026\", \"is there a\u2026\"). ' +\n \"It searches every catalogue at once, the package's and each app's Storybook, for a part that already does the job, \" +\n 'and lists the mistakes this codebase keeps making.'\n\n/** The search, allowed without a prompt in the folder's committed `.claude/settings.json`. */\nexport const SEARCH_RULES = ['Bash(npm run ui:find *)', 'Bash(npm --prefix * run ui:find *)', 'PowerShell(npm run ui:find *)', 'PowerShell(npm --prefix * run ui:find *)']\n\n/** The skill's text, inside a package folder. */\nexport const skillTextPath = (packageRoot: string) => join(packageRoot, 'skill', `${SKILL_NAME}.md`)\n\n/** Where a folder keeps its loader. */\nexport const loaderPath = (folder: string) => join(folder, '.claude', 'skills', SKILL_NAME, 'SKILL.md')\n\n/** Where a folder keeps the settings that allow the search. */\nexport const settingsPath = (folder: string) => join(folder, '.claude', 'settings.json')\n\n/** The loader for `folder`, reading the text from `packageRoot`, from where a session in `folder` starts. */\nexport function loaderText(folder: string, packageRoot: string): string {\n const fromTop = relative(folder, skillTextPath(packageRoot)).split(sep).join('/')\n return [\n '---',\n `name: ${SKILL_NAME}`,\n `description: ${JSON.stringify(SKILL_DESCRIPTION)}`,\n '---',\n '',\n '<!-- Written by `estiva-ui skill` from @estiva-app/ui; `estiva-ui check` fails when it differs. The text is the installed package\u2019s, read in below. -->',\n '',\n `!\\`cat \"\\${CLAUDE_PROJECT_DIR}/${fromTop}\"\\``,\n '',\n ].join('\\n')\n}\n\ntype Settings = { permissions?: { allow?: string[] } & Record<string, unknown> } & Record<string, unknown>\n\nconst readSettings = (folder: string): Settings | null => {\n const path = settingsPath(folder)\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Settings\n } catch {\n return null\n }\n}\n\n/** `folder`'s settings with the search allowed, everything else as it was; null when they cannot be read. */\nexport function settingsWithSearch(folder: string): string | null {\n const settings = readSettings(folder)\n if (!settings) return null\n const allow = settings.permissions?.allow ?? []\n const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule))\n if (!missing.length && existsSync(settingsPath(folder))) return null\n settings.permissions = { ...settings.permissions, allow: [...allow, ...missing] }\n return `${JSON.stringify(settings, null, 2)}\\n`\n}\n\n/** The repository `from` is in: the nearest folder with `.git`, or `from` when there is none. */\nexport function repositoryOf(from: string): string {\n let at = resolve(from)\n for (;;) {\n if (existsSync(join(at, '.git'))) return at\n const up = dirname(at)\n if (up === at) return resolve(from)\n at = up\n }\n}\n\n/** What is wrong with `folder`'s loader, or null when it is what `loaderText` writes. */\nexport function loaderProblem(folder: string, packageRoot: string): string | null {\n const path = loaderPath(folder)\n const shown = relative(process.cwd(), path).split(sep).join('/') || path\n if (!existsSync(path)) return `${shown} is missing: run \"estiva-ui skill\" and commit it`\n if (readFileSync(path, 'utf8').replace(/\\r\\n/g, '\\n') !== loaderText(folder, packageRoot)) return `${shown} is not what the package writes: run \"estiva-ui skill\" and commit it`\n if (!existsSync(skillTextPath(packageRoot))) return `${shown} reads ${skillTextPath(packageRoot)}, which is not there`\n const allow = readSettings(folder)?.permissions?.allow ?? []\n const missing = SEARCH_RULES.filter((rule) => !allow.includes(rule))\n if (missing.length) return `.claude/settings.json does not allow the search (${missing.join(', ')}): run \"estiva-ui skill\" and commit it`\n return null\n}\n", "/// <reference types=\"node\" />\n/**\n * The hand-written map of an app's Storybook, held against the Storybook it\n * describes (moved here from Peek's own status checks, audit A3).\n *\n * An app may keep two maps of its sidebar: the order of its top-level headings\n * (`const HEADINGS = [...]` in `.storybook/preview.tsx`, read by `storySort`),\n * and the Introduction page (`src/stories/Introduction.mdx`), which names each\n * heading in a table row and states how many parts are worth reusing. Both are\n * written by hand and both describe something that moves, so both rot: Peek's\n * named a Huddles heading for a day after its stories were deleted, and said 49\n * reusable parts when the catalogue built 41. Its own checks saw it, but they\n * ran only in `gates:status`, never in CI.\n *\n * Every check here runs only when the app states the thing it checks: an app\n * with no `HEADINGS` list, or no Introduction, has nothing to rot.\n */\nimport { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Registry } from './schema'\n\nconst PREVIEW = '.storybook/preview.tsx'\nconst INTRODUCTION = 'src/stories/Introduction.mdx'\n/** The classes an app's Introduction counts as \"worth reusing\". */\nconst WORTH_REUSING = ['reusable', 'promote-candidate']\n\nfunction storyFiles(root: string): string[] {\n const out: string[] = []\n const walk = (dir: string) => {\n if (!existsSync(dir)) return\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n const path = join(dir, entry.name)\n if (entry.isDirectory()) walk(path)\n else if (entry.name.endsWith('.stories.tsx') || entry.name.endsWith('.mdx')) out.push(path)\n }\n }\n walk(join(root, 'src'))\n return out\n}\n\n/** Every top-level heading the sidebar has: the first segment of each story file's or page's title. */\nexport function sidebarHeadings(root: string): Set<string> {\n const real = new Set<string>()\n for (const file of storyFiles(root)) {\n const text = readFileSync(file, 'utf8')\n const start = text.indexOf('const meta')\n const meta = start === -1 ? '' : text.slice(start, text.indexOf('satisfies Meta', start) + 1 || undefined)\n const title = meta.match(/\\btitle: *['\"]([^'\"]+)['\"]/) ?? text.match(/<Meta\\s+title=['\"]([^'\"]+)['\"]/)\n if (title) real.add(title[1].split('/')[0].trim())\n }\n return real\n}\n\n/** The headings `preview.tsx` orders, or null when it keeps no list. */\nexport function orderedHeadings(previewText: string): string[] | null {\n const block = previewText.match(/const HEADINGS = \\[([\\s\\S]*?)\\]/)\n if (!block) return null\n // Comments first: the list is commented per heading, and an apostrophe in a\n // comment would open a string that swallows the rest.\n const listed = block[1].replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/\\/\\/.*$/gm, '')\n return [...listed.matchAll(/'([^']+)'/g)].map((m) => m[1])\n}\n\n/** The headings the Introduction names, one per table row that opens with a bold name. */\nexport function introductionHeadings(introText: string): Set<string> {\n return new Set([...introText.matchAll(/^\\|\\s*\\*\\*([^*]+)\\*\\*\\s*\\|/gm)].map((m) => m[1].trim()))\n}\n\n/** The number the Introduction states, or null when it states none. */\nexport function introductionCount(introText: string): number | null {\n const said = introText.match(/\\*\\*(\\d+) parts worth reusing\\*\\*/)\n return said ? Number(said[1]) : null\n}\n\n/** What is wrong with the app's hand-written map of its Storybook; empty when nothing is. */\nexport function storyMapProblems(registry: Registry, root: string): string[] {\n const problems: string[] = []\n const real = sidebarHeadings(root)\n const previewPath = join(root, PREVIEW)\n if (existsSync(previewPath)) {\n const ordered = orderedHeadings(readFileSync(previewPath, 'utf8'))\n if (ordered) {\n const unordered = [...real].filter((r) => !ordered.includes(r))\n const empty = ordered.filter((o) => !real.has(o))\n if (unordered.length) problems.push(`${PREVIEW}: not named in HEADINGS, so they fall to the bottom of the sidebar: ${unordered.join(', ')}`)\n if (empty.length) problems.push(`${PREVIEW}: named in HEADINGS but no story uses them: ${empty.join(', ')}`)\n }\n }\n const introPath = join(root, INTRODUCTION)\n if (existsSync(introPath)) {\n const intro = readFileSync(introPath, 'utf8')\n const named = introductionHeadings(intro)\n if (named.size) {\n const missing = [...real].filter((r) => !named.has(r))\n const stale = [...named].filter((n) => !real.has(n))\n if (missing.length) problems.push(`${INTRODUCTION}: the sidebar has, the page does not name: ${missing.join(', ')}`)\n if (stale.length) problems.push(`${INTRODUCTION}: the page names, the sidebar does not have: ${stale.join(', ')}`)\n }\n const said = introductionCount(intro)\n if (said !== null) {\n const reusable = registry.entries.filter((entry) => WORTH_REUSING.includes(entry.app?.class ?? '')).length\n if (said !== reusable) problems.push(`${INTRODUCTION}: says ${said} parts worth reusing; the catalogue builds ${reusable}`)\n }\n }\n return problems\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["existsSync", "readFileSync", "basename", "dirname", "join", "relative", "resolve", "existsSync", "readFileSync", "dirname", "join", "resolve", "packageRoot", "existsSync", "readdirSync", "readFileSync", "join", "root", "resolve", "readFileSync", "existsSync", "join", "basename", "serializeRegistry", "registry", "linked", "shown", "loaded", "relative", "dirname"]
7
7
  }
@@ -0,0 +1,12 @@
1
+ import type { Registry } from './schema';
2
+ /** Every top-level heading the sidebar has: the first segment of each story file's or page's title. */
3
+ export declare function sidebarHeadings(root: string): Set<string>;
4
+ /** The headings `preview.tsx` orders, or null when it keeps no list. */
5
+ export declare function orderedHeadings(previewText: string): string[] | null;
6
+ /** The headings the Introduction names, one per table row that opens with a bold name. */
7
+ export declare function introductionHeadings(introText: string): Set<string>;
8
+ /** The number the Introduction states, or null when it states none. */
9
+ export declare function introductionCount(introText: string): number | null;
10
+ /** What is wrong with the app's hand-written map of its Storybook; empty when nothing is. */
11
+ export declare function storyMapProblems(registry: Registry, root: string): string[];
12
+ //# sourceMappingURL=storymap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storymap.d.ts","sourceRoot":"","sources":["../../src/registry/storymap.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAsBxC,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUzD;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAOpE;AAED,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAEnE;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8B3E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/registry.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.34.0",
7
+ "packageVersion": "0.35.0",
8
8
  "exports": 87,
9
9
  "typeExports": 96,
10
10
  "files": null
@@ -23,7 +23,7 @@
23
23
  import type { CheckResult, GateCheck, GateHelpers, GateTicket } from './status'
24
24
 
25
25
  export interface AppCheckOptions {
26
- /** Where the app is inside the repository: `web` in Ship, `.` elsewhere. */
26
+ /** Where the app is inside the repository: `.` unless it sits in a folder of its own. */
27
27
  app?: string
28
28
  /** A real page of the app, relative to the app, full of elements that are not controls. */
29
29
  page: string
package/src/gates/cli.ts CHANGED
@@ -13,7 +13,8 @@
13
13
  * node "$CLAUDE_PROJECT_DIR/node_modules/@estiva-app/ui/dist/gates/cli.js" hook
14
14
  *
15
15
  * A repo whose app sits in a folder of its own runs both by path, from the
16
- * repo's top folder, and says where the app is — Ship (UIG-32):
16
+ * repo's top folder, and says where the app is (Ship did, from UIG-32 until
17
+ * PER-19 moved its app to the top folder):
17
18
  *
18
19
  * node "$CLAUDE_PROJECT_DIR/web/node_modules/@estiva-app/ui/dist/gates/cli.js" hook --app web
19
20
  * node web/node_modules/@estiva-app/ui/dist/gates/cli.js status --app web
@@ -44,9 +45,19 @@ async function main(): Promise<number> {
44
45
  return result.failures.length ? 1 : 0
45
46
  }
46
47
  case 'hook': {
47
- const result = await runHook({ app: value('app'), audience })
48
- if (result.code === 2) process.stderr.write(result.message)
49
- return result.code
48
+ // A hook that fails open guards nothing: Claude Code lets a write through
49
+ // on any exit but 2, and a crash exits 1 (audit A2). So a gate that cannot
50
+ // run — no ESLint in the app's install, a config that does not load —
51
+ // refuses the write and says why, and the session fixes the install.
52
+ try {
53
+ const result = await runHook({ app: value('app'), audience })
54
+ if (result.code === 2) process.stderr.write(result.message)
55
+ return result.code
56
+ } catch (error) {
57
+ const why = error instanceof Error ? error.message.split('\n')[0] : String(error)
58
+ process.stderr.write(`The UI Guardrails could not check this write, so it was not made: ${why}\nRun \`npm ci\` in the app's folder, then try again.\n`)
59
+ return 2
60
+ }
50
61
  }
51
62
  case 'status': {
52
63
  process.stdout.write(`${await runStatus({ app: value('app'), json: flag('json'), detail: flag('detail') })}${flag('json') ? '' : '\n'}`)
@@ -1,9 +1,12 @@
1
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { execFileSync } from 'node:child_process'
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
2
3
  import { dirname, join } from 'node:path'
3
4
  import { afterAll, describe, expect, it } from 'vitest'
4
5
  import { buildAppRegistry } from '../registry/app'
5
6
  import { validateRegistry } from '../registry/schema'
7
+ import { appChecks } from './app-checks'
6
8
  import { appFiles, ASKED_OF_NPM, createApp, themes } from './create-app'
9
+ import { helpers, type GateHelpers } from './status'
7
10
 
8
11
  /**
9
12
  * `create-estiva-app` (UIG-10). What the app does when it runs is proved on a
@@ -148,3 +151,55 @@ describe('create-estiva-app', () => {
148
151
  expect(() => createApp({ name: 'once', parent: scratch, versions })).toThrow(/already exists/)
149
152
  })
150
153
  })
154
+
155
+ /**
156
+ * The made app, checked the way gates:status checks Peek and Ship (audit B5).
157
+ * The tests above pin the starter's files one by one; they could not see that
158
+ * its required job `gate` skipped the token lint, which every app check since
159
+ * 0.30.0 asks for. This runs every check an app runs on a freshly made app, for
160
+ * real: the package is linked in as the app's install, and ESLint, TypeScript
161
+ * and the plugins resolve from this repository's own, as they do for the gate
162
+ * tests. Only what needs GitHub or a commit is left out.
163
+ */
164
+ describe('a made app, under the checks every app runs', () => {
165
+ it('passes every one on its first commit, but the tickets not built yet', async () => {
166
+ const dir = join(scratch, 'checked')
167
+ for (const [path, text] of Object.entries(files)) {
168
+ mkdirSync(dirname(join(dir, path)), { recursive: true })
169
+ writeFileSync(join(dir, path), text)
170
+ }
171
+ mkdirSync(join(dir, 'node_modules', '@estiva-app'), { recursive: true })
172
+ symlinkSync(process.cwd(), join(dir, 'node_modules', '@estiva-app', 'ui'), 'junction')
173
+ // The one plugin the app asks npm for that this repository does not use: the
174
+ // gate only needs its name known, so a stand-in with no rules does.
175
+ const hooks = join(dir, 'node_modules', 'eslint-plugin-react-hooks')
176
+ mkdirSync(hooks, { recursive: true })
177
+ writeFileSync(join(hooks, 'package.json'), JSON.stringify({ name: 'eslint-plugin-react-hooks', type: 'module', main: 'index.js' }))
178
+ writeFileSync(join(hooks, 'index.js'), 'export default { rules: {}, configs: { flat: { recommended: { rules: {} } } } }\n')
179
+ // Its first commit, as the ticket asks: the count file must be committed.
180
+ const git = (...a: string[]) => execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', '-c', 'core.autocrlf=false', ...a], { cwd: dir, stdio: 'pipe' })
181
+ git('init', '-q', '-b', 'main')
182
+ git('add', '-A')
183
+ git('commit', '-q', '-m', 'made by create-estiva-app')
184
+ const real = helpers(dir)
185
+ // "The installed package exports Link" reads its declarations, which CI's
186
+ // test step does not build (`pretest` bundles only): the same exports are read
187
+ // from their source when the declarations are not there.
188
+ const exportsFrom = (rel: string) => (rel.endsWith('dist/index.d.ts') && !real.exists(rel) ? join(process.cwd(), 'src', 'index.ts') : rel)
189
+ const h: GateHelpers = {
190
+ ...real,
191
+ contains: (rel, needle, label) => real.contains(exportsFrom(rel), needle, label),
192
+ protectedBranch: () => real.UNKNOWN('a rule on GitHub'),
193
+ gh: () => real.UNKNOWN('a question for GitHub'),
194
+ }
195
+ const failed: string[] = []
196
+ for (const ticket of appChecks(h, { page: 'src/pages/HomePage.tsx' })) {
197
+ for (const check of ticket.checks) {
198
+ const result = await check.run()
199
+ if (result.result === 'fail' || result.result === 'part') failed.push(`${ticket.ref} ${check.what}: ${result.detail}`)
200
+ }
201
+ }
202
+ // RichText is UIG-30's, not built yet: no app passes that one.
203
+ expect(failed.filter((f) => !f.startsWith('UIG-30 '))).toEqual([])
204
+ }, 300_000)
205
+ })
@@ -365,9 +365,10 @@ jobs:
365
365
  exit 1
366
366
  fi
367
367
 
368
- # The UI Guardrails' rules, on their own: a raw element, behaviour a part owns
369
- # written by hand, a part restyled. \`npm run lint:rules\` also writes
370
- # .gates-count.json and fails on an eslint-disable that switches a rule off.
368
+ # The UI Guardrails, the one check a merge requires: the package's gate rules
369
+ # (each refusal names what to use instead), the token contract, and the
370
+ # catalogue. \`npm run lint:rules\` also writes .gates-count.json and fails on
371
+ # an eslint-disable that switches a rule off.
371
372
  #
372
373
  # A job of its own, because GitHub can require only a whole job, by its name:
373
374
  # the rule on main requires \`gate\`. Renaming it leaves every pull request
@@ -383,6 +384,10 @@ jobs:
383
384
  - run: npm ci
384
385
  - name: Gate lint
385
386
  run: npm run lint:rules
387
+ # Colours, type, corners and shadows from the preset only. In \`check\` too,
388
+ # inside \`npm run lint\`, but only \`gate\` blocks a merge.
389
+ - name: Token lint
390
+ run: npm run lint:tokens
386
391
  # Every part of the app says in one line what it is for, so the catalogue
387
392
  # (\`npm run ui:find\`) can offer it before someone builds it again.
388
393
  - name: Every part is described
@@ -1011,8 +1016,9 @@ UIG-6 as not done.
1011
1016
  ${title} is an Estiva app. Its parts, tokens and gates come from \`@estiva-app/ui\`.
1012
1017
 
1013
1018
  **Use the package's parts, never a raw element or a hand-built look.** The gate
1014
- refuses a raw control, behaviour a part owns written by hand, and a part restyled
1015
- through \`className\`, and names what to use instead. It runs before you write
1019
+ refuses code that reaches past the package, and each refusal names what to use
1020
+ instead. What it refuses is the package's to say, and grows with it: the
1021
+ \`estiva-ui\` skill lists it. It runs before you write
1016
1022
  (the hook in \`.claude/settings.json\`, for a session started in this folder), in
1017
1023
  \`npm run lint:rules\` and in CI. A session started elsewhere: run
1018
1024
  \`npm run lint:rules\` after changing \`src/\`, and fix what it reports. Keep something
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Until UIG-10 every repo wrote this itself: `eslint.gates.js` (the rules and
6
6
  * where they apply) and `eslint.gates.config.js` (the gate: those rules alone)
7
- * in Peek, in Ship's `web/`, and here pointed inward. The rules were always the
7
+ * in Peek, in Ship, and here pointed inward. The rules were always the
8
8
  * package's; these two files were copies. Now they are two functions.
9
9
  *
10
10
  * Where: every `.ts` and `.tsx` under `src`, stories included (a story is what
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from 'node:child_process'
1
2
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
3
  import { join } from 'node:path'
3
4
  import { pathToFileURL } from 'node:url'
@@ -6,7 +7,7 @@ import { afterAll, describe, expect, it } from 'vitest'
6
7
  import { writeGateCount } from './count'
7
8
  import { gateConfig, gateLint } from './gate-config'
8
9
  import { runHook } from './hook'
9
- import { helpers, runStatus } from './status'
10
+ import { behindMain, helpers, runStatus } from './status'
10
11
  import { tokenConfig, tokenLint, tokenValues } from './token-lint'
11
12
 
12
13
  /**
@@ -233,10 +234,42 @@ describe('gates:status', () => {
233
234
  const h = helpers(dir)
234
235
  expect(h.ciJob('gate', 'lint:rules').result).toBe('pass')
235
236
  expect(h.ciJob('check', 'lint:rules').result).toBe('fail')
236
- expect(h.hook('.claude/settings.json', 'gates').result).toBe('pass')
237
237
  expect(h.script('package.json', 'gates:status').result).toBe('pass')
238
238
  })
239
239
 
240
+ // Audit A2: a hook is judged by running it, the way Claude Code does, on a raw
241
+ // <button>. Only exit 2 blocks a write; a hook that cannot run exits 1, and
242
+ // the write goes through.
243
+ it('runs the committed hook and passes only when it refuses the write', () => {
244
+ const settings = (matcher: string, command: string) => ({ '.claude/settings.json': JSON.stringify({ hooks: { PreToolUse: [{ matcher, hooks: [{ type: 'command', command }] }] } }) })
245
+ const refuses = 'node -e "process.exit(2)" -- gates'
246
+ expect(helpers(app('hook-refuses', settings('Edit|Write', refuses))).hook('.claude/settings.json', 'gates')).toMatchObject({ result: 'pass' })
247
+ const broken = helpers(app('hook-broken', settings('Edit|Write', 'node "$CLAUDE_PROJECT_DIR/node_modules/@estiva-app/ui/dist/gates/cli.js" hook'))).hook('.claude/settings.json', 'gates')
248
+ expect(broken).toMatchObject({ result: 'fail', detail: expect.stringContaining('let a raw <button> through (exit 1)') })
249
+ expect(helpers(app('hook-waves', settings('Edit|Write', 'node -e "process.exit(0)" -- gates'))).hook('.claude/settings.json', 'gates')).toMatchObject({ result: 'fail' })
250
+ expect(helpers(app('hook-never', settings('Read', refuses))).hook('.claude/settings.json', 'gates')).toMatchObject({ result: 'fail', detail: expect.stringContaining('never for a Write') })
251
+ // The hook is handed the real project folder, as Claude Code hands it.
252
+ const seen = 'node -e "process.exit(String(process.env.CLAUDE_PROJECT_DIR).endsWith(process.argv[1]) ? 2 : 1)" hook-dir gates'
253
+ expect(helpers(app('hook-dir', settings('Edit|Write', seen))).hook('.claude/settings.json', 'gates')).toMatchObject({ result: 'pass' })
254
+ })
255
+
256
+ // Audit A4: a checkout seven commits behind reported a finished ticket as not started.
257
+ it('says how far a checkout is behind main, after fetching it', () => {
258
+ const base = app('behind', {})
259
+ const git = (cwd: string, ...a: string[]) => execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { cwd, stdio: 'pipe' }).toString().trim()
260
+ git(base, 'init', '-q', '--bare', '-b', 'main', 'origin.git')
261
+ git(base, 'clone', '-q', 'origin.git', 'writer')
262
+ const writer = join(base, 'writer')
263
+ git(writer, 'commit', '-q', '--allow-empty', '-m', 'one')
264
+ git(writer, 'push', '-q', 'origin', 'HEAD:main')
265
+ git(base, 'clone', '-q', 'origin.git', 'reader')
266
+ const reader = join(base, 'reader')
267
+ expect(behindMain(reader)).toBe('')
268
+ git(writer, 'commit', '-q', '--allow-empty', '-m', 'two')
269
+ git(writer, 'push', '-q', 'origin', 'HEAD:main')
270
+ expect(behindMain(reader)).toContain('1 commit behind origin/main')
271
+ })
272
+
240
273
  it('counts what loads into every session, imports included, and refuses a rule file with no paths (UIG-21)', () => {
241
274
  const lines = (n: number) => Array.from({ length: n }, (_, i) => `line ${i}`).join('\n') + '\n'
242
275
  const scoped = '---\npaths:\n - "src/**"\n---\n\nA rule.\n'
package/src/gates/hook.ts CHANGED
@@ -44,7 +44,7 @@ import { repositoryOf } from '../registry/skill'
44
44
  export interface HookOptions {
45
45
  /** The repository's top folder. Defaults to `$CLAUDE_PROJECT_DIR`, then the tool call's `cwd`. */
46
46
  root?: string
47
- /** The folder that holds the app, relative to `root`: `web` in Ship, `.` elsewhere. */
47
+ /** The folder that holds the app, relative to `root`: `.` unless it sits in a folder of its own. */
48
48
  app?: string
49
49
  /** `package` gates `.tsx` only, as this package's inward lint does. */
50
50
  audience?: 'app' | 'package'