@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.
Files changed (44) hide show
  1. package/dist/change/cross-check.d.ts +28 -0
  2. package/dist/change/cross-check.d.ts.map +1 -0
  3. package/dist/change/cross-check.js +53 -0
  4. package/dist/change/cross-check.js.map +1 -0
  5. package/dist/change/impact.d.ts +57 -0
  6. package/dist/change/impact.d.ts.map +1 -0
  7. package/dist/change/impact.js +288 -0
  8. package/dist/change/impact.js.map +1 -0
  9. package/dist/change/index.d.ts +12 -0
  10. package/dist/change/index.d.ts.map +1 -0
  11. package/dist/change/index.js +11 -0
  12. package/dist/change/index.js.map +1 -0
  13. package/dist/change/parser.d.ts +52 -0
  14. package/dist/change/parser.d.ts.map +1 -0
  15. package/dist/change/parser.js +339 -0
  16. package/dist/change/parser.js.map +1 -0
  17. package/dist/change/reopen.d.ts +80 -0
  18. package/dist/change/reopen.d.ts.map +1 -0
  19. package/dist/change/reopen.js +227 -0
  20. package/dist/change/reopen.js.map +1 -0
  21. package/dist/change/types.d.ts +144 -0
  22. package/dist/change/types.d.ts.map +1 -0
  23. package/dist/change/types.js +8 -0
  24. package/dist/change/types.js.map +1 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +2 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/commands/amend.d.ts +22 -0
  29. package/dist/commands/amend.d.ts.map +1 -0
  30. package/dist/commands/amend.js +355 -0
  31. package/dist/commands/amend.js.map +1 -0
  32. package/dist/commands/docs.d.ts.map +1 -1
  33. package/dist/commands/docs.js +6 -0
  34. package/dist/commands/docs.js.map +1 -1
  35. package/dist/core/lifecycle.d.ts +3 -0
  36. package/dist/core/lifecycle.d.ts.map +1 -1
  37. package/dist/core/lifecycle.js +13 -2
  38. package/dist/core/lifecycle.js.map +1 -1
  39. package/dist/core/types.d.ts +37 -2
  40. package/dist/core/types.d.ts.map +1 -1
  41. package/dist/menu/roster.d.ts.map +1 -1
  42. package/dist/menu/roster.js +4 -0
  43. package/dist/menu/roster.js.map +1 -1
  44. 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