@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,80 @@
1
+ /**
2
+ * The label vocabulary the projection uses.
3
+ *
4
+ * Two kinds of label, doing two different jobs:
5
+ *
6
+ * - **The discovery label** (`aidlc` by default) is how the whole issue map is found in
7
+ * one `gh issue list` call. It is identity: an issue without it is invisible to the
8
+ * projection.
9
+ * - **A status label** (`aidlc:backlog`, `aidlc:done`, …) is how a reader tells planned
10
+ * from in-flight without opening anything.
11
+ *
12
+ * The status names are *derived* from `ROADMAP_STATUSES` rather than listed here, so the
13
+ * two vocabularies cannot drift: adding a sixth status becomes a compile-time fact
14
+ * instead of a label nobody remembered to add.
15
+ *
16
+ * `LABEL_COLOR` and `LABEL_DESCRIPTION` exist because `gh label create` exits 1 on a
17
+ * label that already exists, so creation must pass `--force` — and `gh` chooses a
18
+ * *random* colour when none is given, so `--force` without an explicit colour would
19
+ * repaint the label on every single run. Fixed values make the ensure genuinely
20
+ * idempotent.
21
+ *
22
+ * Requirements: roadmap-issue-projection/AC-17, roadmap-issue-projection/AC-43
23
+ *
24
+ * @module
25
+ */
26
+ import { ROADMAP_STATUSES } from '../paths.js';
27
+ /** Default discovery label. Overridable via `roadmap.sync.label`. */
28
+ export const DEFAULT_DISCOVERY_LABEL = 'aidlc';
29
+ /** GitHub's own label-name limit. */
30
+ export const MAX_LABEL_LENGTH = 50;
31
+ /** Prefix marking a label this projection owns. */
32
+ export const STATUS_LABEL_PREFIX = 'aidlc:';
33
+ /** The label carrying an item's status. */
34
+ export function statusLabel(status) {
35
+ return `${STATUS_LABEL_PREFIX}${status}`;
36
+ }
37
+ /** Every status label, derived. */
38
+ export const ALL_STATUS_LABELS = ROADMAP_STATUSES.map(statusLabel);
39
+ /**
40
+ * True when a label name is one this projection owns.
41
+ *
42
+ * Used to decide what `update-labels` may remove. Anything failing this test is a
43
+ * human's label and is never touched — which is why the update uses `--add-label` /
44
+ * `--remove-label` rather than a wholesale `--label` replacement.
45
+ */
46
+ export function isStatusLabel(name) {
47
+ return ALL_STATUS_LABELS.includes(name);
48
+ }
49
+ /** Fixed colours, so `--force` is idempotent rather than a repaint. */
50
+ export const LABEL_COLOR = {
51
+ [DEFAULT_DISCOVERY_LABEL]: '5319e7',
52
+ [statusLabel('inbox')]: 'bfd4f2',
53
+ [statusLabel('backlog')]: '1d76db',
54
+ [statusLabel('in-progress')]: '0e8a16',
55
+ [statusLabel('done')]: '6f42c1',
56
+ [statusLabel('hold')]: 'd4c5f9',
57
+ };
58
+ /** Fallback colour for a custom discovery label. */
59
+ export const FALLBACK_LABEL_COLOR = '5319e7';
60
+ /** Fixed descriptions, for the same reason as the colours. */
61
+ export const LABEL_DESCRIPTION = {
62
+ [statusLabel('inbox')]: 'AIDLC roadmap: captured, not yet triaged',
63
+ [statusLabel('backlog')]: 'AIDLC roadmap: agreed, not started',
64
+ [statusLabel('in-progress')]: 'AIDLC roadmap: a lifecycle instance exists',
65
+ [statusLabel('done')]: 'AIDLC roadmap: shipped',
66
+ [statusLabel('hold')]: 'AIDLC roadmap: parked or declined',
67
+ };
68
+ /** Fallback description for the discovery label, whatever it is named. */
69
+ export function discoveryLabelDescription() {
70
+ return 'Projected from this repository’s AIDLC roadmap';
71
+ }
72
+ /** Colour for a label name, falling back for a custom discovery label. */
73
+ export function colorFor(name) {
74
+ return LABEL_COLOR[name] ?? FALLBACK_LABEL_COLOR;
75
+ }
76
+ /** Description for a label name, falling back for a custom discovery label. */
77
+ export function descriptionFor(name) {
78
+ return LABEL_DESCRIPTION[name] ?? discoveryLabelDescription();
79
+ }
80
+ //# sourceMappingURL=labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labels.js","sourceRoot":"","sources":["../../../src/roadmap/sync/labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,gBAAgB,EAAsB,MAAM,aAAa,CAAC;AAEnE,qEAAqE;AACrE,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAE/C,qCAAqC;AACrC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC,mDAAmD;AACnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC;AAE5C,2CAA2C;AAC3C,MAAM,UAAU,WAAW,CAAC,MAAqB;IAC/C,OAAO,GAAG,mBAAmB,GAAG,MAAM,EAAE,CAAC;AAC3C,CAAC;AAED,mCAAmC;AACnC,MAAM,CAAC,MAAM,iBAAiB,GAAsB,gBAAgB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAQ,iBAAuC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,WAAW,GAAqC;IAC3D,CAAC,uBAAuB,CAAC,EAAE,QAAQ;IACnC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ;IAChC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,QAAQ;IAClC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,EAAE,QAAQ;IACtC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ;IAC/B,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ;CAChC,CAAC;AAEF,oDAAoD;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAE7C,8DAA8D;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAqC;IACjE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,0CAA0C;IAClE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,oCAAoC;IAC9D,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,EAAE,4CAA4C;IAC1E,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,wBAAwB;IAC/C,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,mCAAmC;CAC3D,CAAC;AAEF,0EAA0E;AAC1E,MAAM,UAAU,yBAAyB;IACvC,OAAO,gDAAgD,CAAC;AAC1D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC;AACnD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,iBAAiB,CAAC,IAAI,CAAC,IAAI,yBAAyB,EAAE,CAAC;AAChE,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Where markdown is code and where it is prose.
3
+ *
4
+ * One primitive, three consumers — the sanitizer, the item-reference linker, and the
5
+ * marker scanner. They have to agree: if the sanitizer defuses something the marker
6
+ * scanner still sees as a comment, the projection refuses runs for a hazard that is no
7
+ * longer there; if they disagree the other way, a quoted marker injects itself into an
8
+ * issue. Agreement is cheap to state as a shared function and expensive to maintain as a
9
+ * convention across three regexes.
10
+ *
11
+ * Measured on this project's roadmap (76 items, 451,503 characters of body text) on
12
+ * 2026-09-07: 40 of the 41 hazard spans a naive sanitizer would rewrite are **inside
13
+ * code** — `@clack/prompts`, `actions/checkout@v4`, CSS colour hexes such as `#0891b2`
14
+ * that a `#123` pattern also matches, and one HTML comment. Exactly one is in prose. So
15
+ * code-awareness is not a refinement of the sanitizer; it is the difference between
16
+ * corrupting 40 spans and defusing 1.
17
+ *
18
+ * The inline-code pattern here is deliberately stronger than the prior art in
19
+ * `packages/website/scripts/blog/mdx-escape.mjs`, whose `` /(`[^`\n]*`)/ `` recognises
20
+ * single-backtick spans only. Two of the figures above were wrong under that weaker
21
+ * pattern: it read a prose `#17` as code, and it could not see that all 32 item-id
22
+ * references are code spans. Tightening the pattern is the reason this module exists
23
+ * rather than a detail of it — which is also why `mdx-escape.mjs` is left alone: it
24
+ * escapes for a different dialect, in a different package, and widening it would be a
25
+ * change to the blog pipeline nobody asked for.
26
+ *
27
+ * Requirements: richer-issue-bodies/AC-8
28
+ *
29
+ * @module
30
+ */
31
+ /** One inline code span, with its bounds in the source and its content between the ticks. */
32
+ export interface CodeSpan {
33
+ /** Index of the first backtick. */
34
+ start: number;
35
+ /** Index one past the last backtick. */
36
+ end: number;
37
+ /** Text between the backtick runs, unmodified. */
38
+ content: string;
39
+ /** How many backticks delimit it, so a rewrite can re-wrap with the same number. */
40
+ ticks: number;
41
+ }
42
+ /**
43
+ * A byte per character: 1 inside a fenced block or an inline code span, 0 in prose.
44
+ *
45
+ * Fence opener and closer lines are themselves marked as code. That is deliberate: an
46
+ * info string such as ```` ```ts ```` is not prose, and a rule that rewrote it would
47
+ * change what language a block claims to be.
48
+ *
49
+ * Linear in the length of the text: one pass over lines, and one regex walk per prose
50
+ * line.
51
+ */
52
+ export declare function codeMask(text: string): Uint8Array;
53
+ /**
54
+ * Every inline code span outside a fenced block, in order of appearance.
55
+ *
56
+ * Spans *inside* a fence are not returned: their backticks are literal text there, and a
57
+ * caller rewriting one would be editing the contents of a code block.
58
+ */
59
+ export declare function inlineCodeSpans(text: string): CodeSpan[];
60
+ /**
61
+ * Rewrite the prose runs of a document, copying every code run byte-identical.
62
+ *
63
+ * `fn` is called once per maximal prose run. It may return text of any length — offsets
64
+ * are not preserved across the call, which is why the mask is computed once up front
65
+ * rather than consulted per match.
66
+ */
67
+ export declare function mapProse(text: string, fn: (prose: string) => string): string;
68
+ /**
69
+ * The last index at or before `limit` where a line break sits in **prose**.
70
+ *
71
+ * Used to choose a truncation point. A blank line inside a fenced block is masked as
72
+ * code, so restricting the cut to an unmasked newline is what stops truncation leaving a
73
+ * fence open — a body ending mid-fence renders the rest of the issue as one code block.
74
+ *
75
+ * Returns -1 when no such position exists, which the caller must treat as "cannot
76
+ * truncate safely" rather than as position 0.
77
+ */
78
+ export declare function lastProseBreakBefore(text: string, limit: number): number;
79
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAkBH,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAkCjD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CA2BxD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CAc5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMxE"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Where markdown is code and where it is prose.
3
+ *
4
+ * One primitive, three consumers — the sanitizer, the item-reference linker, and the
5
+ * marker scanner. They have to agree: if the sanitizer defuses something the marker
6
+ * scanner still sees as a comment, the projection refuses runs for a hazard that is no
7
+ * longer there; if they disagree the other way, a quoted marker injects itself into an
8
+ * issue. Agreement is cheap to state as a shared function and expensive to maintain as a
9
+ * convention across three regexes.
10
+ *
11
+ * Measured on this project's roadmap (76 items, 451,503 characters of body text) on
12
+ * 2026-09-07: 40 of the 41 hazard spans a naive sanitizer would rewrite are **inside
13
+ * code** — `@clack/prompts`, `actions/checkout@v4`, CSS colour hexes such as `#0891b2`
14
+ * that a `#123` pattern also matches, and one HTML comment. Exactly one is in prose. So
15
+ * code-awareness is not a refinement of the sanitizer; it is the difference between
16
+ * corrupting 40 spans and defusing 1.
17
+ *
18
+ * The inline-code pattern here is deliberately stronger than the prior art in
19
+ * `packages/website/scripts/blog/mdx-escape.mjs`, whose `` /(`[^`\n]*`)/ `` recognises
20
+ * single-backtick spans only. Two of the figures above were wrong under that weaker
21
+ * pattern: it read a prose `#17` as code, and it could not see that all 32 item-id
22
+ * references are code spans. Tightening the pattern is the reason this module exists
23
+ * rather than a detail of it — which is also why `mdx-escape.mjs` is left alone: it
24
+ * escapes for a different dialect, in a different package, and widening it would be a
25
+ * change to the blog pipeline nobody asked for.
26
+ *
27
+ * Requirements: richer-issue-bodies/AC-8
28
+ *
29
+ * @module
30
+ */
31
+ /**
32
+ * A fence opener or closer: up to three leading spaces, then three or more backticks or
33
+ * tildes. CommonMark's rule, and the same one `mdx-escape.mjs` applies.
34
+ */
35
+ const FENCE_RE = /^\s{0,3}(`{3,}|~{3,})/;
36
+ /**
37
+ * One inline code span.
38
+ *
39
+ * A run of N backticks closed by a run of **exactly** N — not by a prefix of a longer
40
+ * run, which is what the lookarounds buy. Without them `` ``a`` `` reads as a one-backtick
41
+ * span containing `` `a `` and the classification of everything after it on the line
42
+ * shifts by one span.
43
+ */
44
+ const INLINE_CODE_RE = /(?<!`)(`+)(?!`)([^\n]*?)(?<!`)\1(?!`)/g;
45
+ /**
46
+ * A byte per character: 1 inside a fenced block or an inline code span, 0 in prose.
47
+ *
48
+ * Fence opener and closer lines are themselves marked as code. That is deliberate: an
49
+ * info string such as ```` ```ts ```` is not prose, and a rule that rewrote it would
50
+ * change what language a block claims to be.
51
+ *
52
+ * Linear in the length of the text: one pass over lines, and one regex walk per prose
53
+ * line.
54
+ */
55
+ export function codeMask(text) {
56
+ const mask = new Uint8Array(text.length);
57
+ let pos = 0;
58
+ let fence = null;
59
+ for (const line of text.split('\n')) {
60
+ const opener = FENCE_RE.exec(line);
61
+ if (opener !== null) {
62
+ const marker = opener[1];
63
+ if (fence === null) {
64
+ fence = marker;
65
+ }
66
+ else if (marker[0] === fence[0] && marker.length >= fence.length) {
67
+ fence = null;
68
+ }
69
+ // `+ 1` covers the trailing newline. Without it the line breaks *between* fenced
70
+ // lines read as prose, and `lastProseBreakBefore` would happily choose one — which is
71
+ // exactly the mid-fence truncation it exists to prevent. Found by that test, not by
72
+ // reading this loop.
73
+ mask.fill(1, pos, pos + line.length + 1);
74
+ }
75
+ else if (fence !== null) {
76
+ mask.fill(1, pos, pos + line.length + 1);
77
+ }
78
+ else {
79
+ INLINE_CODE_RE.lastIndex = 0;
80
+ let match;
81
+ while ((match = INLINE_CODE_RE.exec(line)) !== null) {
82
+ mask.fill(1, pos + match.index, pos + match.index + match[0].length);
83
+ }
84
+ }
85
+ // +1 for the newline `split` consumed. The final line has no newline, but writing
86
+ // past the end of a `fill` range is a no-op, so no special case is needed.
87
+ pos += line.length + 1;
88
+ }
89
+ return mask;
90
+ }
91
+ /**
92
+ * Every inline code span outside a fenced block, in order of appearance.
93
+ *
94
+ * Spans *inside* a fence are not returned: their backticks are literal text there, and a
95
+ * caller rewriting one would be editing the contents of a code block.
96
+ */
97
+ export function inlineCodeSpans(text) {
98
+ const spans = [];
99
+ let pos = 0;
100
+ let fence = null;
101
+ for (const line of text.split('\n')) {
102
+ const opener = FENCE_RE.exec(line);
103
+ if (opener !== null) {
104
+ const marker = opener[1];
105
+ if (fence === null)
106
+ fence = marker;
107
+ else if (marker[0] === fence[0] && marker.length >= fence.length)
108
+ fence = null;
109
+ }
110
+ else if (fence === null) {
111
+ INLINE_CODE_RE.lastIndex = 0;
112
+ let match;
113
+ while ((match = INLINE_CODE_RE.exec(line)) !== null) {
114
+ spans.push({
115
+ start: pos + match.index,
116
+ end: pos + match.index + match[0].length,
117
+ content: match[2],
118
+ ticks: match[1].length,
119
+ });
120
+ }
121
+ }
122
+ pos += line.length + 1;
123
+ }
124
+ return spans;
125
+ }
126
+ /**
127
+ * Rewrite the prose runs of a document, copying every code run byte-identical.
128
+ *
129
+ * `fn` is called once per maximal prose run. It may return text of any length — offsets
130
+ * are not preserved across the call, which is why the mask is computed once up front
131
+ * rather than consulted per match.
132
+ */
133
+ export function mapProse(text, fn) {
134
+ const mask = codeMask(text);
135
+ const out = [];
136
+ let runStart = 0;
137
+ for (let i = 1; i <= text.length; i++) {
138
+ const boundary = i === text.length || mask[i] !== mask[runStart];
139
+ if (!boundary)
140
+ continue;
141
+ const run = text.slice(runStart, i);
142
+ out.push(mask[runStart] === 1 ? run : fn(run));
143
+ runStart = i;
144
+ }
145
+ return out.join('');
146
+ }
147
+ /**
148
+ * The last index at or before `limit` where a line break sits in **prose**.
149
+ *
150
+ * Used to choose a truncation point. A blank line inside a fenced block is masked as
151
+ * code, so restricting the cut to an unmasked newline is what stops truncation leaving a
152
+ * fence open — a body ending mid-fence renders the rest of the issue as one code block.
153
+ *
154
+ * Returns -1 when no such position exists, which the caller must treat as "cannot
155
+ * truncate safely" rather than as position 0.
156
+ */
157
+ export function lastProseBreakBefore(text, limit) {
158
+ const mask = codeMask(text);
159
+ for (let i = Math.min(limit, text.length - 1); i >= 0; i--) {
160
+ if (text[i] === '\n' && mask[i] === 0)
161
+ return i;
162
+ }
163
+ return -1;
164
+ }
165
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../../src/roadmap/sync/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The durable link between a roadmap item and its GitHub issue.
3
+ *
4
+ * The link is the item `id`, stamped on the issue as an HTML-comment marker. It is not
5
+ * the `remote:` frontmatter field: creating an issue and writing a local link are two
6
+ * writes to two systems, and every failure of the second leaves an issue with no link,
7
+ * which the next run turns into a duplicate. With the id living server-side there is no
8
+ * second write to fail, so the projection is a convergent rendering rather than
9
+ * stateful sync — and can be thrown away and rebuilt.
10
+ *
11
+ * Two properties matter and both are easy to get wrong:
12
+ *
13
+ * - **Matching is position-independent.** A created body ends with the marker, but as
14
+ * soon as somebody comments or edits, it will not. Matching that depended on position
15
+ * would silently stop finding issues the moment the tracker was used.
16
+ * - **Matching consumes the whole marker, terminator included.** The obvious
17
+ * implementation, `body.includes(id)`, resolves `item-20260907-foo` against the issue
18
+ * for `item-20260907-foo-2` — and that exact pair is manufactured by the roadmap
19
+ * skill's own collision rule, which suffixes a duplicate slug with `-2`. So the id is
20
+ * *extracted* and compared with `===`, never searched for as a substring.
21
+ *
22
+ * A third property was added by `richer-issue-bodies`:
23
+ *
24
+ * - **Matching is code-aware.** Once item prose is projected into the body, a marker
25
+ * quoted inside a fence or an inline code span is *visible code*, not a comment —
26
+ * GitHub renders it as text. A scanner that counted it would refuse runs as ambiguous
27
+ * over something no reader sees as a marker. The corpus holds exactly one such
28
+ * occurrence today, in a code span, so this is a measured case rather than a
29
+ * hypothetical one. The invariant to keep is that the scanner and the renderer agree.
30
+ *
31
+ * Requirements: roadmap-issue-projection/AC-47, roadmap-issue-projection/AC-48,
32
+ * roadmap-issue-projection/AC-49, richer-issue-bodies/AC-15
33
+ *
34
+ * @module
35
+ */
36
+ /** Render the marker for an item id. */
37
+ export declare function renderMarker(id: string): string;
38
+ /**
39
+ * Every item id marked in a body, in order of appearance, de-duplicated.
40
+ *
41
+ * Returns an **array** rather than the first id. A single-id return would make the
42
+ * two-markers case undetectable: the function would pick one and the projection would
43
+ * confidently retitle or close the wrong issue. The plural return costs nothing and
44
+ * makes the ambiguity representable, so `planProjection` can refuse instead of guess.
45
+ */
46
+ export declare function findMarkerIds(body: string): string[];
47
+ //# sourceMappingURL=marker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marker.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAaH,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAapD"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The durable link between a roadmap item and its GitHub issue.
3
+ *
4
+ * The link is the item `id`, stamped on the issue as an HTML-comment marker. It is not
5
+ * the `remote:` frontmatter field: creating an issue and writing a local link are two
6
+ * writes to two systems, and every failure of the second leaves an issue with no link,
7
+ * which the next run turns into a duplicate. With the id living server-side there is no
8
+ * second write to fail, so the projection is a convergent rendering rather than
9
+ * stateful sync — and can be thrown away and rebuilt.
10
+ *
11
+ * Two properties matter and both are easy to get wrong:
12
+ *
13
+ * - **Matching is position-independent.** A created body ends with the marker, but as
14
+ * soon as somebody comments or edits, it will not. Matching that depended on position
15
+ * would silently stop finding issues the moment the tracker was used.
16
+ * - **Matching consumes the whole marker, terminator included.** The obvious
17
+ * implementation, `body.includes(id)`, resolves `item-20260907-foo` against the issue
18
+ * for `item-20260907-foo-2` — and that exact pair is manufactured by the roadmap
19
+ * skill's own collision rule, which suffixes a duplicate slug with `-2`. So the id is
20
+ * *extracted* and compared with `===`, never searched for as a substring.
21
+ *
22
+ * A third property was added by `richer-issue-bodies`:
23
+ *
24
+ * - **Matching is code-aware.** Once item prose is projected into the body, a marker
25
+ * quoted inside a fence or an inline code span is *visible code*, not a comment —
26
+ * GitHub renders it as text. A scanner that counted it would refuse runs as ambiguous
27
+ * over something no reader sees as a marker. The corpus holds exactly one such
28
+ * occurrence today, in a code span, so this is a measured case rather than a
29
+ * hypothetical one. The invariant to keep is that the scanner and the renderer agree.
30
+ *
31
+ * Requirements: roadmap-issue-projection/AC-47, roadmap-issue-projection/AC-48,
32
+ * roadmap-issue-projection/AC-49, richer-issue-bodies/AC-15
33
+ *
34
+ * @module
35
+ */
36
+ import { codeMask } from './markdown.js';
37
+ /**
38
+ * Marker scanner.
39
+ *
40
+ * The id charset deliberately excludes `-->`'s characters, so the match cannot run past
41
+ * its own terminator. Global, because an issue carrying two markers is a real state the
42
+ * caller has to be able to see.
43
+ */
44
+ const MARKER_RE = /<!--\s*aidlc-item:\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*-->/g;
45
+ /** Render the marker for an item id. */
46
+ export function renderMarker(id) {
47
+ return `<!-- aidlc-item: ${id} -->`;
48
+ }
49
+ /**
50
+ * Every item id marked in a body, in order of appearance, de-duplicated.
51
+ *
52
+ * Returns an **array** rather than the first id. A single-id return would make the
53
+ * two-markers case undetectable: the function would pick one and the projection would
54
+ * confidently retitle or close the wrong issue. The plural return costs nothing and
55
+ * makes the ambiguity representable, so `planProjection` can refuse instead of guess.
56
+ */
57
+ export function findMarkerIds(body) {
58
+ const ids = [];
59
+ const mask = codeMask(body);
60
+ // `matchAll` on a fresh iterator each call — the regex is module-level and `g`-flagged,
61
+ // so relying on `lastIndex` across calls would make results depend on call order.
62
+ for (const match of body.matchAll(MARKER_RE)) {
63
+ // A marker inside a fence or an inline code span is code the reader can see, not a
64
+ // comment. Skipping it is what keeps this scanner in step with GitHub's renderer.
65
+ if (mask[match.index] === 1)
66
+ continue;
67
+ const id = match[1];
68
+ if (!ids.includes(id))
69
+ ids.push(id);
70
+ }
71
+ return ids;
72
+ }
73
+ //# sourceMappingURL=marker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marker.js","sourceRoot":"","sources":["../../../src/roadmap/sync/marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,0DAA0D,CAAC;AAE7E,wCAAwC;AACxC,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,OAAO,oBAAoB,EAAE,MAAM,CAAC;AACtC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,wFAAwF;IACxF,kFAAkF;IAClF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,mFAAmF;QACnF,kFAAkF;QAClF,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,SAAS;QACtC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,47 @@
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 { type BodyContext } from './body.js';
19
+ import type { SyncConfig } from './config.js';
20
+ import { type Plan, type ProjectableItem, type RemoteIssue, type UnprojectableRef } from './types.js';
21
+ /** GitHub's own issue-title limit. */
22
+ export declare const MAX_ISSUE_TITLE_LENGTH = 256;
23
+ export interface PlanInput {
24
+ config: SyncConfig;
25
+ items: readonly ProjectableItem[];
26
+ unprojectable: readonly UnprojectableRef[];
27
+ issues: readonly RemoteIssue[];
28
+ retireIneligible: boolean;
29
+ /** Printed in the plan header and the prompt. */
30
+ repoLabel: string;
31
+ /**
32
+ * Everything body rendering needs. Required rather than defaulted: a silent default
33
+ * would render every body with no permalinks and no reference links, and nothing
34
+ * anywhere would say so.
35
+ */
36
+ bodyContext: BodyContext;
37
+ }
38
+ export type PlanResult = {
39
+ kind: 'ok';
40
+ plan: Plan;
41
+ warnings: readonly string[];
42
+ } | {
43
+ kind: 'refused';
44
+ error: string;
45
+ };
46
+ export declare function planProjection(input: PlanInput): PlanResult;
47
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC9F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAIL,KAAK,IAAI,EAET,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAEpB,sCAAsC;AACtC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC3C,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAgE3D"}