@nusoft/nuos-build-catalogue 0.10.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 +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;
|