@nusoft/nuos-build-catalogue 0.25.0 → 0.27.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.
Files changed (37) hide show
  1. package/dist/cli.js +24 -0
  2. package/dist/commands/init.js +10 -32
  3. package/dist/commands/mode.d.ts +24 -0
  4. package/dist/commands/mode.js +82 -0
  5. package/dist/commands/render.d.ts +25 -0
  6. package/dist/commands/render.js +40 -0
  7. package/dist/render/architecture.d.ts +18 -0
  8. package/dist/render/architecture.js +74 -0
  9. package/dist/render/design-system.d.ts +26 -0
  10. package/dist/render/design-system.js +308 -0
  11. package/dist/render/html.d.ts +47 -0
  12. package/dist/render/html.js +258 -0
  13. package/dist/render/maps.d.ts +18 -0
  14. package/dist/render/maps.js +67 -0
  15. package/dist/render/parser.d.ts +54 -0
  16. package/dist/render/parser.js +162 -0
  17. package/dist/render/run.d.ts +29 -0
  18. package/dist/render/run.js +92 -0
  19. package/dist/render/surfaces.d.ts +23 -0
  20. package/dist/render/surfaces.js +144 -0
  21. package/package.json +2 -2
  22. package/templates/agents/coder.md +7 -5
  23. package/templates/agents/reviewer.md +6 -4
  24. package/templates/protocols/build-wu.md +13 -63
  25. package/templates/protocols/end-of-session.md +9 -3
  26. package/templates/protocols/persona-new.md +1 -1
  27. package/templates/protocols/plan-architecture.md +10 -24
  28. package/templates/protocols/plan-initial-wu.md +6 -26
  29. package/templates/protocols/plan-maps.md +5 -17
  30. package/templates/protocols/plan-orientation.md +13 -29
  31. package/templates/protocols/plan-review.md +2 -0
  32. package/templates/protocols/plan-uiux.md +35 -86
  33. package/templates/protocols/start-of-session.md +13 -1
  34. package/templates/protocols/wu-new.md +1 -1
  35. package/templates/starter-kit/CLAUDE.md +9 -37
  36. package/templates/starter-kit/docs/build/OPERATOR-MODES.md +44 -0
  37. package/templates/starter-kit/methodfile.json +5 -0
package/dist/cli.js CHANGED
@@ -424,6 +424,12 @@ Usage:
424
424
 
425
425
  nuos-catalogue plan status show planning progress across the 5-phase arc
426
426
 
427
+ nuos-catalogue mode print the current operator mode
428
+ nuos-catalogue mode <name> set operator mode: coaching | standard | developer
429
+
430
+ nuos-catalogue render regenerate HTML companion views for visual registers
431
+ nuos-catalogue render <register> just one register: surfaces | design-system | maps | architecture
432
+
427
433
  nuos-catalogue swarm status [--limit=N] list recent /build-wu runs
428
434
  nuos-catalogue swarm cost aggregate cost across swarm runs
429
435
 
@@ -557,6 +563,24 @@ async function main() {
557
563
  console.error('available: plan status');
558
564
  process.exit(1);
559
565
  }
566
+ case 'mode': {
567
+ const { cmdMode } = await import('./commands/mode.js');
568
+ const code = await cmdMode({ cwd: process.cwd(), mode: args.positional[0] });
569
+ if (code !== 0)
570
+ process.exit(code);
571
+ break;
572
+ }
573
+ case 'render': {
574
+ const { cmdRender } = await import('./commands/render.js');
575
+ const code = await cmdRender({
576
+ cwd: process.cwd(),
577
+ positional: args.positional[0],
578
+ buildRootFlag: args.flags['build-root'],
579
+ });
580
+ if (code !== 0)
581
+ process.exit(code);
582
+ break;
583
+ }
560
584
  case 'swarm': {
561
585
  const sub = args.positional[0];
562
586
  const { cmdSwarmStatus, cmdSwarmCost } = await import('./commands/swarm.js');
@@ -517,43 +517,21 @@ async function ensureGitignoreEntries(gitignorePath, log_line) {
517
517
  ? ' · created .gitignore with catalogue rules'
518
518
  : ' · appended catalogue rules to .gitignore');
519
519
  }
