@rasensio/aidlc 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/change/cross-check.d.ts +28 -0
- package/dist/change/cross-check.d.ts.map +1 -0
- package/dist/change/cross-check.js +53 -0
- package/dist/change/cross-check.js.map +1 -0
- package/dist/change/impact.d.ts +57 -0
- package/dist/change/impact.d.ts.map +1 -0
- package/dist/change/impact.js +288 -0
- package/dist/change/impact.js.map +1 -0
- package/dist/change/index.d.ts +12 -0
- package/dist/change/index.d.ts.map +1 -0
- package/dist/change/index.js +11 -0
- package/dist/change/index.js.map +1 -0
- package/dist/change/parser.d.ts +52 -0
- package/dist/change/parser.d.ts.map +1 -0
- package/dist/change/parser.js +339 -0
- package/dist/change/parser.js.map +1 -0
- package/dist/change/reopen.d.ts +80 -0
- package/dist/change/reopen.d.ts.map +1 -0
- package/dist/change/reopen.js +227 -0
- package/dist/change/reopen.js.map +1 -0
- package/dist/change/types.d.ts +144 -0
- package/dist/change/types.d.ts.map +1 -0
- package/dist/change/types.js +8 -0
- package/dist/change/types.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/amend.d.ts +22 -0
- package/dist/commands/amend.d.ts.map +1 -0
- package/dist/commands/amend.js +355 -0
- package/dist/commands/amend.js.map +1 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +6 -0
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/roadmap.d.ts +66 -0
- package/dist/commands/roadmap.d.ts.map +1 -0
- package/dist/commands/roadmap.js +188 -0
- package/dist/commands/roadmap.js.map +1 -0
- package/dist/core/lifecycle.d.ts +3 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +13 -2
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/types.d.ts +37 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/menu/roster.d.ts.map +1 -1
- package/dist/menu/roster.js +8 -0
- package/dist/menu/roster.js.map +1 -1
- package/dist/roadmap/sync/body.d.ts +37 -0
- package/dist/roadmap/sync/body.d.ts.map +1 -0
- package/dist/roadmap/sync/body.js +52 -0
- package/dist/roadmap/sync/body.js.map +1 -0
- package/dist/roadmap/sync/collect.d.ts +45 -0
- package/dist/roadmap/sync/collect.d.ts.map +1 -0
- package/dist/roadmap/sync/collect.js +93 -0
- package/dist/roadmap/sync/collect.js.map +1 -0
- package/dist/roadmap/sync/config.d.ts +95 -0
- package/dist/roadmap/sync/config.d.ts.map +1 -0
- package/dist/roadmap/sync/config.js +236 -0
- package/dist/roadmap/sync/config.js.map +1 -0
- package/dist/roadmap/sync/discover.d.ts +56 -0
- package/dist/roadmap/sync/discover.d.ts.map +1 -0
- package/dist/roadmap/sync/discover.js +126 -0
- package/dist/roadmap/sync/discover.js.map +1 -0
- package/dist/roadmap/sync/execute.d.ts +41 -0
- package/dist/roadmap/sync/execute.d.ts.map +1 -0
- package/dist/roadmap/sync/execute.js +164 -0
- package/dist/roadmap/sync/execute.js.map +1 -0
- package/dist/roadmap/sync/gh.d.ts +64 -0
- package/dist/roadmap/sync/gh.d.ts.map +1 -0
- package/dist/roadmap/sync/gh.js +86 -0
- package/dist/roadmap/sync/gh.js.map +1 -0
- package/dist/roadmap/sync/import.d.ts +73 -0
- package/dist/roadmap/sync/import.d.ts.map +1 -0
- package/dist/roadmap/sync/import.js +269 -0
- package/dist/roadmap/sync/import.js.map +1 -0
- package/dist/roadmap/sync/labels.d.ts +57 -0
- package/dist/roadmap/sync/labels.d.ts.map +1 -0
- package/dist/roadmap/sync/labels.js +80 -0
- package/dist/roadmap/sync/labels.js.map +1 -0
- package/dist/roadmap/sync/marker.d.ts +38 -0
- package/dist/roadmap/sync/marker.d.ts.map +1 -0
- package/dist/roadmap/sync/marker.js +58 -0
- package/dist/roadmap/sync/marker.js.map +1 -0
- package/dist/roadmap/sync/plan.d.ts +40 -0
- package/dist/roadmap/sync/plan.d.ts.map +1 -0
- package/dist/roadmap/sync/plan.js +272 -0
- package/dist/roadmap/sync/plan.js.map +1 -0
- package/dist/roadmap/sync/render.d.ts +28 -0
- package/dist/roadmap/sync/render.d.ts.map +1 -0
- package/dist/roadmap/sync/render.js +110 -0
- package/dist/roadmap/sync/render.js.map +1 -0
- package/dist/roadmap/sync/types.d.ts +111 -0
- package/dist/roadmap/sync/types.d.ts.map +1 -0
- package/dist/roadmap/sync/types.js +43 -0
- package/dist/roadmap/sync/types.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the id → issue map the planner reconciles against.
|
|
3
|
+
*
|
|
4
|
+
* One `gh issue list` invocation over the discovery label. Three details are load-bearing
|
|
5
|
+
* and all three were wrong in the first draft:
|
|
6
|
+
*
|
|
7
|
+
* - **`--limit` must be explicit.** `gh issue list` defaults to 30 results. A single call
|
|
8
|
+
* without a limit therefore returns at most 30 issues on a repo that has more, and every
|
|
9
|
+
* item missing from the map plans a `create` — producing exactly the duplicate issues the
|
|
10
|
+
* whole design exists to prevent, on every run, undetectably.
|
|
11
|
+
* - **Truncation must be a refusal, not a shrug.** A response of N issues is otherwise
|
|
12
|
+
* indistinguishable from "N is all there are". So the requested limit travels with the
|
|
13
|
+
* result and `count === limit` refuses before anything can plan a create.
|
|
14
|
+
* - **`labels` are objects.** `gh issue list --json labels` returns
|
|
15
|
+
* `{id,name,description,color}`, not strings. Treating them as strings makes every
|
|
16
|
+
* comparison against `aidlc:<status>` fail, so every item plans `update-labels` on every
|
|
17
|
+
* run and the projection never converges. They are flattened to names here, at the
|
|
18
|
+
* boundary, once.
|
|
19
|
+
*
|
|
20
|
+
* Requirements: roadmap-issue-projection/AC-11, roadmap-issue-projection/AC-12,
|
|
21
|
+
* roadmap-issue-projection/AC-13
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { firstLine, repoArgs } from './gh.js';
|
|
26
|
+
/**
|
|
27
|
+
* How many issues one map may hold.
|
|
28
|
+
*
|
|
29
|
+
* `gh` pages internally up to `--limit`, so this is one paginated sequence rather than a
|
|
30
|
+
* cap on a single page. A repo that reaches it gets a named refusal; that is a better
|
|
31
|
+
* outcome than a partial map, and better than reimplementing cursors against an interface
|
|
32
|
+
* (`gh issue list`) that exposes none.
|
|
33
|
+
*/
|
|
34
|
+
export const ISSUE_LIST_LIMIT = 1000;
|
|
35
|
+
/** Read every labelled issue, open and closed. */
|
|
36
|
+
export function buildIssueMap(gh, opts) {
|
|
37
|
+
const limit = opts.limit ?? ISSUE_LIST_LIMIT;
|
|
38
|
+
const result = gh.run([
|
|
39
|
+
'issue',
|
|
40
|
+
'list',
|
|
41
|
+
...repoArgs(opts.repo),
|
|
42
|
+
'--label',
|
|
43
|
+
opts.label,
|
|
44
|
+
'--state',
|
|
45
|
+
'all',
|
|
46
|
+
'--limit',
|
|
47
|
+
String(limit),
|
|
48
|
+
'--json',
|
|
49
|
+
'number,title,state,labels,body',
|
|
50
|
+
]);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
return { kind: 'failed', error: firstLine(result.error) };
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(result.stdout);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { kind: 'failed', error: `gh returned output that is not JSON: ${err.message}` };
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(parsed)) {
|
|
62
|
+
return { kind: 'failed', error: `gh returned ${typeof parsed} where a list of issues was expected` };
|
|
63
|
+
}
|
|
64
|
+
const issues = [];
|
|
65
|
+
for (const raw of parsed) {
|
|
66
|
+
const issue = toRemoteIssue(raw);
|
|
67
|
+
if (issue === null) {
|
|
68
|
+
return {
|
|
69
|
+
kind: 'failed',
|
|
70
|
+
error: 'gh returned an issue this version does not understand — expected number, title, ' +
|
|
71
|
+
`state, labels[].name and body, got keys ${describeKeys(raw)}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
issues.push(issue);
|
|
75
|
+
}
|
|
76
|
+
// Deliberately after parsing, so a truncated response is still shape-checked: a limit
|
|
77
|
+
// hit alongside a shape change should report the shape change too on the next run.
|
|
78
|
+
if (issues.length === limit) {
|
|
79
|
+
return { kind: 'truncated', count: issues.length, limit };
|
|
80
|
+
}
|
|
81
|
+
return { kind: 'ok', issues };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Narrow one `gh` issue object, or null when the shape is not what this version expects.
|
|
85
|
+
*
|
|
86
|
+
* Refusing beats coercing. A `title` read as `undefined` would compare unequal to every
|
|
87
|
+
* item title and plan an `update-title` for the whole roadmap.
|
|
88
|
+
*/
|
|
89
|
+
function toRemoteIssue(raw) {
|
|
90
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
91
|
+
return null;
|
|
92
|
+
const r = raw;
|
|
93
|
+
if (typeof r.number !== 'number')
|
|
94
|
+
return null;
|
|
95
|
+
if (typeof r.title !== 'string')
|
|
96
|
+
return null;
|
|
97
|
+
if (typeof r.body !== 'string')
|
|
98
|
+
return null;
|
|
99
|
+
if (r.state !== 'OPEN' && r.state !== 'CLOSED')
|
|
100
|
+
return null;
|
|
101
|
+
if (!Array.isArray(r.labels))
|
|
102
|
+
return null;
|
|
103
|
+
const labels = [];
|
|
104
|
+
for (const entry of r.labels) {
|
|
105
|
+
if (typeof entry !== 'object' || entry === null)
|
|
106
|
+
return null;
|
|
107
|
+
const name = entry.name;
|
|
108
|
+
if (typeof name !== 'string')
|
|
109
|
+
return null;
|
|
110
|
+
labels.push(name);
|
|
111
|
+
}
|
|
112
|
+
return { number: r.number, title: r.title, state: r.state, labels, body: r.body };
|
|
113
|
+
}
|
|
114
|
+
/** Key list of an unexpected payload, for the shape-failure message. */
|
|
115
|
+
function describeKeys(raw) {
|
|
116
|
+
if (typeof raw !== 'object' || raw === null)
|
|
117
|
+
return typeof raw;
|
|
118
|
+
return Object.keys(raw).sort().join(', ') || '(none)';
|
|
119
|
+
}
|
|
120
|
+
/** The message for a truncated map. */
|
|
121
|
+
export function truncatedMessage(count, limit) {
|
|
122
|
+
return (`the issue map may be incomplete: gh returned ${count} issues, which is the limit ` +
|
|
123
|
+
`requested (${limit}). Refusing rather than planning creates for issues that may already ` +
|
|
124
|
+
'exist — a partial map turns every missing issue into a duplicate.');
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/roadmap/sync/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAiB,MAAM,SAAS,CAAC;AAG7D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAarC,kDAAkD;AAClD,MAAM,UAAU,aAAa,CAAC,EAAY,EAAE,IAAkB;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,gBAAgB,CAAC;IAE7C,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,CAAC;QACpB,OAAO;QACP,MAAM;QACN,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QACtB,SAAS;QACT,IAAI,CAAC,KAAK;QACV,SAAS;QACT,KAAK;QACL,SAAS;QACT,MAAM,CAAC,KAAK,CAAC;QACb,QAAQ;QACR,gCAAgC;KACjC,CAAC,CAAC;IAEH,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IAC5D,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,wCAAyC,GAAa,CAAC,OAAO,EAAE,EAAE,CAAC;IACrG,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,OAAO,MAAM,sCAAsC,EAAE,CAAC;IACvG,CAAC;IAED,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO;gBACL,IAAI,EAAE,QAAQ;gBACd,KAAK,EACH,kFAAkF;oBAClF,2CAA2C,YAAY,CAAC,GAAG,CAAC,EAAE;aACjE,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,sFAAsF;IACtF,mFAAmF;IACnF,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC5B,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;IAC5D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,GAAY;IACjC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/E,MAAM,CAAC,GAAG,GAA8B,CAAC;IAEzC,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC9C,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAE1C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC7D,MAAM,IAAI,GAAI,KAAiC,CAAC,IAAI,CAAC;QACrD,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACpF,CAAC;AAED,wEAAwE;AACxE,SAAS,YAAY,CAAC,GAAY;IAChC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,OAAO,GAAG,CAAC;IAC/D,OAAO,MAAM,CAAC,IAAI,CAAC,GAA8B,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;AACnF,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,KAAa;IAC3D,OAAO,CACL,gDAAgD,KAAK,8BAA8B;QACnF,cAAc,KAAK,uEAAuE;QAC1F,mEAAmE,CACpE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply a plan. The only module in this feature that writes anything to GitHub.
|
|
3
|
+
*
|
|
4
|
+
* It imports no `node:fs` — deliberately, and pinned by a test. `sync` writes nothing to
|
|
5
|
+
* disk, and the cheapest way to keep that true is for the writing module to have no
|
|
6
|
+
* ability to. Snapshotting a directory around a run would be the weaker check: cost
|
|
7
|
+
* capture writes into `.aidlc/` during a session, so a whole-tree snapshot is either
|
|
8
|
+
* flaky or has to exclude so much that it stops proving anything.
|
|
9
|
+
*
|
|
10
|
+
* A `create` is **one** invocation carrying the discovery label, the status label, and the
|
|
11
|
+
* body marker. If the label were applied by a second call, a process death between the two
|
|
12
|
+
* would strand an issue invisible to discovery — which the next run would duplicate. One
|
|
13
|
+
* invocation makes the issue discoverable the instant it exists, and that atomicity is
|
|
14
|
+
* what the whole no-local-state design rests on.
|
|
15
|
+
*
|
|
16
|
+
* Execution **aborts on the first failure**. A rate limit or an authorization failure will
|
|
17
|
+
* fail everything after it too, so continuing would turn one legible error into forty.
|
|
18
|
+
*
|
|
19
|
+
* Requirements: roadmap-issue-projection/AC-23, roadmap-issue-projection/AC-41,
|
|
20
|
+
* roadmap-issue-projection/AC-42, roadmap-issue-projection/AC-43,
|
|
21
|
+
* roadmap-issue-projection/AC-46
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { type GhRunner } from './gh.js';
|
|
26
|
+
import { type Plan, type PlanLine } from './types.js';
|
|
27
|
+
export interface ExecReport {
|
|
28
|
+
applied: readonly PlanLine[];
|
|
29
|
+
/** Set when a write failed; everything in `notAttempted` was skipped because of it. */
|
|
30
|
+
failure: {
|
|
31
|
+
line: PlanLine;
|
|
32
|
+
error: string;
|
|
33
|
+
} | null;
|
|
34
|
+
notAttempted: readonly PlanLine[];
|
|
35
|
+
}
|
|
36
|
+
export declare function applyPlan(gh: GhRunner, plan: Plan, discoveryLabel: string): ExecReport;
|
|
37
|
+
/** The `gh` argv for one write outcome. */
|
|
38
|
+
export declare function invocationFor(plan: Plan, line: PlanLine, discoveryLabel: string): string[];
|
|
39
|
+
/** Render an execution report for stdout. */
|
|
40
|
+
export declare function renderExecReport(report: ExecReport): string;
|
|
41
|
+
//# sourceMappingURL=execute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/execute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE7D,OAAO,EAAW,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC7B,uFAAuF;IACvF,OAAO,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,YAAY,EAAE,SAAS,QAAQ,EAAE,CAAC;CACnC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,UAAU,CAyBtF;AAED,2CAA2C;AAC3C,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,CA6C1F;AAyCD,6CAA6C;AAC7C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAuB3D"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply a plan. The only module in this feature that writes anything to GitHub.
|
|
3
|
+
*
|
|
4
|
+
* It imports no `node:fs` — deliberately, and pinned by a test. `sync` writes nothing to
|
|
5
|
+
* disk, and the cheapest way to keep that true is for the writing module to have no
|
|
6
|
+
* ability to. Snapshotting a directory around a run would be the weaker check: cost
|
|
7
|
+
* capture writes into `.aidlc/` during a session, so a whole-tree snapshot is either
|
|
8
|
+
* flaky or has to exclude so much that it stops proving anything.
|
|
9
|
+
*
|
|
10
|
+
* A `create` is **one** invocation carrying the discovery label, the status label, and the
|
|
11
|
+
* body marker. If the label were applied by a second call, a process death between the two
|
|
12
|
+
* would strand an issue invisible to discovery — which the next run would duplicate. One
|
|
13
|
+
* invocation makes the issue discoverable the instant it exists, and that atomicity is
|
|
14
|
+
* what the whole no-local-state design rests on.
|
|
15
|
+
*
|
|
16
|
+
* Execution **aborts on the first failure**. A rate limit or an authorization failure will
|
|
17
|
+
* fail everything after it too, so continuing would turn one legible error into forty.
|
|
18
|
+
*
|
|
19
|
+
* Requirements: roadmap-issue-projection/AC-23, roadmap-issue-projection/AC-41,
|
|
20
|
+
* roadmap-issue-projection/AC-42, roadmap-issue-projection/AC-43,
|
|
21
|
+
* roadmap-issue-projection/AC-46
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { renderIssueBody } from './body.js';
|
|
26
|
+
import { firstLine, repoArgs } from './gh.js';
|
|
27
|
+
import { colorFor, descriptionFor } from './labels.js';
|
|
28
|
+
import { isWrite } from './types.js';
|
|
29
|
+
export function applyPlan(gh, plan, discoveryLabel) {
|
|
30
|
+
const writes = plan.lines.filter((l) => isWrite(l.outcome));
|
|
31
|
+
const applied = [];
|
|
32
|
+
const ensured = new Set();
|
|
33
|
+
for (let i = 0; i < writes.length; i++) {
|
|
34
|
+
const line = writes[i];
|
|
35
|
+
const labelFailure = ensureLabels(gh, plan, line, discoveryLabel, ensured);
|
|
36
|
+
if (labelFailure !== null) {
|
|
37
|
+
return { applied, failure: { line, error: labelFailure }, notAttempted: writes.slice(i) };
|
|
38
|
+
}
|
|
39
|
+
const result = gh.run(invocationFor(plan, line, discoveryLabel));
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
return {
|
|
42
|
+
applied,
|
|
43
|
+
failure: { line, error: firstLine(result.error) },
|
|
44
|
+
notAttempted: writes.slice(i + 1),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
applied.push(line);
|
|
48
|
+
}
|
|
49
|
+
return { applied, failure: null, notAttempted: [] };
|
|
50
|
+
}
|
|
51
|
+
/** The `gh` argv for one write outcome. */
|
|
52
|
+
export function invocationFor(plan, line, discoveryLabel) {
|
|
53
|
+
const repo = [...repoArgs(plan.repo)];
|
|
54
|
+
switch (line.outcome) {
|
|
55
|
+
case 'create': {
|
|
56
|
+
if (line.item === null)
|
|
57
|
+
throw new Error('create without an item');
|
|
58
|
+
return [
|
|
59
|
+
'issue',
|
|
60
|
+
'create',
|
|
61
|
+
...repo,
|
|
62
|
+
'--title',
|
|
63
|
+
line.item.title,
|
|
64
|
+
'--body',
|
|
65
|
+
renderIssueBody(line.item),
|
|
66
|
+
'--label',
|
|
67
|
+
discoveryLabel,
|
|
68
|
+
...line.addLabels.flatMap((l) => ['--label', l]),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
case 'update-title': {
|
|
72
|
+
if (line.item === null || line.issueNumber === null) {
|
|
73
|
+
throw new Error('update-title without an item and issue');
|
|
74
|
+
}
|
|
75
|
+
return ['issue', 'edit', String(line.issueNumber), ...repo, '--title', line.item.title];
|
|
76
|
+
}
|
|
77
|
+
case 'update-labels': {
|
|
78
|
+
if (line.issueNumber === null)
|
|
79
|
+
throw new Error('update-labels without an issue');
|
|
80
|
+
// --add-label / --remove-label, never a wholesale --label: a human's labels on a
|
|
81
|
+
// projected issue are theirs, and replacing the set would silently delete them.
|
|
82
|
+
return [
|
|
83
|
+
'issue',
|
|
84
|
+
'edit',
|
|
85
|
+
String(line.issueNumber),
|
|
86
|
+
...repo,
|
|
87
|
+
...line.addLabels.flatMap((l) => ['--add-label', l]),
|
|
88
|
+
...line.removeLabels.flatMap((l) => ['--remove-label', l]),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
case 'close': {
|
|
92
|
+
if (line.issueNumber === null)
|
|
93
|
+
throw new Error('close without an issue');
|
|
94
|
+
return ['issue', 'close', String(line.issueNumber), ...repo];
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
throw new Error(`not a write outcome: ${line.outcome}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create every label this line needs, once per run.
|
|
102
|
+
*
|
|
103
|
+
* `gh label create` **exits 1 when the label already exists**, so bare creation plus
|
|
104
|
+
* abort-on-first-failure would kill every run after the first. `--force` fixes that — but
|
|
105
|
+
* `gh` chooses a *random* colour when none is given, so `--force` alone would repaint the
|
|
106
|
+
* label on every run. The colour and description are therefore always passed, from a fixed
|
|
107
|
+
* table, which makes the ensure genuinely idempotent rather than merely non-failing.
|
|
108
|
+
*/
|
|
109
|
+
function ensureLabels(gh, plan, line, discoveryLabel, ensured) {
|
|
110
|
+
const needed = line.outcome === 'create' ? [discoveryLabel, ...line.addLabels] : [...line.addLabels];
|
|
111
|
+
for (const name of needed) {
|
|
112
|
+
if (ensured.has(name))
|
|
113
|
+
continue;
|
|
114
|
+
const result = gh.run([
|
|
115
|
+
'label',
|
|
116
|
+
'create',
|
|
117
|
+
name,
|
|
118
|
+
...repoArgs(plan.repo),
|
|
119
|
+
'--color',
|
|
120
|
+
colorFor(name),
|
|
121
|
+
'--description',
|
|
122
|
+
descriptionFor(name),
|
|
123
|
+
'--force',
|
|
124
|
+
]);
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
return `could not create the label ${JSON.stringify(name)}: ${firstLine(result.error)}`;
|
|
127
|
+
}
|
|
128
|
+
ensured.add(name);
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/** Render an execution report for stdout. */
|
|
133
|
+
export function renderExecReport(report) {
|
|
134
|
+
const out = [];
|
|
135
|
+
if (report.applied.length > 0) {
|
|
136
|
+
out.push(`Applied ${report.applied.length} write${report.applied.length === 1 ? '' : 's'}:`);
|
|
137
|
+
for (const l of report.applied)
|
|
138
|
+
out.push(` ${l.outcome} ${subjectOf(l)}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
out.push('No writes were applied.');
|
|
142
|
+
}
|
|
143
|
+
if (report.failure !== null) {
|
|
144
|
+
out.push('');
|
|
145
|
+
out.push(`Stopped at ${report.failure.line.outcome} ${subjectOf(report.failure.line)}`);
|
|
146
|
+
out.push(` gh said: ${report.failure.error}`);
|
|
147
|
+
if (report.notAttempted.length > 0) {
|
|
148
|
+
out.push(` ${report.notAttempted.length} further write(s) were not attempted:`);
|
|
149
|
+
for (const l of report.notAttempted)
|
|
150
|
+
out.push(` ${l.outcome} ${subjectOf(l)}`);
|
|
151
|
+
}
|
|
152
|
+
out.push('');
|
|
153
|
+
out.push('Re-run once the cause is fixed — the projection is idempotent, so completed writes are not repeated.');
|
|
154
|
+
}
|
|
155
|
+
return out.join('\n');
|
|
156
|
+
}
|
|
157
|
+
function subjectOf(line) {
|
|
158
|
+
if (line.item !== null) {
|
|
159
|
+
return `${line.item.status}/${line.item.filename}` +
|
|
160
|
+
(line.issueNumber === null ? '' : ` (#${line.issueNumber})`);
|
|
161
|
+
}
|
|
162
|
+
return `#${line.issueNumber}`;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=execute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../../src/roadmap/sync/execute.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single subprocess boundary between this feature and the `gh` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every `gh` invocation in the projection goes through `GhRunner.run(args)`, and
|
|
5
|
+
* `args` is an array. That is structural rather than habitual on purpose: item titles
|
|
6
|
+
* and config values come from tracked files, so in a cloned repo they are the repo
|
|
7
|
+
* author's strings, not the user's. With one seam there is no call site for somebody
|
|
8
|
+
* in a hurry to add a template string or `shell: true` to, and the fake runner used in
|
|
9
|
+
* tests records every argv — which is what lets the token-secrecy property be checked
|
|
10
|
+
* by *value* (set `GH_TOKEN` to a sentinel, assert it appears in no recorded argv)
|
|
11
|
+
* rather than by grepping the source for a variable name. This project has broken
|
|
12
|
+
* prose-scoped greps six separate times.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here reads `GH_TOKEN` or `GITHUB_TOKEN`. `gh` owns the credential; this
|
|
15
|
+
* module owns the argv.
|
|
16
|
+
*
|
|
17
|
+
* Requirements: roadmap-issue-projection/AC-30, roadmap-issue-projection/AC-50
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
export type GhResult = {
|
|
22
|
+
ok: true;
|
|
23
|
+
stdout: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
error: string;
|
|
27
|
+
exitCode: number | null;
|
|
28
|
+
};
|
|
29
|
+
export interface GhRunner {
|
|
30
|
+
run(args: readonly string[]): GhResult;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 256 MiB.
|
|
34
|
+
*
|
|
35
|
+
* `execFileSync` defaults to 1 MiB, and discovery asks for every projected issue's
|
|
36
|
+
* body so the marker can be read. Measured: the default throws `ENOBUFS` at roughly
|
|
37
|
+
* 2 MiB of output, which a few hundred issues with ordinary bodies will exceed — two
|
|
38
|
+
* orders of magnitude below the 1000-issue map this feature otherwise supports. The
|
|
39
|
+
* failure is a thrown error rather than a truncated read, so it would be loud; it
|
|
40
|
+
* would also be entirely avoidable, which is why the limit is stated.
|
|
41
|
+
*/
|
|
42
|
+
export declare const GH_MAX_BUFFER: number;
|
|
43
|
+
/** The real runner. Argument array, no shell, explicit buffer. */
|
|
44
|
+
export declare function realGhRunner(): GhRunner;
|
|
45
|
+
/**
|
|
46
|
+
* Check that `gh` is present and authenticated.
|
|
47
|
+
*
|
|
48
|
+
* **Authentication only.** `gh auth status` cannot tell whether the token may write
|
|
49
|
+
* issues on the target repository, so a token that is authenticated but not authorized
|
|
50
|
+
* passes here and fails on the first write — where `applyPlan` names the item and
|
|
51
|
+
* prints `gh`'s own message. Claiming otherwise would put the check in the wrong
|
|
52
|
+
* place and make the first-run failure less legible, not more.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ghPreflight(gh: GhRunner): {
|
|
55
|
+
ok: true;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
error: string;
|
|
59
|
+
};
|
|
60
|
+
/** First non-empty line of a `gh` message, for one-line error reporting. */
|
|
61
|
+
export declare function firstLine(text: string): string;
|
|
62
|
+
/** `['--repo', repo]` when a coordinate was resolved, else `[]`. */
|
|
63
|
+
export declare function repoArgs(repo: string | null): readonly string[];
|
|
64
|
+
//# sourceMappingURL=gh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gh.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/gh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,MAAM,QAAQ,GAChB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,QAAoB,CAAC;AAE/C,kEAAkE;AAClE,wBAAgB,YAAY,IAAI,QAAQ,CAkBvC;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASrF;AAED,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,MAAM,EAAE,CAE/D"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single subprocess boundary between this feature and the `gh` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every `gh` invocation in the projection goes through `GhRunner.run(args)`, and
|
|
5
|
+
* `args` is an array. That is structural rather than habitual on purpose: item titles
|
|
6
|
+
* and config values come from tracked files, so in a cloned repo they are the repo
|
|
7
|
+
* author's strings, not the user's. With one seam there is no call site for somebody
|
|
8
|
+
* in a hurry to add a template string or `shell: true` to, and the fake runner used in
|
|
9
|
+
* tests records every argv — which is what lets the token-secrecy property be checked
|
|
10
|
+
* by *value* (set `GH_TOKEN` to a sentinel, assert it appears in no recorded argv)
|
|
11
|
+
* rather than by grepping the source for a variable name. This project has broken
|
|
12
|
+
* prose-scoped greps six separate times.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here reads `GH_TOKEN` or `GITHUB_TOKEN`. `gh` owns the credential; this
|
|
15
|
+
* module owns the argv.
|
|
16
|
+
*
|
|
17
|
+
* Requirements: roadmap-issue-projection/AC-30, roadmap-issue-projection/AC-50
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
/**
|
|
23
|
+
* 256 MiB.
|
|
24
|
+
*
|
|
25
|
+
* `execFileSync` defaults to 1 MiB, and discovery asks for every projected issue's
|
|
26
|
+
* body so the marker can be read. Measured: the default throws `ENOBUFS` at roughly
|
|
27
|
+
* 2 MiB of output, which a few hundred issues with ordinary bodies will exceed — two
|
|
28
|
+
* orders of magnitude below the 1000-issue map this feature otherwise supports. The
|
|
29
|
+
* failure is a thrown error rather than a truncated read, so it would be loud; it
|
|
30
|
+
* would also be entirely avoidable, which is why the limit is stated.
|
|
31
|
+
*/
|
|
32
|
+
export const GH_MAX_BUFFER = 256 * 1024 * 1024;
|
|
33
|
+
/** The real runner. Argument array, no shell, explicit buffer. */
|
|
34
|
+
export function realGhRunner() {
|
|
35
|
+
return {
|
|
36
|
+
run(args) {
|
|
37
|
+
try {
|
|
38
|
+
const stdout = execFileSync('gh', [...args], {
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
maxBuffer: GH_MAX_BUFFER,
|
|
42
|
+
});
|
|
43
|
+
return { ok: true, stdout };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const e = err;
|
|
47
|
+
const stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString('utf8');
|
|
48
|
+
const message = (stderr ?? e.message ?? 'gh invocation failed').trim();
|
|
49
|
+
return { ok: false, error: message, exitCode: e.status ?? null };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check that `gh` is present and authenticated.
|
|
56
|
+
*
|
|
57
|
+
* **Authentication only.** `gh auth status` cannot tell whether the token may write
|
|
58
|
+
* issues on the target repository, so a token that is authenticated but not authorized
|
|
59
|
+
* passes here and fails on the first write — where `applyPlan` names the item and
|
|
60
|
+
* prints `gh`'s own message. Claiming otherwise would put the check in the wrong
|
|
61
|
+
* place and make the first-run failure less legible, not more.
|
|
62
|
+
*/
|
|
63
|
+
export function ghPreflight(gh) {
|
|
64
|
+
const result = gh.run(['auth', 'status']);
|
|
65
|
+
if (result.ok)
|
|
66
|
+
return { ok: true };
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: 'gh is not available or not authenticated — install the GitHub CLI and run ' +
|
|
70
|
+
`\`gh auth login\`, then try again.\n gh said: ${firstLine(result.error)}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** First non-empty line of a `gh` message, for one-line error reporting. */
|
|
74
|
+
export function firstLine(text) {
|
|
75
|
+
for (const line of text.split('\n')) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (trimmed.length > 0)
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
return text.trim();
|
|
81
|
+
}
|
|
82
|
+
/** `['--repo', repo]` when a coordinate was resolved, else `[]`. */
|
|
83
|
+
export function repoArgs(repo) {
|
|
84
|
+
return repo === null ? [] : ['--repo', repo];
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=gh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gh.js","sourceRoot":"","sources":["../../../src/roadmap/sync/gh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/C,kEAAkE;AAClE,MAAM,UAAU,YAAY;IAC1B,OAAO;QACL,GAAG,CAAC,IAAuB;YACzB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE;oBAC3C,QAAQ,EAAE,MAAM;oBAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;oBACjC,SAAS,EAAE,aAAa;iBACzB,CAAC,CAAC;gBACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;YAC9B,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACtB,MAAM,CAAC,GAAG,GAA6E,CAAC;gBACxF,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACpF,MAAM,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,IAAI,sBAAsB,CAAC,CAAC,IAAI,EAAE,CAAC;gBACvE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;YACnE,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,EAAY;IACtC,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC1C,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACnC,OAAO;QACL,EAAE,EAAE,KAAK;QACT,KAAK,EACH,4EAA4E;YAC5E,kDAAkD,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;KAC9E,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,QAAQ,CAAC,IAAmB;IAC1C,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot import: a GitHub issue seeds a roadmap item.
|
|
3
|
+
*
|
|
4
|
+
* Import, not sync. Nothing reconciles afterwards, nothing is written back to the issue,
|
|
5
|
+
* and running it twice over the same issue is reported rather than duplicated.
|
|
6
|
+
*
|
|
7
|
+
* The `title` is **always** double-quoted, with no judgement call about whether this one
|
|
8
|
+
* needs it. An unquoted YAML plain scalar may not begin with a reserved indicator
|
|
9
|
+
* character, so the single most natural way to name a code defect — an identifier in
|
|
10
|
+
* backticks — does not parse; and nothing reports it, because an item that will not parse
|
|
11
|
+
* and an item that was never written look identical. Five of this project's first 49 items
|
|
12
|
+
* were lost that way for up to ten days, and GitHub issue titles are precisely the corpus
|
|
13
|
+
* that produces them.
|
|
14
|
+
*
|
|
15
|
+
* Requirements: roadmap-issue-projection/AC-51 … roadmap-issue-projection/AC-60
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { type GhRunner } from './gh.js';
|
|
20
|
+
/** A GitHub issue, reduced to what identifies it. */
|
|
21
|
+
export interface IssueCoord {
|
|
22
|
+
host: string;
|
|
23
|
+
owner: string;
|
|
24
|
+
repo: string;
|
|
25
|
+
number: number;
|
|
26
|
+
}
|
|
27
|
+
export type ImportResult = {
|
|
28
|
+
kind: 'written';
|
|
29
|
+
path: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'duplicate';
|
|
32
|
+
existing: string;
|
|
33
|
+
coord: IssueCoord;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'refused';
|
|
36
|
+
error: string;
|
|
37
|
+
};
|
|
38
|
+
/** Slug used when a title yields nothing usable. */
|
|
39
|
+
export declare const FALLBACK_SLUG_PREFIX = "imported-issue";
|
|
40
|
+
/**
|
|
41
|
+
* Parse an issue reference into a comparable tuple.
|
|
42
|
+
*
|
|
43
|
+
* Normalising is the point: `https://github.com/o/r/issues/12`, the same with a trailing
|
|
44
|
+
* slash, with a `#issuecomment-…` fragment, with `www.`, and the
|
|
45
|
+
* `api.github.com/repos/o/r/issues/12` form all denote one issue. Comparing raw strings
|
|
46
|
+
* would report false duplicates in one direction and miss real ones in the other — and a
|
|
47
|
+
* naive substring compare makes `issues/1` match `issues/12`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseIssueUrl(raw: string): IssueCoord | null;
|
|
50
|
+
/** Canonical string form of a coordinate, for map keys. */
|
|
51
|
+
export declare function coordKey(coord: IssueCoord): string;
|
|
52
|
+
/**
|
|
53
|
+
* Every issue already linked by a roadmap item, keyed by coordinate.
|
|
54
|
+
*
|
|
55
|
+
* This reads item **bodies**, which the roadmap skill's frontmatter-only rule otherwise
|
|
56
|
+
* discourages. That rule governs *browse*, which is a list and must stay cheap enough to
|
|
57
|
+
* run over the whole roadmap. Import is neither a list nor cheap-by-contract, and there is
|
|
58
|
+
* nowhere else the local half of a duplicate check could come from — DD-1 means no
|
|
59
|
+
* `remote:` field exists to consult.
|
|
60
|
+
*/
|
|
61
|
+
export declare function linkedCoords(cwd: string): Map<string, string>;
|
|
62
|
+
/**
|
|
63
|
+
* A filename-safe slug from an issue title.
|
|
64
|
+
*
|
|
65
|
+
* Must satisfy `ITEM_FILENAME_RE`'s `[a-z0-9][a-z0-9-]*`, which a title of punctuation or
|
|
66
|
+
* of a non-Latin script will not survive — hence the fallback rather than a refusal. An
|
|
67
|
+
* issue that cannot be imported because of its title would be a strange thing to tell
|
|
68
|
+
* somebody.
|
|
69
|
+
*/
|
|
70
|
+
export declare function slugFromTitle(title: string, number: number): string;
|
|
71
|
+
/** Import one issue into `inbox/`. */
|
|
72
|
+
export declare function importIssue(gh: GhRunner, cwd: string, url: string): ImportResult;
|
|
73
|
+
//# sourceMappingURL=import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../../src/roadmap/sync/import.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAYH,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnD,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,oDAAoD;AACpD,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAKrD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA2B5D;AAQD,2DAA2D;AAC3D,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAElD;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA8B7D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAYnE;AAuBD,sCAAsC;AACtC,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CA+ChF"}
|