@rasensio/aidlc 1.20.0 → 1.22.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/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/roadmap.d.ts +66 -0
- package/dist/commands/roadmap.d.ts.map +1 -0
- package/dist/commands/roadmap.js +206 -0
- package/dist/commands/roadmap.js.map +1 -0
- 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/dist/roadmap/paths.d.ts +21 -0
- package/dist/roadmap/paths.d.ts.map +1 -1
- package/dist/roadmap/paths.js +17 -1
- package/dist/roadmap/paths.js.map +1 -1
- package/dist/roadmap/sync/body.d.ts +123 -0
- package/dist/roadmap/sync/body.d.ts.map +1 -0
- package/dist/roadmap/sync/body.js +289 -0
- package/dist/roadmap/sync/body.js.map +1 -0
- package/dist/roadmap/sync/collect.d.ts +54 -0
- package/dist/roadmap/sync/collect.d.ts.map +1 -0
- package/dist/roadmap/sync/collect.js +138 -0
- package/dist/roadmap/sync/collect.js.map +1 -0
- package/dist/roadmap/sync/config.d.ts +95 -0
- package/dist/roadmap/sync/config.d.ts.map +1 -0
- package/dist/roadmap/sync/config.js +236 -0
- package/dist/roadmap/sync/config.js.map +1 -0
- package/dist/roadmap/sync/discover.d.ts +56 -0
- package/dist/roadmap/sync/discover.d.ts.map +1 -0
- package/dist/roadmap/sync/discover.js +126 -0
- package/dist/roadmap/sync/discover.js.map +1 -0
- package/dist/roadmap/sync/execute.d.ts +41 -0
- package/dist/roadmap/sync/execute.d.ts.map +1 -0
- package/dist/roadmap/sync/execute.js +174 -0
- package/dist/roadmap/sync/execute.js.map +1 -0
- package/dist/roadmap/sync/gh.d.ts +74 -0
- package/dist/roadmap/sync/gh.d.ts.map +1 -0
- package/dist/roadmap/sync/gh.js +110 -0
- package/dist/roadmap/sync/gh.js.map +1 -0
- package/dist/roadmap/sync/import.d.ts +73 -0
- package/dist/roadmap/sync/import.d.ts.map +1 -0
- package/dist/roadmap/sync/import.js +269 -0
- package/dist/roadmap/sync/import.js.map +1 -0
- package/dist/roadmap/sync/labels.d.ts +57 -0
- package/dist/roadmap/sync/labels.d.ts.map +1 -0
- package/dist/roadmap/sync/labels.js +80 -0
- package/dist/roadmap/sync/labels.js.map +1 -0
- package/dist/roadmap/sync/markdown.d.ts +79 -0
- package/dist/roadmap/sync/markdown.d.ts.map +1 -0
- package/dist/roadmap/sync/markdown.js +165 -0
- package/dist/roadmap/sync/markdown.js.map +1 -0
- package/dist/roadmap/sync/marker.d.ts +47 -0
- package/dist/roadmap/sync/marker.d.ts.map +1 -0
- package/dist/roadmap/sync/marker.js +73 -0
- package/dist/roadmap/sync/marker.js.map +1 -0
- package/dist/roadmap/sync/plan.d.ts +47 -0
- package/dist/roadmap/sync/plan.d.ts.map +1 -0
- package/dist/roadmap/sync/plan.js +290 -0
- package/dist/roadmap/sync/plan.js.map +1 -0
- package/dist/roadmap/sync/render.d.ts +28 -0
- package/dist/roadmap/sync/render.d.ts.map +1 -0
- package/dist/roadmap/sync/render.js +112 -0
- package/dist/roadmap/sync/render.js.map +1 -0
- package/dist/roadmap/sync/sanitize.d.ts +56 -0
- package/dist/roadmap/sync/sanitize.d.ts.map +1 -0
- package/dist/roadmap/sync/sanitize.js +140 -0
- package/dist/roadmap/sync/sanitize.js.map +1 -0
- package/dist/roadmap/sync/types.d.ts +139 -0
- package/dist/roadmap/sync/types.d.ts.map +1 -0
- package/dist/roadmap/sync/types.js +50 -0
- package/dist/roadmap/sync/types.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The projection planner. Pure — and the purity is the point.
|
|
3
|
+
*
|
|
4
|
+
* This module imports no `node:fs`, no `node:child_process`, and not `gh.ts`. A test
|
|
5
|
+
* pins that over the import graph, which is what makes "planning needs no network" a
|
|
6
|
+
* property of the code rather than a claim about the test suite: hand the command a
|
|
7
|
+
* runner that throws on every invocation and planning still succeeds, because the
|
|
8
|
+
* planner never had one.
|
|
9
|
+
*
|
|
10
|
+
* Refusals are **values**, in a fixed order, so a tree with two problems always yields
|
|
11
|
+
* the same message and the command layer — not this module — decides the exit code.
|
|
12
|
+
*
|
|
13
|
+
* Requirements: roadmap-issue-projection/AC-14 … roadmap-issue-projection/AC-32,
|
|
14
|
+
* roadmap-issue-projection/AC-44
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { ROADMAP_STATUSES } from '../paths.js';
|
|
19
|
+
import { extractRegion, mergeBody, renderGeneratedRegion } from './body.js';
|
|
20
|
+
import { isStatusLabel, statusLabel } from './labels.js';
|
|
21
|
+
import { findMarkerIds } from './marker.js';
|
|
22
|
+
import { WRITE_ORDER, isWrite, } from './types.js';
|
|
23
|
+
/** GitHub's own issue-title limit. */
|
|
24
|
+
export const MAX_ISSUE_TITLE_LENGTH = 256;
|
|
25
|
+
export function planProjection(input) {
|
|
26
|
+
const { config, items, unprojectable, issues, retireIneligible, repoLabel, bodyContext } = input;
|
|
27
|
+
const eligible = new Set(config.statuses);
|
|
28
|
+
// -------------------------------------------------------------------------
|
|
29
|
+
// Refusals, in a fixed order (design: one deterministic message per tree)
|
|
30
|
+
// -------------------------------------------------------------------------
|
|
31
|
+
const duplicateIds = findDuplicateIds(items);
|
|
32
|
+
if (duplicateIds !== null)
|
|
33
|
+
return { kind: 'refused', error: duplicateIds };
|
|
34
|
+
const blockingItems = unprojectable.filter((u) => eligible.has(u.status));
|
|
35
|
+
if (blockingItems.length > 0) {
|
|
36
|
+
return { kind: 'refused', error: unprojectableMessage(blockingItems) };
|
|
37
|
+
}
|
|
38
|
+
const markerIndex = indexByMarker(issues);
|
|
39
|
+
if (markerIndex.kind === 'ambiguous') {
|
|
40
|
+
return { kind: 'refused', error: markerIndex.error };
|
|
41
|
+
}
|
|
42
|
+
const overLong = items.filter((i) => eligible.has(i.status) && i.title.length > MAX_ISSUE_TITLE_LENGTH);
|
|
43
|
+
if (overLong.length > 0) {
|
|
44
|
+
return {
|
|
45
|
+
kind: 'refused',
|
|
46
|
+
error: `these item titles exceed GitHub's ${MAX_ISSUE_TITLE_LENGTH}-character issue title limit ` +
|
|
47
|
+
'and would fail mid-run, after consent:\n' +
|
|
48
|
+
overLong.map((i) => ` ${i.status}/${i.filename} (${i.title.length} characters)`).join('\n'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// -------------------------------------------------------------------------
|
|
52
|
+
// Outcomes
|
|
53
|
+
// -------------------------------------------------------------------------
|
|
54
|
+
const warnings = unprojectable
|
|
55
|
+
.filter((u) => !eligible.has(u.status))
|
|
56
|
+
.map((u) => `${u.label} is not projectable (${u.reason}), and its status is not projected`);
|
|
57
|
+
const byId = markerIndex.byId;
|
|
58
|
+
const lines = [];
|
|
59
|
+
const claimedIssues = new Set();
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const issue = byId.get(item.id) ?? null;
|
|
62
|
+
if (issue !== null)
|
|
63
|
+
claimedIssues.add(issue.number);
|
|
64
|
+
lines.push(...linesForItem(item, issue, eligible, retireIneligible, bodyContext));
|
|
65
|
+
}
|
|
66
|
+
// Issue-only lines: an issue nothing on disk claims.
|
|
67
|
+
for (const issue of issues) {
|
|
68
|
+
if (claimedIssues.has(issue.number))
|
|
69
|
+
continue;
|
|
70
|
+
const markers = findMarkerIds(issue.body);
|
|
71
|
+
if (markers.length === 0) {
|
|
72
|
+
lines.push(line('label-without-marker', null, issue.number, 'carries the discovery label but no aidlc-item marker'));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
lines.push(line('orphan-missing', null, issue.number, `marker names ${markers[0]}, which no item on disk has`));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { kind: 'ok', plan: assemble(lines, config, repoLabel), warnings };
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Outcome derivation
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
/**
|
|
84
|
+
* The lines one item produces.
|
|
85
|
+
*
|
|
86
|
+
* A **terminal report** — `state-divergent`, `ineligible-with-issue` — is the only line
|
|
87
|
+
* its item emits. That is not a stylistic choice: collecting outcomes unconditionally
|
|
88
|
+
* would emit `update-title` alongside `state-divergent`, writing to an issue a human
|
|
89
|
+
* deliberately closed. Closing an issue is the primary triage act of the very user this
|
|
90
|
+
* feature is for, so a projection that edits a closed issue relocates the two-writer
|
|
91
|
+
* loop rather than removing it.
|
|
92
|
+
*
|
|
93
|
+
* Non-terminal branches *are* collected, so a `done` item that was also retitled in the
|
|
94
|
+
* same run emits both `update-title` and `close` and converges in one pass.
|
|
95
|
+
*/
|
|
96
|
+
function linesForItem(item, issue, eligible, retireIneligible, bodyContext) {
|
|
97
|
+
const isEligible = eligible.has(item.status);
|
|
98
|
+
if (issue === null) {
|
|
99
|
+
if (!isEligible)
|
|
100
|
+
return [];
|
|
101
|
+
if (item.status === 'done') {
|
|
102
|
+
return [line('skip', item, null, 'not-backfilled')];
|
|
103
|
+
}
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
outcome: 'create',
|
|
107
|
+
item,
|
|
108
|
+
issueNumber: null,
|
|
109
|
+
detail: null,
|
|
110
|
+
addLabels: [statusLabel(item.status)],
|
|
111
|
+
removeLabels: [],
|
|
112
|
+
newBody: renderGeneratedRegion(item, bodyContext),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
if (!isEligible) {
|
|
117
|
+
if (retireIneligible) {
|
|
118
|
+
return [line('close', item, issue.number, `${item.status} is not a projected status`)];
|
|
119
|
+
}
|
|
120
|
+
return [
|
|
121
|
+
line('ineligible-with-issue', item, issue.number, `item is in ${item.status}, which is not projected; the issue is still open`),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
if (item.status !== 'done' && issue.state === 'CLOSED') {
|
|
125
|
+
return [
|
|
126
|
+
line('state-divergent', item, issue.number, `issue is closed but the item is still in ${item.status} — left alone`),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
const collected = [];
|
|
130
|
+
if (issue.title !== item.title) {
|
|
131
|
+
collected.push(line('update-title', item, issue.number, null));
|
|
132
|
+
}
|
|
133
|
+
const wanted = statusLabel(item.status);
|
|
134
|
+
const stale = issue.labels.filter((l) => isStatusLabel(l) && l !== wanted);
|
|
135
|
+
const hasWanted = issue.labels.includes(wanted);
|
|
136
|
+
if (!hasWanted || stale.length > 0) {
|
|
137
|
+
collected.push({
|
|
138
|
+
outcome: 'update-labels',
|
|
139
|
+
item,
|
|
140
|
+
issueNumber: issue.number,
|
|
141
|
+
detail: null,
|
|
142
|
+
addLabels: hasWanted ? [] : [wanted],
|
|
143
|
+
removeLabels: stale,
|
|
144
|
+
newBody: null,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Region-to-region comparison, not body-to-body: the whole point of the delimiters is
|
|
148
|
+
// that text outside them is not the projection's to have an opinion about, so an issue
|
|
149
|
+
// somebody added a note to must still read as `unchanged`.
|
|
150
|
+
const region = renderGeneratedRegion(item, bodyContext);
|
|
151
|
+
if (extractRegion(issue.body) !== region) {
|
|
152
|
+
collected.push({
|
|
153
|
+
outcome: 'update-body',
|
|
154
|
+
item,
|
|
155
|
+
issueNumber: issue.number,
|
|
156
|
+
detail: null,
|
|
157
|
+
addLabels: [],
|
|
158
|
+
removeLabels: [],
|
|
159
|
+
newBody: mergeBody(issue.body, region, item.id),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (item.status === 'done' && issue.state === 'OPEN') {
|
|
163
|
+
collected.push(line('close', item, issue.number, null));
|
|
164
|
+
}
|
|
165
|
+
if (collected.length === 0) {
|
|
166
|
+
return [line('unchanged', item, issue.number, null)];
|
|
167
|
+
}
|
|
168
|
+
return collected;
|
|
169
|
+
}
|
|
170
|
+
function line(outcome, item, issueNumber, detail) {
|
|
171
|
+
return { outcome, item, issueNumber, detail, addLabels: [], removeLabels: [], newBody: null };
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Indexing and refusal messages
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
/** Two items sharing an id would plan opposite writes against one issue, every run. */
|
|
177
|
+
function findDuplicateIds(items) {
|
|
178
|
+
const seen = new Map();
|
|
179
|
+
const clashes = [];
|
|
180
|
+
for (const item of items) {
|
|
181
|
+
const prior = seen.get(item.id);
|
|
182
|
+
if (prior === undefined) {
|
|
183
|
+
seen.set(item.id, item);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
clashes.push(` ${item.id}\n ${prior.status}/${prior.filename}\n ${item.status}/${item.filename}`);
|
|
187
|
+
}
|
|
188
|
+
if (clashes.length === 0)
|
|
189
|
+
return null;
|
|
190
|
+
return ('two roadmap items share one id, so the projection cannot tell which one an issue ' +
|
|
191
|
+
'belongs to:\n' +
|
|
192
|
+
clashes.join('\n') +
|
|
193
|
+
'\nRemove or re-id one of each pair, then run again.');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Index issues by the item id in their marker.
|
|
197
|
+
*
|
|
198
|
+
* Two issues carrying one marker is refused rather than resolved: picking either would
|
|
199
|
+
* mean confidently retitling or closing the wrong one.
|
|
200
|
+
*/
|
|
201
|
+
function indexByMarker(issues) {
|
|
202
|
+
const byId = new Map();
|
|
203
|
+
const clashes = [];
|
|
204
|
+
for (const issue of issues) {
|
|
205
|
+
for (const id of findMarkerIds(issue.body)) {
|
|
206
|
+
const prior = byId.get(id);
|
|
207
|
+
if (prior === undefined) {
|
|
208
|
+
byId.set(id, issue);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
clashes.push(` ${id} — issues #${prior.number} and #${issue.number}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (clashes.length > 0) {
|
|
215
|
+
return {
|
|
216
|
+
kind: 'ambiguous',
|
|
217
|
+
error: 'more than one issue carries the same aidlc-item marker, so the projection cannot ' +
|
|
218
|
+
'tell which is canonical:\n' +
|
|
219
|
+
clashes.join('\n') +
|
|
220
|
+
'\nRemove the marker from whichever issue is not the canonical one, then run again.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { kind: 'ok', byId };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The unprojectable-item refusal.
|
|
227
|
+
*
|
|
228
|
+
* Split by class, because the remedies differ and only one of them has a tool. Sending
|
|
229
|
+
* somebody to `aidlc doctor` for a missing `id` would send them to a command that reports
|
|
230
|
+
* nothing at all.
|
|
231
|
+
*/
|
|
232
|
+
function unprojectableMessage(blocking) {
|
|
233
|
+
const repairable = blocking.filter((u) => u.cls === 'unparseable');
|
|
234
|
+
const manual = blocking.filter((u) => u.cls === 'missing-id');
|
|
235
|
+
const parts = [
|
|
236
|
+
'these roadmap items are in a projected status but cannot be read, so the projection ' +
|
|
237
|
+
'would silently leave them out of both the repo and the tracker:',
|
|
238
|
+
];
|
|
239
|
+
if (repairable.length > 0) {
|
|
240
|
+
parts.push('', ' frontmatter will not parse — run `aidlc doctor`, which repairs the common case ' +
|
|
241
|
+
'(an unquoted title):', ...repairable.map((u) => ` ${u.label} — ${u.reason}`));
|
|
242
|
+
}
|
|
243
|
+
if (manual.length > 0) {
|
|
244
|
+
parts.push('', ' no `id:` — `aidlc doctor` does not detect this class; add an id by hand:', ...manual.map((u) => ` ${u.label} — ${u.reason}`));
|
|
245
|
+
}
|
|
246
|
+
return parts.join('\n');
|
|
247
|
+
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Assembly and ordering
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
/**
|
|
252
|
+
* Total order (so two runs over identical inputs print byte-identically):
|
|
253
|
+
* item lines by status then filename then execution order; issue-only lines after them,
|
|
254
|
+
* by issue number.
|
|
255
|
+
*
|
|
256
|
+
* The outcome component is compared through `WRITE_ORDER`, not as a string, so two lines
|
|
257
|
+
* for one item appear in the order they will be executed rather than in alphabetical
|
|
258
|
+
* accident.
|
|
259
|
+
*/
|
|
260
|
+
function assemble(lines, config, repoLabel) {
|
|
261
|
+
const sorted = [...lines].sort(compareLines);
|
|
262
|
+
const counts = {};
|
|
263
|
+
let writeCount = 0;
|
|
264
|
+
for (const l of sorted) {
|
|
265
|
+
counts[l.outcome] = (counts[l.outcome] ?? 0) + 1;
|
|
266
|
+
if (isWrite(l.outcome))
|
|
267
|
+
writeCount += 1;
|
|
268
|
+
}
|
|
269
|
+
return { repoLabel, repo: config.repo, lines: sorted, counts, writeCount };
|
|
270
|
+
}
|
|
271
|
+
function compareLines(a, b) {
|
|
272
|
+
if (a.item === null && b.item === null)
|
|
273
|
+
return (a.issueNumber ?? 0) - (b.issueNumber ?? 0);
|
|
274
|
+
if (a.item === null)
|
|
275
|
+
return 1;
|
|
276
|
+
if (b.item === null)
|
|
277
|
+
return -1;
|
|
278
|
+
const statusDelta = ROADMAP_STATUSES.indexOf(a.item.status) - ROADMAP_STATUSES.indexOf(b.item.status);
|
|
279
|
+
if (statusDelta !== 0)
|
|
280
|
+
return statusDelta;
|
|
281
|
+
if (a.item.filename !== b.item.filename)
|
|
282
|
+
return a.item.filename < b.item.filename ? -1 : 1;
|
|
283
|
+
return outcomeRank(a.outcome) - outcomeRank(b.outcome);
|
|
284
|
+
}
|
|
285
|
+
/** Execution order first, then everything else in a fixed position. */
|
|
286
|
+
function outcomeRank(outcome) {
|
|
287
|
+
const index = WRITE_ORDER.indexOf(outcome);
|
|
288
|
+
return index === -1 ? WRITE_ORDER.length : index;
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../src/roadmap/sync/plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan → text.
|
|
3
|
+
*
|
|
4
|
+
* Two rules the plan output has to keep:
|
|
5
|
+
*
|
|
6
|
+
* - **The resolved repository is named in the header.** A well-formed but wrong
|
|
7
|
+
* repository coordinate publishes the roadmap to somebody else's repo, and that is only
|
|
8
|
+
* reversible before the first write. Printing it is the whole mitigation.
|
|
9
|
+
* - **Every outcome prints as a word.** Colour, if ever added, is decoration; the
|
|
10
|
+
* accessibility guidance forbids colour as the sole carrier of meaning, and a piped or
|
|
11
|
+
* logged plan has no colour at all.
|
|
12
|
+
*
|
|
13
|
+
* Requirements: roadmap-issue-projection/AC-40
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { type Plan } from './types.js';
|
|
18
|
+
export declare function renderPlan(plan: Plan): string;
|
|
19
|
+
/**
|
|
20
|
+
* The counts, writes first.
|
|
21
|
+
*
|
|
22
|
+
* Printed before consent so a first run against a mature roadmap — forty creates — is a
|
|
23
|
+
* visible decision rather than a surprise.
|
|
24
|
+
*/
|
|
25
|
+
export declare function summary(plan: Plan): string;
|
|
26
|
+
/** The confirmation question. Names the repository again, deliberately. */
|
|
27
|
+
export declare function confirmMessage(plan: Plan): string;
|
|
28
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAyB,KAAK,IAAI,EAAiB,MAAM,YAAY,CAAC;AAgC7E,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAoB7C;AAwBD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAiB1C;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAEjD"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan → text.
|
|
3
|
+
*
|
|
4
|
+
* Two rules the plan output has to keep:
|
|
5
|
+
*
|
|
6
|
+
* - **The resolved repository is named in the header.** A well-formed but wrong
|
|
7
|
+
* repository coordinate publishes the roadmap to somebody else's repo, and that is only
|
|
8
|
+
* reversible before the first write. Printing it is the whole mitigation.
|
|
9
|
+
* - **Every outcome prints as a word.** Colour, if ever added, is decoration; the
|
|
10
|
+
* accessibility guidance forbids colour as the sole carrier of meaning, and a piped or
|
|
11
|
+
* logged plan has no colour at all.
|
|
12
|
+
*
|
|
13
|
+
* Requirements: roadmap-issue-projection/AC-40
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { isWrite } from './types.js';
|
|
18
|
+
/** Display order for the counts summary: writes first, then reports. */
|
|
19
|
+
const COUNT_ORDER = [
|
|
20
|
+
'create',
|
|
21
|
+
'update-title',
|
|
22
|
+
'update-labels',
|
|
23
|
+
'update-body',
|
|
24
|
+
'close',
|
|
25
|
+
'unchanged',
|
|
26
|
+
'skip',
|
|
27
|
+
'state-divergent',
|
|
28
|
+
'ineligible-with-issue',
|
|
29
|
+
'orphan-missing',
|
|
30
|
+
'label-without-marker',
|
|
31
|
+
];
|
|
32
|
+
/** One-line human gloss per outcome, so the plan reads without a legend. */
|
|
33
|
+
const GLOSS = {
|
|
34
|
+
create: 'create an issue',
|
|
35
|
+
'update-title': 'update the issue title',
|
|
36
|
+
'update-labels': 'update the aidlc: status label',
|
|
37
|
+
'update-body': 'refresh the projected description',
|
|
38
|
+
close: 'close the issue',
|
|
39
|
+
unchanged: 'already matches',
|
|
40
|
+
skip: 'not projected',
|
|
41
|
+
'state-divergent': 'closed remotely, item still live — left alone',
|
|
42
|
+
'ineligible-with-issue': 'status no longer projected, issue still open',
|
|
43
|
+
'orphan-missing': 'no item on disk',
|
|
44
|
+
'label-without-marker': 'labelled but not projected by aidlc',
|
|
45
|
+
};
|
|
46
|
+
export function renderPlan(plan) {
|
|
47
|
+
const out = [];
|
|
48
|
+
out.push(`Roadmap projection plan — ${plan.repoLabel}`);
|
|
49
|
+
out.push('');
|
|
50
|
+
if (plan.lines.length === 0) {
|
|
51
|
+
out.push(' nothing to project: no items in a projected status, and no labelled issues.');
|
|
52
|
+
out.push('');
|
|
53
|
+
return out.join('\n');
|
|
54
|
+
}
|
|
55
|
+
for (const line of plan.lines) {
|
|
56
|
+
out.push(` ${renderLine(line)}`);
|
|
57
|
+
}
|
|
58
|
+
out.push('');
|
|
59
|
+
out.push(summary(plan));
|
|
60
|
+
out.push('');
|
|
61
|
+
return out.join('\n');
|
|
62
|
+
}
|
|
63
|
+
function renderLine(line) {
|
|
64
|
+
const subject = line.item === null
|
|
65
|
+
? `#${line.issueNumber}`
|
|
66
|
+
: `${line.item.status}/${line.item.filename}` +
|
|
67
|
+
(line.issueNumber === null ? '' : ` → #${line.issueNumber}`);
|
|
68
|
+
const parts = [pad(line.outcome), subject];
|
|
69
|
+
const notes = [];
|
|
70
|
+
if (line.addLabels.length > 0)
|
|
71
|
+
notes.push(`+${line.addLabels.join(' +')}`);
|
|
72
|
+
if (line.removeLabels.length > 0)
|
|
73
|
+
notes.push(`-${line.removeLabels.join(' -')}`);
|
|
74
|
+
if (line.detail !== null)
|
|
75
|
+
notes.push(line.detail);
|
|
76
|
+
if (notes.length > 0)
|
|
77
|
+
parts.push(`(${notes.join('; ')})`);
|
|
78
|
+
return parts.join(' ');
|
|
79
|
+
}
|
|
80
|
+
/** Left-pad the outcome word so the subjects line up without a table. */
|
|
81
|
+
function pad(outcome) {
|
|
82
|
+
const width = Math.max(...COUNT_ORDER.map((o) => o.length));
|
|
83
|
+
return outcome.padEnd(width, ' ');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The counts, writes first.
|
|
87
|
+
*
|
|
88
|
+
* Printed before consent so a first run against a mature roadmap — forty creates — is a
|
|
89
|
+
* visible decision rather than a surprise.
|
|
90
|
+
*/
|
|
91
|
+
export function summary(plan) {
|
|
92
|
+
const present = COUNT_ORDER.filter((o) => (plan.counts[o] ?? 0) > 0);
|
|
93
|
+
const writes = present.filter(isWrite);
|
|
94
|
+
const reports = present.filter((o) => !isWrite(o));
|
|
95
|
+
const lines = [];
|
|
96
|
+
lines.push(plan.writeCount === 0
|
|
97
|
+
? ' No writes. Nothing on GitHub would change.'
|
|
98
|
+
: ` ${plan.writeCount} write${plan.writeCount === 1 ? '' : 's'}:`);
|
|
99
|
+
for (const o of writes)
|
|
100
|
+
lines.push(` ${plan.counts[o]} × ${o} — ${GLOSS[o]}`);
|
|
101
|
+
if (reports.length > 0) {
|
|
102
|
+
lines.push(' Reported only:');
|
|
103
|
+
for (const o of reports)
|
|
104
|
+
lines.push(` ${plan.counts[o]} × ${o} — ${GLOSS[o]}`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
/** The confirmation question. Names the repository again, deliberately. */
|
|
109
|
+
export function confirmMessage(plan) {
|
|
110
|
+
return `Apply ${plan.writeCount} write${plan.writeCount === 1 ? '' : 's'} to ${plan.repoLabel}?`;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/roadmap/sync/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make one item's prose safe to publish, and make its references clickable.
|
|
3
|
+
*
|
|
4
|
+
* The body used to carry no item prose at all, and the reason was three real hazards. Two
|
|
5
|
+
* of them are properties of GitHub's renderer and one is a property of this feature's own
|
|
6
|
+
* marker scanner:
|
|
7
|
+
*
|
|
8
|
+
* - **Marker injection.** The item↔issue link is an HTML comment. An item that quotes one
|
|
9
|
+
* would stamp a second marker onto a different issue, and `indexByMarker` then refuses
|
|
10
|
+
* the whole run as ambiguous — or, before that guard existed, retitled the wrong issue.
|
|
11
|
+
* - **Notification side effects.** `@name` and `#123` notify people and cross-link issues
|
|
12
|
+
* the instant they are published. A sync is not a reason for somebody's inbox to move.
|
|
13
|
+
* - **Field limits.** GitHub caps a body at 65,536 characters, so the prose needs a budget
|
|
14
|
+
* (that half lives in `body.ts`).
|
|
15
|
+
*
|
|
16
|
+
* The rejected alternative was to publish nothing and make the reader follow a link, which
|
|
17
|
+
* is what shipped in v1.21.0 and what this instance exists to undo: a body carrying no
|
|
18
|
+
* item-specific words is a redirect, and thirty of them on a board are thirty copies of one
|
|
19
|
+
* paragraph.
|
|
20
|
+
*
|
|
21
|
+
* **Everything here is code-aware, and that is the whole design.** Measured over this
|
|
22
|
+
* project's 76 items on 2026-09-07, 40 of the 41 hazard spans are inside a fence or an
|
|
23
|
+
* inline code span — `@clack/prompts`, `actions/checkout@v4`, colour hexes like `#0891b2`
|
|
24
|
+
* — and exactly one is in prose. A rule that ignored code would corrupt forty spans in
|
|
25
|
+
* order to defuse one. See `markdown.ts`.
|
|
26
|
+
*
|
|
27
|
+
* Requirements: richer-issue-bodies/AC-9, richer-issue-bodies/AC-10,
|
|
28
|
+
* richer-issue-bodies/AC-11, richer-issue-bodies/AC-12, richer-issue-bodies/AC-29
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
export interface SanitizeContext {
|
|
33
|
+
/** Item id → repo-relative path. An empty map disables reference linking entirely. */
|
|
34
|
+
itemPaths: ReadonlyMap<string, string>;
|
|
35
|
+
/** Repo-relative path → absolute URL, or null when coordinates are unknown. */
|
|
36
|
+
fileUrl: ((relPath: string) => string) | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize and enrich an item body for publication.
|
|
40
|
+
*
|
|
41
|
+
* Four rules, and the **order is load-bearing**, because two of them insert text a later
|
|
42
|
+
* rule must not re-examine:
|
|
43
|
+
*
|
|
44
|
+
* 1. Item references become links. This runs first because it is the only rule that
|
|
45
|
+
* deliberately rewrites a code span, and because the link it emits contains no `@`, `#`
|
|
46
|
+
* or `<!--` for rules 2–4 to find.
|
|
47
|
+
* 2. Prose HTML comments are removed — the marker-injection defence, and lossless, since a
|
|
48
|
+
* comment renders as nothing.
|
|
49
|
+
* 3. Prose mentions are wrapped in backticks.
|
|
50
|
+
* 4. Prose issue references are wrapped in backticks.
|
|
51
|
+
*
|
|
52
|
+
* Rules 2–4 run through `mapProse`, so code is untouched by construction rather than by a
|
|
53
|
+
* filter that could be got wrong.
|
|
54
|
+
*/
|
|
55
|
+
export declare function sanitizeItemBody(body: string, ctx: SanitizeContext): string;
|
|
56
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAsBH,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,+EAA+E;IAC/E,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,MAAM,CAG3E"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make one item's prose safe to publish, and make its references clickable.
|
|
3
|
+
*
|
|
4
|
+
* The body used to carry no item prose at all, and the reason was three real hazards. Two
|
|
5
|
+
* of them are properties of GitHub's renderer and one is a property of this feature's own
|
|
6
|
+
* marker scanner:
|
|
7
|
+
*
|
|
8
|
+
* - **Marker injection.** The item↔issue link is an HTML comment. An item that quotes one
|
|
9
|
+
* would stamp a second marker onto a different issue, and `indexByMarker` then refuses
|
|
10
|
+
* the whole run as ambiguous — or, before that guard existed, retitled the wrong issue.
|
|
11
|
+
* - **Notification side effects.** `@name` and `#123` notify people and cross-link issues
|
|
12
|
+
* the instant they are published. A sync is not a reason for somebody's inbox to move.
|
|
13
|
+
* - **Field limits.** GitHub caps a body at 65,536 characters, so the prose needs a budget
|
|
14
|
+
* (that half lives in `body.ts`).
|
|
15
|
+
*
|
|
16
|
+
* The rejected alternative was to publish nothing and make the reader follow a link, which
|
|
17
|
+
* is what shipped in v1.21.0 and what this instance exists to undo: a body carrying no
|
|
18
|
+
* item-specific words is a redirect, and thirty of them on a board are thirty copies of one
|
|
19
|
+
* paragraph.
|
|
20
|
+
*
|
|
21
|
+
* **Everything here is code-aware, and that is the whole design.** Measured over this
|
|
22
|
+
* project's 76 items on 2026-09-07, 40 of the 41 hazard spans are inside a fence or an
|
|
23
|
+
* inline code span — `@clack/prompts`, `actions/checkout@v4`, colour hexes like `#0891b2`
|
|
24
|
+
* — and exactly one is in prose. A rule that ignored code would corrupt forty spans in
|
|
25
|
+
* order to defuse one. See `markdown.ts`.
|
|
26
|
+
*
|
|
27
|
+
* Requirements: richer-issue-bodies/AC-9, richer-issue-bodies/AC-10,
|
|
28
|
+
* richer-issue-bodies/AC-11, richer-issue-bodies/AC-12, richer-issue-bodies/AC-29
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
import { codeMask, inlineCodeSpans, mapProse } from './markdown.js';
|
|
33
|
+
/** An item id, as the roadmap skill mints them. */
|
|
34
|
+
const ITEM_ID_RE = /item-\d{8}-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/g;
|
|
35
|
+
/**
|
|
36
|
+
* An HTML comment, terminated or not.
|
|
37
|
+
*
|
|
38
|
+
* The unterminated alternative matters: GitHub's renderer swallows everything after a bare
|
|
39
|
+
* `<!--` to the end of the document, so leaving one in would blank the rest of the body.
|
|
40
|
+
* Dropping it is both safer and closer to what a reader would have seen anyway.
|
|
41
|
+
*/
|
|
42
|
+
const HTML_COMMENT_RE = /<!--[\s\S]*?-->|<!--[\s\S]*$/g;
|
|
43
|
+
/** A mention GitHub would linkify. Requires a letter or digit after the `@`. */
|
|
44
|
+
const MENTION_RE = /@[A-Za-z0-9][-A-Za-z0-9]*/g;
|
|
45
|
+
/** An issue or pull-request cross-reference GitHub would link and notify. */
|
|
46
|
+
const ISSUE_REF_RE = /#[0-9]+/g;
|
|
47
|
+
/**
|
|
48
|
+
* Sanitize and enrich an item body for publication.
|
|
49
|
+
*
|
|
50
|
+
* Four rules, and the **order is load-bearing**, because two of them insert text a later
|
|
51
|
+
* rule must not re-examine:
|
|
52
|
+
*
|
|
53
|
+
* 1. Item references become links. This runs first because it is the only rule that
|
|
54
|
+
* deliberately rewrites a code span, and because the link it emits contains no `@`, `#`
|
|
55
|
+
* or `<!--` for rules 2–4 to find.
|
|
56
|
+
* 2. Prose HTML comments are removed — the marker-injection defence, and lossless, since a
|
|
57
|
+
* comment renders as nothing.
|
|
58
|
+
* 3. Prose mentions are wrapped in backticks.
|
|
59
|
+
* 4. Prose issue references are wrapped in backticks.
|
|
60
|
+
*
|
|
61
|
+
* Rules 2–4 run through `mapProse`, so code is untouched by construction rather than by a
|
|
62
|
+
* filter that could be got wrong.
|
|
63
|
+
*/
|
|
64
|
+
export function sanitizeItemBody(body, ctx) {
|
|
65
|
+
const linked = linkItemReferences(body, ctx);
|
|
66
|
+
return mapProse(linked, defuseProse);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Apply rules 2–4 to one prose run.
|
|
70
|
+
*
|
|
71
|
+
* Comments go first: a marker *is* a comment, so removing comments is what closes the
|
|
72
|
+
* injection hazard, and doing it before the backtick rules means a `@name` inside a
|
|
73
|
+
* comment is deleted rather than wrapped.
|
|
74
|
+
*/
|
|
75
|
+
function defuseProse(prose) {
|
|
76
|
+
return prose
|
|
77
|
+
.replace(HTML_COMMENT_RE, '')
|
|
78
|
+
.replace(MENTION_RE, (m) => `\`${m}\``)
|
|
79
|
+
.replace(ISSUE_REF_RE, (m) => `\`${m}\``);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Turn item references into links, in code spans and in prose alike.
|
|
83
|
+
*
|
|
84
|
+
* All 32 references in the real corpus are inline code spans whose **entire** content is
|
|
85
|
+
* the id (`` `item-20260902-review-gate-vs-tasks-review` ``) — measured, and the reason
|
|
86
|
+
* the first draft of this rule was retired by Amendment 1: a prose-only rule would never
|
|
87
|
+
* have fired once.
|
|
88
|
+
*
|
|
89
|
+
* The rewrite is deliberately narrow. Only a span whose whole content is a known id is
|
|
90
|
+
* touched, and the replacement re-wraps the same content in the same number of backticks
|
|
91
|
+
* inside a link, so a multi-backtick span stays valid. An id with no entry in `itemPaths`
|
|
92
|
+
* is left byte-identical, because an unresolvable link is worse than no link.
|
|
93
|
+
*/
|
|
94
|
+
function linkItemReferences(body, ctx) {
|
|
95
|
+
const { itemPaths, fileUrl } = ctx;
|
|
96
|
+
if (fileUrl === null || itemPaths.size === 0)
|
|
97
|
+
return body;
|
|
98
|
+
const edits = [];
|
|
99
|
+
// Code spans whose entire content is a known id.
|
|
100
|
+
const spans = inlineCodeSpans(body);
|
|
101
|
+
for (const span of spans) {
|
|
102
|
+
const relPath = itemPaths.get(span.content);
|
|
103
|
+
if (relPath === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
const ticks = '`'.repeat(span.ticks);
|
|
106
|
+
edits.push({
|
|
107
|
+
start: span.start,
|
|
108
|
+
end: span.end,
|
|
109
|
+
text: `[${ticks}${span.content}${ticks}](${fileUrl(relPath)})`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Bare ids in prose. The mask — not `spans` — decides what counts as prose here:
|
|
113
|
+
// `inlineCodeSpans` deliberately omits spans inside fenced blocks, so testing against it
|
|
114
|
+
// alone would rewrite an id sitting in a code sample and corrupt the block.
|
|
115
|
+
const mask = codeMask(body);
|
|
116
|
+
ITEM_ID_RE.lastIndex = 0;
|
|
117
|
+
let match;
|
|
118
|
+
while ((match = ITEM_ID_RE.exec(body)) !== null) {
|
|
119
|
+
if (mask[match.index] === 1)
|
|
120
|
+
continue;
|
|
121
|
+
const relPath = itemPaths.get(match[0]);
|
|
122
|
+
if (relPath === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
edits.push({
|
|
125
|
+
start: match.index,
|
|
126
|
+
end: match.index + match[0].length,
|
|
127
|
+
text: `[\`${match[0]}\`](${fileUrl(relPath)})`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (edits.length === 0)
|
|
131
|
+
return body;
|
|
132
|
+
// Right to left, so earlier offsets stay valid as later text is spliced in.
|
|
133
|
+
edits.sort((a, b) => b.start - a.start);
|
|
134
|
+
let out = body;
|
|
135
|
+
for (const edit of edits) {
|
|
136
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=sanitize.js.map
|