@nusoft/nuos-build-catalogue 0.9.0 → 0.10.1

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 (90) hide show
  1. package/dist/cli.d.ts +13 -0
  2. package/dist/cli.js +472 -0
  3. package/dist/commands/create.d.ts +70 -0
  4. package/dist/commands/create.js +341 -0
  5. package/dist/commands/format.d.ts +19 -0
  6. package/dist/commands/format.js +89 -0
  7. package/dist/commands/handlers.d.ts +35 -0
  8. package/dist/commands/handlers.js +132 -0
  9. package/dist/commands/init.d.ts +41 -0
  10. package/dist/commands/init.js +289 -0
  11. package/dist/commands/prompt.d.ts +44 -0
  12. package/dist/commands/prompt.js +100 -0
  13. package/dist/commands/write.d.ts +39 -0
  14. package/dist/commands/write.js +247 -0
  15. package/dist/embedder/ollama.d.ts +54 -0
  16. package/dist/embedder/ollama.js +164 -0
  17. package/dist/embedder/openai.d.ts +21 -0
  18. package/dist/embedder/openai.js +56 -0
  19. package/dist/embedder/select.d.ts +9 -0
  20. package/dist/embedder/select.js +27 -0
  21. package/dist/embedder/stub.d.ts +15 -0
  22. package/dist/embedder/stub.js +40 -0
  23. package/dist/embedder/types.d.ts +21 -0
  24. package/dist/embedder/types.js +6 -0
  25. package/dist/embedder/vertex.d.ts +41 -0
  26. package/dist/embedder/vertex.js +94 -0
  27. package/dist/indexer/chunk.d.ts +20 -0
  28. package/dist/indexer/chunk.js +196 -0
  29. package/dist/indexer/crawl.d.ts +20 -0
  30. package/dist/indexer/crawl.js +66 -0
  31. package/dist/indexer/metadata.d.ts +21 -0
  32. package/dist/indexer/metadata.js +126 -0
  33. package/dist/indexer/upsert.d.ts +26 -0
  34. package/dist/indexer/upsert.js +152 -0
  35. package/dist/migrate/parsers.d.ts +17 -0
  36. package/dist/migrate/parsers.js +123 -0
  37. package/dist/migrate/run.d.ts +22 -0
  38. package/dist/migrate/run.js +142 -0
  39. package/dist/migrate/store.d.ts +20 -0
  40. package/dist/migrate/store.js +52 -0
  41. package/dist/migrate/types.d.ts +57 -0
  42. package/dist/migrate/types.js +13 -0
  43. package/dist/regenerate/check.d.ts +11 -0
  44. package/dist/regenerate/check.js +97 -0
  45. package/dist/regenerate/diff.d.ts +18 -0
  46. package/dist/regenerate/diff.js +38 -0
  47. package/dist/regenerate/types.d.ts +52 -0
  48. package/dist/regenerate/types.js +14 -0
  49. package/dist/runtime/ac-parse.d.ts +63 -0
  50. package/dist/runtime/ac-parse.js +196 -0
  51. package/dist/runtime/markdown-edit.d.ts +53 -0
  52. package/dist/runtime/markdown-edit.js +101 -0
  53. package/dist/runtime/markdown-render.d.ts +27 -0
  54. package/dist/runtime/markdown-render.js +209 -0
  55. package/dist/runtime/mis-adapter.d.ts +35 -0
  56. package/dist/runtime/mis-adapter.js +364 -0
  57. package/dist/runtime/runtime.d.ts +20 -0
  58. package/dist/runtime/runtime.js +39 -0
  59. package/dist/search/format.d.ts +6 -0
  60. package/dist/search/format.js +23 -0
  61. package/dist/search/query.d.ts +29 -0
  62. package/dist/search/query.js +71 -0
  63. package/dist/store/open.d.ts +14 -0
  64. package/dist/store/open.js +16 -0
  65. package/package.json +5 -3
  66. package/templates/protocols/end-of-session.md +19 -0
  67. package/templates/protocols/persona-new.md +43 -0
  68. package/templates/protocols/start-of-session.md +19 -0
  69. package/templates/protocols/wu-new.md +52 -0
  70. package/templates/starter-kit/CLAUDE.md +73 -0
  71. package/templates/starter-kit/README.md +116 -0
  72. package/templates/starter-kit/docs/build/END-OF-SESSION.md +62 -0
  73. package/templates/starter-kit/docs/build/START-OF-SESSION.md +33 -0
  74. package/templates/starter-kit/docs/build/STATE.md +47 -0
  75. package/templates/starter-kit/docs/build/decisions/D001-template.md +38 -0
  76. package/templates/starter-kit/docs/build/decisions/_index.md +30 -0
  77. package/templates/starter-kit/docs/build/maps/01-template.md +126 -0
  78. package/templates/starter-kit/docs/build/maps/_index.md +63 -0
  79. package/templates/starter-kit/docs/build/open-questions/_index.md +26 -0
  80. package/templates/starter-kit/docs/build/personas/001-template.md +68 -0
  81. package/templates/starter-kit/docs/build/personas/_index.md +77 -0
  82. package/templates/starter-kit/docs/build/risks/_index.md +28 -0
  83. package/templates/starter-kit/docs/build/sessions/0000-00-00-template.md +47 -0
  84. package/templates/starter-kit/docs/build/sessions/_index.md +27 -0
  85. package/templates/starter-kit/docs/build/work-units/001-template.md +82 -0
  86. package/templates/starter-kit/docs/build/work-units/_index.md +34 -0
  87. package/templates/starter-kit/docs/contracts/_index.md +26 -0
  88. package/templates/starter-kit/docs/guides/_index.md +26 -0
  89. package/templates/starter-kit/docs/philosophy/_index.md +26 -0
  90. package/templates/starter-kit/methodfile.json +54 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Per-register markdown renderers — Phase H part 3.
