@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,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase H part 3 — interactive create commands.
|
|
3
|
+
*
|
|
4
|
+
* Four commands: `wu create`, `decision create`, `question create`,
|
|
5
|
+
* `persona create`. Each walks the operator through the relevant
|
|
6
|
+
* register's protocol body (per `scripts/protocols/wu-new.md` etc.)
|
|
7
|
+
* and drives the workflow lifecycle to commit.
|
|
8
|
+
*
|
|
9
|
+
* Capture-builders are split out as pure functions so tests can verify
|
|
10
|
+
* the typed-payload shape without exercising readline. The interactive
|
|
11
|
+
* shell (`cmdWu*Create`) prompts, calls the builder, and drives the
|
|
12
|
+
* runtime.
|
|
13
|
+
*/
|
|
14
|
+
import { askUntilValid, validate } from './prompt.js';
|
|
15
|
+
const BUILD_MAINTAINER = {
|
|
16
|
+
kind: 'staff',
|
|
17
|
+
id: 'build-maintainer',
|
|
18
|
+
role: 'build-maintainer',
|
|
19
|
+
};
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Existing-numbers helper — pulls register's prior numbers from the store
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export function existingNumbersForRegister(store, register) {
|
|
24
|
+
return store
|
|
25
|
+
.list()
|
|
26
|
+
.filter((r) => r.register === register)
|
|
27
|
+
.map((r) => r.number);
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// decision create
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
export async function cmdDecisionCreate(store, runtime, prompt) {
|
|
33
|
+
prompt.print('Creating a new decision (D-NNN).');
|
|
34
|
+
prompt.print('');
|
|
35
|
+
const title = await askUntilValid(prompt, 'Title (one short sentence — what was decided): ', (v) => validate.nonEmpty(v, 'title'));
|
|
36
|
+
const context = await askUntilValid(prompt, 'Context — why this decision was needed (multi-line):', (v) => validate.nonEmpty(v, 'context'), { multiline: true });
|
|
37
|
+
const decision = await askUntilValid(prompt, 'Decision — what was decided, in one or two sentences (multi-line):', (v) => validate.nonEmpty(v, 'decision'), { multiline: true });
|
|
38
|
+
const consequences = await askUntilValid(prompt, 'Consequences — what this commits us to and forecloses (multi-line):', (v) => validate.nonEmpty(v, 'consequences'), { multiline: true });
|
|
39
|
+
const captureAlternatives = await prompt.confirm('Capture alternatives considered?', true);
|
|
40
|
+
const alternativesConsidered = captureAlternatives
|
|
41
|
+
? await prompt.askMultiline('Alternatives considered (multi-line):')
|
|
42
|
+
: undefined;
|
|
43
|
+
const metadata = {
|
|
44
|
+
existingDecisionNumbers: existingNumbersForRegister(store, 'decision'),
|
|
45
|
+
context,
|
|
46
|
+
decision,
|
|
47
|
+
consequences,
|
|
48
|
+
alternativesConsidered,
|
|
49
|
+
};
|
|
50
|
+
return await driveCreateLifecycle(runtime, 'decision.create', {
|
|
51
|
+
channel: 'typed_note',
|
|
52
|
+
content: title,
|
|
53
|
+
subjects: [{ kind: 'decision', id: 'd-pending' }],
|
|
54
|
+
metadata: metadata,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// question create
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
export async function cmdQuestionCreate(store, runtime, prompt) {
|
|
61
|
+
prompt.print('Creating a new open question (Q-NNN).');
|
|
62
|
+
prompt.print('');
|
|
63
|
+
const title = await askUntilValid(prompt, 'The question — one sentence ending in "?": ', (v) => validate.nonEmpty(v, 'title'));
|
|
64
|
+
const whyItMatters = await askUntilValid(prompt, 'Why it matters — what is blocked, what choice depends on it (multi-line):', (v) => validate.nonEmpty(v, 'whyItMatters'), { multiline: true });
|
|
65
|
+
const captureOptions = await prompt.confirm('Sketch options under consideration?', true);
|
|
66
|
+
const options = captureOptions
|
|
67
|
+
? await prompt.askMultiline('Options (multi-line):')
|
|
68
|
+
: undefined;
|
|
69
|
+
const captureEvidence = await prompt.confirm('Note what evidence would resolve this?', true);
|
|
70
|
+
const evidenceNeeded = captureEvidence
|
|
71
|
+
? await prompt.askMultiline('Evidence needed (multi-line):')
|
|
72
|
+
: undefined;
|
|
73
|
+
const blocksCsv = (await prompt.ask('WUs this blocks (comma-separated WU handles, or empty): ')).trim();
|
|
74
|
+
const blocks = blocksCsv ? blocksCsv.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
75
|
+
const metadata = {
|
|
76
|
+
existingQuestionNumbers: existingNumbersForRegister(store, 'open_question'),
|
|
77
|
+
whyItMatters,
|
|
78
|
+
options: options || undefined,
|
|
79
|
+
evidenceNeeded: evidenceNeeded || undefined,
|
|
80
|
+
blocks,
|
|
81
|
+
};
|
|
82
|
+
return await driveCreateLifecycle(runtime, 'open_question.create', {
|
|
83
|
+
channel: 'typed_note',
|
|
84
|
+
content: title,
|
|
85
|
+
subjects: [{ kind: 'open_question', id: 'q-pending' }],
|
|
86
|
+
metadata: metadata,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// persona create
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export async function cmdPersonaCreate(store, runtime, prompt) {
|
|
93
|
+
prompt.print('Creating a new persona (P-NNN). Per D046, walk the seven dimensions + acid-test.');
|
|
94
|
+
prompt.print('A persona is a design constraint, not a demographic snapshot. If a dimension');
|
|
95
|
+
prompt.print('does not change a future design decision, rewrite it until it does.');
|
|
96
|
+
prompt.print('');
|
|
97
|
+
const title = await askUntilValid(prompt, 'Persona name (e.g. "The DSL on call for 18 months"): ', (v) => validate.nonEmpty(v, 'title'));
|
|
98
|
+
prompt.print('');
|
|
99
|
+
prompt.print('Each of the next seven dimensions is multi-line input.');
|
|
100
|
+
prompt.print('');
|
|
101
|
+
const identity = await askDimension(prompt, '1. Identity — who they are in the context of THIS system');
|
|
102
|
+
const reality = await askDimension(prompt, '2. Reality — physical environment when they use the outcome');
|
|
103
|
+
const psychology = await askDimension(prompt, '3. Psychology — technical confidence, stress level, tolerance for confusion');
|
|
104
|
+
const trigger = await askDimension(prompt, '4. Trigger — what brings them to this outcome (a real-world event)');
|
|
105
|
+
const history = await askDimension(prompt, '5. History — what they have done before arriving here');
|
|
106
|
+
const success = await askDimension(prompt, '6. Success — what "done" looks like from THEIR perspective');
|
|
107
|
+
const constraints = await askDimension(prompt, '7. Constraints — what they cannot or will not do');
|
|
108
|
+
const acidTest = await askUntilValid(prompt, 'Acid-test refinement — the hardest legitimate combination of constraints (multi-line):', (v) => validate.nonEmpty(v, 'acidTest'), { multiline: true });
|
|
109
|
+
const metadata = {
|
|
110
|
+
existingPersonaNumbers: existingNumbersForRegister(store, 'persona'),
|
|
111
|
+
identity,
|
|
112
|
+
reality,
|
|
113
|
+
psychology,
|
|
114
|
+
trigger,
|
|
115
|
+
history,
|
|
116
|
+
success,
|
|
117
|
+
constraints,
|
|
118
|
+
acidTest,
|
|
119
|
+
};
|
|
120
|
+
return await driveCreateLifecycle(runtime, 'persona.create', {
|
|
121
|
+
channel: 'typed_note',
|
|
122
|
+
content: title,
|
|
123
|
+
subjects: [{ kind: 'persona', id: 'p-pending' }],
|
|
124
|
+
metadata: metadata,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function askDimension(prompt, label) {
|
|
128
|
+
return askUntilValid(prompt, `${label} (multi-line):`, (v) => validate.nonEmpty(v, 'dimension'), { multiline: true });
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// wu create
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
export async function cmdWuCreate(store, runtime, prompt) {
|
|
134
|
+
prompt.print('Creating a new work unit (WU-NNN).');
|
|
135
|
+
prompt.print('Per D046, a WU carries the six-field outcome shape (persona / trigger / walkthrough');
|
|
136
|
+
prompt.print('/ acceptance criteria / contracts produced / contracts consumed). Infrastructure WUs');
|
|
137
|
+
prompt.print('(build, publish, hardening, refactor) skip persona/trigger/walkthrough.');
|
|
138
|
+
prompt.print('');
|
|
139
|
+
const title = await askUntilValid(prompt, 'Title (one short sentence — what does this WU deliver?): ', (v) => validate.nonEmpty(v, 'title'));
|
|
140
|
+
const kind = (await prompt.askChoice('Kind?', [
|
|
141
|
+
'feature',
|
|
142
|
+
'infrastructure',
|
|
143
|
+
'spike',
|
|
144
|
+
'remediation',
|
|
145
|
+
]));
|
|
146
|
+
const phaseAnswer = (await prompt.ask('Phase tag (e.g. "0 — NuOS Foundation"; empty for none): ')).trim();
|
|
147
|
+
const phase = phaseAnswer || undefined;
|
|
148
|
+
const dependsCsv = (await prompt.ask('Depends on (comma-separated handles or empty): ')).trim();
|
|
149
|
+
const dependsOn = dependsCsv ? dependsCsv.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
150
|
+
const blocksCsv = (await prompt.ask('Blocks (comma-separated handles or empty): ')).trim();
|
|
151
|
+
const blocks = blocksCsv ? blocksCsv.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
152
|
+
const isInfrastructure = kind === 'infrastructure';
|
|
153
|
+
let personaRef;
|
|
154
|
+
let trigger;
|
|
155
|
+
let walkthrough;
|
|
156
|
+
if (isInfrastructure) {
|
|
157
|
+
personaRef = 'N/A — infrastructure WU';
|
|
158
|
+
trigger = 'N/A — infrastructure WU';
|
|
159
|
+
walkthrough = 'N/A — infrastructure WU';
|
|
160
|
+
prompt.print('(persona/trigger/walkthrough auto-marked N/A for infrastructure WU)');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
personaRef = (await prompt.ask('Persona ref (P-NNN handle, or leave empty to skip): ')).trim() || undefined;
|
|
164
|
+
trigger = await askUntilValid(prompt, 'Trigger — the real-world event that makes this outcome necessary (multi-line):', (v) => validate.nonEmpty(v, 'trigger'), { multiline: true });
|
|
165
|
+
walkthrough = await askUntilValid(prompt, 'Walkthrough — numbered steps from the persona\'s perspective; surface failure paths inline (multi-line):', (v) => validate.nonEmpty(v, 'walkthrough'), { multiline: true });
|
|
166
|
+
}
|
|
167
|
+
prompt.print('');
|
|
168
|
+
prompt.print('Acceptance criteria (5–10; each evaluable by inspection alone — auditor\'s test).');
|
|
169
|
+
prompt.print('Enter one per line; blank line ends.');
|
|
170
|
+
const acceptanceCriteria = [];
|
|
171
|
+
while (true) {
|
|
172
|
+
const ac = (await prompt.ask(`AC ${acceptanceCriteria.length + 1} (blank to finish): `)).trim();
|
|
173
|
+
if (!ac)
|
|
174
|
+
break;
|
|
175
|
+
acceptanceCriteria.push(ac);
|
|
176
|
+
}
|
|
177
|
+
prompt.print('');
|
|
178
|
+
prompt.print('Contracts produced — what this WU makes available to other WUs once it lands.');
|
|
179
|
+
prompt.print('One per line; blank line ends.');
|
|
180
|
+
const contractsProduced = [];
|
|
181
|
+
while (true) {
|
|
182
|
+
const c = (await prompt.ask(`Produced ${contractsProduced.length + 1}: `)).trim();
|
|
183
|
+
if (!c)
|
|
184
|
+
break;
|
|
185
|
+
contractsProduced.push(c);
|
|
186
|
+
}
|
|
187
|
+
prompt.print('');
|
|
188
|
+
prompt.print('Contracts consumed — what must already exist before this WU can run.');
|
|
189
|
+
prompt.print('One per line; blank line ends.');
|
|
190
|
+
const contractsConsumed = [];
|
|
191
|
+
while (true) {
|
|
192
|
+
const c = (await prompt.ask(`Consumed ${contractsConsumed.length + 1}: `)).trim();
|
|
193
|
+
if (!c)
|
|
194
|
+
break;
|
|
195
|
+
contractsConsumed.push(c);
|
|
196
|
+
}
|
|
197
|
+
const wantApproach = await prompt.confirm('Capture an Approach paragraph?', false);
|
|
198
|
+
const approach = wantApproach
|
|
199
|
+
? await prompt.askMultiline('Approach (multi-line):')
|
|
200
|
+
: undefined;
|
|
201
|
+
const metadata = {
|
|
202
|
+
kind,
|
|
203
|
+
phase,
|
|
204
|
+
existingWUNumbers: existingNumbersForRegister(store, 'work_unit'),
|
|
205
|
+
dependsOn,
|
|
206
|
+
blocks,
|
|
207
|
+
personaRef,
|
|
208
|
+
trigger,
|
|
209
|
+
walkthrough,
|
|
210
|
+
acceptanceCriteria,
|
|
211
|
+
contractsProduced,
|
|
212
|
+
contractsConsumed,
|
|
213
|
+
approach,
|
|
214
|
+
};
|
|
215
|
+
return await driveCreateLifecycle(runtime, 'work_unit.create', {
|
|
216
|
+
channel: 'typed_note',
|
|
217
|
+
content: title,
|
|
218
|
+
subjects: [{ kind: 'work_unit', id: 'wu-pending' }],
|
|
219
|
+
metadata: metadata,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Lifecycle driver
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
async function driveCreateLifecycle(runtime, workflowType, capture) {
|
|
226
|
+
let workflow;
|
|
227
|
+
try {
|
|
228
|
+
workflow = await runtime.startWorkflow(workflowType, BUILD_MAINTAINER, capture);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
output: `${workflowType} rejected at start: ${err.message}`,
|
|
233
|
+
exitCode: 1,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (workflow.status !== 'waiting_for_confirmation') {
|
|
237
|
+
return {
|
|
238
|
+
output: `${workflowType} unexpected post-start status: ${workflow.status}`,
|
|
239
|
+
exitCode: 1,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
workflow = await runtime.confirmIntent(workflow.id, BUILD_MAINTAINER.id);
|
|
243
|
+
if (workflow.status === 'waiting_for_approval') {
|
|
244
|
+
workflow = await runtime.approveIntent(workflow.id, BUILD_MAINTAINER.id);
|
|
245
|
+
}
|
|
246
|
+
if (workflow.status !== 'committing') {
|
|
247
|
+
return {
|
|
248
|
+
output: `${workflowType} unexpected pre-commit status: ${workflow.status}`,
|
|
249
|
+
exitCode: 1,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
workflow = await runtime.commitIntent(workflow.id, BUILD_MAINTAINER.id);
|
|
253
|
+
if (workflow.status !== 'completed') {
|
|
254
|
+
return {
|
|
255
|
+
output: `${workflowType} commit failed: status=${workflow.status}`,
|
|
256
|
+
exitCode: 1,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const handle = workflow.writeIntent?.payload?.handle ?? 'unknown';
|
|
260
|
+
return {
|
|
261
|
+
output: `${workflowType} ✅ created ${handle} (commit ${workflow.commitRef?.commitRef ?? '?'})`,
|
|
262
|
+
exitCode: 0,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export function buildDecisionCreateCapture(store, inputs) {
|
|
266
|
+
return {
|
|
267
|
+
channel: 'typed_note',
|
|
268
|
+
content: inputs.title,
|
|
269
|
+
subjects: [{ kind: 'decision', id: 'd-pending' }],
|
|
270
|
+
metadata: {
|
|
271
|
+
existingDecisionNumbers: existingNumbersForRegister(store, 'decision'),
|
|
272
|
+
context: inputs.context,
|
|
273
|
+
decision: inputs.decision,
|
|
274
|
+
consequences: inputs.consequences,
|
|
275
|
+
alternativesConsidered: inputs.alternativesConsidered,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function buildQuestionCreateCapture(store, inputs) {
|
|
280
|
+
return {
|
|
281
|
+
channel: 'typed_note',
|
|
282
|
+
content: inputs.title,
|
|
283
|
+
subjects: [{ kind: 'open_question', id: 'q-pending' }],
|
|
284
|
+
metadata: {
|
|
285
|
+
existingQuestionNumbers: existingNumbersForRegister(store, 'open_question'),
|
|
286
|
+
whyItMatters: inputs.whyItMatters,
|
|
287
|
+
options: inputs.options,
|
|
288
|
+
evidenceNeeded: inputs.evidenceNeeded,
|
|
289
|
+
blocks: inputs.blocks,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
export function buildPersonaCreateCapture(store, inputs) {
|
|
294
|
+
return {
|
|
295
|
+
channel: 'typed_note',
|
|
296
|
+
content: inputs.title,
|
|
297
|
+
subjects: [{ kind: 'persona', id: 'p-pending' }],
|
|
298
|
+
metadata: {
|
|
299
|
+
existingPersonaNumbers: existingNumbersForRegister(store, 'persona'),
|
|
300
|
+
identity: inputs.identity,
|
|
301
|
+
reality: inputs.reality,
|
|
302
|
+
psychology: inputs.psychology,
|
|
303
|
+
trigger: inputs.trigger,
|
|
304
|
+
history: inputs.history,
|
|
305
|
+
success: inputs.success,
|
|
306
|
+
constraints: inputs.constraints,
|
|
307
|
+
acidTest: inputs.acidTest,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function buildWuCreateCapture(store, inputs) {
|
|
312
|
+
const isInfra = inputs.kind === 'infrastructure';
|
|
313
|
+
const personaRef = isInfra
|
|
314
|
+
? 'N/A — infrastructure WU'
|
|
315
|
+
: inputs.personaRef;
|
|
316
|
+
const trigger = isInfra
|
|
317
|
+
? 'N/A — infrastructure WU'
|
|
318
|
+
: inputs.trigger;
|
|
319
|
+
const walkthrough = isInfra
|
|
320
|
+
? 'N/A — infrastructure WU'
|
|
321
|
+
: inputs.walkthrough;
|
|
322
|
+
return {
|
|
323
|
+
channel: 'typed_note',
|
|
324
|
+
content: inputs.title,
|
|
325
|
+
subjects: [{ kind: 'work_unit', id: 'wu-pending' }],
|
|
326
|
+
metadata: {
|
|
327
|
+
kind: inputs.kind,
|
|
328
|
+
phase: inputs.phase,
|
|
329
|
+
existingWUNumbers: existingNumbersForRegister(store, 'work_unit'),
|
|
330
|
+
dependsOn: inputs.dependsOn,
|
|
331
|
+
blocks: inputs.blocks,
|
|
332
|
+
personaRef,
|
|
333
|
+
trigger,
|
|
334
|
+
walkthrough,
|
|
335
|
+
acceptanceCriteria: inputs.acceptanceCriteria,
|
|
336
|
+
contractsProduced: inputs.contractsProduced,
|
|
337
|
+
contractsConsumed: inputs.contractsConsumed,
|
|
338
|
+
approach: inputs.approach,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatters for the read-side CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Two output modes per list/show pair: human-readable (default) for
|
|
5
|
+
* terminal use; JSON (`--json`) for piping into other tools. Tabular
|
|
6
|
+
* output uses a minimal per-line width-aware formatter to keep the
|
|
7
|
+
* dep graph small (no `cli-table3` etc.).
|
|
8
|
+
*/
|
|
9
|
+
import type { MigratedRecord, Register } from '../migrate/types.js';
|
|
10
|
+
export interface ListFilter {
|
|
11
|
+
status?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function filterRecords(records: readonly MigratedRecord[], register: Register, filter: ListFilter): MigratedRecord[];
|
|
15
|
+
export declare function sortByHandle(records: readonly MigratedRecord[]): MigratedRecord[];
|
|
16
|
+
export declare function formatListHuman(records: readonly MigratedRecord[]): string;
|
|
17
|
+
export declare function formatListJson(records: readonly MigratedRecord[]): string;
|
|
18
|
+
export declare function formatShowHuman(record: MigratedRecord): string;
|
|
19
|
+
export declare function formatShowJson(record: MigratedRecord): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatters for the read-side CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Two output modes per list/show pair: human-readable (default) for
|
|
5
|
+
* terminal use; JSON (`--json`) for piping into other tools. Tabular
|
|
6
|
+
* output uses a minimal per-line width-aware formatter to keep the
|
|
7
|
+
* dep graph small (no `cli-table3` etc.).
|
|
8
|
+
*/
|
|
9
|
+
const STATUS_FALLBACK = '—';
|
|
10
|
+
export function filterRecords(records, register, filter) {
|
|
11
|
+
let result = records.filter((r) => r.register === register);
|
|
12
|
+
if (filter.status) {
|
|
13
|
+
const needle = filter.status.toLowerCase();
|
|
14
|
+
result = result.filter((r) => (r.status ?? '').toLowerCase().includes(needle));
|
|
15
|
+
}
|
|
16
|
+
result = sortByHandle(result);
|
|
17
|
+
if (filter.limit && filter.limit > 0) {
|
|
18
|
+
result = result.slice(0, filter.limit);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
export function sortByHandle(records) {
|
|
23
|
+
return [...records].sort((a, b) => {
|
|
24
|
+
if (a.number !== b.number)
|
|
25
|
+
return a.number - b.number;
|
|
26
|
+
return a.handle.localeCompare(b.handle);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function formatListHuman(records) {
|
|
30
|
+
if (records.length === 0)
|
|
31
|
+
return '(no records match)';
|
|
32
|
+
const widths = computeWidths(records);
|
|
33
|
+
const lines = [];
|
|
34
|
+
for (const r of records) {
|
|
35
|
+
const handle = r.handle.padEnd(widths.handle);
|
|
36
|
+
const status = (r.status ?? STATUS_FALLBACK).padEnd(widths.status);
|
|
37
|
+
lines.push(` ${handle} ${status} ${r.title}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push(` (${records.length} record${records.length === 1 ? '' : 's'})`);
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
export function formatListJson(records) {
|
|
44
|
+
return JSON.stringify(records.map((r) => ({
|
|
45
|
+
handle: r.handle,
|
|
46
|
+
number: r.number,
|
|
47
|
+
register: r.register,
|
|
48
|
+
title: r.title,
|
|
49
|
+
status: r.status,
|
|
50
|
+
slug: r.slug,
|
|
51
|
+
sourcePath: r.sourcePath,
|
|
52
|
+
})), null, 2);
|
|
53
|
+
}
|
|
54
|
+
export function formatShowHuman(record) {
|
|
55
|
+
return [
|
|
56
|
+
`# ${record.handle} — ${record.title}`,
|
|
57
|
+
'',
|
|
58
|
+
`register: ${record.register}`,
|
|
59
|
+
`number: ${record.number}`,
|
|
60
|
+
`status: ${record.status ?? STATUS_FALLBACK}`,
|
|
61
|
+
`slug: ${record.slug}`,
|
|
62
|
+
`source path: ${record.sourcePath}`,
|
|
63
|
+
`file modified: ${record.fileModifiedAt}`,
|
|
64
|
+
`migrated at: ${record.migratedAt} (from ${record.migratedFrom})`,
|
|
65
|
+
'',
|
|
66
|
+
`--- markdown body (${record.rawMarkdown.length} chars) ---`,
|
|
67
|
+
truncateBody(record.rawMarkdown, 2000),
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
export function formatShowJson(record) {
|
|
71
|
+
return JSON.stringify(record, null, 2);
|
|
72
|
+
}
|
|
73
|
+
function computeWidths(records) {
|
|
74
|
+
let handle = 0;
|
|
75
|
+
let status = 0;
|
|
76
|
+
for (const r of records) {
|
|
77
|
+
if (r.handle.length > handle)
|
|
78
|
+
handle = r.handle.length;
|
|
79
|
+
const s = (r.status ?? STATUS_FALLBACK).length;
|
|
80
|
+
if (s > status)
|
|
81
|
+
status = s;
|
|
82
|
+
}
|
|
83
|
+
return { handle, status };
|
|
84
|
+
}
|
|
85
|
+
function truncateBody(body, max) {
|
|
86
|
+
if (body.length <= max)
|
|
87
|
+
return body;
|
|
88
|
+
return body.slice(0, max) + `\n…[truncated; use --json for full body]`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-register read-side handlers (list + show).
|
|
3
|
+
*
|
|
4
|
+
* Pure-ish: these read from a `WorkflowStore` and produce strings. The
|
|
5
|
+
* CLI surface in `src/cli.ts` opens the store, calls the handler, and
|
|
6
|
+
* prints the result.
|
|
7
|
+
*/
|
|
8
|
+
import type { Register, MigratedRecord } from '../migrate/types.js';
|
|
9
|
+
import type { WorkflowStore } from '../migrate/store.js';
|
|
10
|
+
import { type ListFilter } from './format.js';
|
|
11
|
+
export interface ListOptions extends ListFilter {
|
|
12
|
+
asJson?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ShowOptions {
|
|
15
|
+
asJson?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface HandlerResult {
|
|
18
|
+
output: string;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function listRegister(store: WorkflowStore, register: Register, options: ListOptions): HandlerResult;
|
|
22
|
+
export declare function showRecord(store: WorkflowStore, register: Register, handle: string, options: ShowOptions): HandlerResult;
|
|
23
|
+
/**
|
|
24
|
+
* Accept friendly variants like `111` (assumes work_unit), `wu-111`,
|
|
25
|
+
* `WU 111`, `D45`, `Q9` — and normalise to the stored handle form.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normaliseHandle(register: Register, raw: string): string;
|
|
28
|
+
export declare function registerCommand(register: Register): string;
|
|
29
|
+
export declare function listAcrossRegisters(store: WorkflowStore): {
|
|
30
|
+
byRegister: Record<Register, number>;
|
|
31
|
+
total: number;
|
|
32
|
+
};
|
|
33
|
+
export declare function isRegister(value: string): value is Register;
|
|
34
|
+
export declare function commandToRegister(command: string): Register | null;
|
|
35
|
+
export type { MigratedRecord };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-register read-side handlers (list + show).
|
|
3
|
+
*
|
|
4
|
+
* Pure-ish: these read from a `WorkflowStore` and produce strings. The
|
|
5
|
+
* CLI surface in `src/cli.ts` opens the store, calls the handler, and
|
|
6
|
+
* prints the result.
|
|
7
|
+
*/
|
|
8
|
+
import { filterRecords, formatListHuman, formatListJson, formatShowHuman, formatShowJson, } from './format.js';
|
|
9
|
+
export function listRegister(store, register, options) {
|
|
10
|
+
const all = store.list();
|
|
11
|
+
const matches = filterRecords(all, register, options);
|
|
12
|
+
const output = options.asJson ? formatListJson(matches) : formatListHuman(matches);
|
|
13
|
+
return { output, exitCode: 0 };
|
|
14
|
+
}
|
|
15
|
+
export function showRecord(store, register, handle, options) {
|
|
16
|
+
const normalised = normaliseHandle(register, handle);
|
|
17
|
+
const record = store.get(normalised);
|
|
18
|
+
if (!record) {
|
|
19
|
+
return {
|
|
20
|
+
output: `no ${register} record with handle "${normalised}" — try \`nuos-catalogue ${registerCommand(register)} list\``,
|
|
21
|
+
exitCode: 1,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (record.register !== register) {
|
|
25
|
+
return {
|
|
26
|
+
output: `handle ${normalised} resolves to a ${record.register}, not a ${register}`,
|
|
27
|
+
exitCode: 1,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const output = options.asJson ? formatShowJson(record) : formatShowHuman(record);
|
|
31
|
+
return { output, exitCode: 0 };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Accept friendly variants like `111` (assumes work_unit), `wu-111`,
|
|
35
|
+
* `WU 111`, `D45`, `Q9` — and normalise to the stored handle form.
|
|
36
|
+
*/
|
|
37
|
+
export function normaliseHandle(register, raw) {
|
|
38
|
+
const trimmed = raw.trim();
|
|
39
|
+
if (!trimmed)
|
|
40
|
+
return trimmed;
|
|
41
|
+
// If already in canonical form, accept verbatim.
|
|
42
|
+
if (matchesCanonical(register, trimmed))
|
|
43
|
+
return trimmed;
|
|
44
|
+
// Tolerate "WU 111" → "wu-111".
|
|
45
|
+
const wu = /^WU\s*0?(\d{1,4})([a-z]?)$/i.exec(trimmed);
|
|
46
|
+
if (wu && register === 'work_unit') {
|
|
47
|
+
const num = wu[1].padStart(3, '0');
|
|
48
|
+
return `wu-${num}${(wu[2] ?? '').toLowerCase()}`;
|
|
49
|
+
}
|
|
50
|
+
// Tolerate plain integers when the register is unambiguous.
|
|
51
|
+
const intOnly = /^(\d{1,4})([a-z]?)$/i.exec(trimmed);
|
|
52
|
+
if (intOnly) {
|
|
53
|
+
const num = intOnly[1].padStart(3, '0');
|
|
54
|
+
const suffix = (intOnly[2] ?? '').toLowerCase();
|
|
55
|
+
switch (register) {
|
|
56
|
+
case 'work_unit':
|
|
57
|
+
return `wu-${num}${suffix}`;
|
|
58
|
+
case 'decision':
|
|
59
|
+
return `D${num}`;
|
|
60
|
+
case 'open_question':
|
|
61
|
+
return `Q${num}`;
|
|
62
|
+
case 'persona':
|
|
63
|
+
return `P${num}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Tolerate D45 / Q9 / P1 (under-padded) forms.
|
|
67
|
+
const letter = /^([DQP])(\d{1,4})$/i.exec(trimmed);
|
|
68
|
+
if (letter) {
|
|
69
|
+
const prefix = letter[1].toUpperCase();
|
|
70
|
+
const num = letter[2].padStart(3, '0');
|
|
71
|
+
return `${prefix}${num}`;
|
|
72
|
+
}
|
|
73
|
+
// Fallback: return verbatim. The store lookup will return null and the
|
|
74
|
+
// CLI surfaces a helpful error.
|
|
75
|
+
return trimmed;
|
|
76
|
+
}
|
|
77
|
+
function matchesCanonical(register, handle) {
|
|
78
|
+
switch (register) {
|
|
79
|
+
case 'work_unit':
|
|
80
|
+
return /^wu-\d{3,}[a-z]?$/.test(handle);
|
|
81
|
+
case 'decision':
|
|
82
|
+
return /^D\d{3,}$/.test(handle);
|
|
83
|
+
case 'open_question':
|
|
84
|
+
return /^Q\d{3,}$/.test(handle);
|
|
85
|
+
case 'persona':
|
|
86
|
+
return /^P\d{3,}$/.test(handle);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function registerCommand(register) {
|
|
90
|
+
switch (register) {
|
|
91
|
+
case 'work_unit':
|
|
92
|
+
return 'wu';
|
|
93
|
+
case 'decision':
|
|
94
|
+
return 'decision';
|
|
95
|
+
case 'open_question':
|
|
96
|
+
return 'question';
|
|
97
|
+
case 'persona':
|
|
98
|
+
return 'persona';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function listAcrossRegisters(store) {
|
|
102
|
+
const byRegister = {
|
|
103
|
+
work_unit: 0,
|
|
104
|
+
decision: 0,
|
|
105
|
+
open_question: 0,
|
|
106
|
+
persona: 0,
|
|
107
|
+
};
|
|
108
|
+
let total = 0;
|
|
109
|
+
for (const r of store.list()) {
|
|
110
|
+
byRegister[r.register] += 1;
|
|
111
|
+
total += 1;
|
|
112
|
+
}
|
|
113
|
+
return { byRegister, total };
|
|
114
|
+
}
|
|
115
|
+
// Convenience guard for the CLI dispatcher.
|
|
116
|
+
export function isRegister(value) {
|
|
117
|
+
return value === 'work_unit' || value === 'decision' || value === 'open_question' || value === 'persona';
|
|
118
|
+
}
|
|
119
|
+
export function commandToRegister(command) {
|
|
120
|
+
switch (command) {
|
|
121
|
+
case 'wu':
|
|
122
|
+
return 'work_unit';
|
|
123
|
+
case 'decision':
|
|
124
|
+
return 'decision';
|
|
125
|
+
case 'question':
|
|
126
|
+
return 'open_question';
|
|
127
|
+
case 'persona':
|
|
128
|
+
return 'persona';
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nuos-catalogue init` — bootstrap a new project's catalogue.
|
|
3
|
+
*
|
|
4
|
+
* Single command that does what was previously a six-step manual scaffold:
|
|
5
|
+
* 1. mkdir docs/build/ + copy starter-kit content
|
|
6
|
+
* 2. Substitute {{PROJECT_NAME}} / {{PROJECT_TAGLINE}} / {{TODAY}} in
|
|
7
|
+
* STATE.md and methodfile.json
|
|
8
|
+
* 3. Copy the four protocols into .claude/commands/ (preserving
|
|
9
|
+
* existing files)
|
|
10
|
+
* 4. Append a "Build catalogue (NuOS Build Method)" section to
|
|
11
|
+
* CLAUDE.md (creating it if missing; preserving existing content)
|
|
12
|
+
* 5. Update .gitignore: !docs/build/ override (if `build/` is present
|
|
13
|
+
* anywhere, the catalogue would otherwise be silently ignored —
|
|
14
|
+
* same gotcha caught at nuos Session 53); ignore .nuos-catalogue/
|
|
15
|
+
* per D047
|
|
16
|
+
* 6. Run a first migrate to verify
|
|
17
|
+
*
|
|
18
|
+
* Companion command: `install-protocols` refreshes just step 3 from the
|
|
19
|
+
* canonical bodies bundled in this CLI package.
|
|
20
|
+
*/
|
|
21
|
+
import type { Prompt } from './prompt.js';
|
|
22
|
+
export interface InitOptions {
|
|
23
|
+
/** Target project root (default: cwd). */
|
|
24
|
+
cwd?: string;
|
|
25
|
+
/** Pre-supplied values; if any are missing, the prompt fills them in. */
|
|
26
|
+
name?: string;
|
|
27
|
+
tagline?: string;
|
|
28
|
+
domain?: string;
|
|
29
|
+
role?: string;
|
|
30
|
+
/** When true, runs all steps without prompts (uses defaults / supplied values). */
|
|
31
|
+
nonInteractive?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface InitResult {
|
|
34
|
+
output: string;
|
|
35
|
+
exitCode: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function cmdInit(prompt: Prompt, options?: InitOptions): Promise<InitResult>;
|
|
38
|
+
export interface InstallProtocolsOptions {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function cmdInstallProtocols(prompt: Prompt, options?: InstallProtocolsOptions): Promise<InitResult>;
|