@rasensio/aidlc 1.18.0 → 1.20.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/dist/change/cross-check.d.ts +28 -0
- package/dist/change/cross-check.d.ts.map +1 -0
- package/dist/change/cross-check.js +53 -0
- package/dist/change/cross-check.js.map +1 -0
- package/dist/change/impact.d.ts +57 -0
- package/dist/change/impact.d.ts.map +1 -0
- package/dist/change/impact.js +288 -0
- package/dist/change/impact.js.map +1 -0
- package/dist/change/index.d.ts +12 -0
- package/dist/change/index.d.ts.map +1 -0
- package/dist/change/index.js +11 -0
- package/dist/change/index.js.map +1 -0
- package/dist/change/parser.d.ts +52 -0
- package/dist/change/parser.d.ts.map +1 -0
- package/dist/change/parser.js +339 -0
- package/dist/change/parser.js.map +1 -0
- package/dist/change/reopen.d.ts +80 -0
- package/dist/change/reopen.d.ts.map +1 -0
- package/dist/change/reopen.js +227 -0
- package/dist/change/reopen.js.map +1 -0
- package/dist/change/types.d.ts +144 -0
- package/dist/change/types.d.ts.map +1 -0
- package/dist/change/types.js +8 -0
- package/dist/change/types.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/amend.d.ts +22 -0
- package/dist/commands/amend.d.ts.map +1 -0
- package/dist/commands/amend.js +355 -0
- package/dist/commands/amend.js.map +1 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +6 -0
- package/dist/commands/docs.js.map +1 -1
- package/dist/core/lifecycle.d.ts +3 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +13 -2
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/types.d.ts +37 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/menu/roster.d.ts.map +1 -1
- package/dist/menu/roster.js +4 -0
- package/dist/menu/roster.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amendment parser: closed grammar over the `## Amendment <n>` sections of
|
|
3
|
+
* requirements.md (FR1).
|
|
4
|
+
*
|
|
5
|
+
* The grammar was derived from the corpus, not invented. Measured over 39
|
|
6
|
+
* `requirements.md` and 29 `tasks.md` on 2026-09-07, two conventions exist:
|
|
7
|
+
*
|
|
8
|
+
* ## Amendment 1 — 2026-09-05, after the design review (5 real, accepted)
|
|
9
|
+
* ### Amendments from the design review (2026-09-05) (4 real, warned)
|
|
10
|
+
*
|
|
11
|
+
* The second is unnumbered and undated and cannot be applied, so it warns
|
|
12
|
+
* rather than vanishing — a near-miss that reads as absence is this project's
|
|
13
|
+
* single most repeated defect class (AC-53).
|
|
14
|
+
*
|
|
15
|
+
* Two rules exist because a prototype got them wrong on real input:
|
|
16
|
+
*
|
|
17
|
+
* - **Three emphasis variants, copied from `ACTIVE_RE`** (`traceability/parser.ts:23-24`).
|
|
18
|
+
* The first draft accepted `Retires:` and `**Retires**:` but not
|
|
19
|
+
* `**Retires:**` — which is the form this instance's own Amendment 1 was
|
|
20
|
+
* written with, so its 23 retirements parsed as zero (AC-78).
|
|
21
|
+
* - **A wrapped bullet is one bullet.** Ending the field block at "the first
|
|
22
|
+
* non-bullet non-blank line" truncates any field that wraps: measured, a
|
|
23
|
+
* wrapped `Retires:` yielded 12 of 23 ids and dropped `Issues`, `Reason`
|
|
24
|
+
* and `Reopens` outright, and the impact report would then have said "no
|
|
25
|
+
* retired ids" for an amendment that retired 23 criteria (AC-78, AC-94).
|
|
26
|
+
*
|
|
27
|
+
* Requirements: change-management/AC-3, AC-5, AC-7, AC-8, AC-51, AC-53,
|
|
28
|
+
* AC-54, AC-78, AC-79
|
|
29
|
+
*/
|
|
30
|
+
// Heading: `##` or `###`, `Amendment <n>`, em dash or hyphen, ISO date,
|
|
31
|
+
// optional `, <context>` tail (AC-51).
|
|
32
|
+
//
|
|
33
|
+
// Up to three spaces of indentation are allowed, which is CommonMark's rule for
|
|
34
|
+
// an ATX heading. Anchoring at column 0 instead would silently drop an indented
|
|
35
|
+
// heading — and a near-miss that reads as absence is this project's most
|
|
36
|
+
// repeated defect class, so the permissive form is the safe one here.
|
|
37
|
+
const HEADING_RE = /^ {0,3}(#{2,3})\s+Amendment\s+([1-9][0-9]*)\s+[—-]\s+(\d{4}-\d{2}-\d{2})\s*(?:,\s*(\S.*?))?\s*$/;
|
|
38
|
+
// Any heading whose text begins with Amendment/Amendments — the near-miss
|
|
39
|
+
// lint (AC-53). Deliberately wider than HEADING_RE at every axis: any level,
|
|
40
|
+
// singular or plural, no date required, same indent allowance.
|
|
41
|
+
const AMENDMENT_SHAPED_RE = /^ {0,3}(#{1,6})\s+Amendments?\b/i;
|
|
42
|
+
/** Any ATX heading — bounds a section (AC-51). */
|
|
43
|
+
const ANY_HEADING_RE = /^ {0,3}(#{1,6})\s/;
|
|
44
|
+
/**
|
|
45
|
+
* A dash bullet and its indentation.
|
|
46
|
+
*
|
|
47
|
+
* Field bullets sit at the section's own indentation; a line indented MORE than
|
|
48
|
+
* the bullet it follows is a continuation of it, not a new item. That
|
|
49
|
+
* distinction is the whole of AC-78 — the rule it replaced ended the field block
|
|
50
|
+
* at the first non-bullet line, so a wrapped `Retires:` list lost half its ids
|
|
51
|
+
* and three whole fields without a diagnostic.
|
|
52
|
+
*/
|
|
53
|
+
const BULLET_RE = /^(\s*)-\s+(.*)$/;
|
|
54
|
+
const FIELD_NAMES = 'Retires|Issues|Reopens|Reason';
|
|
55
|
+
// The same three emphasis variants `ACTIVE_RE` accepts, for the same reason:
|
|
56
|
+
// authors write all three and two of them are invisible to a narrower rule.
|
|
57
|
+
const FIELD_RE = new RegExp(String.raw `^(?:` +
|
|
58
|
+
String.raw `(${FIELD_NAMES}):|` +
|
|
59
|
+
String.raw `\*\*(${FIELD_NAMES})\*\*:|` +
|
|
60
|
+
String.raw `\*\*(${FIELD_NAMES}):\*\*` +
|
|
61
|
+
String.raw `)\s*(.*)$`);
|
|
62
|
+
// A co-located AC definition bullet is not an unrecognised field — it is a
|
|
63
|
+
// legitimate definition living inside the amendment section, which is what
|
|
64
|
+
// `agent-portability` AM-2 and AM-3 do. Skipped silently rather than warned.
|
|
65
|
+
const AC_DEF_BULLET_RE = /^(?:AC-[1-9][0-9]*|\*\*AC-[1-9][0-9]*)/;
|
|
66
|
+
// A bold-prefixed bullet that is not a known field: kept and warned, mirroring
|
|
67
|
+
// the unrecognized-token handling in `tasks/parser.ts:124-126`.
|
|
68
|
+
const BOLD_PREFIX_RE = /^\*\*([^*]+?):?\*\*:?\s/;
|
|
69
|
+
/** A whole `AC-<n>` token and nothing else (AC-3). */
|
|
70
|
+
const AC_TOKEN_RE = /^AC-([1-9][0-9]*)$/;
|
|
71
|
+
/** A phase name as written — shape only; validation belongs to reopen.ts. */
|
|
72
|
+
const PHASE_TOKEN_RE = /^\S+$/;
|
|
73
|
+
function excerpt(text, max = 60) {
|
|
74
|
+
const trimmed = text.trim();
|
|
75
|
+
return trimmed.length > max ? `${trimmed.slice(0, max - 3)}...` : trimmed;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Walk lines outside fenced code blocks (AC-79).
|
|
79
|
+
*
|
|
80
|
+
* CommonMark rules: a fence opens with 3+ backticks or tildes and closes only
|
|
81
|
+
* on the same character at the same length or longer. Written correctly rather
|
|
82
|
+
* than as a naive toggle because it is cheap to do so — the corpus currently
|
|
83
|
+
* cannot tell the two apart (1047 AC-shaped lines, zero inside fences), which
|
|
84
|
+
* is a reason to write the correct one, not evidence that it does not matter.
|
|
85
|
+
*
|
|
86
|
+
* Indentation is deliberately permissive (`\s*`, not ` {0,3}`): AIDLC markdown
|
|
87
|
+
* nests fenced examples inside list items routinely — see the `ac-coverage`
|
|
88
|
+
* example at `packages/content/skills/50-testing.md:43-45`, indented five
|
|
89
|
+
* spaces — and an absolute three-space rule is blind to every one of them.
|
|
90
|
+
*/
|
|
91
|
+
export function scanOutsideFences(lines, visit) {
|
|
92
|
+
let fence = null;
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const opener = /^\s*(`{3,}|~{3,})/.exec(lines[i]);
|
|
95
|
+
if (opener) {
|
|
96
|
+
const marker = opener[1];
|
|
97
|
+
if (fence === null) {
|
|
98
|
+
fence = marker;
|
|
99
|
+
}
|
|
100
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) {
|
|
101
|
+
fence = null;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (fence !== null)
|
|
106
|
+
continue;
|
|
107
|
+
visit(lines[i], i);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Split a `Retires:`/`Issues:` value into ids, reporting non-tokens (AC-3). */
|
|
111
|
+
function parseAcList(value) {
|
|
112
|
+
const ids = [];
|
|
113
|
+
const rejected = [];
|
|
114
|
+
for (const raw of value.split(',')) {
|
|
115
|
+
const token = raw.trim();
|
|
116
|
+
if (token === '')
|
|
117
|
+
continue;
|
|
118
|
+
const match = AC_TOKEN_RE.exec(token);
|
|
119
|
+
if (match)
|
|
120
|
+
ids.push(Number(match[1]));
|
|
121
|
+
else
|
|
122
|
+
rejected.push(token);
|
|
123
|
+
}
|
|
124
|
+
return { ids, rejected };
|
|
125
|
+
}
|
|
126
|
+
/** Split a `Reopens:` value into phase-name tokens as written. */
|
|
127
|
+
function parsePhaseList(value) {
|
|
128
|
+
return value
|
|
129
|
+
.split(',')
|
|
130
|
+
.map((part) => part.trim())
|
|
131
|
+
.filter((part) => part !== '' && PHASE_TOKEN_RE.test(part));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Collect an amendment section's field bullets.
|
|
135
|
+
*
|
|
136
|
+
* The block starts at the section's first `-` bullet, and that bullet's
|
|
137
|
+
* indentation becomes the block's. Subsequent bullets at that indentation or
|
|
138
|
+
* shallower are new fields; a blank line, or any non-blank line indented more
|
|
139
|
+
* deeply than the current bullet, **continues that bullet** — the AC-78 rule,
|
|
140
|
+
* and the whole reason a wrapped `Retires:` list no longer truncates. The block
|
|
141
|
+
* ends at the first line at or inside the block indentation that is not a bullet.
|
|
142
|
+
*/
|
|
143
|
+
function collectFields(body) {
|
|
144
|
+
const fields = [];
|
|
145
|
+
const warnings = [];
|
|
146
|
+
let current = null;
|
|
147
|
+
let started = false;
|
|
148
|
+
// The field block's own indentation, taken from its first bullet.
|
|
149
|
+
let blockIndent = null;
|
|
150
|
+
const flush = () => {
|
|
151
|
+
if (!current)
|
|
152
|
+
return;
|
|
153
|
+
const field = FIELD_RE.exec(current.text);
|
|
154
|
+
if (field) {
|
|
155
|
+
const name = (field[1] ?? field[2] ?? field[3]).toLowerCase();
|
|
156
|
+
fields.push({ name, value: field[4].trim(), line: current.line });
|
|
157
|
+
}
|
|
158
|
+
else if (!AC_DEF_BULLET_RE.test(current.text)) {
|
|
159
|
+
const bold = BOLD_PREFIX_RE.exec(current.text);
|
|
160
|
+
warnings.push({
|
|
161
|
+
line: current.line,
|
|
162
|
+
text: bold ? bold[1] : excerpt(current.text, 44),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
current = null;
|
|
166
|
+
};
|
|
167
|
+
for (const { line, index } of body) {
|
|
168
|
+
const bullet = BULLET_RE.exec(line);
|
|
169
|
+
const indent = bullet ? bullet[1].length : 0;
|
|
170
|
+
if (bullet && (blockIndent === null || indent <= blockIndent)) {
|
|
171
|
+
flush();
|
|
172
|
+
if (blockIndent === null)
|
|
173
|
+
blockIndent = indent;
|
|
174
|
+
current = { text: bullet[2], line: index + 1, indent };
|
|
175
|
+
started = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.trim() === '')
|
|
179
|
+
continue;
|
|
180
|
+
// Indented more deeply than the bullet it follows: a continuation of it.
|
|
181
|
+
// A nested bullet lands here too, which is correct — it belongs to the
|
|
182
|
+
// bullet above rather than starting a new field.
|
|
183
|
+
if (current !== null && /^\s/.test(line) && line.search(/\S/) > current.indent) {
|
|
184
|
+
current.text += ` ${line.trim()}`;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (started)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
flush();
|
|
191
|
+
return { fields, warnings };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse every amendment section in one requirements.md.
|
|
195
|
+
*
|
|
196
|
+
* @param content - File content (UTF-8). Never throws.
|
|
197
|
+
*/
|
|
198
|
+
export function parseAmendments(content) {
|
|
199
|
+
const lines = content.split('\n');
|
|
200
|
+
const warnings = [];
|
|
201
|
+
const byNumber = new Map();
|
|
202
|
+
const order = [];
|
|
203
|
+
// Heading positions, gathered outside fences so a fenced example of an
|
|
204
|
+
// amendment heading is documentation rather than a record.
|
|
205
|
+
const headings = [];
|
|
206
|
+
const liveLines = new Set();
|
|
207
|
+
scanOutsideFences(lines, (line, index) => {
|
|
208
|
+
liveLines.add(index);
|
|
209
|
+
const heading = HEADING_RE.exec(line);
|
|
210
|
+
if (heading) {
|
|
211
|
+
headings.push({
|
|
212
|
+
index,
|
|
213
|
+
level: heading[1].length,
|
|
214
|
+
n: Number(heading[2]),
|
|
215
|
+
date: heading[3],
|
|
216
|
+
...(heading[4] !== undefined && { context: heading[4] }),
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (AMENDMENT_SHAPED_RE.test(line)) {
|
|
221
|
+
warnings.push({
|
|
222
|
+
line: index + 1,
|
|
223
|
+
text: `"${excerpt(line)}" resembles an amendment heading but does not match ` +
|
|
224
|
+
`the grammar (expected "## Amendment <n> — YYYY-MM-DD[, context]")`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
for (const heading of headings) {
|
|
229
|
+
// Section: heading to the next heading at the same or shallower level, or
|
|
230
|
+
// end of file (AC-51). Only lines outside fences participate.
|
|
231
|
+
let end = lines.length;
|
|
232
|
+
for (let j = heading.index + 1; j < lines.length; j++) {
|
|
233
|
+
if (!liveLines.has(j))
|
|
234
|
+
continue;
|
|
235
|
+
const other = ANY_HEADING_RE.exec(lines[j]);
|
|
236
|
+
if (other && other[1].length <= heading.level) {
|
|
237
|
+
end = j;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const body = [];
|
|
242
|
+
for (let j = heading.index + 1; j < end; j++) {
|
|
243
|
+
if (liveLines.has(j))
|
|
244
|
+
body.push({ line: lines[j], index: j });
|
|
245
|
+
}
|
|
246
|
+
const { fields, warnings: fieldWarnings } = collectFields(body);
|
|
247
|
+
for (const warning of fieldWarnings) {
|
|
248
|
+
warnings.push({
|
|
249
|
+
line: warning.line,
|
|
250
|
+
text: `Amendment ${heading.n}: unrecognised field bullet "${warning.text}" — kept`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
let retires = [];
|
|
255
|
+
let issues = [];
|
|
256
|
+
let reopens = [];
|
|
257
|
+
let retiresPresent = false;
|
|
258
|
+
let reason;
|
|
259
|
+
for (const field of fields) {
|
|
260
|
+
if (seen.has(field.name)) {
|
|
261
|
+
warnings.push({
|
|
262
|
+
line: field.line,
|
|
263
|
+
text: `Amendment ${heading.n}: duplicate "${field.name}" field — first wins`,
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
seen.add(field.name);
|
|
268
|
+
if (field.name === 'retires' || field.name === 'issues') {
|
|
269
|
+
const { ids, rejected } = parseAcList(field.value);
|
|
270
|
+
for (const token of rejected) {
|
|
271
|
+
warnings.push({
|
|
272
|
+
line: field.line,
|
|
273
|
+
text: `Amendment ${heading.n}: "${token}" in ${field.name} is not a whole AC-<n> ` +
|
|
274
|
+
`token and contributes no id (ranges such as AC-4..AC-6 are not accepted)`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (field.name === 'retires') {
|
|
278
|
+
retires = ids;
|
|
279
|
+
retiresPresent = true;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
issues = ids;
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (field.name === 'reopens') {
|
|
287
|
+
reopens = parsePhaseList(field.value);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (field.value !== '')
|
|
291
|
+
reason = field.value;
|
|
292
|
+
}
|
|
293
|
+
if (reason === undefined) {
|
|
294
|
+
warnings.push({
|
|
295
|
+
line: heading.index + 1,
|
|
296
|
+
text: `Amendment ${heading.n} has no Reason: field`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const amendment = {
|
|
300
|
+
n: heading.n,
|
|
301
|
+
date: heading.date,
|
|
302
|
+
...(heading.context !== undefined && { context: heading.context }),
|
|
303
|
+
line: heading.index + 1,
|
|
304
|
+
level: heading.level,
|
|
305
|
+
retires,
|
|
306
|
+
issues,
|
|
307
|
+
reopens,
|
|
308
|
+
retiresPresent,
|
|
309
|
+
...(reason !== undefined && { reason }),
|
|
310
|
+
};
|
|
311
|
+
const existing = byNumber.get(amendment.n);
|
|
312
|
+
if (existing) {
|
|
313
|
+
warnings.push({
|
|
314
|
+
line: amendment.line,
|
|
315
|
+
text: `duplicate Amendment ${amendment.n}: lines ${existing.line} and ` +
|
|
316
|
+
`${amendment.line} (first occurrence wins)`,
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
byNumber.set(amendment.n, amendment);
|
|
321
|
+
order.push(amendment);
|
|
322
|
+
}
|
|
323
|
+
// Numbering gaps (AC-7), over parsed numbers.
|
|
324
|
+
if (byNumber.size > 0) {
|
|
325
|
+
const max = Math.max(...byNumber.keys());
|
|
326
|
+
for (let n = 1; n <= max; n++) {
|
|
327
|
+
if (!byNumber.has(n)) {
|
|
328
|
+
warnings.push({ line: 0, text: `Amendment ${n} is never defined (numbering gap)` });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
amendments: order.sort((a, b) => a.n - b.n),
|
|
334
|
+
// Ordered by source line; the synthetic line 0 of a gap warning sorts first
|
|
335
|
+
// deliberately — it is a statement about the file, not about a place in it.
|
|
336
|
+
warnings: warnings.sort((a, b) => a.line - b.line).map((w) => w.text),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/change/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reopen planning and application (FR4).
|
|
3
|
+
*
|
|
4
|
+
* `planReopen` is pure — no I/O — so every refusal branch is testable without a
|
|
5
|
+
* filesystem. `applyReopen` is the single writing function in the module.
|
|
6
|
+
*
|
|
7
|
+
* **Why the range is contiguous.** Reopening only the phases an author named
|
|
8
|
+
* would leave a `complete` phase sitting behind an `in-progress` one, and two
|
|
9
|
+
* shipped behaviours then misfire: `findNextPendingPhase`
|
|
10
|
+
* (`core/lifecycle.ts:244-269`) jumps forward over the intermediates writing a
|
|
11
|
+
* `type: 'normal'` record with no skip recorded, and `checkDeploymentEntry`
|
|
12
|
+
* (`:491-505`) keys on template adjacency rather than the actual target, so a
|
|
13
|
+
* reopened `design` whose next pending phase is `deployment` would enter
|
|
14
|
+
* deployment with that gate never evaluated. A contiguous range fixes both
|
|
15
|
+
* without touching `evaluateTransition`, which is what lets this feature add no
|
|
16
|
+
* gate at all.
|
|
17
|
+
*
|
|
18
|
+
* **Why validation is an allowlist AND a character check.** Membership in
|
|
19
|
+
* `phasesForScope` serves three jobs — traversal guard, Gate-1 correctness guard
|
|
20
|
+
* (`lifecycle.ts:310-318` skips the required-artifacts gate entirely when
|
|
21
|
+
* `current_phase` is not in the list, so a typo silently switches the gate
|
|
22
|
+
* *off*), and scope guard. But membership alone is not enough:
|
|
23
|
+
* `validateTemplate` (`core/template-resolver.ts:323-326`) accepts any non-empty
|
|
24
|
+
* string as a phase name, so a project-local template declaring
|
|
25
|
+
* `- name: ../../evil` puts a traversal token *inside* the allowlist, and
|
|
26
|
+
* `saveSnapshot`'s `join` has no containment (`state/snapshot-store.ts:129-133`).
|
|
27
|
+
* This repo already treats `.aidlc/templates/` as hostile for exactly this
|
|
28
|
+
* reason (`traceability/gate-criterion.ts:64-71`).
|
|
29
|
+
*
|
|
30
|
+
* Requirements: change-management/AC-27, AC-50, AC-82, AC-83, AC-84, AC-85,
|
|
31
|
+
* AC-87, AC-89, AC-92
|
|
32
|
+
*/
|
|
33
|
+
import type { InstanceState, PhaseState, WorkflowTemplate } from '../core/types.js';
|
|
34
|
+
import type { Amendment, ReopenPlan } from './types.js';
|
|
35
|
+
export interface PlanInput {
|
|
36
|
+
amendment: Amendment;
|
|
37
|
+
instanceState: InstanceState;
|
|
38
|
+
phaseStates: Map<string, PhaseState>;
|
|
39
|
+
resolvedTemplate: WorkflowTemplate;
|
|
40
|
+
/** Actor identity for the record's `by` (AC-83). */
|
|
41
|
+
actor: string;
|
|
42
|
+
/** Injected so tests are deterministic. */
|
|
43
|
+
now: Date;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Plan a reopen. Pure: one of five variants out, no writes, no reads.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planReopen(input: PlanInput): ReopenPlan;
|
|
49
|
+
export interface ApplyInput {
|
|
50
|
+
projectRoot: string;
|
|
51
|
+
instance: string;
|
|
52
|
+
plan: Extract<ReopenPlan, {
|
|
53
|
+
kind: 'reopen';
|
|
54
|
+
}>;
|
|
55
|
+
instanceState: InstanceState;
|
|
56
|
+
phaseStates: Map<string, PhaseState>;
|
|
57
|
+
resolvedTemplate: WorkflowTemplate;
|
|
58
|
+
amendment: Amendment;
|
|
59
|
+
now: Date;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Apply a reopen plan.
|
|
63
|
+
*
|
|
64
|
+
* **Write order is deliberate** (AC-89): every phase state file first, then
|
|
65
|
+
* `instance.yaml` (`current_phase` and `applied_amendments` in one write), then
|
|
66
|
+
* the appended record last. A failure part-way therefore leaves phases reopened
|
|
67
|
+
* with the amendment unrecorded, which a re-run completes; the reverse order
|
|
68
|
+
* would leave the amendment recorded with phases untouched, which a re-run would
|
|
69
|
+
* refuse to fix.
|
|
70
|
+
*/
|
|
71
|
+
export declare function applyReopen(input: ApplyInput): void;
|
|
72
|
+
/**
|
|
73
|
+
* Record an amendment as applied without touching phase state — the no-op path
|
|
74
|
+
* (AC-87). An accounted-for amendment must not become destructive later when
|
|
75
|
+
* `current_phase` moves on its own.
|
|
76
|
+
*/
|
|
77
|
+
export declare function recordApplied(projectRoot: string, instance: string, instanceState: InstanceState, amendmentNumber: number): void;
|
|
78
|
+
/** Sorted, deduplicated union — so a repeat run is stable rather than growing. */
|
|
79
|
+
export declare function mergeApplied(existing: number[] | undefined, n: number): number[];
|
|
80
|
+
//# sourceMappingURL=reopen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reopen.d.ts","sourceRoot":"","sources":["../../src/change/reopen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EAEV,gBAAgB,EACjB,MAAM,kBAAkB,CAAC;AAK1B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAYxD,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAoIvD;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;IAC9C,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAuDnD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,MAAM,GACtB,IAAI,CAON;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhF"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reopen planning and application (FR4).
|
|
3
|
+
*
|
|
4
|
+
* `planReopen` is pure — no I/O — so every refusal branch is testable without a
|
|
5
|
+
* filesystem. `applyReopen` is the single writing function in the module.
|
|
6
|
+
*
|
|
7
|
+
* **Why the range is contiguous.** Reopening only the phases an author named
|
|
8
|
+
* would leave a `complete` phase sitting behind an `in-progress` one, and two
|
|
9
|
+
* shipped behaviours then misfire: `findNextPendingPhase`
|
|
10
|
+
* (`core/lifecycle.ts:244-269`) jumps forward over the intermediates writing a
|
|
11
|
+
* `type: 'normal'` record with no skip recorded, and `checkDeploymentEntry`
|
|
12
|
+
* (`:491-505`) keys on template adjacency rather than the actual target, so a
|
|
13
|
+
* reopened `design` whose next pending phase is `deployment` would enter
|
|
14
|
+
* deployment with that gate never evaluated. A contiguous range fixes both
|
|
15
|
+
* without touching `evaluateTransition`, which is what lets this feature add no
|
|
16
|
+
* gate at all.
|
|
17
|
+
*
|
|
18
|
+
* **Why validation is an allowlist AND a character check.** Membership in
|
|
19
|
+
* `phasesForScope` serves three jobs — traversal guard, Gate-1 correctness guard
|
|
20
|
+
* (`lifecycle.ts:310-318` skips the required-artifacts gate entirely when
|
|
21
|
+
* `current_phase` is not in the list, so a typo silently switches the gate
|
|
22
|
+
* *off*), and scope guard. But membership alone is not enough:
|
|
23
|
+
* `validateTemplate` (`core/template-resolver.ts:323-326`) accepts any non-empty
|
|
24
|
+
* string as a phase name, so a project-local template declaring
|
|
25
|
+
* `- name: ../../evil` puts a traversal token *inside* the allowlist, and
|
|
26
|
+
* `saveSnapshot`'s `join` has no containment (`state/snapshot-store.ts:129-133`).
|
|
27
|
+
* This repo already treats `.aidlc/templates/` as hostile for exactly this
|
|
28
|
+
* reason (`traceability/gate-criterion.ts:64-71`).
|
|
29
|
+
*
|
|
30
|
+
* Requirements: change-management/AC-27, AC-50, AC-82, AC-83, AC-84, AC-85,
|
|
31
|
+
* AC-87, AC-89, AC-92
|
|
32
|
+
*/
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { phasesForScope } from '../core/template-resolver.js';
|
|
35
|
+
import { effectiveRequiredArtifacts } from '../core/gate.js';
|
|
36
|
+
import { saveSnapshot } from '../state/snapshot-store.js';
|
|
37
|
+
import { appendTransition } from '../state/transition-log.js';
|
|
38
|
+
/**
|
|
39
|
+
* A phase name that could escape the instance directory, whatever list it
|
|
40
|
+
* appears in (AC-82). Applied to template members too, not only to author input.
|
|
41
|
+
*/
|
|
42
|
+
function isSafePhaseName(name) {
|
|
43
|
+
return (name !== '' && !name.includes('/') && !name.includes('\\') && !name.includes('..'));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Plan a reopen. Pure: one of five variants out, no writes, no reads.
|
|
47
|
+
*/
|
|
48
|
+
export function planReopen(input) {
|
|
49
|
+
const { amendment, instanceState, phaseStates, resolvedTemplate, actor, now } = input;
|
|
50
|
+
const applicable = phasesForScope(resolvedTemplate, instanceState.scope);
|
|
51
|
+
const order = applicable.map((p) => String(p.name));
|
|
52
|
+
// Every member of the allowlist itself must be filename-safe, because the
|
|
53
|
+
// template it came from is tracked and therefore untrusted.
|
|
54
|
+
const unsafeTemplatePhases = order.filter((name) => !isSafePhaseName(name));
|
|
55
|
+
if (unsafeTemplatePhases.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
kind: 'invalid-phase',
|
|
58
|
+
names: unsafeTemplatePhases,
|
|
59
|
+
validPhases: order.filter(isSafePhaseName),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const valid = new Set(order);
|
|
63
|
+
// AC-82: named phases must be members, and filename-safe.
|
|
64
|
+
const invalid = amendment.reopens.filter((name) => !valid.has(name) || !isSafePhaseName(name));
|
|
65
|
+
if (invalid.length > 0) {
|
|
66
|
+
return { kind: 'invalid-phase', names: invalid, validPhases: order };
|
|
67
|
+
}
|
|
68
|
+
const currentPhase = String(instanceState.current_phase);
|
|
69
|
+
const currentIndex = order.indexOf(currentPhase);
|
|
70
|
+
if (currentIndex === -1) {
|
|
71
|
+
// Reachable through a scope change. Also the state in which Gate 1 is off,
|
|
72
|
+
// so proceeding would write a reopen into an instance whose gates are not
|
|
73
|
+
// running (AC-92).
|
|
74
|
+
return {
|
|
75
|
+
kind: 'blocked',
|
|
76
|
+
code: 'current-phase-outside-template',
|
|
77
|
+
message: `current_phase "${currentPhase}" is not a phase of template ` +
|
|
78
|
+
`"${resolvedTemplate.name}" at scope "${instanceState.scope}" ` +
|
|
79
|
+
`(${order.join(' → ')}) — the required-artifacts gate is not running for ` +
|
|
80
|
+
`this instance, so a reopen would not re-arm anything`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (amendment.reopens.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
kind: 'noop',
|
|
86
|
+
reason: 'the amendment declares no Reopens: field',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const namedIndexes = amendment.reopens.map((name) => order.indexOf(name));
|
|
90
|
+
const earliest = Math.min(...namedIndexes);
|
|
91
|
+
// AC-83: a Reopens: naming only phases at or after the current one is a hard
|
|
92
|
+
// error with its own variant, never a silent drop.
|
|
93
|
+
if (earliest >= currentIndex) {
|
|
94
|
+
return {
|
|
95
|
+
kind: 'phase-ahead',
|
|
96
|
+
names: amendment.reopens.filter((name) => order.indexOf(name) >= currentIndex),
|
|
97
|
+
currentPhase,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// The contiguous range, `skipped` phases removed (AC-85). A skip is not a
|
|
101
|
+
// completion: resurrecting it would demand artifacts deliberately never
|
|
102
|
+
// produced and leave `skip_reason` dangling beside `status: in-progress`.
|
|
103
|
+
const range = [];
|
|
104
|
+
for (let i = earliest; i <= currentIndex; i++) {
|
|
105
|
+
const name = order[i];
|
|
106
|
+
const state = phaseStates.get(name);
|
|
107
|
+
if (!state) {
|
|
108
|
+
// 15 live instances have no `phase-design.yaml` while `design` is in
|
|
109
|
+
// their applicable list. Advancing into a phase with no state file leaves
|
|
110
|
+
// Gate 1 failing permanently with "missing (no phase state)", so refuse
|
|
111
|
+
// rather than fabricate one (AC-92).
|
|
112
|
+
return {
|
|
113
|
+
kind: 'blocked',
|
|
114
|
+
code: 'phase-state-missing',
|
|
115
|
+
message: `phase "${name}" lies in the reopen range but has no phase-${name}.yaml ` +
|
|
116
|
+
`— refusing rather than creating one`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (state.status === 'skipped')
|
|
120
|
+
continue;
|
|
121
|
+
range.push(name);
|
|
122
|
+
}
|
|
123
|
+
if (range.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'noop',
|
|
126
|
+
reason: 'every phase in the range is skipped, so there is nothing to reopen',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const to = range[0];
|
|
130
|
+
if (to === currentPhase) {
|
|
131
|
+
// `from === to` must never reach the log: `metrics/report.ts:204-213` sets
|
|
132
|
+
// `opened` from `r.to` BEFORE reading `r.from`, so a self-referential record
|
|
133
|
+
// overwrites the phase's real entry timestamp, computes a zero interval and
|
|
134
|
+
// then discards the open interval — losing the first visit rather than
|
|
135
|
+
// summing it. Made unrepresentable rather than handled (AC-87).
|
|
136
|
+
return {
|
|
137
|
+
kind: 'noop',
|
|
138
|
+
reason: `"${currentPhase}" is already the open phase, so nothing needs reopening`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const record = {
|
|
142
|
+
at: now.toISOString(),
|
|
143
|
+
by: actor,
|
|
144
|
+
from: currentPhase,
|
|
145
|
+
to,
|
|
146
|
+
type: 'reopen',
|
|
147
|
+
detail: {
|
|
148
|
+
amendment: amendment.n,
|
|
149
|
+
retires: [...amendment.retires].sort((a, b) => a - b),
|
|
150
|
+
issues: [...amendment.issues].sort((a, b) => a - b),
|
|
151
|
+
// The range actually rewritten, not the names the author wrote — they
|
|
152
|
+
// differ, and this is the audit record (AC-84).
|
|
153
|
+
reopened: range,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
kind: 'reopen',
|
|
158
|
+
phases: range,
|
|
159
|
+
from: currentPhase,
|
|
160
|
+
to,
|
|
161
|
+
newCurrentPhase: to,
|
|
162
|
+
record,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Apply a reopen plan.
|
|
167
|
+
*
|
|
168
|
+
* **Write order is deliberate** (AC-89): every phase state file first, then
|
|
169
|
+
* `instance.yaml` (`current_phase` and `applied_amendments` in one write), then
|
|
170
|
+
* the appended record last. A failure part-way therefore leaves phases reopened
|
|
171
|
+
* with the amendment unrecorded, which a re-run completes; the reverse order
|
|
172
|
+
* would leave the amendment recorded with phases untouched, which a re-run would
|
|
173
|
+
* refuse to fix.
|
|
174
|
+
*/
|
|
175
|
+
export function applyReopen(input) {
|
|
176
|
+
const { projectRoot, instance, plan, instanceState, phaseStates, resolvedTemplate, amendment, now, } = input;
|
|
177
|
+
const stateDir = join(projectRoot, '.aidlc', 'state');
|
|
178
|
+
const stamp = now.toISOString();
|
|
179
|
+
// 1. Phase states.
|
|
180
|
+
for (const phaseName of plan.phases) {
|
|
181
|
+
const state = phaseStates.get(phaseName);
|
|
182
|
+
if (!state)
|
|
183
|
+
continue; // planReopen already refused this case.
|
|
184
|
+
const templatePhase = resolvedTemplate.phases.find((p) => String(p.name) === phaseName);
|
|
185
|
+
const required = templatePhase
|
|
186
|
+
? effectiveRequiredArtifacts(templatePhase, state).map((decl) => decl.name)
|
|
187
|
+
: [];
|
|
188
|
+
const requiredNames = new Set(required);
|
|
189
|
+
state.status = 'in-progress';
|
|
190
|
+
state.completed_at = null;
|
|
191
|
+
state.completed_by = null;
|
|
192
|
+
state.reopened_at = stamp;
|
|
193
|
+
// `entered_at` is deliberately untouched — see the docblock on
|
|
194
|
+
// `PhaseState.reopened_at`.
|
|
195
|
+
// Only artifacts ALREADY TRACKED whose name is in the effective required
|
|
196
|
+
// set. No entry is added or removed, so the `tasks.md` legacy filter at
|
|
197
|
+
// `core/gate.ts:123-131` is not defeated, and optional artifacts and
|
|
198
|
+
// `metrics` are left alone (AC-85).
|
|
199
|
+
for (const artifact of state.artifacts) {
|
|
200
|
+
if (requiredNames.has(artifact.name) && artifact.status === 'complete') {
|
|
201
|
+
artifact.status = 'in-progress';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
saveSnapshot(stateDir, instance, `phase-${phaseName}.yaml`, state);
|
|
205
|
+
}
|
|
206
|
+
// 2. instance.yaml — current_phase and applied_amendments in one write.
|
|
207
|
+
instanceState.current_phase = plan.newCurrentPhase;
|
|
208
|
+
instanceState.applied_amendments = mergeApplied(instanceState.applied_amendments, amendment.n);
|
|
209
|
+
saveSnapshot(stateDir, instance, 'instance.yaml', instanceState);
|
|
210
|
+
// 3. The record, last.
|
|
211
|
+
appendTransition(join(stateDir, instance, 'transitions.log'), plan.record);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Record an amendment as applied without touching phase state — the no-op path
|
|
215
|
+
* (AC-87). An accounted-for amendment must not become destructive later when
|
|
216
|
+
* `current_phase` moves on its own.
|
|
217
|
+
*/
|
|
218
|
+
export function recordApplied(projectRoot, instance, instanceState, amendmentNumber) {
|
|
219
|
+
const stateDir = join(projectRoot, '.aidlc', 'state');
|
|
220
|
+
instanceState.applied_amendments = mergeApplied(instanceState.applied_amendments, amendmentNumber);
|
|
221
|
+
saveSnapshot(stateDir, instance, 'instance.yaml', instanceState);
|
|
222
|
+
}
|
|
223
|
+
/** Sorted, deduplicated union — so a repeat run is stable rather than growing. */
|
|
224
|
+
export function mergeApplied(existing, n) {
|
|
225
|
+
return [...new Set([...(existing ?? []), n])].sort((a, b) => a - b);
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=reopen.js.map
|