@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,289 @@
1
+ /**
2
+ * The body of a projected issue.
3
+ *
4
+ * v1.21.0 emitted a fixed six-line notice carrying no item prose at all, on the grounds
5
+ * that projecting prose risked marker injection, mention notifications, and the 65,536
6
+ * character field limit. The hazards were real; the conclusion was not. What shipped was a
7
+ * redirect — thirty issues on a board with thirty copies of one paragraph, unreadable to
8
+ * exactly the person the feature was for: somebody triaging in the tracker who will not
9
+ * clone the repository. `richer-issue-bodies` answers all three hazards instead of avoiding
10
+ * them: `sanitize.ts` neutralises comments and autolink triggers code-awarely, and the
11
+ * budget below bounds the field.
12
+ *
13
+ * Two structural decisions carry the rest of the design.
14
+ *
15
+ * **The region is delimited, and everything outside it is somebody else's.** A body is
16
+ * regenerated on every sync, so it must be possible to say precisely which bytes the
17
+ * projection owns. Text before `BODY_BEGIN` or after `BODY_END` survives untouched, which
18
+ * is what makes a projected issue still usable as a tracker: a reader can add a note to the
19
+ * description and keep it.
20
+ *
21
+ * **The body is rewritten, not written once.** v1.21.0 froze it at creation, reasoning that
22
+ * an update "would delete the context around [comments]". That does not hold — GitHub issue
23
+ * comments are separate entities from the body, and a body edit keeps its own revision
24
+ * history — and the cost of freezing is that every issue created before this change stays
25
+ * thin forever while every later item edit makes its issue quietly wrong. The rejected
26
+ * alternative was a one-off migration command; this project has a recorded lesson that a
27
+ * capability needing a manual post-deploy command will be un-run.
28
+ *
29
+ * Nothing volatile may enter this module's output. A timestamp, a commit sha or a metric
30
+ * would make the rendered region differ from the stored one on every single run, so every
31
+ * item would plan `update-body` forever and the projection would never converge — the same
32
+ * defect class as `discover.ts`'s labels-are-objects bug. That is why the region is a pure
33
+ * function of the item and the repository coordinates, and why a test pins that this file
34
+ * imports neither `node:fs` nor `node:child_process`.
35
+ *
36
+ * Requirements: richer-issue-bodies/AC-1 … richer-issue-bodies/AC-4,
37
+ * richer-issue-bodies/AC-6, richer-issue-bodies/AC-7, richer-issue-bodies/AC-16,
38
+ * richer-issue-bodies/AC-17, richer-issue-bodies/AC-21 … richer-issue-bodies/AC-24,
39
+ * richer-issue-bodies/AC-28
40
+ *
41
+ * @module
42
+ */
43
+ import { lastProseBreakBefore } from './markdown.js';
44
+ import { renderMarker } from './marker.js';
45
+ import { sanitizeItemBody } from './sanitize.js';
46
+ /** Start of the projection-owned region. */
47
+ export const BODY_BEGIN = '<!-- aidlc-body:begin -->';
48
+ /** End of the projection-owned region. */
49
+ export const BODY_END = '<!-- aidlc-body:end -->';
50
+ /**
51
+ * The ceiling this module will not cross.
52
+ *
53
+ * GitHub's own limit is 65,536. The margin is not decoration: the largest real item in this
54
+ * project's roadmap is 44,481 characters, so nothing today comes close, and the headroom is
55
+ * what keeps a future item from failing a write *after* consent was given — the one moment
56
+ * in the projection where a failure is expensive.
57
+ */
58
+ export const MAX_BODY_LENGTH = 60_000;
59
+ /**
60
+ * A permalink to a file on the default branch.
61
+ *
62
+ * `blob/HEAD/` rather than a resolved branch name, and this was measured rather than
63
+ * reasoned: on 2026-09-07, `github.com/cli/cli/blob/HEAD/README.md` returned 200 while
64
+ * `blob/main/README.md` returned 404, because that repository's default branch is `trunk`.
65
+ * Hardcoding `main` would ship broken links to every project that renamed its default
66
+ * branch. `HEAD` also needs no extra `gh` call and puts no remote state in the body.
67
+ */
68
+ export function fileUrlFor(repo, relPath) {
69
+ return `https://github.com/${repo.owner}/${repo.name}/blob/HEAD/${relPath}`;
70
+ }
71
+ /** The context's URL builder, or null when coordinates are unknown. */
72
+ function urlBuilder(ctx) {
73
+ const { repo } = ctx;
74
+ return repo === null ? null : (relPath) => fileUrlFor(repo, relPath);
75
+ }
76
+ /**
77
+ * The projection-owned region, delimiters included.
78
+ *
79
+ * Facts first, because a triager reads the top of a body the way they read the top of an
80
+ * item. The marker sits *inside* the region, so replacing the region can never orphan or
81
+ * duplicate it.
82
+ */
83
+ export function renderGeneratedRegion(item, ctx) {
84
+ const url = urlBuilder(ctx);
85
+ // Assemble around a prose slot, so the fixed cost is measured rather than estimated.
86
+ const assemble = (prose) => [
87
+ BODY_BEGIN,
88
+ '',
89
+ renderFacts(item, ctx, url),
90
+ '',
91
+ '---',
92
+ '',
93
+ prose,
94
+ '',
95
+ '---',
96
+ '',
97
+ renderFooter(),
98
+ '',
99
+ renderMarker(item.id),
100
+ BODY_END,
101
+ // No trailing newline. `extractRegion` ends AT `BODY_END`, so a region that ended one
102
+ // byte later would never compare equal to the one read back off an issue — every item
103
+ // would plan `update-body` on every run and the projection would never converge. The
104
+ // convergence test is what caught it; the two functions have to agree byte for byte.
105
+ ].join('\n');
106
+ // Trimmed: an item body starts with the newline that followed its closing `---`, and the
107
+ // assembly already puts blank lines around the prose slot. Without this the region renders
108
+ // with a double blank line above the first heading — visible in the first real sample,
109
+ // invisible in every fixture, which is why generated prose gets read as rendered output.
110
+ const sanitized = sanitizeItemBody(item.body, { itemPaths: ctx.itemPaths, fileUrl: url }).trim();
111
+ return assemble(fitProse(sanitized, item, url, assemble('').length));
112
+ }
113
+ /**
114
+ * The facts block.
115
+ *
116
+ * `not recorded` for a key the frontmatter does not carry — an admitted gap a reader can
117
+ * tell apart from a real value, which an empty cell cannot. `promoted_to` is the one
118
+ * exception: absent and explicitly `null` both mean the same thing there, and *not
119
+ * promoted* is what that means.
120
+ */
121
+ function renderFacts(item, ctx, url) {
122
+ const { facts } = item;
123
+ const value = (v) => (v === null ? '_not recorded_' : v);
124
+ const depends = facts.dependsOn.length === 0
125
+ ? '_none_'
126
+ : facts.dependsOn.map((id) => renderItemRef(id, ctx, url)).join(', ');
127
+ const filePath = url === null ? `\`${item.relPath}\`` : `[${item.relPath}](${url(item.relPath)})`;
128
+ // A bullet list, not backslash hard breaks: it survives any renderer, and a screen
129
+ // reader announces four labelled facts rather than one run-on paragraph.
130
+ return [
131
+ `- **Status:** \`${item.status}\``,
132
+ `- **Created:** ${value(facts.createdAt)}`,
133
+ `- **Author:** ${value(facts.author)}`,
134
+ `- **Source:** ${value(facts.source)}`,
135
+ `- **Depends on:** ${depends}`,
136
+ `- **Promoted to:** ${facts.promotedTo === null ? '_not promoted_' : `\`${facts.promotedTo}\``}`,
137
+ `- **Roadmap file:** ${filePath}`,
138
+ ].join('\n');
139
+ }
140
+ /** One `depends_on` entry: a link when the item exists, a named gap when it does not. */
141
+ function renderItemRef(id, ctx, url) {
142
+ const relPath = ctx.itemPaths.get(id);
143
+ if (relPath === undefined)
144
+ return `\`${id}\` (no such item)`;
145
+ if (url === null)
146
+ return `\`${id}\``;
147
+ return `[\`${id}\`](${url(relPath)})`;
148
+ }
149
+ /** The standing explanation, identical on every issue. */
150
+ function renderFooter() {
151
+ return [
152
+ 'Projected from this repository’s AIDLC roadmap by `aidlc roadmap sync`. The roadmap',
153
+ 'file named above is the source of truth — edit it there. This region is regenerated on',
154
+ 'every sync; anything written outside it is preserved. Comments are never copied back',
155
+ 'into the repository.',
156
+ ].join('\n');
157
+ }
158
+ /**
159
+ * Fit sanitized prose inside the remaining budget.
160
+ *
161
+ * The cut point is the last newline before the budget that sits in **prose**, which is what
162
+ * stops truncation leaving a fence open — a body ending mid-fence renders everything after
163
+ * it as one code block. When no safe break exists the prose is dropped entirely rather than
164
+ * cut mid-construct, and the notice says so; that case cannot arise from any real item
165
+ * today and a silent hard slice would be the worse failure if it ever did.
166
+ */
167
+ function fitProse(prose, item, url, fixedLength) {
168
+ const budget = MAX_BODY_LENGTH - fixedLength;
169
+ if (prose.length <= budget)
170
+ return prose;
171
+ const link = url === null ? `\`${item.relPath}\`` : `[${item.relPath}](${url(item.relPath)})`;
172
+ // Reserve room for the notice itself, which is why the break is sought below a reduced
173
+ // limit rather than at the budget.
174
+ const notice = (omitted) => `\n_… ${omitted.toLocaleString('en-US')} further characters are not shown. Read the whole item at ${link}._`;
175
+ const reserve = notice(prose.length).length;
176
+ const cut = lastProseBreakBefore(prose, Math.max(0, budget - reserve));
177
+ if (cut === -1)
178
+ return notice(prose.length).trimStart();
179
+ return prose.slice(0, cut) + notice(prose.length - cut);
180
+ }
181
+ /**
182
+ * The region bytes of an existing body, or null when it carries no region.
183
+ *
184
+ * Delimiters included, so the value can be compared directly against
185
+ * `renderGeneratedRegion`'s output — comparing the insides would let a delimiter change go
186
+ * undetected.
187
+ */
188
+ export function extractRegion(body) {
189
+ const start = body.indexOf(BODY_BEGIN);
190
+ if (start === -1)
191
+ return null;
192
+ const end = body.indexOf(BODY_END, start);
193
+ if (end === -1)
194
+ return null;
195
+ return body.slice(start, end + BODY_END.length);
196
+ }
197
+ /**
198
+ * v1.21.0's body, reproduced exactly.
199
+ *
200
+ * **Frozen history, not a template.** Its only job is to be recognised so it can be
201
+ * replaced, so it must keep emitting the bytes v1.21.0 emitted even as the real renderer
202
+ * changes. A test pins it as a literal for that reason: edit it and issues created before
203
+ * this change stop being recognised, and every one of them gains a new region below a stale
204
+ * notice instead of replacing it.
205
+ */
206
+ export function renderLegacyIssueBody(item) {
207
+ return [
208
+ 'Projected from this repository’s AIDLC roadmap.',
209
+ '',
210
+ `- **Item:** \`${item.id}\``,
211
+ `- **File:** \`${item.relPath}\``,
212
+ '',
213
+ 'The roadmap file is the source of truth. Edit it there — this issue is a rendering',
214
+ 'of it, and its title and `aidlc:` labels are overwritten by `aidlc roadmap sync`.',
215
+ 'Comments here are never copied back into the repository, and this description is',
216
+ 'never rewritten after the issue is created.',
217
+ '',
218
+ renderMarker(item.id),
219
+ '',
220
+ ].join('\n');
221
+ }
222
+ /**
223
+ * The body to write: a fresh region merged into whatever the issue already has.
224
+ *
225
+ * Four cases, first match wins:
226
+ *
227
+ * 1. **Both delimiters present** — replace the span between them and keep everything else.
228
+ * This is the steady state, and the reason a human's note survives a sync.
229
+ * 2. **Exactly the legacy notice** — the region alone. An issue created by v1.21.0 stops
230
+ * being thin.
231
+ * 3. **Legacy notice plus other text** — replace only the recognised notice. Whatever
232
+ * somebody added stays.
233
+ * 4. **Anything else** — append the region below the existing body, after stripping any
234
+ * marker naming *this* item, so the body ends up with exactly one.
235
+ *
236
+ * Case 4 deliberately leaves markers naming *other* ids alone. Those are the ambiguity
237
+ * `indexByMarker` exists to refuse, and quietly deleting one would hide a genuine
238
+ * two-issue collision rather than resolve it.
239
+ */
240
+ export function mergeBody(existing, region, itemId) {
241
+ const start = existing.indexOf(BODY_BEGIN);
242
+ if (start !== -1) {
243
+ const end = existing.indexOf(BODY_END, start);
244
+ if (end !== -1) {
245
+ return existing.slice(0, start) + region + existing.slice(end + BODY_END.length);
246
+ }
247
+ }
248
+ const legacy = legacyNoticeFor(itemId, existing);
249
+ if (legacy !== null) {
250
+ if (existing === legacy)
251
+ return region;
252
+ const at = existing.indexOf(legacy);
253
+ if (at !== -1)
254
+ return existing.slice(0, at) + region + existing.slice(at + legacy.length);
255
+ }
256
+ const kept = stripOwnMarkers(existing, itemId).trimEnd();
257
+ return kept.length === 0 ? region : `${kept}\n\n${region}`;
258
+ }
259
+ /**
260
+ * The legacy notice for this item, if the body contains one.
261
+ *
262
+ * `renderLegacyIssueBody` needs a whole `ProjectableItem`, but a merge only knows the id
263
+ * and the body. The file path is recoverable from the notice itself, which is the one place
264
+ * v1.21.0 recorded it — so the notice is rebuilt from what it claims rather than from what
265
+ * the item currently says. That matters: an item that has since moved between statuses has
266
+ * a different `relPath` today, and rebuilding from the item would fail to match its own
267
+ * issue.
268
+ */
269
+ function legacyNoticeFor(itemId, existing) {
270
+ const match = /- \*\*File:\*\* `([^`\n]+)`/.exec(existing);
271
+ if (match === null)
272
+ return null;
273
+ return renderLegacyIssueBody({
274
+ id: itemId,
275
+ relPath: match[1],
276
+ // Unused by the legacy renderer; present only to satisfy the shape.
277
+ title: '',
278
+ status: 'backlog',
279
+ filename: '',
280
+ body: '',
281
+ facts: { createdAt: null, author: null, source: null, promotedTo: null, dependsOn: [] },
282
+ });
283
+ }
284
+ /** Remove markers naming this item, leaving markers for other ids in place. */
285
+ function stripOwnMarkers(body, itemId) {
286
+ const escaped = itemId.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
287
+ return body.replace(new RegExp(String.raw `<!--\s*aidlc-item:\s*${escaped}\s*-->\n?`, 'g'), '');
288
+ }
289
+ //# sourceMappingURL=body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../../../src/roadmap/sync/body.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Read the roadmap off disk into what the planner needs.
3
+ *
4
+ * Reads only. It does not judge: every refusal lives in `planProjection`, in one tested
5
+ * order, so a tree with two problems always produces the same message.
6
+ *
7
+ * Two things this fixes about the obvious implementation:
8
+ *
9
+ * - `listStatus` silently skips an item whose frontmatter will not parse, because
10
+ * `readItem` returns null for both "absent" and "unparseable". A projection built on it
11
+ * alone would inherit that: an item invisible in the repo *and* on the board, with
12
+ * nothing anywhere saying so. Five of this project's first 49 items were invisible for
13
+ * up to ten days from exactly this. So `unparseableItems` is read alongside it.
14
+ * - An item can parse perfectly and still carry no `id` — `readItem` returns `id: null`
15
+ * for a missing or empty key. `unparseableItems` does not detect that class, so
16
+ * `aidlc doctor` is silent about it. It is collected here as its own class so the
17
+ * refusal message can send the user somewhere useful.
18
+ *
19
+ * Everything resolves through `primaryCheckout`. A roadmap read from a worktree's copy is
20
+ * a roadmap stranded on that branch, and for an inbox that is indistinguishable from
21
+ * losing it.
22
+ *
23
+ * Requirements: roadmap-issue-projection/AC-33, roadmap-issue-projection/AC-34,
24
+ * roadmap-issue-projection/AC-51
25
+ *
26
+ * @module
27
+ */
28
+ import type { ProjectableItem, UnprojectableRef } from './types.js';
29
+ export interface Collected {
30
+ /** The primary checkout's working tree — everything below is relative to it. */
31
+ root: string;
32
+ items: readonly ProjectableItem[];
33
+ unprojectable: readonly UnprojectableRef[];
34
+ /**
35
+ * Every item id → its repo-relative path, across **all five statuses**.
36
+ *
37
+ * Not restricted to the eligible ones. A `backlog` item routinely names the four `hold`
38
+ * items it supersedes, and those references are the most useful links in the body
39
+ * precisely because a reader cannot guess where a parked item went. Restricting this map
40
+ * to projected statuses would drop them silently.
41
+ */
42
+ itemPaths: ReadonlyMap<string, string>;
43
+ }
44
+ /**
45
+ * A title for an item whose frontmatter has none.
46
+ *
47
+ * The filename slug, de-hyphenated. Refusing would be worse: the item is perfectly
48
+ * projectable and the slug is a serviceable title, so a missing title is a cosmetic gap
49
+ * and not a reason to block the whole run.
50
+ */
51
+ export declare function titleFromFilename(filename: string): string;
52
+ /** Read every status directory of the primary checkout containing `cwd`. */
53
+ export declare function collectItems(cwd: string): Collected;
54
+ //# sourceMappingURL=collect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAaH,OAAO,KAAK,EAAa,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/E,MAAM,WAAW,SAAS;IACxB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC3C;;;;;;;OAOG;IACH,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAmDnD"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Read the roadmap off disk into what the planner needs.
3
+ *
4
+ * Reads only. It does not judge: every refusal lives in `planProjection`, in one tested
5
+ * order, so a tree with two problems always produces the same message.
6
+ *
7
+ * Two things this fixes about the obvious implementation:
8
+ *
9
+ * - `listStatus` silently skips an item whose frontmatter will not parse, because
10
+ * `readItem` returns null for both "absent" and "unparseable". A projection built on it
11
+ * alone would inherit that: an item invisible in the repo *and* on the board, with
12
+ * nothing anywhere saying so. Five of this project's first 49 items were invisible for
13
+ * up to ten days from exactly this. So `unparseableItems` is read alongside it.
14
+ * - An item can parse perfectly and still carry no `id` — `readItem` returns `id: null`
15
+ * for a missing or empty key. `unparseableItems` does not detect that class, so
16
+ * `aidlc doctor` is silent about it. It is collected here as its own class so the
17
+ * refusal message can send the user somewhere useful.
18
+ *
19
+ * Everything resolves through `primaryCheckout`. A roadmap read from a worktree's copy is
20
+ * a roadmap stranded on that branch, and for an inbox that is indistinguishable from
21
+ * losing it.
22
+ *
23
+ * Requirements: roadmap-issue-projection/AC-33, roadmap-issue-projection/AC-34,
24
+ * roadmap-issue-projection/AC-51
25
+ *
26
+ * @module
27
+ */
28
+ import { ITEM_FILENAME_RE, ROADMAP_DIR, ROADMAP_STATUSES, frontmatter, itemBody, listStatus, primaryCheckout, unparseableItems, } from '../paths.js';
29
+ /**
30
+ * A title for an item whose frontmatter has none.
31
+ *
32
+ * The filename slug, de-hyphenated. Refusing would be worse: the item is perfectly
33
+ * projectable and the slug is a serviceable title, so a missing title is a cosmetic gap
34
+ * and not a reason to block the whole run.
35
+ */
36
+ export function titleFromFilename(filename) {
37
+ const match = ITEM_FILENAME_RE.exec(filename);
38
+ const slug = match?.[2] ?? filename.replace(/\.md$/, '');
39
+ return slug.split('-').join(' ');
40
+ }
41
+ /** Read every status directory of the primary checkout containing `cwd`. */
42
+ export function collectItems(cwd) {
43
+ const root = primaryCheckout(cwd);
44
+ const items = [];
45
+ const unprojectable = [];
46
+ const itemPaths = new Map();
47
+ // Unparseable first, so its entries keep their own reasons rather than being
48
+ // overwritten by the weaker "no id" classification below.
49
+ const unparseableLabels = new Set();
50
+ for (const bad of unparseableItems(root)) {
51
+ unparseableLabels.add(bad.label);
52
+ unprojectable.push({
53
+ label: bad.label,
54
+ status: bad.status,
55
+ cls: 'unparseable',
56
+ reason: bad.detail === null ? bad.reason : `${bad.reason}: ${bad.detail}`,
57
+ });
58
+ }
59
+ for (const status of ROADMAP_STATUSES) {
60
+ for (const item of listStatus(root, status)) {
61
+ const label = `${status}/${item.filename}`;
62
+ if (unparseableLabels.has(label))
63
+ continue;
64
+ if (item.id === null) {
65
+ unprojectable.push({
66
+ label,
67
+ status,
68
+ cls: 'missing-id',
69
+ reason: 'frontmatter has no `id:` value',
70
+ });
71
+ continue;
72
+ }
73
+ const relPath = relPathOf(status, item.filename);
74
+ // First writer wins, matching `findByPromotedTo` and `indexByMarker`: a duplicate id
75
+ // is refused by `planProjection` anyway, so silently preferring one here cannot
76
+ // change an outcome — it only keeps this map from depending on directory order.
77
+ if (!itemPaths.has(item.id))
78
+ itemPaths.set(item.id, relPath);
79
+ items.push({
80
+ id: item.id,
81
+ title: item.title ?? titleFromFilename(item.filename),
82
+ status,
83
+ filename: item.filename,
84
+ relPath,
85
+ body: itemBody(item.raw),
86
+ facts: factsOf(item.raw),
87
+ });
88
+ }
89
+ }
90
+ return { root, items, unprojectable, itemPaths };
91
+ }
92
+ /**
93
+ * The frontmatter facts, from the `raw` `readItem` already returned.
94
+ *
95
+ * No second disk read, and no second parse of the whole file: `readItem` keeps `raw`
96
+ * precisely so a caller can work from it. Every value is validated at this boundary and
97
+ * becomes `null` on anything unexpected — a wrong-typed `author` is a cosmetic gap, not a
98
+ * reason to make the whole item unprojectable, which is the same call the existing
99
+ * missing-`title` fallback already makes.
100
+ */
101
+ function factsOf(raw) {
102
+ const data = frontmatter(raw)?.data ?? {};
103
+ const str = (key) => {
104
+ const value = data[key];
105
+ if (typeof value === 'string' && value.trim().length > 0)
106
+ return value.trim();
107
+ // `created_at: 2026-09-05` parses as a Date under YAML 1.1 timestamp rules, so a
108
+ // string check alone would drop the one fact every item has.
109
+ if (value instanceof Date)
110
+ return value.toISOString().slice(0, 10);
111
+ return null;
112
+ };
113
+ const dependsOn = [];
114
+ if (Array.isArray(data.depends_on)) {
115
+ for (const entry of data.depends_on) {
116
+ if (typeof entry === 'string' && entry.trim().length > 0)
117
+ dependsOn.push(entry.trim());
118
+ }
119
+ }
120
+ return {
121
+ createdAt: str('created_at'),
122
+ author: str('author'),
123
+ source: str('source'),
124
+ promotedTo: str('promoted_to'),
125
+ dependsOn,
126
+ };
127
+ }
128
+ /**
129
+ * Repo-relative path of an item, with forward slashes on every platform.
130
+ *
131
+ * Built from the known layout rather than by `relative()` over the item's absolute path,
132
+ * so the result is identical on Windows without a separator dance. The value ends up in
133
+ * an issue body, where a backslash would render wrong.
134
+ */
135
+ function relPathOf(status, filename) {
136
+ return `${ROADMAP_DIR}/${status}/${filename}`;
137
+ }
138
+ //# sourceMappingURL=collect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.js","sourceRoot":"","sources":["../../../src/roadmap/sync/collect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The `roadmap.sync` section of `.aidlc/config.yaml`.
3
+ *
4
+ * Follows the convention every other module's config reader in this package follows —
5
+ * there is no central config loader here, ten call sites each parse the file inline —
6
+ * with one boundary drawn explicitly, because the requirements review found revision 1
7
+ * contradicting itself about it:
8
+ *
9
+ * - A malformed **container** reads as *not configured*: unparseable YAML, `roadmap`
10
+ * not a mapping, `roadmap.sync` not a mapping. One bad section must never break an
11
+ * unrelated command.
12
+ * - Anything wrong **inside** a well-formed `sync` mapping is a *named refusal*,
13
+ * including a wrong-typed value. `provider: 123` is a mistake somebody made on
14
+ * purpose and wants told about; silently reading it as "you have not configured this"
15
+ * would send them looking in the wrong place.
16
+ *
17
+ * The section is read from the **primary checkout**, the same tree the items come from.
18
+ * Items and eligibility are one fact; reading them from two trees would let a feature
19
+ * branch change what a colleague's `sync` publishes.
20
+ *
21
+ * Requirements: roadmap-issue-projection/AC-1 … roadmap-issue-projection/AC-9
22
+ *
23
+ * @module
24
+ */
25
+ import { type RoadmapStatus } from '../paths.js';
26
+ /** The `roadmap.sync` section, validated. */
27
+ export interface SyncConfig {
28
+ provider: 'github';
29
+ /** `owner/name`, or null when `gh` resolves the current repository. */
30
+ repo: string | null;
31
+ statuses: readonly RoadmapStatus[];
32
+ label: string;
33
+ }
34
+ export type SyncConfigResult = {
35
+ kind: 'ok';
36
+ config: SyncConfig;
37
+ } | {
38
+ kind: 'not-configured';
39
+ } | {
40
+ kind: 'invalid';
41
+ error: string;
42
+ };
43
+ /**
44
+ * The default eligible statuses.
45
+ *
46
+ * The line falls between "thinking" and "work". `inbox/` means captured, not committed
47
+ * to — projecting it puts issues on a board for decisions nobody has made. `hold/`
48
+ * holds parked and declined work, which on this project means pricing thinking and a
49
+ * competitor teardown; a markdown file in a repo and an indexed, notification-generating
50
+ * issue are not the same exposure even when both are technically public.
51
+ */
52
+ export declare const DEFAULT_STATUSES: readonly RoadmapStatus[];
53
+ /**
54
+ * Validate a repository coordinate. Shared by the config value and `--repo`, so the two
55
+ * can never diverge.
56
+ */
57
+ export declare function validateRepo(raw: unknown, source: string): {
58
+ ok: true;
59
+ repo: string;
60
+ } | {
61
+ ok: false;
62
+ error: string;
63
+ };
64
+ /**
65
+ * Validate the discovery label.
66
+ *
67
+ * A comma is refused explicitly rather than folded into a charset message, because the
68
+ * failure it causes is silent and expensive: `gh`'s `--label` is a comma-split slice, so
69
+ * `aidlc,urgent` would AND two labels in discovery, return an empty issue map, and
70
+ * re-create every issue on every run. The discovery label *is* the identity of a
71
+ * projected issue, so a wrong one is a duplicate-issue generator, not a cosmetic error.
72
+ */
73
+ export declare function validateLabel(raw: unknown): {
74
+ ok: true;
75
+ label: string;
76
+ } | {
77
+ ok: false;
78
+ error: string;
79
+ };
80
+ /**
81
+ * Read and validate the `roadmap.sync` section from a checkout's config.
82
+ *
83
+ * @param primaryCheckoutRoot - The primary checkout's working tree.
84
+ */
85
+ export declare function readSyncConfig(primaryCheckoutRoot: string): SyncConfigResult;
86
+ /**
87
+ * Apply a `--repo` flag over a config, with identical validation.
88
+ *
89
+ * The flag wins. It is the likelier source of a wrong value — typed by hand, once —
90
+ * which is why it goes through the same validator rather than a looser one.
91
+ */
92
+ export declare function withRepoOverride(config: SyncConfig, repo: string | undefined): SyncConfigResult;
93
+ /** The message for a project that has not opted in. */
94
+ export declare function notConfiguredMessage(): string;
95
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAGnE,6CAA6C;AAC7C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,uEAAuE;IACvE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,aAAa,EAAuC,CAAC;AAkC7F;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAmBpH;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0BtG;AA+BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,mBAAmB,EAAE,MAAM,GAAG,gBAAgB,CAiD5E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAK/F;AAED,uDAAuD;AACvD,wBAAgB,oBAAoB,IAAI,MAAM,CAW7C"}