@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.
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +472 -0
- package/dist/commands/create.d.ts +70 -0
- package/dist/commands/create.js +341 -0
- package/dist/commands/format.d.ts +19 -0
- package/dist/commands/format.js +89 -0
- package/dist/commands/handlers.d.ts +35 -0
- package/dist/commands/handlers.js +132 -0
- package/dist/commands/init.d.ts +41 -0
- package/dist/commands/init.js +289 -0
- package/dist/commands/prompt.d.ts +44 -0
- package/dist/commands/prompt.js +100 -0
- package/dist/commands/write.d.ts +39 -0
- package/dist/commands/write.js +247 -0
- package/dist/embedder/ollama.d.ts +54 -0
- package/dist/embedder/ollama.js +164 -0
- package/dist/embedder/openai.d.ts +21 -0
- package/dist/embedder/openai.js +56 -0
- package/dist/embedder/select.d.ts +9 -0
- package/dist/embedder/select.js +27 -0
- package/dist/embedder/stub.d.ts +15 -0
- package/dist/embedder/stub.js +40 -0
- package/dist/embedder/types.d.ts +21 -0
- package/dist/embedder/types.js +6 -0
- package/dist/embedder/vertex.d.ts +41 -0
- package/dist/embedder/vertex.js +94 -0
- package/dist/indexer/chunk.d.ts +20 -0
- package/dist/indexer/chunk.js +196 -0
- package/dist/indexer/crawl.d.ts +20 -0
- package/dist/indexer/crawl.js +66 -0
- package/dist/indexer/metadata.d.ts +21 -0
- package/dist/indexer/metadata.js +126 -0
- package/dist/indexer/upsert.d.ts +26 -0
- package/dist/indexer/upsert.js +152 -0
- package/dist/migrate/parsers.d.ts +17 -0
- package/dist/migrate/parsers.js +123 -0
- package/dist/migrate/run.d.ts +22 -0
- package/dist/migrate/run.js +142 -0
- package/dist/migrate/store.d.ts +20 -0
- package/dist/migrate/store.js +52 -0
- package/dist/migrate/types.d.ts +57 -0
- package/dist/migrate/types.js +13 -0
- package/dist/regenerate/check.d.ts +11 -0
- package/dist/regenerate/check.js +97 -0
- package/dist/regenerate/diff.d.ts +18 -0
- package/dist/regenerate/diff.js +38 -0
- package/dist/regenerate/types.d.ts +52 -0
- package/dist/regenerate/types.js +14 -0
- package/dist/runtime/ac-parse.d.ts +63 -0
- package/dist/runtime/ac-parse.js +196 -0
- package/dist/runtime/markdown-edit.d.ts +53 -0
- package/dist/runtime/markdown-edit.js +101 -0
- package/dist/runtime/markdown-render.d.ts +27 -0
- package/dist/runtime/markdown-render.js +209 -0
- package/dist/runtime/mis-adapter.d.ts +35 -0
- package/dist/runtime/mis-adapter.js +364 -0
- package/dist/runtime/runtime.d.ts +20 -0
- package/dist/runtime/runtime.js +39 -0
- package/dist/search/format.d.ts +6 -0
- package/dist/search/format.js +23 -0
- package/dist/search/query.d.ts +29 -0
- package/dist/search/query.js +71 -0
- package/dist/store/open.d.ts +14 -0
- package/dist/store/open.js +16 -0
- package/package.json +5 -3
- package/templates/protocols/end-of-session.md +19 -0
- package/templates/protocols/persona-new.md +43 -0
- package/templates/protocols/start-of-session.md +19 -0
- package/templates/protocols/wu-new.md +52 -0
- package/templates/starter-kit/CLAUDE.md +73 -0
- package/templates/starter-kit/README.md +116 -0
- package/templates/starter-kit/docs/build/END-OF-SESSION.md +62 -0
- package/templates/starter-kit/docs/build/START-OF-SESSION.md +33 -0
- package/templates/starter-kit/docs/build/STATE.md +47 -0
- package/templates/starter-kit/docs/build/decisions/D001-template.md +38 -0
- package/templates/starter-kit/docs/build/decisions/_index.md +30 -0
- package/templates/starter-kit/docs/build/maps/01-template.md +126 -0
- package/templates/starter-kit/docs/build/maps/_index.md +63 -0
- package/templates/starter-kit/docs/build/open-questions/_index.md +26 -0
- package/templates/starter-kit/docs/build/personas/001-template.md +68 -0
- package/templates/starter-kit/docs/build/personas/_index.md +77 -0
- package/templates/starter-kit/docs/build/risks/_index.md +28 -0
- package/templates/starter-kit/docs/build/sessions/0000-00-00-template.md +47 -0
- package/templates/starter-kit/docs/build/sessions/_index.md +27 -0
- package/templates/starter-kit/docs/build/work-units/001-template.md +82 -0
- package/templates/starter-kit/docs/build/work-units/_index.md +34 -0
- package/templates/starter-kit/docs/contracts/_index.md +26 -0
- package/templates/starter-kit/docs/guides/_index.md +26 -0
- package/templates/starter-kit/docs/philosophy/_index.md +26 -0
- 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
|
+
}
|