@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.
Files changed (71) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +2 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/roadmap.d.ts +66 -0
  5. package/dist/commands/roadmap.d.ts.map +1 -0
  6. package/dist/commands/roadmap.js +206 -0
  7. package/dist/commands/roadmap.js.map +1 -0
  8. package/dist/menu/roster.d.ts.map +1 -1
  9. package/dist/menu/roster.js +4 -0
  10. package/dist/menu/roster.js.map +1 -1
  11. package/dist/roadmap/paths.d.ts +21 -0
  12. package/dist/roadmap/paths.d.ts.map +1 -1
  13. package/dist/roadmap/paths.js +17 -1
  14. package/dist/roadmap/paths.js.map +1 -1
  15. package/dist/roadmap/sync/body.d.ts +123 -0
  16. package/dist/roadmap/sync/body.d.ts.map +1 -0
  17. package/dist/roadmap/sync/body.js +289 -0
  18. package/dist/roadmap/sync/body.js.map +1 -0
  19. package/dist/roadmap/sync/collect.d.ts +54 -0
  20. package/dist/roadmap/sync/collect.d.ts.map +1 -0
  21. package/dist/roadmap/sync/collect.js +138 -0
  22. package/dist/roadmap/sync/collect.js.map +1 -0
  23. package/dist/roadmap/sync/config.d.ts +95 -0
  24. package/dist/roadmap/sync/config.d.ts.map +1 -0
  25. package/dist/roadmap/sync/config.js +236 -0
  26. package/dist/roadmap/sync/config.js.map +1 -0
  27. package/dist/roadmap/sync/discover.d.ts +56 -0
  28. package/dist/roadmap/sync/discover.d.ts.map +1 -0
  29. package/dist/roadmap/sync/discover.js +126 -0
  30. package/dist/roadmap/sync/discover.js.map +1 -0
  31. package/dist/roadmap/sync/execute.d.ts +41 -0
  32. package/dist/roadmap/sync/execute.d.ts.map +1 -0
  33. package/dist/roadmap/sync/execute.js +174 -0
  34. package/dist/roadmap/sync/execute.js.map +1 -0
  35. package/dist/roadmap/sync/gh.d.ts +74 -0
  36. package/dist/roadmap/sync/gh.d.ts.map +1 -0
  37. package/dist/roadmap/sync/gh.js +110 -0
  38. package/dist/roadmap/sync/gh.js.map +1 -0
  39. package/dist/roadmap/sync/import.d.ts +73 -0
  40. package/dist/roadmap/sync/import.d.ts.map +1 -0
  41. package/dist/roadmap/sync/import.js +269 -0
  42. package/dist/roadmap/sync/import.js.map +1 -0
  43. package/dist/roadmap/sync/labels.d.ts +57 -0
  44. package/dist/roadmap/sync/labels.d.ts.map +1 -0
  45. package/dist/roadmap/sync/labels.js +80 -0
  46. package/dist/roadmap/sync/labels.js.map +1 -0
  47. package/dist/roadmap/sync/markdown.d.ts +79 -0
  48. package/dist/roadmap/sync/markdown.d.ts.map +1 -0
  49. package/dist/roadmap/sync/markdown.js +165 -0
  50. package/dist/roadmap/sync/markdown.js.map +1 -0
  51. package/dist/roadmap/sync/marker.d.ts +47 -0
  52. package/dist/roadmap/sync/marker.d.ts.map +1 -0
  53. package/dist/roadmap/sync/marker.js +73 -0
  54. package/dist/roadmap/sync/marker.js.map +1 -0
  55. package/dist/roadmap/sync/plan.d.ts +47 -0
  56. package/dist/roadmap/sync/plan.d.ts.map +1 -0
  57. package/dist/roadmap/sync/plan.js +290 -0
  58. package/dist/roadmap/sync/plan.js.map +1 -0
  59. package/dist/roadmap/sync/render.d.ts +28 -0
  60. package/dist/roadmap/sync/render.d.ts.map +1 -0
  61. package/dist/roadmap/sync/render.js +112 -0
  62. package/dist/roadmap/sync/render.js.map +1 -0
  63. package/dist/roadmap/sync/sanitize.d.ts +56 -0
  64. package/dist/roadmap/sync/sanitize.d.ts.map +1 -0
  65. package/dist/roadmap/sync/sanitize.js +140 -0
  66. package/dist/roadmap/sync/sanitize.js.map +1 -0
  67. package/dist/roadmap/sync/types.d.ts +139 -0
  68. package/dist/roadmap/sync/types.d.ts.map +1 -0
  69. package/dist/roadmap/sync/types.js +50 -0
  70. package/dist/roadmap/sync/types.js.map +1 -0
  71. 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