520
- function renderCatalogueSection(projectName) {
520
+ function renderCatalogueSection(_projectName) {
521
521
  return `## Build catalogue (NuOS Build Method)
522
522
 
523
- This project uses the **NuOS Build Method catalogue** at [docs/build/](docs/build/). It is the project's memory who it's for, what's been built, what's been decided, what's still unknown, what's at risk. Eleven registers in plain Markdown. The catalogue stays current through two protocols that bookend every session.
523
+ This project's memory lives at [docs/build/](docs/build/) — eleven registers in plain Markdown, kept current by two bookend protocols.
524
524
 
525
- **Start here** if you're new:
525
+ - Run \`/start-of-session\` to begin every session, \`/end-of-session\` to close every session.
526
+ - A brand-new project: \`/start-of-session\` detects the empty catalogue and routes through 5 planning phases (Orientation → Architecture → UI/UX + Design System → Maps → Initial Work Units) before building.
527
+ - Onboarding: [docs/build/WELCOME.md](docs/build/WELCOME.md) (5 min); every term defined in [docs/build/GLOSSARY.md](docs/build/GLOSSARY.md).
526
528
 
527
- - [docs/build/WELCOME.md](docs/build/WELCOME.md) — what this catalogue is, in 5 minutes
528
- - [docs/build/GLOSSARY.md](docs/build/GLOSSARY.md) — every term defined once
529
+ ### Rules
529
530
 
530
- ### Three commands
531
+ - **Never close without \`/end-of-session\`** — work not written down is lost.
532
+ - **Never edit an accepted decision file** — file a superseding decision instead (pre-commit hook blocks silent edits).
533
+ - **Never make an architectural decision in conversation without filing it to \`docs/build/decisions/\` first** — drift kills the catalogue.
531
534
 
532
- That's it. Everything else is automatic.
533
-
534
- \`\`\`text
535
- /start-of-session — every time you begin working
536
- /end-of-session — every time you stop
537
- \`\`\`
538
-
539
- (\`init\` runs once at the start; you've already done that.)
540
-
541
- If this is a brand-new project, \`/start-of-session\` will detect the empty catalogue and walk you through 5 short planning phases (Orientation, Architecture, UI/UX + Design System, Maps, Initial Work Units) before any building starts. Each phase is its own session. Take them in order; pause whenever you need to.
542
-
543
- ### The principle that makes it work
544
-
545
- **Project memory never drifts from project reality.** Every decision made in conversation gets saved before the session ends. Every change to an existing piece flows through a protocol. The pre-commit hook blocks silent edits to accepted decisions; the post-commit hook auto-refreshes the search index after every commit. What the AI finds when you ask "what did we decide about X?" is always current.
546
-
547
- ### What never to do
548
-
549
- - **Never close a session without \`/end-of-session\`.** Work that isn't written down is work that's lost.
550
- - **Never edit an accepted decision file.** If something changes, file a new decision that supersedes the old one. The pre-commit hook will block silent edits.
551
- - **Never make an architectural decision in conversation without filing it.** If you and the AI agree on "let's go with X", file it as a decision *before moving on*. Drift is the failure mode that makes the catalogue worthless.
552
-
553
- ### If you need more
554
-
555
- - All registers and their templates live under [docs/build/](docs/build/)
556
- - The full CLI surface (creating work units / decisions / personas / questions / contracts / surfaces from the command line) is documented at [docs/build/WELCOME.md](docs/build/WELCOME.md)
557
- - To refresh protocols and hooks later (after a CLI upgrade): \`npx @nusoft/nuos-build-catalogue install-protocols\`
535
+ Refresh protocols + hooks after a CLI upgrade with \`npx @nusoft/nuos-build-catalogue install-protocols\`.
558
536
  `;
559
537
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `nuos-catalogue mode` — read or set the operator mode in methodfile.json.
3
+ *
4
+ * nuos-catalogue mode → prints the current mode
5
+ * nuos-catalogue mode <name> → sets the mode and stamps modeSelectedAt
6
+ *
7
+ * Valid names: coaching, standard, developer.
8
+ *
9
+ * The mode shapes the *tone* of every operator-facing protocol — see
10
+ * docs/build/OPERATOR-MODES.md for what each mode means. The picker
11
+ * normally runs once at first /start-of-session; this CLI command exists
12
+ * so the operator can change their mind without hand-editing JSON.
13
+ */
14
+ export declare const VALID_MODES: readonly ["coaching", "standard", "developer"];
15
+ export type OperatorMode = (typeof VALID_MODES)[number];
16
+ export declare function isOperatorMode(v: unknown): v is OperatorMode;
17
+ export interface ModeOptions {
18
+ cwd?: string;
19
+ /** When omitted, the command prints the current mode and exits 0. */
20
+ mode?: string;
21
+ /** Override "now" — used by tests for deterministic timestamps. */
22
+ now?: () => string;
23
+ }
24
+ export declare function cmdMode(options?: ModeOptions): Promise<number>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `nuos-catalogue mode` — read or set the operator mode in methodfile.json.
3
+ *
4
+ * nuos-catalogue mode → prints the current mode
5
+ * nuos-catalogue mode <name> → sets the mode and stamps modeSelectedAt
6
+ *
7
+ * Valid names: coaching, standard, developer.
8
+ *
9
+ * The mode shapes the *tone* of every operator-facing protocol — see
10
+ * docs/build/OPERATOR-MODES.md for what each mode means. The picker
11
+ * normally runs once at first /start-of-session; this CLI command exists
12
+ * so the operator can change their mind without hand-editing JSON.
13
+ */
14
+ import { readFile, writeFile } from 'node:fs/promises';
15
+ import { existsSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ export const VALID_MODES = ['coaching', 'standard', 'developer'];
18
+ export function isOperatorMode(v) {
19
+ return typeof v === 'string' && VALID_MODES.includes(v);
20
+ }
21
+ export async function cmdMode(options = {}) {
22
+ const cwd = options.cwd ?? process.cwd();
23
+ const methodfilePath = path.join(cwd, 'methodfile.json');
24
+ if (!existsSync(methodfilePath)) {
25
+ console.error(`No methodfile.json found at ${cwd}.`);
26
+ console.error('Run `nuos-catalogue init` first to set up a catalogue.');
27
+ return 1;
28
+ }
29
+ let raw;
30
+ try {
31
+ raw = await readFile(methodfilePath, 'utf8');
32
+ }
33
+ catch (err) {
34
+ console.error(`Couldn't read methodfile.json: ${err.message}`);
35
+ return 1;
36
+ }
37
+ let mf;
38
+ try {
39
+ mf = JSON.parse(raw);
40
+ }
41
+ catch (err) {
42
+ console.error(`methodfile.json is not valid JSON: ${err.message}`);
43
+ return 1;
44
+ }
45
+ const operator = mf.operator ?? {};
46
+ const current = operator.mode;
47
+ if (options.mode === undefined) {
48
+ if (isOperatorMode(current)) {
49
+ console.log(current);
50
+ return 0;
51
+ }
52
+ console.log('(unset)');
53
+ console.log('');
54
+ console.log('Run `nuos-catalogue mode <coaching|standard|developer>` to set,');
55
+ console.log('or just start /start-of-session and the picker will run automatically.');
56
+ return 0;
57
+ }
58
+ if (!isOperatorMode(options.mode)) {
59
+ console.error(`unknown mode: ${options.mode}`);
60
+ console.error(`valid modes: ${VALID_MODES.join(', ')}`);
61
+ return 1;
62
+ }
63
+ const today = (options.now ?? (() => new Date().toISOString().slice(0, 10)))();
64
+ const updated = {
65
+ ...mf,
66
+ operator: {
67
+ ...operator,
68
+ mode: options.mode,
69
+ modeSelectedAt: today,
70
+ },
71
+ };
72
+ // Preserve a trailing newline to match the JSON convention in the rest of the
73
+ // project (every other written file ends with a newline; tooling like git diff
74
+ // is happier that way).
75
+ const trailingNewline = raw.endsWith('\n') ? '\n' : '';
76
+ await writeFile(methodfilePath, JSON.stringify(updated, null, 2) + trailingNewline, 'utf8');
77
+ const previous = isOperatorMode(current) ? current : '(unset)';
78
+ console.log(`Operator mode: ${previous} → ${options.mode}`);
79
+ console.log(`Tone for every operator-facing protocol now matches '${options.mode}'.`);
80
+ console.log(`See docs/build/OPERATOR-MODES.md for what that means in practice.`);
81
+ return 0;
82
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `nuos-catalogue render [register]` — regenerate the HTML companion views
3
+ * for the visual registers (ui-ux, design-system, maps, architecture).
4
+ *
5
+ * nuos-catalogue render # render all four
6
+ * nuos-catalogue render surfaces # render just ui-ux/_view.html
7
+ * nuos-catalogue render design-system # render just design-system/_view.html
8
+ * nuos-catalogue render maps # render just maps/_view.html
9
+ * nuos-catalogue render architecture # render just architecture/_view.html
10
+ *
11
+ * Companion HTML files are *generated artefacts*. The canonical source for
12
+ * every register stays markdown — every agent (architect, coder, reviewer)
13
+ * reads markdown. The HTML exists so the operator can review inherently visual
14
+ * artefacts in their natural medium.
15
+ */
16
+ export interface RenderCommandOptions {
17
+ cwd?: string;
18
+ /** Optional register name (`surfaces`, `design-system`, `maps`, `architecture`). */
19
+ positional?: string;
20
+ /** Override for --build-root flag. */
21
+ buildRootFlag?: string | boolean;
22
+ /** Override "now" for deterministic test output. */
23
+ now?: () => Date;
24
+ }
25
+ export declare function cmdRender(options?: RenderCommandOptions): Promise<number>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `nuos-catalogue render [register]` — regenerate the HTML companion views
3
+ * for the visual registers (ui-ux, design-system, maps, architecture).
4
+ *
5
+ * nuos-catalogue render # render all four
6
+ * nuos-catalogue render surfaces # render just ui-ux/_view.html
7
+ * nuos-catalogue render design-system # render just design-system/_view.html
8
+ * nuos-catalogue render maps # render just maps/_view.html
9
+ * nuos-catalogue render architecture # render just architecture/_view.html
10
+ *
11
+ * Companion HTML files are *generated artefacts*. The canonical source for
12
+ * every register stays markdown — every agent (architect, coder, reviewer)
13
+ * reads markdown. The HTML exists so the operator can review inherently visual
14
+ * artefacts in their natural medium.
15
+ */
16
+ import path from 'node:path';
17
+ import { resolveBuildRoot } from '../path-resolution.js';
18
+ import { RENDERABLE_REGISTERS, runRender } from '../render/run.js';
19
+ export async function cmdRender(options = {}) {
20
+ const buildRoot = resolveBuildRoot(options.buildRootFlag, { cwd: options.cwd ?? process.cwd() });
21
+ let only;
22
+ if (options.positional && options.positional !== 'all') {
23
+ if (!RENDERABLE_REGISTERS.includes(options.positional)) {
24
+ console.error(`unknown register: ${options.positional}`);
25
+ console.error(`available: ${RENDERABLE_REGISTERS.join(', ')}`);
26
+ return 1;
27
+ }
28
+ only = [options.positional];
29
+ }
30
+ const report = await runRender({ buildRoot, only, now: options.now });
31
+ for (const r of report.results) {
32
+ const status = r.written ? '✓' : '·';
33
+ const rel = path.relative(process.cwd(), r.outPath);
34
+ console.log(` ${status} ${r.register.padEnd(16)} ${r.detail.padEnd(20)} ${r.written ? rel : ''}`);
35
+ }
36
+ const writtenCount = report.results.filter((r) => r.written).length;
37
+ console.log('');
38
+ console.log(`${writtenCount}/${report.results.length} register${report.results.length === 1 ? '' : 's'} rendered.`);
39
+ return 0;
40
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Render the `architecture/` register as a module gallery with cross-links.
3
+ *
4
+ * Each module becomes a card showing its responsibility (one paragraph), the
5
+ * personas/modules it depends on, and the contracts it owns. The cards link
6
+ * to each other where dependencies are named, so the operator can navigate the
7
+ * module graph visually instead of by grep.
8
+ */
9
+ export interface ArchitectureRenderResult {
10
+ written: boolean;
11
+ outPath: string;
12
+ moduleCount: number;
13
+ }
14
+ export declare function renderArchitecture(opts: {
15
+ buildRoot: string;
16
+ projectName: string;
17
+ generatedAt: string;
18
+ }): Promise<ArchitectureRenderResult>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Render the `architecture/` register as a module gallery with cross-links.
3
+ *
4
+ * Each module becomes a card showing its responsibility (one paragraph), the
5
+ * personas/modules it depends on, and the contracts it owns. The cards link
6
+ * to each other where dependencies are named, so the operator can navigate the
7
+ * module graph visually instead of by grep.
8
+ */
9
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import path from 'node:path';
12
+ import { parseSections, isPlaceholder } from './parser.js';
13
+ import { pageWrapper, card, emptyState, escapeHtml, renderProse, } from './html.js';
14
+ const SKIP_FILES = new Set(['_index.md', 'module-template.md']);
15
+ export async function renderArchitecture(opts) {
16
+ const dir = path.join(opts.buildRoot, 'architecture');
17
+ const outPath = path.join(dir, '_view.html');
18
+ if (!existsSync(dir)) {
19
+ return { written: false, outPath, moduleCount: 0 };
20
+ }
21
+ const entries = await readdir(dir, { withFileTypes: true });
22
+ const modules = [];
23
+ for (const entry of entries) {
24
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
25
+ continue;
26
+ if (SKIP_FILES.has(entry.name))
27
+ continue;
28
+ const md = await readFile(path.join(dir, entry.name), 'utf8');
29
+ if (isPlaceholder(md))
30
+ continue;
31
+ const titleMatch = md.match(/^#\s+(.+)$/m);
32
+ modules.push({
33
+ file: entry.name,
34
+ slug: entry.name.replace(/\.md$/, ''),
35
+ title: titleMatch ? titleMatch[1].trim() : entry.name,
36
+ sections: parseSections(md),
37
+ });
38
+ }
39
+ modules.sort((a, b) => a.title.localeCompare(b.title));
40
+ const body = modules.length === 0
41
+ ? card('No modules filed yet', emptyState('Phase B of planning produces this content.'))
42
+ : [renderModuleIndex(modules), ...modules.map(renderModuleCard)].join('');
43
+ const html = pageWrapper({
44
+ title: 'Architecture',
45
+ projectName: opts.projectName,
46
+ generatedAt: opts.generatedAt,
47
+ sourceNote: '<code>docs/build/architecture/</code>',
48
+ body,
49
+ });
50
+ await mkdir(dir, { recursive: true });
51
+ await writeFile(outPath, html, 'utf8');
52
+ return { written: true, outPath, moduleCount: modules.length };
53
+ }
54
+ function renderModuleIndex(modules) {
55
+ const items = modules
56
+ .map((m) => `<li><a href="#${escapeHtml(m.slug)}">${escapeHtml(m.title)}</a></li>`)
57
+ .join('');
58
+ return card('Modules', `<ul class="sitemap-list">${items}</ul>`);
59
+ }
60
+ function renderModuleCard(m) {
61
+ const sectionBlocks = [];
62
+ for (const [heading, body] of m.sections) {
63
+ if (heading === '' || heading.toLowerCase() === 'notes')
64
+ continue;
65
+ const rendered = body.trim() ? renderProse(body) : '';
66
+ if (!rendered)
67
+ continue;
68
+ sectionBlocks.push(`<h4>${escapeHtml(heading)}</h4>${rendered}`);
69
+ }
70
+ return `<section id="${escapeHtml(m.slug)}" class="card">
71
+ <h3>${escapeHtml(m.title)}</h3>
72
+ <div class="card__body">${sectionBlocks.join('')}</div>
73
+ </section>`;
74
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Render the `design-system/` register as an interactive companion gallery.
3
+ *
4
+ * What gets rendered:
5
+ * - Colour tokens — as visual swatches with the hex value beside the name.
6
+ * - Type scale — each step rendered at its declared px size with its line-height
7
+ * and weight applied so the operator sees the real shape, not a description.
8
+ * - Spacing scale — rendered as horizontal bars at the actual pixel widths.
9
+ * - Radius & elevation — small boxes showing the corner radius applied.
10
+ * - Motion — table only (no animation; would push complexity past MVP scope).
11
+ * - Components + patterns — name list with first-paragraph summary.
12
+ * - Voice + accessibility — first-paragraph summary blocks.
13
+ *
14
+ * Token files use GFM tables with a `Token` column and a value column. The parser
15
+ * looks for either a `Hex` / `Value` / `Size` / `Used for` column shape and falls
16
+ * back to "list the table verbatim" when the shape isn't recognised.
17
+ */
18
+ export interface DesignSystemRenderResult {
19
+ written: boolean;
20
+ outPath: string;
21
+ }
22
+ export declare function renderDesignSystem(opts: {
23
+ buildRoot: string;
24
+ projectName: string;
25
+ generatedAt: string;
26
+ }): Promise<DesignSystemRenderResult>;