@nusoft/nuos-build-catalogue 0.26.0 → 0.28.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.
- package/dist/cli.js +14 -0
- package/dist/commands/render.d.ts +25 -0
- package/dist/commands/render.js +40 -0
- package/dist/render/architecture.d.ts +18 -0
- package/dist/render/architecture.js +74 -0
- package/dist/render/design-system.d.ts +26 -0
- package/dist/render/design-system.js +308 -0
- package/dist/render/html.d.ts +47 -0
- package/dist/render/html.js +258 -0
- package/dist/render/maps.d.ts +18 -0
- package/dist/render/maps.js +67 -0
- package/dist/render/parser.d.ts +54 -0
- package/dist/render/parser.js +162 -0
- package/dist/render/run.d.ts +29 -0
- package/dist/render/run.js +92 -0
- package/dist/render/surfaces.d.ts +23 -0
- package/dist/render/surfaces.js +144 -0
- package/package.json +2 -2
- package/scripts/hooks/pre-commit +2 -1
- package/templates/agents/coder.md +33 -5
- package/templates/agents/reviewer.md +6 -4
- package/templates/claude-hooks/check-design-system-compliance.sh +211 -0
- package/templates/protocols/end-of-session.md +8 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the `ui-ux/` register as a single companion HTML page.
|
|
3
|
+
*
|
|
4
|
+
* Output structure:
|
|
5
|
+
* 1. Sitemap — every surface listed (linked to its card lower down), grouped
|
|
6
|
+
* where possible by the persona handle it serves.
|
|
7
|
+
* 2. Surface cards — one per surface markdown file, showing type, persona, the
|
|
8
|
+
* "What they see" prose, primary actions, contracts touched, design-system
|
|
9
|
+
* pieces used.
|
|
10
|
+
*
|
|
11
|
+
* The intent is the human-reviewable artefact the markdown can't be:
|
|
12
|
+
* a non-developer scanning the whole UI surface set in one scroll.
|
|
13
|
+
*/
|
|
14
|
+
export interface SurfaceRenderResult {
|
|
15
|
+
written: boolean;
|
|
16
|
+
outPath: string;
|
|
17
|
+
surfaceCount: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function renderSurfaces(opts: {
|
|
20
|
+
buildRoot: string;
|
|
21
|
+
projectName: string;
|
|
22
|
+
generatedAt: string;
|
|
23
|
+
}): Promise<SurfaceRenderResult>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the `ui-ux/` register as a single companion HTML page.
|
|
3
|
+
*
|
|
4
|
+
* Output structure:
|
|
5
|
+
* 1. Sitemap — every surface listed (linked to its card lower down), grouped
|
|
6
|
+
* where possible by the persona handle it serves.
|
|
7
|
+
* 2. Surface cards — one per surface markdown file, showing type, persona, the
|
|
8
|
+
* "What they see" prose, primary actions, contracts touched, design-system
|
|
9
|
+
* pieces used.
|
|
10
|
+
*
|
|
11
|
+
* The intent is the human-reviewable artefact the markdown can't be:
|
|
12
|
+
* a non-developer scanning the whole UI surface set in one scroll.
|
|
13
|
+
*/
|
|
14
|
+
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { parseSections, firstParagraph, isPlaceholder, } from './parser.js';
|
|
18
|
+
import { pageWrapper, card, emptyState, escapeHtml, renderProse, renderInlineMarkdown, } from './html.js';
|
|
19
|
+
const SKIP_FILES = new Set(['_index.md', 'surface-template.md']);
|
|
20
|
+
export async function renderSurfaces(opts) {
|
|
21
|
+
const dir = path.join(opts.buildRoot, 'ui-ux');
|
|
22
|
+
const outPath = path.join(dir, '_view.html');
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
return { written: false, outPath, surfaceCount: 0 };
|
|
25
|
+
}
|
|
26
|
+
const surfaces = await loadSurfaces(dir);
|
|
27
|
+
const body = renderBody(surfaces);
|
|
28
|
+
const html = pageWrapper({
|
|
29
|
+
title: 'Surfaces & sitemap',
|
|
30
|
+
projectName: opts.projectName,
|
|
31
|
+
generatedAt: opts.generatedAt,
|
|
32
|
+
sourceNote: '<code>docs/build/ui-ux/</code>',
|
|
33
|
+
body,
|
|
34
|
+
});
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
await writeFile(outPath, html, 'utf8');
|
|
37
|
+
return { written: true, outPath, surfaceCount: surfaces.length };
|
|
38
|
+
}
|
|
39
|
+
async function loadSurfaces(dir) {
|
|
40
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
41
|
+
const surfaces = [];
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
44
|
+
continue;
|
|
45
|
+
if (SKIP_FILES.has(entry.name))
|
|
46
|
+
continue;
|
|
47
|
+
const filePath = path.join(dir, entry.name);
|
|
48
|
+
const md = await readFile(filePath, 'utf8');
|
|
49
|
+
if (isPlaceholder(md))
|
|
50
|
+
continue;
|
|
51
|
+
surfaces.push(parseSurface(entry.name, md));
|
|
52
|
+
}
|
|
53
|
+
surfaces.sort((a, b) => a.title.localeCompare(b.title));
|
|
54
|
+
return surfaces;
|
|
55
|
+
}
|
|
56
|
+
function parseSurface(file, md) {
|
|
57
|
+
const sections = parseSections(md);
|
|
58
|
+
const preamble = sections.get('') ?? '';
|
|
59
|
+
const titleMatch = md.match(/^#\s+(.+)$/m);
|
|
60
|
+
return {
|
|
61
|
+
file,
|
|
62
|
+
slug: file.replace(/\.md$/, ''),
|
|
63
|
+
title: titleMatch ? titleMatch[1].trim() : file,
|
|
64
|
+
type: extractMetadata(preamble, 'Type'),
|
|
65
|
+
status: extractMetadata(preamble, 'Status'),
|
|
66
|
+
personaRefs: extractPersonaRefs(sections.get('Who uses this surface') ?? ''),
|
|
67
|
+
sections,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function extractMetadata(preamble, key) {
|
|
71
|
+
const re = new RegExp(`\\*\\*${key}:\\*\\*\\s*(.+)`, 'i');
|
|
72
|
+
const m = preamble.match(re);
|
|
73
|
+
return m ? m[1].trim() : null;
|
|
74
|
+
}
|
|
75
|
+
function extractPersonaRefs(body) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const re = /P\d{3}/g;
|
|
78
|
+
let m;
|
|
79
|
+
while ((m = re.exec(body)) !== null)
|
|
80
|
+
out.push(m[0]);
|
|
81
|
+
return Array.from(new Set(out));
|
|
82
|
+
}
|
|
83
|
+
function renderBody(surfaces) {
|
|
84
|
+
if (surfaces.length === 0) {
|
|
85
|
+
return card('No surfaces filed yet', emptyState('Phase C of planning produces this content. Once surfaces are filed in docs/build/ui-ux/, this view will render them.'));
|
|
86
|
+
}
|
|
87
|
+
return [renderSitemap(surfaces), ...surfaces.map(renderSurfaceCard)].join('\n');
|
|
88
|
+
}
|
|
89
|
+
function renderSitemap(surfaces) {
|
|
90
|
+
const groups = new Map();
|
|
91
|
+
for (const s of surfaces) {
|
|
92
|
+
const key = s.personaRefs.length > 0 ? s.personaRefs.join(', ') : '(no persona linked)';
|
|
93
|
+
const bucket = groups.get(key) ?? [];
|
|
94
|
+
bucket.push(s);
|
|
95
|
+
groups.set(key, bucket);
|
|
96
|
+
}
|
|
97
|
+
const sections = [...groups.entries()]
|
|
98
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
99
|
+
.map(([persona, group]) => {
|
|
100
|
+
const items = group
|
|
101
|
+
.map((s) => {
|
|
102
|
+
const typeTag = s.type ? ` <span class="tag">${escapeHtml(s.type)}</span>` : '';
|
|
103
|
+
return `<li><a href="#${escapeHtml(s.slug)}">${escapeHtml(s.title)}</a>${typeTag}</li>`;
|
|
104
|
+
})
|
|
105
|
+
.join('');
|
|
106
|
+
return `<h4>${escapeHtml(persona)}</h4><ul class="sitemap-list">${items}</ul>`;
|
|
107
|
+
})
|
|
108
|
+
.join('');
|
|
109
|
+
return card('Sitemap', sections);
|
|
110
|
+
}
|
|
111
|
+
function renderSurfaceCard(s) {
|
|
112
|
+
const tags = [];
|
|
113
|
+
if (s.type)
|
|
114
|
+
tags.push(`<span class="tag">${escapeHtml(s.type)}</span>`);
|
|
115
|
+
if (s.status)
|
|
116
|
+
tags.push(`<span class="tag tag--status">${escapeHtml(s.status)}</span>`);
|
|
117
|
+
for (const ref of s.personaRefs)
|
|
118
|
+
tags.push(`<span class="tag">${escapeHtml(ref)}</span>`);
|
|
119
|
+
const headerTags = tags.length > 0 ? `<div class="card__tags">${tags.join(' ')}</div>` : '';
|
|
120
|
+
const sectionBlocks = [headerTags];
|
|
121
|
+
for (const [heading, body] of s.sections) {
|
|
122
|
+
if (heading === '' || heading.toLowerCase() === 'notes')
|
|
123
|
+
continue;
|
|
124
|
+
if (heading === 'Open questions about this surface' && !body.trim())
|
|
125
|
+
continue;
|
|
126
|
+
const rendered = body.trim() ? renderProse(body) : '';
|
|
127
|
+
if (!rendered)
|
|
128
|
+
continue;
|
|
129
|
+
sectionBlocks.push(`<h4>${escapeHtml(heading)}</h4>${rendered}`);
|
|
130
|
+
}
|
|
131
|
+
// Best-effort wireframe: render the first paragraph of "What they see" as a
|
|
132
|
+
// hint label inside a skeletal frame. Not a real wireframe — a signal that
|
|
133
|
+
// this is a surface and roughly what it contains.
|
|
134
|
+
const seen = s.sections.get('What they see') ?? '';
|
|
135
|
+
const hint = firstParagraph(seen);
|
|
136
|
+
const wireframe = hint
|
|
137
|
+
? `<div class="wireframe"><div class="wireframe__label">${renderInlineMarkdown(hint).slice(0, 240)}</div></div>`
|
|
138
|
+
: '';
|
|
139
|
+
return `<section id="${escapeHtml(s.slug)}" class="card">
|
|
140
|
+
<h3>${escapeHtml(s.title)}</h3>
|
|
141
|
+
${wireframe}
|
|
142
|
+
<div class="card__body">${sectionBlocks.join('')}</div>
|
|
143
|
+
</section>`;
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nusoft/nuos-build-catalogue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "NuOS build-catalogue tooling: semantic search (WU 110) + migration runner that lifts markdown artefacts into JSON-backed workflow records (WU 111, Phase G).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
20
20
|
"prepublishOnly": "npm run build",
|
|
21
21
|
"verify-storage": "tsx scripts/verify-persistence.ts",
|
|
22
|
-
"test": "tsx --test tests/chunk.test.ts tests/metadata.test.ts tests/crawl.test.ts tests/migrate.test.ts tests/commands-read.test.ts tests/regenerate.test.ts tests/commands-write.test.ts tests/ac-parse.test.ts tests/create.test.ts tests/init.test.ts tests/wu-111-soak-findings.test.ts tests/plan.test.ts tests/mode.test.ts tests/swarm.test.ts tests/setup-progress-bar.test.ts tests/setup-ollama-pull.test.ts tests/setup-run-llm-setup.test.ts tests/wu-active.test.ts tests/install-claude-hooks.test.ts",
|
|
22
|
+
"test": "tsx --test tests/chunk.test.ts tests/metadata.test.ts tests/crawl.test.ts tests/migrate.test.ts tests/commands-read.test.ts tests/regenerate.test.ts tests/commands-write.test.ts tests/ac-parse.test.ts tests/create.test.ts tests/init.test.ts tests/wu-111-soak-findings.test.ts tests/plan.test.ts tests/mode.test.ts tests/render.test.ts tests/swarm.test.ts tests/setup-progress-bar.test.ts tests/setup-ollama-pull.test.ts tests/setup-run-llm-setup.test.ts tests/wu-active.test.ts tests/install-claude-hooks.test.ts",
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
24
|
"index": "tsx src/cli.ts index",
|
|
25
25
|
"search": "tsx src/cli.ts search"
|
package/scripts/hooks/pre-commit
CHANGED
|
@@ -68,7 +68,8 @@ check_index_drift() {
|
|
|
68
68
|
ids_in_tree=$(cd "$REPO_ROOT/$dir" && {
|
|
69
69
|
find . -maxdepth 2 -type f -name "*.md" \
|
|
70
70
|
-not -name "_index.md" \
|
|
71
|
-
-not -name "*-template.md"
|
|
71
|
+
-not -name "*-template.md" \
|
|
72
|
+
-not -name "*-template-*.md" 2>/dev/null \
|
|
72
73
|
| sed -nE "$file_regex" \
|
|
73
74
|
| sort -u
|
|
74
75
|
})
|
|
@@ -34,19 +34,47 @@ nuos-catalogue memory store --value="<what worked and why, or what to avoid>" --
|
|
|
34
34
|
|
|
35
35
|
If anything in the work unit is ambiguous, **stop and surface the ambiguity to the coordinator** rather than guessing. A guess produces work that may not match the design.
|
|
36
36
|
|
|
37
|
+
## Design system gate (UI work — enforced by hook)
|
|
38
|
+
|
|
39
|
+
**If this work unit touches any UI file (`.css`, `.scss`, `.less`, `.html`, `.tsx`, `.jsx`, `.vue`, `.svelte`, `.astro`), a write-gate hook is active. It will BLOCK the write and force you back here.**
|
|
40
|
+
|
|
41
|
+
Before writing any UI file, complete these steps in order:
|
|
42
|
+
|
|
43
|
+
1. **Read the design system — all of it:**
|
|
44
|
+
- `docs/build/design-system/tokens-colour.md` — every colour token and its hex value
|
|
45
|
+
- `docs/build/design-system/tokens-typography.md` — font sizes, weights, line heights
|
|
46
|
+
- `docs/build/design-system/tokens-spacing.md` — the spacing scale
|
|
47
|
+
- `docs/build/design-system/tokens-radius-elevation.md` — border radius, shadows
|
|
48
|
+
|
|
49
|
+
2. **Identify the token reference pattern this project uses** — read two or three existing UI files to confirm one of:
|
|
50
|
+
- CSS custom properties: `color: var(--colour-text-primary);`
|
|
51
|
+
- Theme/token object: `color: theme.colour.text.primary`
|
|
52
|
+
- Utility class config: project-configured Tailwind or similar
|
|
53
|
+
|
|
54
|
+
3. **Map every value to a token before writing a single line.** If a colour, size, or spacing value you need has no token in the design system, **stop and surface the gap to the coordinator** — do not invent a value or use a hardcode.
|
|
55
|
+
|
|
56
|
+
The hook checks for:
|
|
57
|
+
- Raw hex literals in colour properties: `color: #1a2b3c` — BLOCKED
|
|
58
|
+
- Hex strings in JSX inline styles: `color: '#fff'` — BLOCKED
|
|
59
|
+
- Hex colours in HTML style attributes — BLOCKED
|
|
60
|
+
|
|
61
|
+
CSS custom property definitions (`--colour-x: #hex`) are allowed — that is where the token value lives. Everything else must use the token by name.
|
|
62
|
+
|
|
37
63
|
## How you work
|
|
38
64
|
|
|
39
65
|
1. **Plan the change in your head first**, then state it in 1-2 sentences before writing code. Match existing code idioms; don't introduce new patterns the project hasn't adopted.
|
|
40
66
|
|
|
41
|
-
2. **
|
|
67
|
+
2. **Search before writing (DRY — strict).** Before adding any new helper, utility, type, component, hook, validator, query, styled primitive, or constant: grep the codebase for one that already does the job — by name (the noun you'd naturally call it), by shape (signature, prop list, structural pattern), and in the conventional locations for the stack (`lib/`, `utils/`, `hooks/`, `components/`, `types/`, `db/queries/`, `schemas/`, equivalents). If found, import and use it (extend in-place if it needs a small addition). If close-but-not-quite, **stop and surface to the coordinator** — extending the existing thing is almost always cheaper than spawning a parallel implementation. This rule applies to new code in *this* WU; pre-existing duplication elsewhere is a follow-up to file, not your job. Search-and-reuse only — this is *not* a directive to extract net-new abstractions; the rule against premature abstraction (point 4) still applies.
|
|
68
|
+
|
|
69
|
+
3. **Make the smallest change that satisfies the work unit's acceptance criteria.** Don't refactor adjacent code "while you're there" unless the work unit explicitly asks for it.
|
|
42
70
|
|
|
43
|
-
|
|
71
|
+
4. **Write code that the tester can verify.** Every acceptance criterion in the work unit should be checkable by looking at the running system — your code should make that easy.
|
|
44
72
|
|
|
45
|
-
|
|
73
|
+
5. **Avoid speculative abstractions.** Three similar lines of code beats a premature abstraction. Don't design for hypothetical future requirements. The architect designs; you implement what's needed now.
|
|
46
74
|
|
|
47
|
-
|
|
75
|
+
6. **No comments unless WHY is non-obvious.** A hidden constraint, a workaround for a specific bug, behaviour that would surprise a reader. If removing the comment wouldn't confuse a future reader, don't write it.
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
7. **Write testable code (vitest gate).** If `methodfile.json` declares `testing.framework: "vitest"` with `testing.enforced: true`, every source file you create or substantially modify in this WU must end up covered by at least one vitest test — the tester writes them, but your job is to make that cheap. Export the units the tester needs to reach; avoid burying observable logic inside untestable closures; keep side effects at the edges. Files that genuinely can't be unit-tested (pure type declarations, config glue) are fine — flag them in your notes so the reviewer doesn't treat them as drift.
|
|
50
78
|
|
|
51
79
|
## When you finish
|
|
52
80
|
|
|
@@ -46,13 +46,15 @@ nuos-catalogue memory store --value="<the pattern and why it matters>" --wu=<han
|
|
|
46
46
|
|
|
47
47
|
4. **Does it match existing code idioms?** New patterns introduced without justification are a yellow flag — surface them to the coordinator as either "rename to match existing X" or "intentional, file as new pattern in architecture/".
|
|
48
48
|
|
|
49
|
-
5. **Does
|
|
49
|
+
5. **Does new code reinvent something the codebase already has? (DRY — strict.)** For each new helper, utility, type, component, hook, validator, query, styled primitive, or constant the coder added in this WU, grep the codebase for an existing implementation that does the same job — by name, by signature/shape, and in the conventional locations for the stack (`lib/`, `utils/`, `hooks/`, `components/`, `types/`, `db/queries/`, `schemas/`, equivalents). If you find one, raise a **BLOCKER** citing the existing path; the coder must reuse it (extending the existing one in place if needed) rather than ship a parallel implementation. This applies only to *new* code in this WU vs. the *existing* codebase — don't flag duplication within the WU's own output as a violation; the rule against premature abstraction still governs net-new shared code.
|
|
50
50
|
|
|
51
|
-
6. **
|
|
51
|
+
6. **Does it surface or hide changes future work needs to know?** If the coder modified an interface that downstream work depends on, the change should be in a decision file or a contract update — not silent.
|
|
52
52
|
|
|
53
|
-
7. **Is
|
|
53
|
+
7. **Is there dead-weight or scope creep?** Refactors adjacent to the work unit that weren't asked for. Speculative abstractions. Unnecessary comments. Half-implementations of features not in this work unit.
|
|
54
54
|
|
|
55
|
-
8. **
|
|
55
|
+
8. **Is jargon being introduced into user-facing copy?** If the work unit serves a non-engineer persona, the surface text should match the project's voice file. Flag anything that sounds like dev-speak in a user-facing surface.
|
|
56
|
+
|
|
57
|
+
9. **Does the vitest gate pass (JS/TS projects)?** If `methodfile.json` declares `testing.framework: "vitest"` with `testing.enforced: true`, run both gates from [build-wu.md §Step 5.5](../protocols/build-wu.md):
|
|
56
58
|
- **Gate A:** Run `npx vitest run` (or whatever `testing.command` says) from the implementation repo root. Capture the full output. Non-zero exit → BLOCKER finding with the failing test list.
|
|
57
59
|
- **Gate B:** Compute `git diff --name-only <swarm-base>...HEAD`, filter to source files (`.ts/.tsx/.js/.jsx` under `src/`, `app/`, `routes/`, `pages/`, `lib/`, `components/`, `api/` — exclude `*.test.*`, `*.spec.*`, `*.d.ts`, configs). For each remaining file, grep the test directories for an import of that module or a colocated `*.test.*` file. Any uncovered file → BLOCKER finding naming the file. The coder may rebut by flagging files as genuinely untestable (type-only, config glue) in the WU notes — accept those rebuttals when reasonable.
|
|
58
60
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# NuOS Build Method — Design System Compliance Hook (PreToolUse)
|
|
4
|
+
#
|
|
5
|
+
# Blocks Write / Edit / MultiEdit when:
|
|
6
|
+
# (a) the target file is a UI file (.css, .scss, .less, .html, .tsx,
|
|
7
|
+
# .jsx, .vue, .svelte, .astro), AND
|
|
8
|
+
# (b) the written content contains hardcoded colour values (hex literals,
|
|
9
|
+
# CSS named colours) in colour-property positions, AND
|
|
10
|
+
# (c) the project has a design system at docs/build/design-system/
|
|
11
|
+
#
|
|
12
|
+
# Rationale
|
|
13
|
+
# ─────────
|
|
14
|
+
# Agents write UI code without first reading the design system, producing
|
|
15
|
+
# raw hex values that bypass the project's token contracts. The reviewer
|
|
16
|
+
# agent catches this after the fact — but by then the coder has already
|
|
17
|
+
# satisfied its acceptance criteria and treats the finding as "cleanup."
|
|
18
|
+
# This hook closes the gap at the moment of writing: the write is blocked
|
|
19
|
+
# and the agent is shown exactly where to look before it can retry.
|
|
20
|
+
#
|
|
21
|
+
# What is checked
|
|
22
|
+
# ───────────────
|
|
23
|
+
# 1. CSS/SCSS/Less: colour property with hardcoded hex literal
|
|
24
|
+
# color: #fff ← BLOCKED
|
|
25
|
+
# --colour-x: #fff ← allowed (token definition line; starts with --)
|
|
26
|
+
# color: var(--x) ← allowed
|
|
27
|
+
# 2. JSX/TSX: inline style object with hex string
|
|
28
|
+
# color: '#1a2b3c' ← BLOCKED
|
|
29
|
+
# 3. HTML: style attribute containing a hex colour
|
|
30
|
+
# style="color: #fff" ← BLOCKED
|
|
31
|
+
#
|
|
32
|
+
# Degrade-safe: if content cannot be reliably parsed (no jq or python3,
|
|
33
|
+
# or parse failure), the hook exits 0. Never block on ambiguous input.
|
|
34
|
+
#
|
|
35
|
+
# Exit codes
|
|
36
|
+
# ──────────
|
|
37
|
+
# 0 — allow (no violations, design system absent, or degrade-safe skip)
|
|
38
|
+
# 2 — block (stderr is surfaced to the model by Claude Code)
|
|
39
|
+
|
|
40
|
+
set -uo pipefail
|
|
41
|
+
|
|
42
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
43
|
+
|
|
44
|
+
# ── Project root ──────────────────────────────────────────────────────────────
|
|
45
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
46
|
+
if [[ -z "$PROJECT_ROOT" ]]; then
|
|
47
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
48
|
+
fi
|
|
49
|
+
if [[ -z "$PROJECT_ROOT" ]]; then exit 0; fi
|
|
50
|
+
|
|
51
|
+
# ── Extract file path ─────────────────────────────────────────────────────────
|
|
52
|
+
FILE=$(printf '%s' "$INPUT" \
|
|
53
|
+
| grep -oE '"(file_path|notebook_path)"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
|
54
|
+
| head -1 \
|
|
55
|
+
| sed -E 's/"(file_path|notebook_path)"[[:space:]]*:[[:space:]]*"//' \
|
|
56
|
+
| tr -d '"')
|
|
57
|
+
|
|
58
|
+
if [[ -z "${FILE:-}" ]]; then exit 0; fi
|
|
59
|
+
|
|
60
|
+
# Normalise to absolute path
|
|
61
|
+
case "$FILE" in
|
|
62
|
+
/*) ABSOLUTE_FILE="$FILE" ;;
|
|
63
|
+
*) ABSOLUTE_FILE="$(pwd)/$FILE" ;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
# ── Skip the design-system directory itself ───────────────────────────────────
|
|
67
|
+
if [[ "$ABSOLUTE_FILE" == *"/docs/build/design-system/"* ]]; then
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Skip generated output directories
|
|
72
|
+
case "$ABSOLUTE_FILE" in
|
|
73
|
+
*/node_modules/*|*/dist/*|*/.next/*|*/build/*|*/.nuxt/*) exit 0 ;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
# ── Only check UI file types ──────────────────────────────────────────────────
|
|
77
|
+
EXTENSION="${FILE##*.}"
|
|
78
|
+
case "$EXTENSION" in
|
|
79
|
+
css|scss|less|html|tsx|jsx|vue|svelte|astro) : ;;
|
|
80
|
+
*) exit 0 ;;
|
|
81
|
+
esac
|
|
82
|
+
|
|
83
|
+
# ── Find the design system ────────────────────────────────────────────────────
|
|
84
|
+
DS_DIR=""
|
|
85
|
+
if [[ -d "$PROJECT_ROOT/docs/build/design-system" ]]; then
|
|
86
|
+
DS_DIR="$PROJECT_ROOT/docs/build/design-system"
|
|
87
|
+
else
|
|
88
|
+
# Split-repo pattern: look for a sibling catalogue that owns the design system
|
|
89
|
+
PARENT="$(dirname "$PROJECT_ROOT")"
|
|
90
|
+
for sibling in "$PARENT"/*/docs/build/design-system; do
|
|
91
|
+
if [[ -d "$sibling" ]]; then
|
|
92
|
+
DS_DIR="$sibling"
|
|
93
|
+
break
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [[ -z "$DS_DIR" ]]; then exit 0; fi
|
|
99
|
+
|
|
100
|
+
COLOUR_FILE="$DS_DIR/tokens-colour.md"
|
|
101
|
+
if [[ ! -f "$COLOUR_FILE" ]]; then exit 0; fi
|
|
102
|
+
|
|
103
|
+
# ── Extract the content being written ─────────────────────────────────────────
|
|
104
|
+
# Write → tool_input.content
|
|
105
|
+
# Edit → tool_input.new_string
|
|
106
|
+
# MultiEdit → tool_input.edits[].new_string (joined)
|
|
107
|
+
CONTENT=""
|
|
108
|
+
if command -v jq &>/dev/null; then
|
|
109
|
+
CONTENT=$(printf '%s' "$INPUT" | jq -r '
|
|
110
|
+
.tool_input.content //
|
|
111
|
+
.tool_input.new_string //
|
|
112
|
+
(.tool_input.edits // [] | map(.new_string // "") | join("\n")) //
|
|
113
|
+
""
|
|
114
|
+
' 2>/dev/null || true)
|
|
115
|
+
elif command -v python3 &>/dev/null; then
|
|
116
|
+
CONTENT=$(printf '%s' "$INPUT" | python3 -c "
|
|
117
|
+
import sys, json
|
|
118
|
+
try:
|
|
119
|
+
d = json.load(sys.stdin)
|
|
120
|
+
ti = d.get('tool_input', {})
|
|
121
|
+
out = ti.get('content') or ti.get('new_string')
|
|
122
|
+
if out is None:
|
|
123
|
+
edits = ti.get('edits', [])
|
|
124
|
+
out = '\n'.join(e.get('new_string', '') for e in edits)
|
|
125
|
+
print(out or '')
|
|
126
|
+
except: pass
|
|
127
|
+
" 2>/dev/null || true)
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [[ -z "${CONTENT:-}" ]]; then exit 0; fi
|
|
131
|
+
|
|
132
|
+
# ── Detect hardcoded colour violations ───────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
# 1. CSS/SCSS/Less: colour property with a hex literal value.
|
|
135
|
+
# Excludes lines that start with optional whitespace followed by '--'
|
|
136
|
+
# (those are CSS custom property definitions — they SET the token value).
|
|
137
|
+
CSS_HEX=$(printf '%s' "$CONTENT" \
|
|
138
|
+
| grep -E '(color|background(-color)?|border(-color)?|fill|stroke|outline(-color)?|accent-color)[[:space:]]*:[[:space:]]*#[0-9a-fA-F]{3,8}' \
|
|
139
|
+
| grep -vE '^\s*--' \
|
|
140
|
+
| grep -oE '(color|background(-color)?|border(-color)?|fill|stroke|outline(-color)?|accent-color)[[:space:]]*:[[:space:]]*#[0-9a-fA-F]{3,8}' \
|
|
141
|
+
| head -3 || true)
|
|
142
|
+
|
|
143
|
+
# 2. JSX/TSX: inline style object with a hex colour string.
|
|
144
|
+
# e.g. color: '#fff' or backgroundColor: "#1a2b3c"
|
|
145
|
+
JSX_HEX=$(printf '%s' "$CONTENT" \
|
|
146
|
+
| grep -oE "(color|backgroundColor|borderColor|fill|stroke|outlineColor)[[:space:]]*:[[:space:]]*['\"]#[0-9a-fA-F]{3,8}" \
|
|
147
|
+
| head -3 || true)
|
|
148
|
+
|
|
149
|
+
# 3. HTML: style attribute that contains a hex colour value.
|
|
150
|
+
HTML_HEX=$(printf '%s' "$CONTENT" \
|
|
151
|
+
| grep -oE 'style=[^>]*#[0-9a-fA-F]{3,8}' \
|
|
152
|
+
| head -3 || true)
|
|
153
|
+
|
|
154
|
+
if [[ -z "${CSS_HEX:-}" && -z "${JSX_HEX:-}" && -z "${HTML_HEX:-}" ]]; then
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# ── Build the violation summary ───────────────────────────────────────────────
|
|
159
|
+
VIOLATIONS=""
|
|
160
|
+
[[ -n "${CSS_HEX:-}" ]] && VIOLATIONS="$VIOLATIONS
|
|
161
|
+
CSS: $(printf '%s' "$CSS_HEX" | head -1)"
|
|
162
|
+
[[ -n "${JSX_HEX:-}" ]] && VIOLATIONS="$VIOLATIONS
|
|
163
|
+
JSX: $(printf '%s' "$JSX_HEX" | head -1)"
|
|
164
|
+
[[ -n "${HTML_HEX:-}" ]] && VIOLATIONS="$VIOLATIONS
|
|
165
|
+
HTML: $(printf '%s' "$HTML_HEX" | head -1)"
|
|
166
|
+
|
|
167
|
+
# ── Read token excerpt for the error message ──────────────────────────────────
|
|
168
|
+
TOKEN_HINT=""
|
|
169
|
+
TOKEN_EXCERPT=$(grep -E '`colour\.' "$COLOUR_FILE" 2>/dev/null | head -12 || true)
|
|
170
|
+
if [[ -n "$TOKEN_EXCERPT" ]]; then
|
|
171
|
+
TOKEN_HINT="
|
|
172
|
+
Available colour tokens (from $(basename "$DS_DIR")/tokens-colour.md):
|
|
173
|
+
$TOKEN_EXCERPT
|
|
174
|
+
|
|
175
|
+
→ Use the token name. Do NOT use the raw hex value."
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# ── Block ─────────────────────────────────────────────────────────────────────
|
|
179
|
+
cat >&2 <<EOF
|
|
180
|
+
✖ nuos: design-system compliance block — hardcoded colour values detected.
|
|
181
|
+
|
|
182
|
+
File: $FILE
|
|
183
|
+
Violations:$VIOLATIONS
|
|
184
|
+
|
|
185
|
+
── Required action ──────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
Before writing any UI file you MUST:
|
|
188
|
+
|
|
189
|
+
1. Read the full design system:
|
|
190
|
+
$DS_DIR/tokens-colour.md
|
|
191
|
+
$DS_DIR/tokens-typography.md
|
|
192
|
+
$DS_DIR/tokens-spacing.md
|
|
193
|
+
$DS_DIR/tokens-radius-elevation.md
|
|
194
|
+
|
|
195
|
+
2. Identify how this project references tokens by reading existing UI
|
|
196
|
+
files in the codebase — look for one of these patterns:
|
|
197
|
+
CSS custom properties: color: var(--colour-text-primary);
|
|
198
|
+
JSX theme object: color: theme.colour.text.primary
|
|
199
|
+
Tailwind config tokens: text-text-primary (if configured)
|
|
200
|
+
|
|
201
|
+
3. Replace EVERY hardcoded hex or named colour with the correct token
|
|
202
|
+
reference. If no token covers the value you need, STOP and surface
|
|
203
|
+
the gap to the coordinator — do NOT invent a one-off value.
|
|
204
|
+
|
|
205
|
+
This hook will block every write that contains a raw colour value.
|
|
206
|
+
The design system is the contract; the implementation must honour it.
|
|
207
|
+
$TOKEN_HINT
|
|
208
|
+
|
|
209
|
+
EOF
|
|
210
|
+
|
|
211
|
+
exit 2
|
|
@@ -67,7 +67,13 @@ Create `docs/build/sessions/YYYY-MM-DD-short-slug.md`. The entry includes:
|
|
|
67
67
|
|
|
68
68
|
Add a row to `sessions/_index.md`.
|
|
69
69
|
|
|
70
|
-
### 8.
|
|
70
|
+
### 8. Regenerate HTML companion views (if any visual register changed)
|
|
71
|
+
|
|
72
|
+
If anything in `ui-ux/`, `design-system/`, `maps/`, or `architecture/` was edited this session, run `nuos-catalogue render` to refresh the companion HTML views (`_view.html` in each of those directories). These are generated artefacts — the markdown stays canonical — but they need to stay in sync so the operator's next review opens current views. Stage the refreshed `_view.html` files for the commit alongside the markdown.
|
|
73
|
+
|
|
74
|
+
If none of those registers changed, skip this step.
|
|
75
|
+
|
|
76
|
+
### 9. Verify nothing is lost
|
|
71
77
|
|
|
72
78
|
Before committing, scan:
|
|
73
79
|
|
|
@@ -78,7 +84,7 @@ Before committing, scan:
|
|
|
78
84
|
- Cross-references resolve (no dead links)
|
|
79
85
|
- Dates are right
|
|
80
86
|
|
|
81
|
-
###
|
|
87
|
+
### 10. Commit
|
|
82
88
|
|
|
83
89
|
Single commit. Message format:
|
|
84
90
|
|