3
+ *
4
+ * Each renderer takes a typed workflow payload (from the build-catalogue
5
+ * pack) and produces a markdown body in the catalogue's house style.
6
+ * The renderers match the conventions used in the live catalogue:
7
+ *
8
+ * - WU files: `# WU NNN — Title` + `**Status:** ...` + sections for
9
+ * outcome / dependencies / contracts produced/consumed / acceptance
10
+ * criteria / etc.
11
+ * - Decision files: `# DNNN — Title` + `**Status:** ...` +
12
+ * Context / Decision / Rationale / Alternatives / Consequences.
13
+ * - Open question files: `# QNNN — Title` + `**Status:** ...` +
14
+ * Why it matters / Options / Evidence needed / Blocks.
15
+ * - Persona files: `# PNNN — Title` + seven dimensions + acid-test.
16
+ *
17
+ * The renderers are deliberately conservative: they produce markdown
18
+ * that matches the live convention closely so future hand-edits don't
19
+ * collide with the renderer's output. Round-trip with the migration
20
+ * runner: render → write → migrate → store record's rawMarkdown ==
21
+ * what we just rendered.
22
+ */
23
+ const INFRASTRUCTURE_MARKER = 'N/A — infrastructure WU';
24
+ export function renderWorkUnit(payload) {
25
+ const lines = [];
26
+ lines.push(`# WU ${formatWuNumber(payload)} — ${payload.title}`);
27
+ lines.push('');
28
+ lines.push(`**Status:** 🔵 proposed`);
29
+ if (payload.phase) {
30
+ lines.push(`**Phase:** ${payload.phase}`);
31
+ }
32
+ lines.push(`**Kind:** ${payload.kind}`);
33
+ if (payload.dependsOn.length > 0) {
34
+ lines.push(`**Depends on:** ${[...payload.dependsOn].join(', ')}`);
35
+ }
36
+ if (payload.blocks.length > 0) {
37
+ lines.push(`**Blocks:** ${[...payload.blocks].join(', ')}`);
38
+ }
39
+ lines.push('');
40
+ lines.push('## Outcome');
41
+ lines.push('');
42
+ const outcome = payload.outcome;
43
+ if (outcome.personaRef && outcome.personaRef !== INFRASTRUCTURE_MARKER) {
44
+ lines.push(`**Persona:** ${outcome.personaRef}`);
45
+ lines.push('');
46
+ }
47
+ if (outcome.trigger && outcome.trigger !== INFRASTRUCTURE_MARKER) {
48
+ lines.push(`**Trigger.** ${outcome.trigger}`);
49
+ lines.push('');
50
+ }
51
+ if (outcome.walkthrough && outcome.walkthrough !== INFRASTRUCTURE_MARKER) {
52
+ lines.push(`**Walkthrough.**`);
53
+ lines.push('');
54
+ lines.push(outcome.walkthrough);
55
+ lines.push('');
56
+ }
57
+ if (outcome.personaRef === INFRASTRUCTURE_MARKER &&
58
+ outcome.trigger === INFRASTRUCTURE_MARKER &&
59
+ outcome.walkthrough === INFRASTRUCTURE_MARKER) {
60
+ lines.push(`**Persona / Trigger / Walkthrough:** \`N/A — infrastructure WU\``);
61
+ lines.push('');
62
+ }
63
+ if (payload.approach) {
64
+ lines.push('## Approach');
65
+ lines.push('');
66
+ lines.push(payload.approach);
67
+ lines.push('');
68
+ }
69
+ lines.push('## Acceptance criteria');
70
+ lines.push('');
71
+ if (outcome.acceptanceCriteria.length === 0) {
72
+ lines.push('(to be filled in)');
73
+ }
74
+ else {
75
+ for (const ac of outcome.acceptanceCriteria) {
76
+ const tick = ac.met ? 'x' : ' ';
77
+ lines.push(`- [${tick}] ${ac.text}`);
78
+ }
79
+ }
80
+ lines.push('');
81
+ lines.push('## Contracts produced');
82
+ lines.push('');
83
+ if (outcome.contractsProduced.length === 0) {
84
+ lines.push('(none)');
85
+ }
86
+ else {
87
+ for (const c of outcome.contractsProduced)
88
+ lines.push(`- ${c}`);
89
+ }
90
+ lines.push('');
91
+ lines.push('## Contracts consumed');
92
+ lines.push('');
93
+ if (outcome.contractsConsumed.length === 0) {
94
+ lines.push('(none)');
95
+ }
96
+ else {
97
+ for (const c of outcome.contractsConsumed)
98
+ lines.push(`- ${c}`);
99
+ }
100
+ lines.push('');
101
+ lines.push('## Notes / log');
102
+ lines.push('');
103
+ lines.push('(Empty until work starts.)');
104
+ lines.push('');
105
+ return lines.join('\n');
106
+ }
107
+ function formatWuNumber(payload) {
108
+ return String(payload.number).padStart(3, '0');
109
+ }
110
+ export function renderDecision(payload) {
111
+ const lines = [];
112
+ lines.push(`# ${payload.handle} — ${payload.title}`);
113
+ lines.push('');
114
+ lines.push(`**Date:** ${new Date().toISOString().slice(0, 10)}`);
115
+ lines.push(`**Status:** ${payload.status}`);
116
+ lines.push('');
117
+ lines.push('## Context');
118
+ lines.push('');
119
+ lines.push(payload.context);
120
+ lines.push('');
121
+ lines.push('## Decision');
122
+ lines.push('');
123
+ lines.push(payload.decision);
124
+ lines.push('');
125
+ lines.push('## Consequences');
126
+ lines.push('');
127
+ lines.push(payload.consequences);
128
+ lines.push('');
129
+ if (payload.alternativesConsidered) {
130
+ lines.push('## Alternatives considered');
131
+ lines.push('');
132
+ lines.push(payload.alternativesConsidered);
133
+ lines.push('');
134
+ }
135
+ return lines.join('\n');
136
+ }
137
+ export function renderOpenQuestion(payload) {
138
+ const lines = [];
139
+ lines.push(`# ${payload.handle} — ${payload.title}`);
140
+ lines.push('');
141
+ lines.push(`**Status:** ${payload.status}`);
142
+ lines.push(`**Raised:** ${new Date().toISOString().slice(0, 10)}`);
143
+ if (payload.blocks.length > 0) {
144
+ lines.push(`**Blocks:** ${[...payload.blocks].join(', ')}`);
145
+ }
146
+ lines.push('');
147
+ lines.push('## Why it matters');
148
+ lines.push('');
149
+ lines.push(payload.whyItMatters);
150
+ lines.push('');
151
+ if (payload.options) {
152
+ lines.push('## Options under consideration');
153
+ lines.push('');
154
+ lines.push(payload.options);
155
+ lines.push('');
156
+ }
157
+ if (payload.evidenceNeeded) {
158
+ lines.push('## What evidence would resolve this');
159
+ lines.push('');
160
+ lines.push(payload.evidenceNeeded);
161
+ lines.push('');
162
+ }
163
+ return lines.join('\n');
164
+ }
165
+ export function renderPersona(payload) {
166
+ const lines = [];
167
+ lines.push(`# ${payload.handle} — ${payload.title}`);
168
+ lines.push('');
169
+ lines.push(`**Created:** ${new Date().toISOString().slice(0, 10)}`);
170
+ lines.push(`**Status:** 🟢 active`);
171
+ lines.push('');
172
+ lines.push('## 1. Identity');
173
+ lines.push('');
174
+ lines.push(payload.identity);
175
+ lines.push('');
176
+ lines.push('## 2. Reality');
177
+ lines.push('');
178
+ lines.push(payload.reality);
179
+ lines.push('');
180
+ lines.push('## 3. Psychology');
181
+ lines.push('');
182
+ lines.push(payload.psychology);
183
+ lines.push('');
184
+ lines.push('## 4. Trigger');
185
+ lines.push('');
186
+ lines.push(payload.trigger);
187
+ lines.push('');
188
+ lines.push('## 5. History');
189
+ lines.push('');
190
+ lines.push(payload.history);
191
+ lines.push('');
192
+ lines.push('## 6. Success');
193
+ lines.push('');
194
+ lines.push(payload.success);
195
+ lines.push('');
196
+ lines.push('## 7. Constraints');
197
+ lines.push('');
198
+ lines.push(payload.constraints);
199
+ lines.push('');
200
+ lines.push('## Acid-test refinement');
201
+ lines.push('');
202
+ lines.push(payload.acidTest);
203
+ lines.push('');
204
+ lines.push('## Used by WUs');
205
+ lines.push('');
206
+ lines.push('(none yet — populated as WUs cite this persona)');
207
+ lines.push('');
208
+ return lines.join('\n');
209
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `BuildCatalogueMisAdapter` — implements `@nusoft/nuflow`'s
3
+ * `MisWriteAdapter` for the build catalogue's write commands.
4
+ *
5
+ * Translates a typed `WriteIntent` from the build-catalogue pack into
6
+ * markdown edits + JSON workflow-store updates. The store and the
7
+ * markdown file stay in sync by always writing both at commit time.
8
+ *
9
+ * **Per-intent behaviour:**
10
+ *
11
+ * - `work_unit.advance_status`: replace the WU's status line in
12
+ * markdown; append a Build catalogue history entry naming the
13
+ * transition + reason; update the store record's `rawMarkdown`.
14
+ *
15
+ * - `work_unit.tick_acceptance_criterion`: append a Build catalogue
16
+ * history entry naming the criterion index + evidence (no AC-list
17
+ * parsing in v0.4; the maintainer hand-edits the AC list if they
18
+ * want, the workflow record is canonical).
19
+ *
20
+ * - `decision.supersede`: edit BOTH the target's status line (`accepted
21
+ * → superseded by D-NNN`) AND append a history entry to the
22
+ * superseding decision noting what it supersedes.
23
+ *
24
+ * - `open_question.resolve`: edit the question's status line
25
+ * (`active → resolved by D-NNN`) and append a history entry to the
26
+ * resolving decision.
27
+ */
28
+ import type { MisWriteAdapter } from '@nusoft/nuflow';
29
+ import type { WorkflowStore } from '../migrate/store.js';
30
+ export interface BuildCatalogueMisAdapterConfig {
31
+ store: WorkflowStore;
32
+ /** Absolute path to `nuos/docs/build/`. */
33
+ catalogueRoot: string;
34
+ }
35
+ export declare function createBuildCatalogueMisAdapter(config: BuildCatalogueMisAdapterConfig): MisWriteAdapter;
@@ -0,0 +1,364 @@
1
+ /**
2
+ * `BuildCatalogueMisAdapter` — implements `@nusoft/nuflow`'s
3
+ * `MisWriteAdapter` for the build catalogue's write commands.
4
+ *
5
+ * Translates a typed `WriteIntent` from the build-catalogue pack into
6
+ * markdown edits + JSON workflow-store updates. The store and the
7
+ * markdown file stay in sync by always writing both at commit time.
8
+ *
9
+ * **Per-intent behaviour:**
10
+ *
11
+ * - `work_unit.advance_status`: replace the WU's status line in
12
+ * markdown; append a Build catalogue history entry naming the
13
+ * transition + reason; update the store record's `rawMarkdown`.
14
+ *
15
+ * - `work_unit.tick_acceptance_criterion`: append a Build catalogue
16
+ * history entry naming the criterion index + evidence (no AC-list
17
+ * parsing in v0.4; the maintainer hand-edits the AC list if they
18
+ * want, the workflow record is canonical).
19
+ *
20
+ * - `decision.supersede`: edit BOTH the target's status line (`accepted
21
+ * → superseded by D-NNN`) AND append a history entry to the
22
+ * superseding decision noting what it supersedes.
23
+ *
24
+ * - `open_question.resolve`: edit the question's status line
25
+ * (`active → resolved by D-NNN`) and append a history entry to the
26
+ * resolving decision.
27
+ */
28
+ import { writeFile, mkdir } from 'node:fs/promises';
29
+ import path from 'node:path';
30
+ import { replaceStatusLine, insertStatusLine, appendChangeLog, } from './markdown-edit.js';
31
+ import { tickAcceptanceCriterion, parseAcceptanceCriteria } from './ac-parse.js';
32
+ import { renderWorkUnit, renderDecision, renderOpenQuestion, renderPersona, } from './markdown-render.js';
33
+ export function createBuildCatalogueMisAdapter(config) {
34
+ const { store, catalogueRoot } = config;
35
+ const adapter = {
36
+ canCommit(intent) {
37
+ // Create intents have placeholder subjects (e.g. wu-pending) that
38
+ // the workflow has rewritten to the real handle inside the typed
39
+ // payload. Skip the existence check for create intents.
40
+ if (isCreateIntent(intent.type))
41
+ return { allowed: true };
42
+ // For mutation intents, verify all subjects resolve to records
43
+ // the store knows about. The build-catalogue pack ensures handles
44
+ // are well-formed; the adapter ensures they exist.
45
+ for (const subject of intent.subjects) {
46
+ if (!store.has(subject.id)) {
47
+ return {
48
+ allowed: false,
49
+ reason: `BuildCatalogueMisAdapter: no record for subject ${subject.kind}:${subject.id}`,
50
+ };
51
+ }
52
+ }
53
+ return { allowed: true };
54
+ },
55
+ async commit(intent) {
56
+ switch (intent.type) {
57
+ case 'work_unit.create':
58
+ await commitCreateRecord(store, catalogueRoot, intent, 'work_unit');
59
+ break;
60
+ case 'decision.create':
61
+ await commitCreateRecord(store, catalogueRoot, intent, 'decision');
62
+ break;
63
+ case 'open_question.create':
64
+ await commitCreateRecord(store, catalogueRoot, intent, 'open_question');
65
+ break;
66
+ case 'persona.create':
67
+ await commitCreateRecord(store, catalogueRoot, intent, 'persona');
68
+ break;
69
+ case 'work_unit.advance_status':
70
+ await commitAdvanceStatus(store, catalogueRoot, intent);
71
+ break;
72
+ case 'work_unit.tick_acceptance_criterion':
73
+ await commitTickAC(store, catalogueRoot, intent);
74
+ break;
75
+ case 'decision.supersede':
76
+ await commitSupersede(store, catalogueRoot, intent);
77
+ break;
78
+ case 'open_question.resolve':
79
+ await commitResolveQuestion(store, catalogueRoot, intent);
80
+ break;
81
+ default:
82
+ throw new Error(`BuildCatalogueMisAdapter: intent type ${intent.type} is not handled by the build-catalogue pack`);
83
+ }
84
+ // Persist the workflow store after the operation so the next
85
+ // CLI invocation sees the change.
86
+ await store.flush();
87
+ return {
88
+ commitRef: `cmt_${intent.intentId}`,
89
+ recordType: intent.type,
90
+ recordId: intent.subjects[0]?.id ?? 'unknown',
91
+ committedAt: new Date().toISOString(),
92
+ };
93
+ },
94
+ };
95
+ return adapter;
96
+ }
97
+ function isCreateIntent(intentType) {
98
+ return (intentType === 'work_unit.create' ||
99
+ intentType === 'decision.create' ||
100
+ intentType === 'open_question.create' ||
101
+ intentType === 'persona.create');
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Per-intent commit handlers
105
+ // ---------------------------------------------------------------------------
106
+ async function commitCreateRecord(store, catalogueRoot, intent, register) {
107
+ let rendered;
108
+ let handle;
109
+ let number;
110
+ let slug;
111
+ let title;
112
+ let registerDir;
113
+ switch (register) {
114
+ case 'work_unit': {
115
+ const payload = intent.payload;
116
+ rendered = renderWorkUnit(payload);
117
+ handle = payload.handle;
118
+ number = payload.number;
119
+ slug = payload.slug;
120
+ title = payload.title;
121
+ registerDir = 'work-units';
122
+ break;
123
+ }
124
+ case 'decision': {
125
+ const payload = intent.payload;
126
+ rendered = renderDecision(payload);
127
+ handle = payload.handle;
128
+ number = payload.number;
129
+ slug = payload.slug;
130
+ title = payload.title;
131
+ registerDir = 'decisions';
132
+ break;
133
+ }
134
+ case 'open_question': {
135
+ const payload = intent.payload;
136
+ rendered = renderOpenQuestion(payload);
137
+ handle = payload.handle;
138
+ number = payload.number;
139
+ slug = payload.slug;
140
+ title = payload.title;
141
+ registerDir = 'open-questions';
142
+ break;
143
+ }
144
+ case 'persona': {
145
+ const payload = intent.payload;
146
+ rendered = renderPersona(payload);
147
+ handle = payload.handle;
148
+ number = payload.number;
149
+ slug = payload.slug;
150
+ title = payload.title;
151
+ registerDir = 'personas';
152
+ break;
153
+ }
154
+ }
155
+ // Filename pattern matches the migration parser:
156
+ // work-units use just the number prefix (e.g. 200-foo.md)
157
+ // decisions/questions/personas use the handle as prefix (D200-foo.md, Q200-foo.md, P200-foo.md)
158
+ const filename = register === 'work_unit'
159
+ ? `${String(number).padStart(3, '0')}-${slug}.md`
160
+ : `${handle}-${slug}.md`;
161
+ const relativeSourcePath = `${registerDir}/${filename}`;
162
+ const absolutePath = path.join(catalogueRoot, relativeSourcePath);
163
+ // Ensure the register dir exists (e.g. personas/ may not exist yet).
164
+ await mkdir(path.dirname(absolutePath), { recursive: true });
165
+ await writeFile(absolutePath, rendered, 'utf8');
166
+ const now = new Date().toISOString();
167
+ const record = {
168
+ handle,
169
+ number,
170
+ register,
171
+ title,
172
+ status: register === 'persona' ? '🟢 active' : initialStatusForRegister(register),
173
+ slug,
174
+ sourcePath: relativeSourcePath,
175
+ rawMarkdown: rendered,
176
+ fileModifiedAt: now,
177
+ migratedAt: now,
178
+ migratedFrom: 'markdown',
179
+ };
180
+ store.put(record);
181
+ }
182
+ function initialStatusForRegister(register) {
183
+ switch (register) {
184
+ case 'work_unit':
185
+ return '🔵 proposed';
186
+ case 'decision':
187
+ return 'proposed';
188
+ case 'open_question':
189
+ return 'active';
190
+ case 'persona':
191
+ return '🟢 active';
192
+ }
193
+ }
194
+ async function commitAdvanceStatus(store, catalogueRoot, intent) {
195
+ const payload = intent.payload;
196
+ const record = store.get(payload.targetHandle);
197
+ if (!record) {
198
+ throw new Error(`commitAdvanceStatus: no record for ${payload.targetHandle}`);
199
+ }
200
+ // Map workflow status names to user-facing markdown status text.
201
+ const statusEmoji = mapStatusToEmojiText(payload.toStatus);
202
+ const replaced = replaceStatusLine(record.rawMarkdown, statusEmoji);
203
+ let updatedMarkdown = replaced.replaced
204
+ ? replaced.updated
205
+ : insertStatusLine(record.rawMarkdown, statusEmoji);
206
+ updatedMarkdown = appendChangeLog(updatedMarkdown, {
207
+ isoTimestamp: new Date().toISOString(),
208
+ summary: `Status advanced ${payload.fromStatus} → ${payload.toStatus}.`,
209
+ details: payload.reason,
210
+ reference: `intent ${intent.intentId}`,
211
+ });
212
+ // Also update the structured `status` field on the record so the next
213
+ // workflow invocation reads the current state, not the stale migrated value.
214
+ await persist(store, catalogueRoot, record, updatedMarkdown, { status: statusEmoji });
215
+ }
216
+ async function commitTickAC(store, catalogueRoot, intent) {
217
+ const payload = intent.payload;
218
+ const record = store.get(payload.targetHandle);
219
+ if (!record) {
220
+ throw new Error(`commitTickAC: no record for ${payload.targetHandle}`);
221
+ }
222
+ // Try to flip the AC line in the markdown. If parsing succeeds we get
223
+ // the structural tick; otherwise we fall back to the audit-log-only
224
+ // approach (older/atypical AC shapes the parser doesn't recognise).
225
+ let workingMarkdown = record.rawMarkdown;
226
+ let acText = payload.criterionText;
227
+ let structuralTick = false;
228
+ try {
229
+ const acs = parseAcceptanceCriteria(record.rawMarkdown);
230
+ if (acs.length === 0) {
231
+ // No AC section recognised — audit-log-only.
232
+ }
233
+ else if (payload.criterionIndex >= acs.length) {
234
+ throw new Error(`commitTickAC: criterion index ${payload.criterionIndex} out of range (${record.handle} has ${acs.length} parsed AC entries)`);
235
+ }
236
+ else {
237
+ acText = acText ?? acs[payload.criterionIndex].text;
238
+ workingMarkdown = tickAcceptanceCriterion(record.rawMarkdown, payload.criterionIndex);
239
+ structuralTick = true;
240
+ }
241
+ }
242
+ catch (err) {
243
+ // Re-throw out-of-range errors; tolerate parse failures.
244
+ if (err instanceof Error && err.message.includes('out of range')) {
245
+ throw err;
246
+ }
247
+ }
248
+ const summary = acText
249
+ ? `Acceptance criterion ${payload.criterionIndex + 1} ticked: "${acText}".`
250
+ : `Acceptance criterion at index ${payload.criterionIndex} ticked.`;
251
+ const updatedMarkdown = appendChangeLog(workingMarkdown, {
252
+ isoTimestamp: new Date().toISOString(),
253
+ summary: structuralTick ? summary : `${summary} (audit-log-only — AC list not recognised)`,
254
+ details: `Evidence: ${payload.evidence}`,
255
+ reference: `intent ${intent.intentId}`,
256
+ });
257
+ await persist(store, catalogueRoot, record, updatedMarkdown);
258
+ }
259
+ async function commitSupersede(store, catalogueRoot, intent) {
260
+ const payload = intent.payload;
261
+ const target = store.get(payload.targetHandle);
262
+ const superseding = store.get(payload.supersedingHandle);
263
+ if (!target)
264
+ throw new Error(`commitSupersede: no record for target ${payload.targetHandle}`);
265
+ if (!superseding) {
266
+ throw new Error(`commitSupersede: no record for superseding ${payload.supersedingHandle}`);
267
+ }
268
+ // Target: status accepted → superseded by D-NNN
269
+ const targetStatus = `superseded by ${payload.supersedingHandle}`;
270
+ const targetReplaced = replaceStatusLine(target.rawMarkdown, targetStatus);
271
+ let targetMarkdown = targetReplaced.replaced
272
+ ? targetReplaced.updated
273
+ : insertStatusLine(target.rawMarkdown, targetStatus);
274
+ targetMarkdown = appendChangeLog(targetMarkdown, {
275
+ isoTimestamp: new Date().toISOString(),
276
+ summary: `Superseded by ${payload.supersedingHandle}.`,
277
+ details: payload.reason,
278
+ reference: `intent ${intent.intentId}`,
279
+ });
280
+ await persist(store, catalogueRoot, target, targetMarkdown, { status: targetStatus });
281
+ // Superseding: append a Build catalogue history entry naming what it supersedes.
282
+ const supersedingMarkdown = appendChangeLog(superseding.rawMarkdown, {
283
+ isoTimestamp: new Date().toISOString(),
284
+ summary: `Supersedes ${payload.targetHandle}.`,
285
+ details: payload.reason,
286
+ reference: `intent ${intent.intentId}`,
287
+ });
288
+ await persist(store, catalogueRoot, superseding, supersedingMarkdown);
289
+ }
290
+ async function commitResolveQuestion(store, catalogueRoot, intent) {
291
+ const payload = intent.payload;
292
+ const question = store.get(payload.targetHandle);
293
+ const decision = store.get(payload.resolvingDecisionHandle);
294
+ if (!question) {
295
+ throw new Error(`commitResolveQuestion: no record for question ${payload.targetHandle}`);
296
+ }
297
+ if (!decision) {
298
+ throw new Error(`commitResolveQuestion: no record for resolving decision ${payload.resolvingDecisionHandle}`);
299
+ }
300
+ const questionStatus = `resolved by ${payload.resolvingDecisionHandle}`;
301
+ const questionReplaced = replaceStatusLine(question.rawMarkdown, questionStatus);
302
+ let questionMarkdown = questionReplaced.replaced
303
+ ? questionReplaced.updated
304
+ : insertStatusLine(question.rawMarkdown, questionStatus);
305
+ questionMarkdown = appendChangeLog(questionMarkdown, {
306
+ isoTimestamp: new Date().toISOString(),
307
+ summary: `Resolved by ${payload.resolvingDecisionHandle}.`,
308
+ details: payload.reason,
309
+ reference: `intent ${intent.intentId}`,
310
+ });
311
+ await persist(store, catalogueRoot, question, questionMarkdown, { status: questionStatus });
312
+ const decisionMarkdown = appendChangeLog(decision.rawMarkdown, {
313
+ isoTimestamp: new Date().toISOString(),
314
+ summary: `Resolves ${payload.targetHandle}.`,
315
+ details: payload.reason,
316
+ reference: `intent ${intent.intentId}`,
317
+ });
318
+ await persist(store, catalogueRoot, decision, decisionMarkdown);
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Helpers
322
+ // ---------------------------------------------------------------------------
323
+ async function persist(store, catalogueRoot, record, newRawMarkdown, fieldUpdates = {}) {
324
+ const sourceAbsolute = path.join(catalogueRoot, record.sourcePath);
325
+ await writeFile(sourceAbsolute, newRawMarkdown, 'utf8');
326
+ const updatedRecord = {
327
+ ...record,
328
+ rawMarkdown: newRawMarkdown,
329
+ fileModifiedAt: new Date().toISOString(),
330
+ // Note: migratedAt is preserved (it's the original migration time);
331
+ // fileModifiedAt is updated to track the latest write.
332
+ ...(fieldUpdates.status !== undefined ? { status: fieldUpdates.status } : {}),
333
+ };
334
+ store.put(updatedRecord);
335
+ }
336
+ function mapStatusToEmojiText(workflowStatus) {
337
+ // The build-catalogue pack uses internal status enum strings
338
+ // (proposed/ready/in_progress/in_review/completed/superseded/...).
339
+ // The catalogue's markdown convention uses emoji-prefixed forms.
340
+ // Map them here so workflows speak in their typed enum but the
341
+ // markdown reads in the human convention.
342
+ switch (workflowStatus) {
343
+ case 'proposed':
344
+ return '🔵 proposed';
345
+ case 'ready':
346
+ return '🟢 ready';
347
+ case 'in_progress':
348
+ return '🟡 in_progress';
349
+ case 'in_review':
350
+ return '🟣 in_review';
351
+ case 'completed':
352
+ return '✅ completed';
353
+ case 'superseded':
354
+ return '🟣 superseded';
355
+ case 'cancelled':
356
+ return '⚫ cancelled';
357
+ case 'deferred-with-trigger':
358
+ return '🔵 deferred-with-trigger';
359
+ case 'blocked-on-question':
360
+ return '🔴 blocked-on-question';
361
+ default:
362
+ return workflowStatus;
363
+ }
364
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Build-catalogue NuFlow runtime factory.
3
+ *
4
+ * Wires the NuFlow runtime with stub memory adapters + the build-
5
+ * catalogue MIS adapter + the build-catalogue workflow pack, ready to
6
+ * accept the four flag-driven write commands of Phase H part 2.
7
+ *
8
+ * Memory adapters are stubs — the build catalogue's "memory" is the
9
+ * JSON workflow store, not NuVector retrieval. The runtime's
10
+ * `MemoryContextAdapter` and `WorkflowMemoryAdapter` are wired with
11
+ * no-op implementations because the workflows in this pack do not
12
+ * use them.
13
+ */
14
+ import { type NuFlowRuntime } from '@nusoft/nuflow';
15
+ import type { WorkflowStore } from '../migrate/store.js';
16
+ export interface CreateBuildCatalogueRuntimeConfig {
17
+ store: WorkflowStore;
18
+ catalogueRoot: string;
19
+ }
20
+ export declare function createBuildCatalogueRuntime(config: CreateBuildCatalogueRuntimeConfig): NuFlowRuntime;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Build-catalogue NuFlow runtime factory.
3
+ *
4
+ * Wires the NuFlow runtime with stub memory adapters + the build-
5
+ * catalogue MIS adapter + the build-catalogue workflow pack, ready to
6
+ * accept the four flag-driven write commands of Phase H part 2.
7
+ *
8
+ * Memory adapters are stubs — the build catalogue's "memory" is the
9
+ * JSON workflow store, not NuVector retrieval. The runtime's
10
+ * `MemoryContextAdapter` and `WorkflowMemoryAdapter` are wired with
11
+ * no-op implementations because the workflows in this pack do not
12
+ * use them.
13
+ */
14
+ import { createNuFlowRuntime } from '@nusoft/nuflow';
15
+ import { nuosBuildCataloguePack } from '@nusoft/nuflow-pack-nuos-build-catalogue';
16
+ import { createBuildCatalogueMisAdapter } from './mis-adapter.js';
17
+ export function createBuildCatalogueRuntime(config) {
18
+ const runtime = createNuFlowRuntime({
19
+ memoryContextAdapter: {
20
+ retrieve: async () => ({
21
+ items: [],
22
+ retrievalId: `r_${Date.now()}`,
23
+ retrievedAt: new Date().toISOString(),
24
+ totalCandidates: 0,
25
+ }),
26
+ },
27
+ workflowMemoryAdapter: {
28
+ remember: async (record) => ({ ref: record.id }),
29
+ },
30
+ misWriteAdapter: createBuildCatalogueMisAdapter({
31
+ store: config.store,
32
+ catalogueRoot: config.catalogueRoot,
33
+ }),
34
+ policyGates: [],
35
+ tenant: 'nuos-build-catalogue',
36
+ });
37
+ nuosBuildCataloguePack.register(runtime);
38
+ return runtime;
39
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Output formatter — human-readable table or JSON.
3
+ */
4
+ import type { SearchHit } from './query.js';
5
+ export declare function formatHumanReadable(hits: SearchHit[]): string;
6
+ export declare function formatJson(hits: SearchHit[]): string;