@polymorphism-tech/morph-spec 8.35.0 → 8.36.0
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/CLAUDE.md +11 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +2 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +24 -23
- package/framework/docs/CLI.md +187 -13
- package/framework/evals/proof-rubrics/eval-de-modelo.md +16 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/evals/rubrics/test-coverage.md +8 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +43 -0
- package/framework/hooks/shared/payload-utils.js +15 -2
- package/framework/hooks/shared/phase-utils.js +1 -0
- package/framework/phases.json +1 -1
- package/framework/rules/morph-workflow.md +1 -1
- package/framework/schemas/config.schema.json +7 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +21 -10
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +20 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +41 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +11 -2
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +4 -3
- package/framework/standards/ai-agents/testing-ai.md +14 -13
- package/framework/templates/dotnet/ai-kit/README.md +14 -8
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +11 -3
- package/package.json +1 -1
- package/src/commands/project/doctor.js +59 -3
- package/src/commands/project/worktree.js +68 -3
- package/src/commands/state/finish.js +48 -5
- package/src/commands/tasks/dag.js +13 -1
- package/src/commands/validation/verify.js +47 -18
- package/src/core/paths/output-schema.js +7 -0
- package/src/lib/detectors/index.js +225 -217
- package/src/lib/git/pr-files.js +56 -0
- package/src/lib/risk-detector.js +38 -18
- package/src/lib/specs/done-criteria-check.js +200 -0
- package/src/lib/specs/usecase-check.js +129 -5
- package/src/lib/tasks/dotnet-test-introspection.js +517 -0
- package/src/lib/tasks/test-nodes.js +190 -20
- package/src/lib/tasks/test-runner.js +122 -2
- package/src/lib/tasks/test-scope.js +567 -0
- package/src/lib/validators/traceability-check.js +9 -2
- package/src/lib/validators/verify-runner.js +150 -18
- package/src/lib/worktree/docker-audit.js +5 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical heuristic: does a task's `doneCriteria` describe an ARTIFACT (a
|
|
3
|
+
* file, a grep, a binary) instead of behaviour observable at runtime?
|
|
4
|
+
*
|
|
5
|
+
* A literal `doneCriteria` travels verbatim into the test that verifies it
|
|
6
|
+
* (`morph-implement` copies it, never paraphrases). A use-case post-condition
|
|
7
|
+
* shaped like "nenhum arquivo em X contém Y" — even a literal `grep` command —
|
|
8
|
+
* becomes a test that reads file text instead of exercising behaviour. This
|
|
9
|
+
* is a WARNING, never an error: `dag`'s exit code never changes because of
|
|
10
|
+
* it, the same way `checkUseCases`'s coverage/literalness findings do not.
|
|
11
|
+
*
|
|
12
|
+
* Runs over EVERY task, unlike `usecase-check.js#checkUseCases` (which
|
|
13
|
+
* short-circuits when the feature has no use cases): a task's own
|
|
14
|
+
* `doneCriteria` can describe an artifact with no use case in sight, and
|
|
15
|
+
* that must still be caught. This is why it lives in its own file — it is a
|
|
16
|
+
* standalone lexical pass, not wiring between use cases and tasks.
|
|
17
|
+
*
|
|
18
|
+
* Two ways a match is silenced:
|
|
19
|
+
* - a task WITHOUT a use case can justify itself inline, with
|
|
20
|
+
* `Por que artefato: …` anywhere in its own `doneCriteria`. A task WITH a
|
|
21
|
+
* use case never can — it justifies in the use case, otherwise the marker
|
|
22
|
+
* would silence every post-condition it carries at once;
|
|
23
|
+
* - a task carrying a use-case post-condition literally is silenced only
|
|
24
|
+
* if THAT post-condition was justified in the use case document (the
|
|
25
|
+
* `> [!note] Por que artefato:` callout `usecase-check.js` parses).
|
|
26
|
+
* Justified text is removed from the task's `doneCriteria` before scanning —
|
|
27
|
+
* not skipped as a whole task — so a justified P1 never silences an
|
|
28
|
+
* unjustified P2 carried in the same `doneCriteria`. The scan runs on the
|
|
29
|
+
* whitespace-collapsed text with its CASE PRESERVED: an uppercase format
|
|
30
|
+
* acronym (`arquivo em CSV`) is the only thing that tells an export from a
|
|
31
|
+
* directory (`arquivo em src`), and lowercasing first would erase it.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { ARTIFACT_MARKER_RE, loadUseCases, normalize } from './usecase-check.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Case-insensitive spelling of `word` inside a regex that must stay
|
|
38
|
+
* case-SENSITIVE elsewhere — Node 18 has no inline `(?i:…)` modifier.
|
|
39
|
+
*/
|
|
40
|
+
const anyCase = (word) => [...word].map((c) => {
|
|
41
|
+
const lo = c.toLowerCase();
|
|
42
|
+
const up = c.toUpperCase();
|
|
43
|
+
return lo === up ? c : `[${lo}${up}]`;
|
|
44
|
+
}).join('');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What follows "arquivo em" in a behavioural export criterion ("exporta o
|
|
48
|
+
* arquivo em CSV", "salva o arquivo em formato aberto"): an uppercase file
|
|
49
|
+
* format acronym or the word `formato`. Case-sensitive on purpose — `src`,
|
|
50
|
+
* `docs` or `lib` after "arquivo em" are places, and places are artifacts.
|
|
51
|
+
*/
|
|
52
|
+
const FORMAT_TOKEN = String.raw`\s+(?:[A-Z]{2,5}(?![\p{L}\d_])|${anyCase('formato')}(?![\p{L}]))`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Vocabulary of artifact-shaped phrasing. `term` is the label surfaced in
|
|
56
|
+
* warnings and cited by the `morph-plan` skill prose — keep it stable, a
|
|
57
|
+
* later parity test imports it verbatim.
|
|
58
|
+
*
|
|
59
|
+
* Unicode lookarounds, never `\b`: in JS, `\b` does not treat accented
|
|
60
|
+
* letters (é, á, ó…) as word characters, so it would false-boundary inside
|
|
61
|
+
* "não" or "binário".
|
|
62
|
+
*/
|
|
63
|
+
export const ARTIFACT_VOCABULARY = [
|
|
64
|
+
{ term: 'grep', re: /(?<![\p{L}\d_])grep(?![\p{L}\d_])/iu },
|
|
65
|
+
{ term: 'contém a string', re: /cont[ée]m\s+as?\s+strings?/iu },
|
|
66
|
+
{ term: 'o binário', re: /(?<![\p{L}])o\s+bin[áa]rio(?![\p{L}])/iu },
|
|
67
|
+
{ term: 'nenhum arquivo', re: /(?<![\p{L}])nenhum\s+arquivo(?![\p{L}])/iu },
|
|
68
|
+
{
|
|
69
|
+
term: 'arquivo/pasta/diretório em',
|
|
70
|
+
re: new RegExp(
|
|
71
|
+
String.raw`(?<![\p{L}])(?:${anyCase('arquivo')}|${anyCase('pasta')}|${anyCase('diret')}[óoÓO]${anyCase('rio')})` +
|
|
72
|
+
String.raw`[sS]?\s+${anyCase('em')}(?![\p{L}])(?!${FORMAT_TOKEN})`,
|
|
73
|
+
'u',
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{ term: 'glob **/', re: /\*\*\/|\/\*\*/ },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Vocabulary terms matched in `text`, in vocabulary order, each at most once.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} text
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
export function findArtifactTerms(text) {
|
|
86
|
+
const s = String(text ?? '');
|
|
87
|
+
const found = [];
|
|
88
|
+
for (const { term, re } of ARTIFACT_VOCABULARY) {
|
|
89
|
+
if (re.test(s)) found.push(term);
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whitespace-collapsed, case PRESERVED — `normalize` without the lowercasing. */
|
|
95
|
+
function collapse(text) {
|
|
96
|
+
return String(text).replace(/\s+/g, ' ').trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove every case-insensitive occurrence of `needle` (already normalized)
|
|
101
|
+
* from `text`, keeping the case of what remains. Relies on `text` and its
|
|
102
|
+
* lowercase form being index-aligned — the caller guarantees it.
|
|
103
|
+
*/
|
|
104
|
+
function cut(text, needle) {
|
|
105
|
+
if (!needle) return text;
|
|
106
|
+
const lower = text.toLowerCase();
|
|
107
|
+
let out = '';
|
|
108
|
+
let from = 0;
|
|
109
|
+
let at;
|
|
110
|
+
while ((at = lower.indexOf(needle, from)) !== -1) {
|
|
111
|
+
out += `${text.slice(from, at)} `;
|
|
112
|
+
from = at + needle.length;
|
|
113
|
+
}
|
|
114
|
+
return out + text.slice(from);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Flag every task whose `doneCriteria` describes an artifact rather than
|
|
119
|
+
* behaviour, after removing use-case text the author already justified.
|
|
120
|
+
*
|
|
121
|
+
* Fail-open: a missing `usecasesDir` still checks every task (it just has no
|
|
122
|
+
* justifications or post-conditions to consult); a malformed `tasksData`
|
|
123
|
+
* (no `tasks` array) or a non-string `doneCriteria` never throws.
|
|
124
|
+
*
|
|
125
|
+
* @param {Object} tasksData - parsed tasks.json
|
|
126
|
+
* @param {string} usecasesDir - absolute path to `2-plan/usecases`
|
|
127
|
+
* @returns {{ warnings: string[], flagged: Array<{ task: string, usecase: string|null, labels: string[], terms: string[] }>, checked: true }}
|
|
128
|
+
*/
|
|
129
|
+
export function checkArtifactCriteria(tasksData, usecasesDir) {
|
|
130
|
+
const warnings = [];
|
|
131
|
+
const flagged = [];
|
|
132
|
+
const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
|
|
133
|
+
|
|
134
|
+
let byId = new Map();
|
|
135
|
+
if (typeof usecasesDir === 'string') {
|
|
136
|
+
try { byId = loadUseCases(usecasesDir); } catch { byId = new Map(); }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const task of tasks) {
|
|
140
|
+
if (typeof task?.doneCriteria !== 'string') continue;
|
|
141
|
+
|
|
142
|
+
let D = collapse(task.doneCriteria);
|
|
143
|
+
if (!D) continue;
|
|
144
|
+
// `cut` needs `D` index-aligned with its lowercase form. The rare letter
|
|
145
|
+
// whose lowercase is longer (`İ`) breaks that; fall back to the lowercase
|
|
146
|
+
// text, which only costs the format-acronym exception for that task.
|
|
147
|
+
if (D.toLowerCase().length !== D.length) D = D.toLowerCase();
|
|
148
|
+
|
|
149
|
+
// Inline justification is for tasks WITHOUT a use case (morph-plan: "Task
|
|
150
|
+
// sem `usecase` justifica no próprio doneCriteria"). On a use-case task it
|
|
151
|
+
// would silence every post-condition carried, justified or not.
|
|
152
|
+
if (!task.usecase && ARTIFACT_MARKER_RE.test(D)) continue;
|
|
153
|
+
|
|
154
|
+
const uc = task.usecase ? byId.get(task.usecase) : null;
|
|
155
|
+
const unjustifiedLabels = [];
|
|
156
|
+
|
|
157
|
+
if (uc) {
|
|
158
|
+
// Remove justified post-condition text first, so its own vocabulary
|
|
159
|
+
// hits (if any) never surface — and so it cannot mask what remains.
|
|
160
|
+
// A wrapped post-condition is removed in both forms a plan legitimately
|
|
161
|
+
// carries: the whole block, or only its first line (what `checkUseCases`
|
|
162
|
+
// treats as THE post-condition). Longest first.
|
|
163
|
+
for (const pc of uc.postConditions) {
|
|
164
|
+
if (!uc.artifactJustified.has(pc.label)) continue;
|
|
165
|
+
for (const text of [uc.artifactJustified.get(pc.label), pc.text]) {
|
|
166
|
+
D = cut(D, normalize(text));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Among what is left, name the unjustified post-conditions actually
|
|
170
|
+
// carried into this doneCriteria AND that themselves read as artifact —
|
|
171
|
+
// judged on the whole block, since a wrapped post-condition can carry
|
|
172
|
+
// its vocabulary on the continuation line.
|
|
173
|
+
const carried = D.toLowerCase();
|
|
174
|
+
for (const pc of uc.postConditions) {
|
|
175
|
+
if (uc.artifactJustified.has(pc.label)) continue;
|
|
176
|
+
const needle = normalize(pc.text);
|
|
177
|
+
const block = uc.fullText.get(pc.label) ?? pc.text;
|
|
178
|
+
if (needle && carried.includes(needle) && findArtifactTerms(block).length > 0) {
|
|
179
|
+
unjustifiedLabels.push(pc.label);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const terms = findArtifactTerms(D);
|
|
185
|
+
if (terms.length === 0) continue;
|
|
186
|
+
|
|
187
|
+
const quoted = terms.join(', ');
|
|
188
|
+
const message = unjustifiedLabels.length > 0
|
|
189
|
+
? `${task.id}.doneCriteria descreve artefato, não comportamento ("${quoted}") — ` +
|
|
190
|
+
`reescreva ${task.usecase} ${unjustifiedLabels.join('/')} como efeito observável em runtime, ` +
|
|
191
|
+
`ou justifique com "> [!note] Por que artefato:" na linha abaixo dela`
|
|
192
|
+
: `${task.id}.doneCriteria descreve artefato, não comportamento ("${quoted}") — ` +
|
|
193
|
+
`reescreva como efeito observável em runtime, ou acrescente "Por que artefato: …" ao próprio critério`;
|
|
194
|
+
|
|
195
|
+
warnings.push(message);
|
|
196
|
+
flagged.push({ task: task.id, usecase: task.usecase || null, labels: unjustifiedLabels, terms });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { warnings, flagged, checked: true };
|
|
200
|
+
}
|
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the mechanical half of the use-case layer. Whether a post-condition is
|
|
5
5
|
* *observable in a test* is semantic and stays with Gate 2 and the evaluator —
|
|
6
|
-
* no checker can decide it.
|
|
7
|
-
*
|
|
6
|
+
* no checker can decide it. A narrower question IS decidable, though: a
|
|
7
|
+
* post-condition phrased as "nenhum arquivo/grep/binário…" describes an
|
|
8
|
+
* ARTIFACT, not behaviour, and that is a lexical warning `done-criteria-check.js`
|
|
9
|
+
* raises against `dag`. A `> [!note] Por que artefato:` callout right below the
|
|
10
|
+
* post-condition is how a use case justifies keeping it literal — this module
|
|
11
|
+
* parses that callout (`parseArtifactJustifications`) so the lexical checker
|
|
12
|
+
* can skip the labels an author already defended. What IS decidable beyond
|
|
13
|
+
* that is the plumbing, and the plumbing is where the requirement actually
|
|
14
|
+
* leaked before:
|
|
8
15
|
*
|
|
9
16
|
* - a task pointing at a use case that does not exist → ERROR (a broken plan)
|
|
10
17
|
* - a post-condition no task ever mentions → warning (orphan)
|
|
@@ -37,11 +44,117 @@ import { useCaseId } from './usecase-promoter.js';
|
|
|
37
44
|
*/
|
|
38
45
|
const POSTCONDITION_RE = /^[ \t]*[-*][ \t]*\*\*(P\d+)\*\*[ \t]*[—–-][ \t]*(.+?)[ \t]*$/gm;
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Single-line form of `POSTCONDITION_RE`, used to walk a document line by line.
|
|
49
|
+
* DERIVED, never retyped: two hand copies drift, and a bullet shape only one
|
|
50
|
+
* of them accepts makes its justification stop working in silence.
|
|
51
|
+
*/
|
|
52
|
+
const POSTCONDITION_LINE_RE = new RegExp(POSTCONDITION_RE.source);
|
|
53
|
+
|
|
54
|
+
/** Any bullet (not just a labelled post-condition) — closes an open block. */
|
|
55
|
+
const BULLET_LINE_RE = /^\s*[-*]\s/;
|
|
56
|
+
|
|
57
|
+
/** A markdown heading — closes an open block. */
|
|
58
|
+
const HEADING_LINE_RE = /^\s*#/;
|
|
59
|
+
|
|
60
|
+
/** A blockquote line — it belongs to a callout, never to the post-condition text. */
|
|
61
|
+
const BLOCKQUOTE_LINE_RE = /^\s*>/;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The justification marker, ONE spelling for both forms: the callout below a
|
|
65
|
+
* use-case post-condition (here) and the inline form in a task's own
|
|
66
|
+
* `doneCriteria` (`done-criteria-check.js`). Tolerant of spacing — including
|
|
67
|
+
* before the colon — because the spacing is a typing accident, not a decision.
|
|
68
|
+
*/
|
|
69
|
+
export const ARTIFACT_MARKER_RE = /por\s+que\s+artefato\s*:/i;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The artifact-justification callout. It must live on its OWN line right
|
|
73
|
+
* below the post-condition — never inline, because `POSTCONDITION_RE`
|
|
74
|
+
* captures to end of line and that text travels literally into `doneCriteria`.
|
|
75
|
+
* The foldable forms (`[!note]-`, `[!note]+`) are the same callout.
|
|
76
|
+
*/
|
|
77
|
+
const ARTIFACT_NOTE_RE = new RegExp(String.raw`^\s*>\s*\[!note\][-+]?\s*` + ARTIFACT_MARKER_RE.source, 'i');
|
|
78
|
+
|
|
40
79
|
/** Case-folded, whitespace-collapsed form used for containment tests. */
|
|
41
|
-
function normalize(text) {
|
|
80
|
+
export function normalize(text) {
|
|
42
81
|
return String(text).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
43
82
|
}
|
|
44
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Parse every post-condition of a use-case document as a BLOCK: its full text
|
|
86
|
+
* (first line + wrapped continuation) and whether a `> [!note] Por que
|
|
87
|
+
* artefato:` callout justifies it.
|
|
88
|
+
*
|
|
89
|
+
* A post-condition bullet opens a block for its label; continuation lines
|
|
90
|
+
* (wrapped post-condition text) are appended to the block's text; the block
|
|
91
|
+
* closes at a blank line, a new bullet, or a heading — same shape as a
|
|
92
|
+
* post-condition itself, so a callout separated by a blank line or living
|
|
93
|
+
* under the NEXT bullet never counts.
|
|
94
|
+
*
|
|
95
|
+
* Blockquote lines are never post-condition text: they belong to the callout,
|
|
96
|
+
* and a justification wrapped over two `>` lines is ordinary Obsidian style.
|
|
97
|
+
* Once the blockquote opens, a following line without `>` is the quote's lazy
|
|
98
|
+
* continuation (CommonMark), so it stays out of the text too. Appending either
|
|
99
|
+
* to the post-condition produced a text no `doneCriteria` ever carries.
|
|
100
|
+
*
|
|
101
|
+
* A repeated label counts once — first wins, as in `parsePostConditions`.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} content
|
|
104
|
+
* @returns {Map<string, { text: string, justified: boolean }>}
|
|
105
|
+
*/
|
|
106
|
+
export function parsePostConditionBlocks(content) {
|
|
107
|
+
const blocks = new Map();
|
|
108
|
+
let current = null; // { label, textParts: string[], justified: boolean, quoted: boolean }
|
|
109
|
+
|
|
110
|
+
const closeBlock = () => {
|
|
111
|
+
if (current && !blocks.has(current.label)) {
|
|
112
|
+
blocks.set(current.label, { text: current.textParts.join(' ').trim(), justified: current.justified });
|
|
113
|
+
}
|
|
114
|
+
current = null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
for (const line of String(content).split(/\r?\n/)) {
|
|
118
|
+
const bulletMatch = line.match(POSTCONDITION_LINE_RE);
|
|
119
|
+
if (bulletMatch) {
|
|
120
|
+
closeBlock();
|
|
121
|
+
current = { label: bulletMatch[1], textParts: [bulletMatch[2].trim()], justified: false, quoted: false };
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!current) continue; // prose outside any post-condition block
|
|
125
|
+
|
|
126
|
+
if (line.trim() === '' || HEADING_LINE_RE.test(line) || BULLET_LINE_RE.test(line)) {
|
|
127
|
+
closeBlock();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (BLOCKQUOTE_LINE_RE.test(line)) {
|
|
131
|
+
if (ARTIFACT_NOTE_RE.test(line)) current.justified = true;
|
|
132
|
+
current.quoted = true;
|
|
133
|
+
continue; // callout lines are metadata, never part of the post-condition text
|
|
134
|
+
}
|
|
135
|
+
if (current.quoted) continue; // lazy continuation of the blockquote
|
|
136
|
+
current.textParts.push(line.trim());
|
|
137
|
+
}
|
|
138
|
+
closeBlock();
|
|
139
|
+
|
|
140
|
+
return blocks;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The justified post-conditions of a use-case document — the labels whose
|
|
145
|
+
* block (see `parsePostConditionBlocks`) carried the callout.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} content
|
|
148
|
+
* @returns {Map<string, string>} label → full post-condition text (first line + continuation)
|
|
149
|
+
*/
|
|
150
|
+
export function parseArtifactJustifications(content) {
|
|
151
|
+
const result = new Map();
|
|
152
|
+
for (const [label, block] of parsePostConditionBlocks(content)) {
|
|
153
|
+
if (block.justified) result.set(label, block.text);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
45
158
|
/**
|
|
46
159
|
* Parse one use-case document into its post-conditions.
|
|
47
160
|
*
|
|
@@ -63,8 +176,12 @@ export function parsePostConditions(content) {
|
|
|
63
176
|
/**
|
|
64
177
|
* Read every use case of a feature, keyed by ID.
|
|
65
178
|
*
|
|
179
|
+
* `fullText` maps EVERY label to its whole block (first line + wrapped
|
|
180
|
+
* continuation); `postConditions[].text` stays the first line, which is what
|
|
181
|
+
* the literal-carry check compares against.
|
|
182
|
+
*
|
|
66
183
|
* @param {string} usecasesDir - absolute path to `2-plan/usecases`
|
|
67
|
-
* @returns {Map<string, { id: string, file: string, postConditions: Array<{label: string, text: string}> }>}
|
|
184
|
+
* @returns {Map<string, { id: string, file: string, postConditions: Array<{label: string, text: string}>, artifactJustified: Map<string, string>, fullText: Map<string, string> }>}
|
|
68
185
|
*/
|
|
69
186
|
export function loadUseCases(usecasesDir) {
|
|
70
187
|
const byId = new Map();
|
|
@@ -82,7 +199,14 @@ export function loadUseCases(usecasesDir) {
|
|
|
82
199
|
if (!id || !file.endsWith('.md')) continue;
|
|
83
200
|
try {
|
|
84
201
|
const content = readFileSync(join(usecasesDir, file), 'utf8');
|
|
85
|
-
|
|
202
|
+
const blocks = parsePostConditionBlocks(content);
|
|
203
|
+
const artifactJustified = new Map();
|
|
204
|
+
const fullText = new Map();
|
|
205
|
+
for (const [label, block] of blocks) {
|
|
206
|
+
fullText.set(label, block.text);
|
|
207
|
+
if (block.justified) artifactJustified.set(label, block.text);
|
|
208
|
+
}
|
|
209
|
+
byId.set(id, { id, file, postConditions: parsePostConditions(content), artifactJustified, fullText });
|
|
86
210
|
} catch { /* one unreadable use case must not sink the rest */ }
|
|
87
211
|
}
|
|
88
212
|
return byId;
|