@nusoft/nuos-build-catalogue 0.10.0 → 0.10.2

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 (65) hide show
  1. package/dist/cli.d.ts +13 -0
  2. package/dist/cli.js +491 -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 +3 -2
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Drift detection: walk the workflow store, compare each record's
3
+ * stored `rawMarkdown` to its source file, report differences.
4
+ */
5
+ import { readFile, writeFile } from 'node:fs/promises';
6
+ import { existsSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { countLineDiff } from './diff.js';
9
+ const ZERO_PER_REGISTER = () => ({
10
+ total: 0,
11
+ identical: 0,
12
+ differs: 0,
13
+ missing: 0,
14
+ unreadable: 0,
15
+ });
16
+ export async function runRegenerate(config) {
17
+ const startedAt = Date.now();
18
+ const records = config.store.list();
19
+ const filtered = config.registerFilter
20
+ ? records.filter((r) => r.register === config.registerFilter)
21
+ : records;
22
+ const byRegister = {
23
+ work_unit: ZERO_PER_REGISTER(),
24
+ decision: ZERO_PER_REGISTER(),
25
+ open_question: ZERO_PER_REGISTER(),
26
+ persona: ZERO_PER_REGISTER(),
27
+ };
28
+ let identical = 0;
29
+ let differs = 0;
30
+ let missing = 0;
31
+ let unreadable = 0;
32
+ const drifted = [];
33
+ for (const record of filtered) {
34
+ const sourceAbsolute = path.join(config.catalogueRoot, record.sourcePath);
35
+ byRegister[record.register].total += 1;
36
+ if (!existsSync(sourceAbsolute)) {
37
+ missing += 1;
38
+ byRegister[record.register].missing += 1;
39
+ drifted.push({
40
+ handle: record.handle,
41
+ register: record.register,
42
+ sourcePath: record.sourcePath,
43
+ kind: 'missing-source',
44
+ errorMessage: `source file does not exist at ${sourceAbsolute}`,
45
+ });
46
+ continue;
47
+ }
48
+ let onDisk;
49
+ try {
50
+ onDisk = await readFile(sourceAbsolute, 'utf8');
51
+ }
52
+ catch (err) {
53
+ unreadable += 1;
54
+ byRegister[record.register].unreadable += 1;
55
+ drifted.push({
56
+ handle: record.handle,
57
+ register: record.register,
58
+ sourcePath: record.sourcePath,
59
+ kind: 'unreadable-source',
60
+ errorMessage: err instanceof Error ? err.message : String(err),
61
+ });
62
+ continue;
63
+ }
64
+ if (onDisk === record.rawMarkdown) {
65
+ identical += 1;
66
+ byRegister[record.register].identical += 1;
67
+ continue;
68
+ }
69
+ if (config.write) {
70
+ // Mode 2 cutover — overwrite the source with the stored canonical
71
+ // form. Recorded as drift but resolved-by-write.
72
+ await writeFile(sourceAbsolute, record.rawMarkdown, 'utf8');
73
+ }
74
+ const counts = countLineDiff(record.rawMarkdown, onDisk);
75
+ differs += 1;
76
+ byRegister[record.register].differs += 1;
77
+ drifted.push({
78
+ handle: record.handle,
79
+ register: record.register,
80
+ sourcePath: record.sourcePath,
81
+ kind: 'differs',
82
+ byteDelta: Math.abs(onDisk.length - record.rawMarkdown.length),
83
+ linesAdded: counts.added,
84
+ linesRemoved: counts.removed,
85
+ });
86
+ }
87
+ return {
88
+ total: filtered.length,
89
+ identical,
90
+ differs,
91
+ missing,
92
+ unreadable,
93
+ byRegister,
94
+ drifted,
95
+ durationMs: Date.now() - startedAt,
96
+ };
97
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Minimal line-counting diff helper.
3
+ *
4
+ * Phase I only needs to know "did the file change, and by roughly how
5
+ * much". Full unified-diff output is a future enhancement; today the
6
+ * drift report names the file and the magnitude.
7
+ */
8
+ export interface LineCounts {
9
+ added: number;
10
+ removed: number;
11
+ }
12
+ /**
13
+ * Counts lines that differ between `before` and `after` using a basic
14
+ * Myers-style diff. For the catalogue's small files this is fast
15
+ * enough; if we ever need to diff multi-MB files we'd swap in `diff`
16
+ * from npm.
17
+ */
18
+ export declare function countLineDiff(before: string, after: string): LineCounts;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Minimal line-counting diff helper.
3
+ *
4
+ * Phase I only needs to know "did the file change, and by roughly how
5
+ * much". Full unified-diff output is a future enhancement; today the
6
+ * drift report names the file and the magnitude.
7
+ */
8
+ /**
9
+ * Counts lines that differ between `before` and `after` using a basic
10
+ * Myers-style diff. For the catalogue's small files this is fast
11
+ * enough; if we ever need to diff multi-MB files we'd swap in `diff`
12
+ * from npm.
13
+ */
14
+ export function countLineDiff(before, after) {
15
+ if (before === after)
16
+ return { added: 0, removed: 0 };
17
+ const beforeLines = before.split('\n');
18
+ const afterLines = after.split('\n');
19
+ // Build LCS table — small files, O(n*m) is fine.
20
+ const m = beforeLines.length;
21
+ const n = afterLines.length;
22
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
23
+ for (let i = m - 1; i >= 0; i--) {
24
+ for (let j = n - 1; j >= 0; j--) {
25
+ if (beforeLines[i] === afterLines[j]) {
26
+ dp[i][j] = dp[i + 1][j + 1] + 1;
27
+ }
28
+ else {
29
+ dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
30
+ }
31
+ }
32
+ }
33
+ const lcsLength = dp[0][0];
34
+ return {
35
+ removed: m - lcsLength,
36
+ added: n - lcsLength,
37
+ };
38
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Phase I — markdown regeneration + drift report (WU 111).
3
+ *
4
+ * **Mode 1 (today):** markdown is canonical; this module is a
5
+ * verification gate. For each record in the workflow store, compare
6
+ * the stored `rawMarkdown` to the file at `record.sourcePath`. Any
7
+ * difference is reported as drift.
8
+ *
9
+ * **Mode 2 (post-WU-113):** workflow state is canonical; this module
10
+ * regenerates markdown from rich field-level data (which we don't have
11
+ * yet — Phase G stopped at count parity, not field-level fidelity).
12
+ * Mode 2 is out of scope until WU 113.
13
+ */
14
+ import type { Register } from '../migrate/types.js';
15
+ export type DriftKind = 'identical' | 'differs' | 'missing-source' | 'unreadable-source';
16
+ export interface DriftEntry {
17
+ handle: string;
18
+ register: Register;
19
+ sourcePath: string;
20
+ kind: DriftKind;
21
+ /** Set when kind === 'differs'. Number of bytes the two contents differ by (rough magnitude). */
22
+ byteDelta?: number;
23
+ /** Set when kind === 'differs'. Lines added vs the stored record. */
24
+ linesAdded?: number;
25
+ /** Set when kind === 'differs'. Lines removed vs the stored record. */
26
+ linesRemoved?: number;
27
+ /** Set when kind === 'missing-source' or 'unreadable-source'. */
28
+ errorMessage?: string;
29
+ }
30
+ export interface DriftReport {
31
+ total: number;
32
+ identical: number;
33
+ differs: number;
34
+ missing: number;
35
+ unreadable: number;
36
+ byRegister: Record<Register, {
37
+ total: number;
38
+ identical: number;
39
+ differs: number;
40
+ missing: number;
41
+ unreadable: number;
42
+ }>;
43
+ /** Only the non-identical entries; the identical ones are summarised by counts. */
44
+ drifted: DriftEntry[];
45
+ durationMs: number;
46
+ }
47
+ export interface RegenerateConfig {
48
+ /** Filter to a single register; default scans all four. */
49
+ registerFilter?: Register;
50
+ /** When true, overwrite source files with stored `rawMarkdown` (Mode 2 cutover). Off by default. */
51
+ write?: boolean;
52
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Phase I — markdown regeneration + drift report (WU 111).
3
+ *
4
+ * **Mode 1 (today):** markdown is canonical; this module is a
5
+ * verification gate. For each record in the workflow store, compare
6
+ * the stored `rawMarkdown` to the file at `record.sourcePath`. Any
7
+ * difference is reported as drift.
8
+ *
9
+ * **Mode 2 (post-WU-113):** workflow state is canonical; this module
10
+ * regenerates markdown from rich field-level data (which we don't have
11
+ * yet — Phase G stopped at count parity, not field-level fidelity).
12
+ * Mode 2 is out of scope until WU 113.
13
+ */
14
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Acceptance-criteria parser + tickr.
3
+ *
4
+ * Two shapes are recognised, matching what the live catalogue uses:
5
+ *
6
+ * 1. Checkbox: `- [ ] text` (unticked) or `- [x] text` (ticked)
7
+ * 2. Numbered + emoji: `1. ✅ text` (ticked) or `1. text` (unticked)
8
+ *
9
+ * Files that don't match either shape produce an empty AC list — the
10
+ * maintainer falls back to hand-editing for those. WU 073 is the only
11
+ * known WU that uses neither shape.
12
+ *
13
+ * The parser scans only inside the `## Acceptance criteria` section
14
+ * (case-insensitive; tolerates the ` (= verification)` suffix per
15
+ * D046). Lines outside that section are ignored even if they happen
16
+ * to look like AC.
17
+ */
18
+ import type { AcceptanceCriterion } from '@nusoft/nuflow-pack-nuos-build-catalogue';
19
+ /** Internal extended shape that retains the source line for byte-accurate replacement. */
20
+ export interface ParsedAcceptanceCriterion {
21
+ /** Zero-based index in the AC list as parsed. */
22
+ index: number;
23
+ /** AC text without the bullet/checkbox/number/emoji prefix. */
24
+ text: string;
25
+ /** True if the source markdown has the ticked form. */
26
+ met: boolean;
27
+ /** The original full line as it appeared in the markdown — used by `tickAcceptanceCriterion` for in-place replacement. */
28
+ rawLine: string;
29
+ /** Bullet style detected for this entry — used to render the ticked form in the same shape. */
30
+ style: 'checkbox' | 'numbered-emoji';
31
+ /** Numeric prefix preserved (e.g. "1") for numbered entries; empty string for checkbox. */
32
+ numberPrefix: string;
33
+ }
34
+ /**
35
+ * Parse the AC list from a WU markdown body.
36
+ *
37
+ * Returns an empty list if no `## Acceptance criteria` heading exists,
38
+ * if the section has no recognisable AC entries, or if the section is
39
+ * empty.
40
+ */
41
+ export declare function parseAcceptanceCriteria(rawMarkdown: string): ParsedAcceptanceCriterion[];
42
+ /**
43
+ * Flip the AC at `targetIndex` from unticked to ticked, preserving the
44
+ * original style. If the AC is already ticked, the markdown is
45
+ * returned unchanged. If the index is out of range or no AC list is
46
+ * found, throws.
47
+ */
48
+ export declare function tickAcceptanceCriterion(rawMarkdown: string, targetIndex: number): string;
49
+ /**
50
+ * Extract AC list in the shape the build-catalogue pack's
51
+ * `work_unit.advance_status` workflow expects in
52
+ * `metadata.acceptanceCriteria` for the completion gate.
53
+ *
54
+ * Evidence inference:
55
+ * - If the AC is ticked in markdown AND there's a Build catalogue
56
+ * history entry naming this AC index, evidence comes from the
57
+ * history entry.
58
+ * - If the AC is ticked in markdown with no history entry, evidence
59
+ * defaults to "Ticked in source markdown."
60
+ * - If the AC is unticked, evidence is undefined and the completion
61
+ * gate will reject.
62
+ */
63
+ export declare function extractForCompletion(rawMarkdown: string): AcceptanceCriterion[];
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Acceptance-criteria parser + tickr.
3
+ *
4
+ * Two shapes are recognised, matching what the live catalogue uses:
5
+ *
6
+ * 1. Checkbox: `- [ ] text` (unticked) or `- [x] text` (ticked)
7
+ * 2. Numbered + emoji: `1. ✅ text` (ticked) or `1. text` (unticked)
8
+ *
9
+ * Files that don't match either shape produce an empty AC list — the
10
+ * maintainer falls back to hand-editing for those. WU 073 is the only
11
+ * known WU that uses neither shape.
12
+ *
13
+ * The parser scans only inside the `## Acceptance criteria` section
14
+ * (case-insensitive; tolerates the ` (= verification)` suffix per
15
+ * D046). Lines outside that section are ignored even if they happen
16
+ * to look like AC.
17
+ */
18
+ const HEADING_RE = /^##\s+Acceptance\s+criteria(\s*\(=\s*verification\))?\s*$/im;
19
+ const CHECKBOX_RE = /^(\s*-\s+\[)([ xX])(\]\s+)(.+)$/;
20
+ const NUMBERED_TICKED_RE = /^(\s*)(\d+)(\.\s+)(✅\s+)(.+)$/u;
21
+ const NUMBERED_UNTICKED_RE = /^(\s*)(\d+)(\.\s+)(?!✅|⏳|⚠|❌|🔴)([^*-].*)$/u;
22
+ /**
23
+ * Parse the AC list from a WU markdown body.
24
+ *
25
+ * Returns an empty list if no `## Acceptance criteria` heading exists,
26
+ * if the section has no recognisable AC entries, or if the section is
27
+ * empty.
28
+ */
29
+ export function parseAcceptanceCriteria(rawMarkdown) {
30
+ const headingMatch = HEADING_RE.exec(rawMarkdown);
31
+ if (!headingMatch)
32
+ return [];
33
+ const sectionStart = headingMatch.index + headingMatch[0].length;
34
+ // Find the next ## or # heading (or EOF) to bound the section
35
+ const tail = rawMarkdown.slice(sectionStart);
36
+ const nextHeadingMatch = /^#{1,2}\s+\S/m.exec(tail);
37
+ const sectionEnd = nextHeadingMatch ? sectionStart + nextHeadingMatch.index : rawMarkdown.length;
38
+ const section = rawMarkdown.slice(sectionStart, sectionEnd);
39
+ const lines = section.split('\n');
40
+ const result = [];
41
+ let index = 0;
42
+ for (const line of lines) {
43
+ // Try checkbox first
44
+ const cb = CHECKBOX_RE.exec(line);
45
+ if (cb) {
46
+ result.push({
47
+ index,
48
+ text: cb[4].trim(),
49
+ met: cb[2].toLowerCase() === 'x',
50
+ rawLine: line,
51
+ style: 'checkbox',
52
+ numberPrefix: '',
53
+ });
54
+ index += 1;
55
+ continue;
56
+ }
57
+ // Then numbered + ✅
58
+ const nt = NUMBERED_TICKED_RE.exec(line);
59
+ if (nt) {
60
+ result.push({
61
+ index,
62
+ text: nt[5].trim(),
63
+ met: true,
64
+ rawLine: line,
65
+ style: 'numbered-emoji',
66
+ numberPrefix: nt[2],
67
+ });
68
+ index += 1;
69
+ continue;
70
+ }
71
+ // Then numbered without leading emoji (treated as unticked)
72
+ const nu = NUMBERED_UNTICKED_RE.exec(line);
73
+ if (nu) {
74
+ result.push({
75
+ index,
76
+ text: nu[4].trim(),
77
+ met: false,
78
+ rawLine: line,
79
+ style: 'numbered-emoji', // round-trips back as numbered+emoji when ticked
80
+ numberPrefix: nu[2],
81
+ });
82
+ index += 1;
83
+ continue;
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Flip the AC at `targetIndex` from unticked to ticked, preserving the
90
+ * original style. If the AC is already ticked, the markdown is
91
+ * returned unchanged. If the index is out of range or no AC list is
92
+ * found, throws.
93
+ */
94
+ export function tickAcceptanceCriterion(rawMarkdown, targetIndex) {
95
+ const acs = parseAcceptanceCriteria(rawMarkdown);
96
+ if (acs.length === 0) {
97
+ throw new Error('tickAcceptanceCriterion: no acceptance-criteria section found in this markdown');
98
+ }
99
+ if (targetIndex < 0 || targetIndex >= acs.length) {
100
+ throw new Error(`tickAcceptanceCriterion: index ${targetIndex} out of range (AC list has ${acs.length} entries)`);
101
+ }
102
+ const target = acs[targetIndex];
103
+ if (target.met) {
104
+ return rawMarkdown; // already ticked; no-op
105
+ }
106
+ const tickedLine = renderTickedLine(target);
107
+ // Replace the FIRST occurrence of the raw line (we expect uniqueness
108
+ // because the same line text appearing twice in the AC section would
109
+ // already be a catalogue-discipline issue).
110
+ return rawMarkdown.replace(target.rawLine, tickedLine);
111
+ }
112
+ function renderTickedLine(ac) {
113
+ switch (ac.style) {
114
+ case 'checkbox':
115
+ return ac.rawLine.replace(CHECKBOX_RE, '$1x$3$4');
116
+ case 'numbered-emoji':
117
+ // Two cases: was numbered-ticked already (caller shouldn't hit this
118
+ // because met==true returns early), or was numbered-unticked.
119
+ if (ac.rawLine.includes('✅')) {
120
+ return ac.rawLine; // defensive: shouldn't happen
121
+ }
122
+ return ac.rawLine.replace(NUMBERED_UNTICKED_RE, '$1$2$3✅ $4');
123
+ }
124
+ }
125
+ /**
126
+ * Extract AC list in the shape the build-catalogue pack's
127
+ * `work_unit.advance_status` workflow expects in
128
+ * `metadata.acceptanceCriteria` for the completion gate.
129
+ *
130
+ * Evidence inference:
131
+ * - If the AC is ticked in markdown AND there's a Build catalogue
132
+ * history entry naming this AC index, evidence comes from the
133
+ * history entry.
134
+ * - If the AC is ticked in markdown with no history entry, evidence
135
+ * defaults to "Ticked in source markdown."
136
+ * - If the AC is unticked, evidence is undefined and the completion
137
+ * gate will reject.
138
+ */
139
+ export function extractForCompletion(rawMarkdown) {
140
+ const parsed = parseAcceptanceCriteria(rawMarkdown);
141
+ const historyEvidence = parseHistoryEvidence(rawMarkdown);
142
+ return parsed.map((ac) => ({
143
+ text: ac.text,
144
+ met: ac.met,
145
+ evidence: ac.met
146
+ ? historyEvidence.get(ac.index) ?? 'Ticked in source markdown.'
147
+ : undefined,
148
+ }));
149
+ }
150
+ /**
151
+ * Parse the `## Build catalogue history` section for tick entries
152
+ * matching `Acceptance criterion <N> ticked` and pull the
153
+ * `Evidence: ...` line from the same entry. Returns a map from AC
154
+ * index (zero-based, matching `parseAcceptanceCriteria` output) to
155
+ * the evidence string.
156
+ *
157
+ * The history log uses 1-based AC numbering in its summary line
158
+ * ("Acceptance criterion 3 ticked: ..."), but we map to 0-based
159
+ * indexing here for consistency with the rest of the pipeline.
160
+ */
161
+ function parseHistoryEvidence(rawMarkdown) {
162
+ const result = new Map();
163
+ const historyHeadingIndex = rawMarkdown.indexOf('## Build catalogue history');
164
+ if (historyHeadingIndex === -1)
165
+ return result;
166
+ const sectionTail = rawMarkdown.slice(historyHeadingIndex);
167
+ // Bound the history section at the next ## heading (if any).
168
+ const nextSectionMatch = /\n## \S/.exec(sectionTail.slice('## Build catalogue history'.length));
169
+ const sectionEnd = nextSectionMatch
170
+ ? '## Build catalogue history'.length + nextSectionMatch.index
171
+ : sectionTail.length;
172
+ const section = sectionTail.slice(0, sectionEnd);
173
+ // Split into entries on the top-level `- **<timestamp>**` bullets.
174
+ // Each entry runs until the next top-level bullet or end-of-section.
175
+ const blocks = section.split(/\n(?=- \*\*)/);
176
+ for (const block of blocks) {
177
+ if (!/^- \*\*/.test(block))
178
+ continue;
179
+ const summaryMatch = /Acceptance criterion (?:at index )?(\d+)/i.exec(block);
180
+ if (!summaryMatch)
181
+ continue;
182
+ const evidenceMatch = /^\s*-\s*Evidence:\s*(.+?)$/m.exec(block);
183
+ let index = parseInt(summaryMatch[1], 10);
184
+ if (/at index/i.test(block)) {
185
+ // already 0-based
186
+ }
187
+ else {
188
+ index = index - 1;
189
+ }
190
+ if (index < 0)
191
+ continue;
192
+ const evidence = evidenceMatch ? evidenceMatch[1].trim() : 'Ticked via workflow.';
193
+ result.set(index, evidence);
194
+ }
195
+ return result;
196
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Minimal markdown editors for the Phase H part 2 write commands.
3
+ *
4
+ * The catalogue's existing files use two status formats:
5
+ * - bold: `**Status:** <text>`
6
+ * - pipe-table: `| Status | <text> |`
7
+ *
8
+ * Both are rewritten in place by `replaceStatusLine`. If neither is
9
+ * found, the helper inserts a `**Status:**` line near the file's H1.
10
+ *
11
+ * For AC ticking we deliberately do NOT parse the AC list (different
12
+ * files use different shapes — `- [ ] text`, numbered lists, prose
13
+ * bullets, sub-headings). Instead, `appendChangeLog` writes a
14
+ * structured footer entry naming the change. The maintainer can then
15
+ * hand-tighten the AC list itself if they want; the workflow record
16
+ * + audit chain are the canonical statements either way.
17
+ */
18
+ /**
19
+ * Replace the file's status line in place.
20
+ *
21
+ * Returns `{ updated: string, replaced: boolean }`. If `replaced` is
22
+ * false, the file did not have a status line in either supported
23
+ * format; the caller decides whether to insert one (`insertStatusLine`).
24
+ */
25
+ export declare function replaceStatusLine(rawMarkdown: string, newStatus: string): {
26
+ updated: string;
27
+ replaced: boolean;
28
+ };
29
+ /**
30
+ * Insert a `**Status:** <newStatus>` line immediately after the file's
31
+ * first H1 heading (with a blank line separator). If there is no H1,
32
+ * prepend the status line at the top.
33
+ */
34
+ export declare function insertStatusLine(rawMarkdown: string, newStatus: string): string;
35
+ export interface ChangeLogEntry {
36
+ isoTimestamp: string;
37
+ summary: string;
38
+ details?: string;
39
+ /** Optional source pointer — commit ref, evidence URL, etc. */
40
+ reference?: string;
41
+ }
42
+ /**
43
+ * Append a structured entry to a markdown file's `## Build catalogue
44
+ * history` section (creating it if missing). This is the audit-trail
45
+ * surface for write operations whose effect on the markdown is
46
+ * non-trivial to express by structural edit alone (e.g. AC ticks,
47
+ * status change rationales).
48
+ *
49
+ * Idempotence: each call appends; running the same workflow twice
50
+ * appends twice. The audit chain in the workflow store is the
51
+ * deduplicating source of truth.
52
+ */
53
+ export declare function appendChangeLog(rawMarkdown: string, entry: ChangeLogEntry): string;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Minimal markdown editors for the Phase H part 2 write commands.
3
+ *
4
+ * The catalogue's existing files use two status formats:
5
+ * - bold: `**Status:** <text>`
6
+ * - pipe-table: `| Status | <text> |`
7
+ *
8
+ * Both are rewritten in place by `replaceStatusLine`. If neither is
9
+ * found, the helper inserts a `**Status:**` line near the file's H1.
10
+ *
11
+ * For AC ticking we deliberately do NOT parse the AC list (different
12
+ * files use different shapes — `- [ ] text`, numbered lists, prose
13
+ * bullets, sub-headings). Instead, `appendChangeLog` writes a
14
+ * structured footer entry naming the change. The maintainer can then
15
+ * hand-tighten the AC list itself if they want; the workflow record
16
+ * + audit chain are the canonical statements either way.
17
+ */
18
+ const BOLD_STATUS_RE = /^(\*\*Status:\*\*\s*)(.+?)(\s*)$/m;
19
+ const TABLE_STATUS_RE = /^(\|\s*Status\s*\|\s*)(.+?)(\s*\|\s*)$/m;
20
+ /**
21
+ * Replace the file's status line in place.
22
+ *
23
+ * Returns `{ updated: string, replaced: boolean }`. If `replaced` is
24
+ * false, the file did not have a status line in either supported
25
+ * format; the caller decides whether to insert one (`insertStatusLine`).
26
+ */
27
+ export function replaceStatusLine(rawMarkdown, newStatus) {
28
+ if (BOLD_STATUS_RE.test(rawMarkdown)) {
29
+ return {
30
+ updated: rawMarkdown.replace(BOLD_STATUS_RE, `$1${newStatus}$3`),
31
+ replaced: true,
32
+ };
33
+ }
34
+ if (TABLE_STATUS_RE.test(rawMarkdown)) {
35
+ return {
36
+ updated: rawMarkdown.replace(TABLE_STATUS_RE, `$1${newStatus}$3`),
37
+ replaced: true,
38
+ };
39
+ }
40
+ return { updated: rawMarkdown, replaced: false };
41
+ }
42
+ /**
43
+ * Insert a `**Status:** <newStatus>` line immediately after the file's
44
+ * first H1 heading (with a blank line separator). If there is no H1,
45
+ * prepend the status line at the top.
46
+ */
47
+ export function insertStatusLine(rawMarkdown, newStatus) {
48
+ const h1Match = /^#\s+.+$/m.exec(rawMarkdown);
49
+ if (!h1Match) {
50
+ return `**Status:** ${newStatus}\n\n${rawMarkdown}`;
51
+ }
52
+ const insertAt = h1Match.index + h1Match[0].length;
53
+ const before = rawMarkdown.slice(0, insertAt);
54
+ const after = rawMarkdown.slice(insertAt);
55
+ return `${before}\n\n**Status:** ${newStatus}${after}`;
56
+ }
57
+ /**
58
+ * Append a structured entry to a markdown file's `## Build catalogue
59
+ * history` section (creating it if missing). This is the audit-trail
60
+ * surface for write operations whose effect on the markdown is
61
+ * non-trivial to express by structural edit alone (e.g. AC ticks,
62
+ * status change rationales).
63
+ *
64
+ * Idempotence: each call appends; running the same workflow twice
65
+ * appends twice. The audit chain in the workflow store is the
66
+ * deduplicating source of truth.
67
+ */
68
+ export function appendChangeLog(rawMarkdown, entry) {
69
+ const heading = '## Build catalogue history';
70
+ const headingIndex = rawMarkdown.indexOf(heading);
71
+ const detailLines = [];
72
+ if (entry.details)
73
+ detailLines.push(` - ${entry.details}`);
74
+ if (entry.reference)
75
+ detailLines.push(` - Reference: ${entry.reference}`);
76
+ const block = [
77
+ `- **${entry.isoTimestamp}** — ${entry.summary}`,
78
+ ...detailLines,
79
+ ].join('\n');
80
+ if (headingIndex === -1) {
81
+ const sep = rawMarkdown.endsWith('\n') ? '' : '\n';
82
+ return `${rawMarkdown}${sep}\n${heading}\n\n${block}\n`;
83
+ }
84
+ // Find the end of the file (or the next section) and insert before that.
85
+ // Simplest: append the block immediately after the existing section's
86
+ // current content. We treat everything from `headingIndex` to the
87
+ // next H1/H2 boundary (or EOF) as the section.
88
+ const tail = rawMarkdown.slice(headingIndex);
89
+ const nextHeadingMatch = /\n##? \S/.exec(tail.slice(heading.length));
90
+ if (!nextHeadingMatch) {
91
+ // History is the last section — append at end of file.
92
+ const sep = rawMarkdown.endsWith('\n') ? '' : '\n';
93
+ return `${rawMarkdown}${sep}${block}\n`;
94
+ }
95
+ const splitAt = headingIndex + heading.length + nextHeadingMatch.index;
96
+ const before = rawMarkdown.slice(0, splitAt);
97
+ const after = rawMarkdown.slice(splitAt);
98
+ const beforeHasTrailingNewline = before.endsWith('\n');
99
+ const sep = beforeHasTrailingNewline ? '' : '\n';
100
+ return `${before}${sep}${block}\n${after}`;
101
+ }
@@ -0,0 +1,27 @@
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
+ import type { WorkUnitCreatePayload, DecisionCreatePayload, OpenQuestionCreatePayload, PersonaCreatePayload } from '@nusoft/nuflow-pack-nuos-build-catalogue';
24
+ export declare function renderWorkUnit(payload: WorkUnitCreatePayload): string;
25
+ export declare function renderDecision(payload: DecisionCreatePayload): string;
26
+ export declare function renderOpenQuestion(payload: OpenQuestionCreatePayload): string;
27
+ export declare function renderPersona(payload: PersonaCreatePayload): string;