@h-rig/standard-plugin 0.0.6-alpha.14 → 0.0.6-alpha.140
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/src/bundle.d.ts +6 -0
- package/dist/src/bundle.js +1602 -0
- package/dist/src/drift/__fixtures__/temp-repo.d.ts +9 -0
- package/dist/src/drift/__fixtures__/temp-repo.js +41 -0
- package/dist/src/drift/detect.d.ts +11 -0
- package/dist/src/drift/detect.js +299 -0
- package/dist/src/drift/extract-refs.d.ts +7 -0
- package/dist/src/drift/extract-refs.js +60 -0
- package/dist/src/drift/git-adapter.d.ts +7 -0
- package/dist/src/drift/git-adapter.js +63 -0
- package/dist/src/drift/judge.d.ts +19 -0
- package/dist/src/drift/judge.js +16 -0
- package/dist/src/drift/plugin.d.ts +63 -0
- package/dist/src/drift/plugin.js +508 -0
- package/dist/src/files-source.d.ts +18 -0
- package/dist/src/files-source.js +4 -3
- package/dist/src/github-issues-source.d.ts +80 -0
- package/dist/src/github-issues-source.js +436 -50
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +1102 -62
- package/dist/src/plugin.d.ts +26 -0
- package/dist/src/plugin.js +1685 -0
- package/package.json +28 -5
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/standard-plugin/src/
|
|
2
|
+
// packages/standard-plugin/src/plugin.ts
|
|
3
|
+
import { resolve as resolve4 } from "path";
|
|
3
4
|
import { definePlugin } from "@rig/core";
|
|
4
5
|
|
|
5
6
|
// packages/standard-plugin/src/github-issues-source.ts
|
|
@@ -14,9 +15,9 @@ function createEnvGitHubCredentialProvider() {
|
|
|
14
15
|
return {
|
|
15
16
|
async resolveGitHubToken(input) {
|
|
16
17
|
if (input.purpose === "selected-repo") {
|
|
17
|
-
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
18
19
|
}
|
|
19
|
-
const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
|
+
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
20
21
|
if (!token) {
|
|
21
22
|
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
22
23
|
}
|
|
@@ -25,34 +26,40 @@ function createEnvGitHubCredentialProvider() {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
function createStateGitHubCredentialProvider(options = {}) {
|
|
28
|
-
const
|
|
29
|
+
const stateFileCandidates = () => {
|
|
30
|
+
const candidates = [];
|
|
29
31
|
const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
|
|
30
32
|
if (explicitFile?.trim())
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
candidates.push(resolve(explicitFile.trim()));
|
|
34
|
+
for (const dir of [options.stateDir, process.env.RIG_STATE_DIR]) {
|
|
35
|
+
if (dir?.trim())
|
|
36
|
+
candidates.push(resolve(dir.trim(), "github-auth.json"));
|
|
37
|
+
}
|
|
38
|
+
return candidates;
|
|
34
39
|
};
|
|
35
40
|
const readToken = () => {
|
|
36
|
-
const stateFile
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
for (const stateFile of stateFileCandidates()) {
|
|
42
|
+
if (!existsSync(stateFile))
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
46
|
+
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
47
|
+
if (token)
|
|
48
|
+
return token;
|
|
49
|
+
} catch {}
|
|
44
50
|
}
|
|
51
|
+
return null;
|
|
45
52
|
};
|
|
46
53
|
return {
|
|
47
54
|
async resolveGitHubToken(input) {
|
|
48
55
|
const token = readToken();
|
|
49
56
|
if (input.purpose === "selected-repo") {
|
|
50
|
-
return { token: token ?? "", source: "signed-in-user" };
|
|
57
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
51
58
|
}
|
|
52
59
|
if (token) {
|
|
53
60
|
return { token, source: "signed-in-user" };
|
|
54
61
|
}
|
|
55
|
-
const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
62
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
56
63
|
if (!fallback) {
|
|
57
64
|
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
58
65
|
}
|
|
@@ -86,17 +93,42 @@ function statusFor(issue) {
|
|
|
86
93
|
return "cancelled";
|
|
87
94
|
return "open";
|
|
88
95
|
}
|
|
96
|
+
function parseIssueRefs(raw) {
|
|
97
|
+
return raw.split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
98
|
+
}
|
|
99
|
+
function parseMetadataList(body, key) {
|
|
100
|
+
const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
|
|
101
|
+
if (!block)
|
|
102
|
+
return [];
|
|
103
|
+
const lines = block[1].split(/\r?\n/);
|
|
104
|
+
const values = [];
|
|
105
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
106
|
+
const line = lines[index];
|
|
107
|
+
const sameLine = line.match(new RegExp(`^${key}:\\s*(.+)$`, "i"));
|
|
108
|
+
if (sameLine) {
|
|
109
|
+
values.push(...parseIssueRefs(sameLine[1]));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!new RegExp(`^${key}:\\s*$`, "i").test(line))
|
|
113
|
+
continue;
|
|
114
|
+
for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
|
|
115
|
+
const item = lines[cursor].match(/^\s*-\s*(.+)$/);
|
|
116
|
+
if (!item)
|
|
117
|
+
break;
|
|
118
|
+
values.push(...parseIssueRefs(item[1]));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...new Set(values)];
|
|
122
|
+
}
|
|
89
123
|
function parseDeps(body) {
|
|
90
124
|
const match = body.match(/^depends-on:\s*([^\n]+)/im);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
125
|
+
const bodyRefs = match ? parseIssueRefs(match[1]) : [];
|
|
126
|
+
return [...new Set([...bodyRefs, ...parseMetadataList(body, "depends-on")])];
|
|
94
127
|
}
|
|
95
128
|
function parseParents(body) {
|
|
96
129
|
const match = body.match(/^parents?:\s*([^\n]+)/im);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
130
|
+
const bodyRefs = match ? parseIssueRefs(match[1]) : [];
|
|
131
|
+
return [...new Set([...bodyRefs, ...parseMetadataList(body, "parents")])];
|
|
100
132
|
}
|
|
101
133
|
function issueTypeFor(issue) {
|
|
102
134
|
const labels = labelNamesFor(issue);
|
|
@@ -107,7 +139,7 @@ function issueTypeFor(issue) {
|
|
|
107
139
|
return "epic";
|
|
108
140
|
return "task";
|
|
109
141
|
}
|
|
110
|
-
function issueToTask(issue, repo) {
|
|
142
|
+
function issueToTask(issue, repo, nativeDependencies) {
|
|
111
143
|
const labelNames = labelNamesFor(issue);
|
|
112
144
|
const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
|
|
113
145
|
const roleLabel = labelNames.find((l) => l.startsWith("role:"));
|
|
@@ -115,10 +147,12 @@ function issueToTask(issue, repo) {
|
|
|
115
147
|
const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
|
|
116
148
|
const body = issue.body ?? "";
|
|
117
149
|
const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
150
|
+
const parsedDeps = parseDeps(body);
|
|
151
|
+
const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
|
|
118
152
|
return {
|
|
119
153
|
id: String(issue.number),
|
|
120
154
|
...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
|
|
121
|
-
deps
|
|
155
|
+
deps,
|
|
122
156
|
status: statusFor(issue),
|
|
123
157
|
title: issue.title,
|
|
124
158
|
body,
|
|
@@ -130,6 +164,7 @@ function issueToTask(issue, repo) {
|
|
|
130
164
|
sourceIssueId: `${repo}#${issue.number}`,
|
|
131
165
|
parentChildDeps: parseParents(body),
|
|
132
166
|
labels: labelNames,
|
|
167
|
+
...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
|
|
133
168
|
raw: issue
|
|
134
169
|
};
|
|
135
170
|
}
|
|
@@ -166,17 +201,41 @@ ${rendered}
|
|
|
166
201
|
` : `${rendered}
|
|
167
202
|
`;
|
|
168
203
|
}
|
|
204
|
+
function buildRigStickyStatusComment(input) {
|
|
205
|
+
const lines = [
|
|
206
|
+
RIG_STATUS_COMMENT_MARKER,
|
|
207
|
+
`### Rig status: ${input.status}`,
|
|
208
|
+
"",
|
|
209
|
+
input.summary
|
|
210
|
+
];
|
|
211
|
+
if (input.runId)
|
|
212
|
+
lines.push("", `- Run: ${input.runId}`);
|
|
213
|
+
if (input.prUrl)
|
|
214
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
215
|
+
for (const detail of input.details ?? [])
|
|
216
|
+
lines.push(`- ${detail}`);
|
|
217
|
+
return lines.join(`
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
169
220
|
function isRigStickyStatusComment(body) {
|
|
170
221
|
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
171
222
|
}
|
|
172
223
|
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
224
|
+
return {
|
|
225
|
+
encoding: "utf-8",
|
|
226
|
+
timeout: timeoutMs,
|
|
227
|
+
env: {
|
|
228
|
+
...process.env,
|
|
229
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
230
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
231
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
232
|
+
...extraEnv ?? {}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
176
235
|
}
|
|
177
236
|
function credentialEnv(token) {
|
|
178
237
|
const clean = token?.trim() ?? "";
|
|
179
|
-
return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
|
|
238
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
180
239
|
}
|
|
181
240
|
async function resolveCredentialEnv(opts, purpose) {
|
|
182
241
|
if (!opts.credentialProvider)
|
|
@@ -191,28 +250,319 @@ async function resolveCredentialEnv(opts, purpose) {
|
|
|
191
250
|
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
192
251
|
return credentialEnv(resolved.token);
|
|
193
252
|
}
|
|
253
|
+
function tokenDiagnostic(value) {
|
|
254
|
+
const clean = value?.trim() ?? "";
|
|
255
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
256
|
+
}
|
|
194
257
|
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
195
|
-
const
|
|
196
|
-
|
|
258
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
259
|
+
const res = spawn(bin, [...args], options);
|
|
260
|
+
assertGhSuccess(args, res, options.env);
|
|
197
261
|
if (!res.stdout || res.stdout.trim() === "")
|
|
198
262
|
return [];
|
|
199
263
|
return JSON.parse(res.stdout);
|
|
200
264
|
}
|
|
201
265
|
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
202
|
-
const
|
|
203
|
-
|
|
266
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
267
|
+
const res = spawn(bin, [...args], options);
|
|
268
|
+
assertGhSuccess(args, res, options.env);
|
|
204
269
|
}
|
|
205
|
-
function assertGhSuccess(args, res) {
|
|
270
|
+
function assertGhSuccess(args, res, env) {
|
|
206
271
|
if (res.error) {
|
|
207
272
|
const msg = res.error.message ?? String(res.error);
|
|
208
273
|
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
209
274
|
}
|
|
210
275
|
if (res.status !== 0) {
|
|
211
|
-
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
276
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
277
|
+
[rig gh env:standard-plugin] GH_TOKEN=${tokenDiagnostic(env.GH_TOKEN)} GITHUB_TOKEN=${tokenDiagnostic(env.GITHUB_TOKEN)} RIG_GITHUB_TOKEN=${tokenDiagnostic(env.RIG_GITHUB_TOKEN)}`);
|
|
212
278
|
}
|
|
213
279
|
}
|
|
280
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
281
|
+
todo: "Todo",
|
|
282
|
+
running: "In Progress",
|
|
283
|
+
prOpen: "In Review",
|
|
284
|
+
ciFixing: "In Review",
|
|
285
|
+
merging: "In Review",
|
|
286
|
+
done: "Done",
|
|
287
|
+
needsAttention: "Needs Attention"
|
|
288
|
+
};
|
|
289
|
+
function asProjectRecord(value) {
|
|
290
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
291
|
+
}
|
|
292
|
+
function projectString(value) {
|
|
293
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
294
|
+
}
|
|
295
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
296
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
297
|
+
switch (normalized) {
|
|
298
|
+
case "draft":
|
|
299
|
+
case "open":
|
|
300
|
+
case "queued":
|
|
301
|
+
case "ready":
|
|
302
|
+
return "todo";
|
|
303
|
+
case "running":
|
|
304
|
+
case "in_progress":
|
|
305
|
+
return "running";
|
|
306
|
+
case "under_review":
|
|
307
|
+
case "review":
|
|
308
|
+
case "pr_open":
|
|
309
|
+
return "prOpen";
|
|
310
|
+
case "ci_fixing":
|
|
311
|
+
case "fixing":
|
|
312
|
+
return "ciFixing";
|
|
313
|
+
case "merging":
|
|
314
|
+
case "merge":
|
|
315
|
+
return "merging";
|
|
316
|
+
case "closed":
|
|
317
|
+
case "completed":
|
|
318
|
+
case "done":
|
|
319
|
+
return "done";
|
|
320
|
+
case "blocked":
|
|
321
|
+
case "cancelled":
|
|
322
|
+
case "failed":
|
|
323
|
+
case "needs_attention":
|
|
324
|
+
return "needsAttention";
|
|
325
|
+
default:
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
330
|
+
return async (query, variables) => {
|
|
331
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
332
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
333
|
+
if (value === undefined || value === null)
|
|
334
|
+
continue;
|
|
335
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
336
|
+
}
|
|
337
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
338
|
+
return asProjectRecord(response)?.data ?? response;
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function issueNodeIdFor(issue) {
|
|
342
|
+
const id = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
343
|
+
return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
|
|
344
|
+
}
|
|
345
|
+
function nativeIssueDependencyRef(value, currentRepo) {
|
|
346
|
+
const record = asProjectRecord(value);
|
|
347
|
+
const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
|
|
348
|
+
if (!number)
|
|
349
|
+
return null;
|
|
350
|
+
const repository = asProjectRecord(record?.repository);
|
|
351
|
+
const owner = projectString(asProjectRecord(repository?.owner)?.login);
|
|
352
|
+
const name = projectString(repository?.name);
|
|
353
|
+
if (!owner || !name || `${owner}/${name}` === currentRepo)
|
|
354
|
+
return number;
|
|
355
|
+
return `${owner}/${name}#${number}`;
|
|
356
|
+
}
|
|
357
|
+
function nativeDependencyRefsFrom(data, currentRepo) {
|
|
358
|
+
const issue = asProjectRecord(asProjectRecord(data)?.node);
|
|
359
|
+
const blockedBy = asProjectRecord(issue?.blockedBy);
|
|
360
|
+
const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
|
|
361
|
+
return [...new Set(nodes.flatMap((node) => {
|
|
362
|
+
const ref = nativeIssueDependencyRef(node, currentRepo);
|
|
363
|
+
return ref ? [ref] : [];
|
|
364
|
+
}))];
|
|
365
|
+
}
|
|
366
|
+
async function readNativeDependenciesForIssue(input) {
|
|
367
|
+
const issueId = issueNodeIdFor(input.issue);
|
|
368
|
+
if (!issueId)
|
|
369
|
+
return { deps: [], degraded: "GitHub issue node id is unavailable." };
|
|
370
|
+
const query = `
|
|
371
|
+
query RigIssueNativeDependencies($issueId: ID!) {
|
|
372
|
+
node(id: $issueId) {
|
|
373
|
+
... on Issue {
|
|
374
|
+
blockedBy(first: 100) {
|
|
375
|
+
nodes {
|
|
376
|
+
number
|
|
377
|
+
repository { name owner { login } }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
`;
|
|
384
|
+
try {
|
|
385
|
+
return {
|
|
386
|
+
deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
390
|
+
return { deps: [], degraded: detail };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function formatIssueReference(ref) {
|
|
394
|
+
const clean = ref.trim().replace(/^#/, "");
|
|
395
|
+
return /^\d+$/.test(clean) ? `#${clean}` : clean;
|
|
396
|
+
}
|
|
397
|
+
function appendReferenceLines(body, deps, parents) {
|
|
398
|
+
const lines = [];
|
|
399
|
+
const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
400
|
+
const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
401
|
+
if (cleanDeps.length > 0)
|
|
402
|
+
lines.push(`depends-on: ${cleanDeps.join(", ")}`);
|
|
403
|
+
if (cleanParents.length > 0)
|
|
404
|
+
lines.push(`parents: ${cleanParents.join(", ")}`);
|
|
405
|
+
if (lines.length === 0)
|
|
406
|
+
return body;
|
|
407
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
408
|
+
|
|
409
|
+
${lines.join(`
|
|
410
|
+
`)}` : lines.join(`
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
function bodyForCreatedTask(input) {
|
|
414
|
+
const metadata = { ...input.metadata ?? {} };
|
|
415
|
+
if (input.deps && input.deps.length > 0)
|
|
416
|
+
metadata["depends-on"] = input.deps.map(formatIssueReference);
|
|
417
|
+
if (input.parents && input.parents.length > 0)
|
|
418
|
+
metadata.parents = input.parents.map(formatIssueReference);
|
|
419
|
+
return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
|
|
420
|
+
}
|
|
421
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
422
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
423
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
424
|
+
const record = asProjectRecord(node);
|
|
425
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
426
|
+
continue;
|
|
427
|
+
const id = projectString(record?.id);
|
|
428
|
+
if (!id)
|
|
429
|
+
continue;
|
|
430
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
431
|
+
const optionRecord = asProjectRecord(option);
|
|
432
|
+
const optionId = projectString(optionRecord?.id);
|
|
433
|
+
const name = projectString(optionRecord?.name);
|
|
434
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
435
|
+
}) : [];
|
|
436
|
+
return { id, name: "Status", options };
|
|
437
|
+
}
|
|
438
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
439
|
+
}
|
|
440
|
+
async function resolveProjectStatusField(input) {
|
|
441
|
+
const query = `
|
|
442
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
443
|
+
node(id: $projectId) {
|
|
444
|
+
... on ProjectV2 {
|
|
445
|
+
fields(first: 50) {
|
|
446
|
+
nodes {
|
|
447
|
+
... on ProjectV2FieldCommon { id name }
|
|
448
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
`;
|
|
455
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
456
|
+
}
|
|
457
|
+
async function ensureIssueProjectItem(input) {
|
|
458
|
+
const query = `
|
|
459
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
460
|
+
node(id: $projectId) {
|
|
461
|
+
... on ProjectV2 {
|
|
462
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
`;
|
|
467
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
468
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
469
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
470
|
+
const record = asProjectRecord(node);
|
|
471
|
+
const content = asProjectRecord(record?.content);
|
|
472
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
473
|
+
const id2 = projectString(record?.id);
|
|
474
|
+
if (id2)
|
|
475
|
+
return { id: id2, created: false };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const mutation = `
|
|
479
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
480
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
481
|
+
}
|
|
482
|
+
`;
|
|
483
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
484
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
485
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
486
|
+
if (!id)
|
|
487
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
488
|
+
return { id, created: true };
|
|
489
|
+
}
|
|
490
|
+
async function updateIssueProjectStatus(input) {
|
|
491
|
+
const mutation = `
|
|
492
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
493
|
+
updateProjectV2ItemFieldValue(input: {
|
|
494
|
+
projectId: $projectId,
|
|
495
|
+
itemId: $itemId,
|
|
496
|
+
fieldId: $fieldId,
|
|
497
|
+
value: { singleSelectOptionId: $optionId }
|
|
498
|
+
}) { projectV2Item { id } }
|
|
499
|
+
}
|
|
500
|
+
`;
|
|
501
|
+
await input.fetchGraphQL(mutation, {
|
|
502
|
+
projectId: input.projectId,
|
|
503
|
+
itemId: input.itemId,
|
|
504
|
+
fieldId: input.fieldId,
|
|
505
|
+
optionId: input.optionId
|
|
506
|
+
}, input.token);
|
|
507
|
+
}
|
|
508
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
509
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
510
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
511
|
+
}
|
|
512
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
513
|
+
if (!projects?.enabled)
|
|
514
|
+
return;
|
|
515
|
+
const projectId = projectString(projects.projectId);
|
|
516
|
+
if (!projectId)
|
|
517
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
518
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
519
|
+
if (!lifecycleStatus)
|
|
520
|
+
return;
|
|
521
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
522
|
+
if (!issueNodeId)
|
|
523
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
524
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
525
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
526
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
527
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
528
|
+
if (!option)
|
|
529
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
530
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
531
|
+
await updateIssueProjectStatus({
|
|
532
|
+
projectId,
|
|
533
|
+
itemId: item.id,
|
|
534
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
535
|
+
optionId: option.id,
|
|
536
|
+
token: "gh-cli",
|
|
537
|
+
fetchGraphQL
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
541
|
+
function normalizeTaskStatusToken(status) {
|
|
542
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
543
|
+
}
|
|
544
|
+
function issueUpdatesMode(value) {
|
|
545
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
546
|
+
}
|
|
547
|
+
function isTerminalTaskStatus(status) {
|
|
548
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
549
|
+
}
|
|
550
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
551
|
+
if (mode === "off")
|
|
552
|
+
return false;
|
|
553
|
+
if (mode === "lifecycle")
|
|
554
|
+
return true;
|
|
555
|
+
return isTerminalTaskStatus(status);
|
|
556
|
+
}
|
|
557
|
+
function isRunningStatus(status) {
|
|
558
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
559
|
+
}
|
|
560
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
561
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
562
|
+
}
|
|
214
563
|
function statusLabelFor(status) {
|
|
215
564
|
switch (status) {
|
|
565
|
+
case "running":
|
|
216
566
|
case "in_progress":
|
|
217
567
|
return "in-progress";
|
|
218
568
|
case "blocked":
|
|
@@ -231,6 +581,8 @@ function statusLabelFor(status) {
|
|
|
231
581
|
return "under-review";
|
|
232
582
|
case "needs_attention":
|
|
233
583
|
return "blocked";
|
|
584
|
+
case "closed":
|
|
585
|
+
case "completed":
|
|
234
586
|
case "open":
|
|
235
587
|
return null;
|
|
236
588
|
default:
|
|
@@ -239,11 +591,13 @@ function statusLabelFor(status) {
|
|
|
239
591
|
}
|
|
240
592
|
function rigStatusLabelFor(status) {
|
|
241
593
|
switch (status) {
|
|
594
|
+
case "running":
|
|
242
595
|
case "in_progress":
|
|
243
596
|
return "rig:running";
|
|
244
597
|
case "under_review":
|
|
245
598
|
return "rig:pr-open";
|
|
246
599
|
case "closed":
|
|
600
|
+
case "completed":
|
|
247
601
|
return "rig:done";
|
|
248
602
|
case "ci_fixing":
|
|
249
603
|
return "rig:ci-fixing";
|
|
@@ -261,9 +615,10 @@ function rigStatusLabelFor(status) {
|
|
|
261
615
|
return null;
|
|
262
616
|
}
|
|
263
617
|
}
|
|
264
|
-
function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
265
|
-
const targetLabel = status === "closed" ? null : statusLabelFor(status);
|
|
618
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
619
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
266
620
|
const targetRigLabel = rigStatusLabelFor(status);
|
|
621
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
267
622
|
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
268
623
|
if (targetLabel !== null && l === targetLabel)
|
|
269
624
|
continue;
|
|
@@ -285,7 +640,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
|
|
|
285
640
|
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
286
641
|
}
|
|
287
642
|
}
|
|
288
|
-
if (status
|
|
643
|
+
if (isRunningStatus(status)) {
|
|
644
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
645
|
+
if (shouldSyncLifecycle) {
|
|
646
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
647
|
+
status: "running",
|
|
648
|
+
summary: "Rig run started."
|
|
649
|
+
}), extraEnv, timeoutMs);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (shouldSyncLifecycle) {
|
|
653
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
654
|
+
}
|
|
655
|
+
if (status === "closed" || status === "completed") {
|
|
289
656
|
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
290
657
|
}
|
|
291
658
|
}
|
|
@@ -355,11 +722,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
|
|
|
355
722
|
} catch {}
|
|
356
723
|
}
|
|
357
724
|
}
|
|
358
|
-
function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
|
|
725
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
359
726
|
if (update.status) {
|
|
360
|
-
applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
|
|
727
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
361
728
|
}
|
|
362
|
-
if (update.comment?.trim()) {
|
|
729
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
363
730
|
if (isRigStickyStatusComment(update.comment)) {
|
|
364
731
|
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
365
732
|
} else {
|
|
@@ -385,6 +752,17 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
385
752
|
const spawnFn = opts.spawn ?? spawnSync;
|
|
386
753
|
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
387
754
|
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
755
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
756
|
+
async function issueToTaskWithOptionalNativeDependencies(issue, env) {
|
|
757
|
+
if (!opts.useNativeDependencies)
|
|
758
|
+
return issueToTask(issue, repo);
|
|
759
|
+
const nativeDependencies = await readNativeDependenciesForIssue({
|
|
760
|
+
issue,
|
|
761
|
+
repo,
|
|
762
|
+
fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
|
|
763
|
+
});
|
|
764
|
+
return issueToTask(issue, repo, nativeDependencies);
|
|
765
|
+
}
|
|
388
766
|
return {
|
|
389
767
|
id: "std:github-issues",
|
|
390
768
|
kind: "github-issues",
|
|
@@ -411,12 +789,13 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
411
789
|
throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
|
|
412
790
|
}
|
|
413
791
|
const issues = rawIssues.filter((issue) => !issue.pull_request);
|
|
414
|
-
return issues.map((
|
|
792
|
+
return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
|
|
415
793
|
},
|
|
416
794
|
async get(id) {
|
|
795
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
796
|
+
let issue;
|
|
417
797
|
try {
|
|
418
|
-
|
|
419
|
-
const issue = runGh(bin, [
|
|
798
|
+
issue = runGh(bin, [
|
|
420
799
|
"issue",
|
|
421
800
|
"view",
|
|
422
801
|
String(id),
|
|
@@ -425,19 +804,23 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
425
804
|
"--json",
|
|
426
805
|
"number,title,body,labels,state,url,assignees,id"
|
|
427
806
|
], spawnFn, env, timeoutMs);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
807
|
+
} catch (error) {
|
|
808
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
809
|
+
if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found|gh issue view\b[\s\S]*failed \(exit \d+\): not found\b/i.test(detail)) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
431
813
|
}
|
|
814
|
+
return issueToTaskWithOptionalNativeDependencies(issue, env);
|
|
432
815
|
},
|
|
433
816
|
async updateStatus(id, status) {
|
|
434
817
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
435
|
-
applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
|
|
818
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
436
819
|
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
437
820
|
},
|
|
438
821
|
async updateTask(id, update) {
|
|
439
822
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
440
|
-
applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
|
|
823
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
441
824
|
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
442
825
|
},
|
|
443
826
|
async addLabels(id, labels) {
|
|
@@ -452,6 +835,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
452
835
|
},
|
|
453
836
|
async createIssue(input) {
|
|
454
837
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
838
|
+
const body = input.body ?? "";
|
|
455
839
|
const args = [
|
|
456
840
|
"api",
|
|
457
841
|
"-X",
|
|
@@ -460,12 +844,31 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
460
844
|
"-f",
|
|
461
845
|
`title=${input.title}`,
|
|
462
846
|
"-f",
|
|
463
|
-
`body=${
|
|
847
|
+
`body=${body}`,
|
|
464
848
|
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
465
849
|
];
|
|
466
850
|
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
467
851
|
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
468
|
-
return issueToTask(issue, repo);
|
|
852
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
853
|
+
},
|
|
854
|
+
async create(input) {
|
|
855
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
856
|
+
const body = bodyForCreatedTask(input);
|
|
857
|
+
const args = [
|
|
858
|
+
"api",
|
|
859
|
+
"-X",
|
|
860
|
+
"POST",
|
|
861
|
+
`repos/${repo}/issues`,
|
|
862
|
+
"-f",
|
|
863
|
+
`title=${input.title}`,
|
|
864
|
+
"-f",
|
|
865
|
+
`body=${body}`,
|
|
866
|
+
"-f",
|
|
867
|
+
"labels[]=rig:generated"
|
|
868
|
+
];
|
|
869
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
870
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
871
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
469
872
|
},
|
|
470
873
|
async getIssueBody(id) {
|
|
471
874
|
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
@@ -476,7 +879,7 @@ function createGitHubIssuesTaskSource(opts) {
|
|
|
476
879
|
|
|
477
880
|
// packages/standard-plugin/src/files-source.ts
|
|
478
881
|
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
|
|
479
|
-
import { join, basename } from "path";
|
|
882
|
+
import { join, basename, isAbsolute, resolve as resolve2 } from "path";
|
|
480
883
|
var DEFAULT_PATTERN = /\.(task\.)?json$/;
|
|
481
884
|
function readTaskFile(file, pattern) {
|
|
482
885
|
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
@@ -497,10 +900,11 @@ function readTaskFile(file, pattern) {
|
|
|
497
900
|
}
|
|
498
901
|
function createFilesTaskSource(opts) {
|
|
499
902
|
const pattern = opts.pattern ?? DEFAULT_PATTERN;
|
|
500
|
-
const
|
|
501
|
-
if (!
|
|
903
|
+
const configured = opts.path ?? opts.dir;
|
|
904
|
+
if (!configured) {
|
|
502
905
|
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
503
906
|
}
|
|
907
|
+
const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
|
|
504
908
|
const findTaskFile = (id) => {
|
|
505
909
|
if (!existsSync2(directory))
|
|
506
910
|
return;
|
|
@@ -577,7 +981,509 @@ function createFilesTaskSource(opts) {
|
|
|
577
981
|
};
|
|
578
982
|
}
|
|
579
983
|
|
|
580
|
-
// packages/standard-plugin/src/
|
|
984
|
+
// packages/standard-plugin/src/drift/plugin.ts
|
|
985
|
+
import { Schema } from "effect";
|
|
986
|
+
import { StageMutation as StageMutationSchema } from "@rig/contracts";
|
|
987
|
+
|
|
988
|
+
// packages/standard-plugin/src/drift/detect.ts
|
|
989
|
+
import { existsSync as existsSync3 } from "fs";
|
|
990
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
991
|
+
import { basename as basename2, extname, relative, resolve as resolve3 } from "path";
|
|
992
|
+
|
|
993
|
+
// packages/standard-plugin/src/drift/extract-refs.ts
|
|
994
|
+
var INLINE_CODE = /`([^`\n]+)`/g;
|
|
995
|
+
var MARKDOWN_LINK = /\[[^\]]+\]\(([^)\s]+)\)/g;
|
|
996
|
+
var SYMBOL_REF = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/;
|
|
997
|
+
var PATH_REF = /^(?:\.\.?\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+$|^[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|mdx|css|scss|html|yml|yaml|toml|rs|go|py|rb|java|kt|swift|c|cc|cpp|h|hpp)$/;
|
|
998
|
+
function stripFenceLines(markdown) {
|
|
999
|
+
const lines = markdown.split(/\r?\n/);
|
|
1000
|
+
let fenced = false;
|
|
1001
|
+
return lines.map((line) => {
|
|
1002
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
1003
|
+
fenced = !fenced;
|
|
1004
|
+
return "";
|
|
1005
|
+
}
|
|
1006
|
+
return fenced ? "" : line;
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function normalizeToken(raw) {
|
|
1010
|
+
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[),.;:]+$/g, "").replace(/#L\d+(?:-L\d+)?$/i, "");
|
|
1011
|
+
}
|
|
1012
|
+
function classifyReference(raw) {
|
|
1013
|
+
if (raw.startsWith("@"))
|
|
1014
|
+
return null;
|
|
1015
|
+
if (PATH_REF.test(raw))
|
|
1016
|
+
return "path";
|
|
1017
|
+
if (SYMBOL_REF.test(raw))
|
|
1018
|
+
return "symbol";
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
function pushReference(refs, seen, raw, line) {
|
|
1022
|
+
const value = normalizeToken(raw);
|
|
1023
|
+
if (!value)
|
|
1024
|
+
return;
|
|
1025
|
+
const kind = classifyReference(value);
|
|
1026
|
+
if (!kind)
|
|
1027
|
+
return;
|
|
1028
|
+
const key = `${kind}:${value}:${line}`;
|
|
1029
|
+
if (seen.has(key))
|
|
1030
|
+
return;
|
|
1031
|
+
seen.add(key);
|
|
1032
|
+
refs.push({ kind, value, line });
|
|
1033
|
+
}
|
|
1034
|
+
function extractDriftReferences(markdown) {
|
|
1035
|
+
const refs = [];
|
|
1036
|
+
const seen = new Set;
|
|
1037
|
+
const lines = stripFenceLines(markdown);
|
|
1038
|
+
for (const [index, line] of lines.entries()) {
|
|
1039
|
+
const lineNumber = index + 1;
|
|
1040
|
+
for (const match of line.matchAll(INLINE_CODE)) {
|
|
1041
|
+
pushReference(refs, seen, match[1] ?? "", lineNumber);
|
|
1042
|
+
}
|
|
1043
|
+
for (const match of line.matchAll(MARKDOWN_LINK)) {
|
|
1044
|
+
pushReference(refs, seen, match[1] ?? "", lineNumber);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return refs;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// packages/standard-plugin/src/drift/git-adapter.ts
|
|
1051
|
+
import { execFile } from "child_process";
|
|
1052
|
+
import { promisify } from "util";
|
|
1053
|
+
var execFileAsync = promisify(execFile);
|
|
1054
|
+
function processError(value) {
|
|
1055
|
+
return value && typeof value === "object" ? value : null;
|
|
1056
|
+
}
|
|
1057
|
+
function lineCount(output) {
|
|
1058
|
+
const trimmed = output.trim();
|
|
1059
|
+
return trimmed ? trimmed.split(/\r?\n/).length : 0;
|
|
1060
|
+
}
|
|
1061
|
+
function makeDriftGit(projectRoot) {
|
|
1062
|
+
async function git(args) {
|
|
1063
|
+
const result = await execFileAsync("git", [...args], {
|
|
1064
|
+
cwd: projectRoot,
|
|
1065
|
+
encoding: "utf8",
|
|
1066
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1067
|
+
});
|
|
1068
|
+
return String(result.stdout);
|
|
1069
|
+
}
|
|
1070
|
+
async function grepCountAt(symbolOrPath, commit) {
|
|
1071
|
+
try {
|
|
1072
|
+
return lineCount(await git(["grep", "-F", "-n", "-e", symbolOrPath, commit, "--"]));
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
const detail = processError(error);
|
|
1075
|
+
if (detail?.code === 1)
|
|
1076
|
+
return 0;
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
async lastCommitTouching(path) {
|
|
1082
|
+
const commit = (await git(["log", "-n", "1", "--format=%H", "--", path])).trim();
|
|
1083
|
+
return commit || "HEAD";
|
|
1084
|
+
},
|
|
1085
|
+
async grepCount(symbolOrPath) {
|
|
1086
|
+
return grepCountAt(symbolOrPath, "HEAD");
|
|
1087
|
+
},
|
|
1088
|
+
async grepCountAtCommit(symbolOrPath, commit) {
|
|
1089
|
+
return grepCountAt(symbolOrPath, commit);
|
|
1090
|
+
},
|
|
1091
|
+
async wasRenamed(symbolOrPath, sinceCommit) {
|
|
1092
|
+
if (!symbolOrPath.includes("/") && !symbolOrPath.includes("."))
|
|
1093
|
+
return false;
|
|
1094
|
+
try {
|
|
1095
|
+
const output = await git(["log", "--name-status", "--format=", `${sinceCommit}..HEAD`]);
|
|
1096
|
+
return output.split(/\r?\n/).some((line) => {
|
|
1097
|
+
const match = line.match(/^R\d*\s+(.+?)\s+(.+)$/);
|
|
1098
|
+
return Boolean(match && (match[1] === symbolOrPath || match[2] === symbolOrPath));
|
|
1099
|
+
});
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
const detail = processError(error);
|
|
1102
|
+
if (detail?.code === 128)
|
|
1103
|
+
return false;
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// packages/standard-plugin/src/drift/detect.ts
|
|
1111
|
+
var DEFAULT_IGNORED_DIRS = {
|
|
1112
|
+
".git": true,
|
|
1113
|
+
node_modules: true,
|
|
1114
|
+
dist: true,
|
|
1115
|
+
build: true,
|
|
1116
|
+
coverage: true,
|
|
1117
|
+
".next": true,
|
|
1118
|
+
vendor: true
|
|
1119
|
+
};
|
|
1120
|
+
var SOURCE_EXTENSIONS = {
|
|
1121
|
+
".ts": true,
|
|
1122
|
+
".tsx": true,
|
|
1123
|
+
".js": true,
|
|
1124
|
+
".jsx": true,
|
|
1125
|
+
".mjs": true,
|
|
1126
|
+
".cjs": true,
|
|
1127
|
+
".rs": true,
|
|
1128
|
+
".go": true,
|
|
1129
|
+
".py": true,
|
|
1130
|
+
".rb": true,
|
|
1131
|
+
".java": true,
|
|
1132
|
+
".kt": true,
|
|
1133
|
+
".swift": true,
|
|
1134
|
+
".c": true,
|
|
1135
|
+
".cc": true,
|
|
1136
|
+
".cpp": true,
|
|
1137
|
+
".h": true,
|
|
1138
|
+
".hpp": true,
|
|
1139
|
+
".json": true,
|
|
1140
|
+
".toml": true,
|
|
1141
|
+
".yml": true,
|
|
1142
|
+
".yaml": true
|
|
1143
|
+
};
|
|
1144
|
+
function globLikeMatch(path, pattern) {
|
|
1145
|
+
if (pattern === path)
|
|
1146
|
+
return true;
|
|
1147
|
+
if (pattern.startsWith("**/*"))
|
|
1148
|
+
return path.endsWith(pattern.slice(4));
|
|
1149
|
+
if (pattern.endsWith("/**"))
|
|
1150
|
+
return path.startsWith(pattern.slice(0, -3));
|
|
1151
|
+
if (pattern.includes("*")) {
|
|
1152
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1153
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
1154
|
+
}
|
|
1155
|
+
return path.startsWith(pattern);
|
|
1156
|
+
}
|
|
1157
|
+
function isDefaultDoc(path) {
|
|
1158
|
+
const lower = basename2(path).toLowerCase();
|
|
1159
|
+
return (path.endsWith(".md") || path.endsWith(".mdx")) && !lower.startsWith("changelog") && !lower.includes("generated");
|
|
1160
|
+
}
|
|
1161
|
+
function isIgnored(path, patterns) {
|
|
1162
|
+
return (patterns ?? []).some((pattern) => globLikeMatch(path, pattern));
|
|
1163
|
+
}
|
|
1164
|
+
async function collectFiles(root, options) {
|
|
1165
|
+
const files = [];
|
|
1166
|
+
async function visit(dir) {
|
|
1167
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
1168
|
+
if (entry.isDirectory() && DEFAULT_IGNORED_DIRS[entry.name])
|
|
1169
|
+
continue;
|
|
1170
|
+
const absolute = resolve3(dir, entry.name);
|
|
1171
|
+
const rel = relative(root, absolute).replace(/\\/g, "/");
|
|
1172
|
+
if (isIgnored(rel, options.ignore))
|
|
1173
|
+
continue;
|
|
1174
|
+
if (entry.isDirectory()) {
|
|
1175
|
+
await visit(absolute);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (!entry.isFile())
|
|
1179
|
+
continue;
|
|
1180
|
+
if (options.docs) {
|
|
1181
|
+
const matchesConfigured = options.patterns && options.patterns.length > 0 ? options.patterns.some((pattern) => globLikeMatch(rel, pattern)) : isDefaultDoc(rel);
|
|
1182
|
+
if (matchesConfigured)
|
|
1183
|
+
files.push(rel);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (SOURCE_EXTENSIONS[extname(entry.name)])
|
|
1187
|
+
files.push(rel);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
await visit(root);
|
|
1191
|
+
return files.sort();
|
|
1192
|
+
}
|
|
1193
|
+
async function sourceReferenceCount(projectRoot, reference, docPath) {
|
|
1194
|
+
if (reference.kind === "path")
|
|
1195
|
+
return existsSync3(resolve3(projectRoot, reference.value)) ? 1 : 0;
|
|
1196
|
+
let count = 0;
|
|
1197
|
+
const sourceFiles = await collectFiles(projectRoot, { docs: false });
|
|
1198
|
+
for (const sourceFile of sourceFiles) {
|
|
1199
|
+
if (sourceFile === docPath)
|
|
1200
|
+
continue;
|
|
1201
|
+
const text = await readFile(resolve3(projectRoot, sourceFile), "utf8").catch(() => "");
|
|
1202
|
+
if (text.includes(reference.value))
|
|
1203
|
+
count += 1;
|
|
1204
|
+
}
|
|
1205
|
+
return count;
|
|
1206
|
+
}
|
|
1207
|
+
function deletedReferenceFinding(docPath, reference) {
|
|
1208
|
+
return {
|
|
1209
|
+
kind: "deleted-reference",
|
|
1210
|
+
docPath,
|
|
1211
|
+
line: reference.line,
|
|
1212
|
+
reference: reference.value,
|
|
1213
|
+
detail: `Documented reference "${reference.value}" no longer exists in the source tree.`,
|
|
1214
|
+
confidence: "high"
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function staleAnchorFinding(docPath, reference) {
|
|
1218
|
+
return {
|
|
1219
|
+
kind: "stale-anchor",
|
|
1220
|
+
docPath,
|
|
1221
|
+
line: reference.line,
|
|
1222
|
+
reference: reference.value,
|
|
1223
|
+
detail: `Documented path "${reference.value}" changed after this doc was last updated.`,
|
|
1224
|
+
confidence: "medium"
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
async function detectDeletedReferences(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
|
|
1228
|
+
const markdown = await readFile(resolve3(projectRoot, docPath), "utf8");
|
|
1229
|
+
const docCommit = await git.lastCommitTouching(docPath);
|
|
1230
|
+
const findings = [];
|
|
1231
|
+
for (const reference of extractDriftReferences(markdown)) {
|
|
1232
|
+
if (await sourceReferenceCount(projectRoot, reference, docPath) > 0)
|
|
1233
|
+
continue;
|
|
1234
|
+
if (await git.wasRenamed(reference.value, docCommit))
|
|
1235
|
+
continue;
|
|
1236
|
+
findings.push(deletedReferenceFinding(docPath, reference));
|
|
1237
|
+
}
|
|
1238
|
+
return findings;
|
|
1239
|
+
}
|
|
1240
|
+
async function detectStaleAnchors(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
|
|
1241
|
+
const markdown = await readFile(resolve3(projectRoot, docPath), "utf8");
|
|
1242
|
+
const docCommit = await git.lastCommitTouching(docPath);
|
|
1243
|
+
const findings = [];
|
|
1244
|
+
for (const reference of extractDriftReferences(markdown).filter((ref) => ref.kind === "path")) {
|
|
1245
|
+
if (!existsSync3(resolve3(projectRoot, reference.value)))
|
|
1246
|
+
continue;
|
|
1247
|
+
const sourceStat = await stat(resolve3(projectRoot, reference.value)).catch(() => null);
|
|
1248
|
+
if (!sourceStat?.isFile())
|
|
1249
|
+
continue;
|
|
1250
|
+
const sourceCommit = await git.lastCommitTouching(reference.value);
|
|
1251
|
+
if (sourceCommit !== docCommit && !await git.wasRenamed(reference.value, docCommit)) {
|
|
1252
|
+
findings.push(staleAnchorFinding(docPath, reference));
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return findings;
|
|
1256
|
+
}
|
|
1257
|
+
async function detectDrift(options) {
|
|
1258
|
+
const git = options.git ?? makeDriftGit(options.projectRoot);
|
|
1259
|
+
const docs = await collectFiles(options.projectRoot, {
|
|
1260
|
+
docs: true,
|
|
1261
|
+
...options.docsGlobs !== undefined ? { patterns: options.docsGlobs } : {},
|
|
1262
|
+
...options.ignoreGlobs !== undefined ? { ignore: options.ignoreGlobs } : {}
|
|
1263
|
+
});
|
|
1264
|
+
const findings = [];
|
|
1265
|
+
let degraded = false;
|
|
1266
|
+
for (const docPath of docs) {
|
|
1267
|
+
try {
|
|
1268
|
+
findings.push(...await detectDeletedReferences(options.projectRoot, docPath, git));
|
|
1269
|
+
findings.push(...await detectStaleAnchors(options.projectRoot, docPath, git));
|
|
1270
|
+
} catch {
|
|
1271
|
+
degraded = true;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
generatedAt: new Date().toISOString(),
|
|
1276
|
+
scanned: docs.length,
|
|
1277
|
+
degraded,
|
|
1278
|
+
findings
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// packages/standard-plugin/src/drift/plugin.ts
|
|
1283
|
+
var DOCS_DRIFT_VALIDATOR_ID = "std:docs-drift";
|
|
1284
|
+
var DOCS_DRIFT_CLI_ID = "std:drift";
|
|
1285
|
+
var DOCS_DRIFT_STAGE_ID = "docs-drift";
|
|
1286
|
+
var DOCS_DRIFT_CAPABILITY_ID = "std:docs-drift-capability";
|
|
1287
|
+
var DOCS_DRIFT_VALIDATOR = {
|
|
1288
|
+
id: DOCS_DRIFT_VALIDATOR_ID,
|
|
1289
|
+
category: "regression",
|
|
1290
|
+
description: "Detect documentation references that drifted from the source tree."
|
|
1291
|
+
};
|
|
1292
|
+
var DOCS_DRIFT_STAGE_MUTATION = Schema.decodeUnknownSync(StageMutationSchema)({
|
|
1293
|
+
op: "insert",
|
|
1294
|
+
stage: {
|
|
1295
|
+
id: DOCS_DRIFT_STAGE_ID,
|
|
1296
|
+
kind: "gate",
|
|
1297
|
+
before: ["merge-gate"],
|
|
1298
|
+
after: ["open-pr"]
|
|
1299
|
+
},
|
|
1300
|
+
contributedBy: DOCS_DRIFT_STAGE_ID
|
|
1301
|
+
});
|
|
1302
|
+
var DOCS_DRIFT_CLI_COMMAND = "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]";
|
|
1303
|
+
function highConfidenceDriftFindings(report) {
|
|
1304
|
+
return report.findings.filter((finding) => finding.confidence === "high");
|
|
1305
|
+
}
|
|
1306
|
+
function driftGateResult(report, mode = "enforce") {
|
|
1307
|
+
const high = highConfidenceDriftFindings(report);
|
|
1308
|
+
if (mode === "enforce" && high.length > 0) {
|
|
1309
|
+
return { kind: "block", reason: `${high.length} high-confidence documentation drift finding(s).` };
|
|
1310
|
+
}
|
|
1311
|
+
return { kind: "allow" };
|
|
1312
|
+
}
|
|
1313
|
+
function createDocsDriftGateStage(options = {}) {
|
|
1314
|
+
return async (ctx) => {
|
|
1315
|
+
const projectRoot = typeof ctx.metadata?.projectRoot === "string" ? ctx.metadata.projectRoot : process.cwd();
|
|
1316
|
+
const report = await detectDrift({
|
|
1317
|
+
projectRoot,
|
|
1318
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
1319
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
1320
|
+
});
|
|
1321
|
+
return driftGateResult(report, options.failOnDrift ? "enforce" : "observe");
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
async function runDocsDriftValidation(options) {
|
|
1325
|
+
const report = await detectDrift(options);
|
|
1326
|
+
const high = highConfidenceDriftFindings(report);
|
|
1327
|
+
const passed = options.failOnDrift ? high.length === 0 : true;
|
|
1328
|
+
const findingWord = report.findings.length === 1 ? "finding" : "findings";
|
|
1329
|
+
return {
|
|
1330
|
+
id: DOCS_DRIFT_VALIDATOR_ID,
|
|
1331
|
+
passed,
|
|
1332
|
+
summary: `docs drift scanned ${report.scanned} doc(s), ${report.findings.length} ${findingWord}`,
|
|
1333
|
+
details: JSON.stringify(report)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function createDocsDriftValidator(options = {}) {
|
|
1337
|
+
return {
|
|
1338
|
+
...DOCS_DRIFT_VALIDATOR,
|
|
1339
|
+
async run(ctx) {
|
|
1340
|
+
return runDocsDriftValidation({
|
|
1341
|
+
projectRoot: ctx.workspaceRoot,
|
|
1342
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
1343
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
|
|
1344
|
+
...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
function takeOptionValue(args, index, flag) {
|
|
1350
|
+
const value = args[index + 1];
|
|
1351
|
+
if (!value)
|
|
1352
|
+
throw new Error(`${flag} requires a value`);
|
|
1353
|
+
return value;
|
|
1354
|
+
}
|
|
1355
|
+
function takeFlag(args, flag) {
|
|
1356
|
+
const rest = [...args];
|
|
1357
|
+
const index = rest.indexOf(flag);
|
|
1358
|
+
if (index < 0)
|
|
1359
|
+
return { value: false, rest };
|
|
1360
|
+
rest.splice(index, 1);
|
|
1361
|
+
return { value: true, rest };
|
|
1362
|
+
}
|
|
1363
|
+
function takeOption(args, flag) {
|
|
1364
|
+
const rest = [...args];
|
|
1365
|
+
const index = rest.indexOf(flag);
|
|
1366
|
+
if (index < 0)
|
|
1367
|
+
return { rest };
|
|
1368
|
+
const value = rest[index + 1];
|
|
1369
|
+
if (!value || value.startsWith("-"))
|
|
1370
|
+
throw new Error(`${flag} requires a value.`);
|
|
1371
|
+
rest.splice(index, 2);
|
|
1372
|
+
return { value, rest };
|
|
1373
|
+
}
|
|
1374
|
+
function requireNoExtraArgs(args, usage) {
|
|
1375
|
+
if (args.length > 0)
|
|
1376
|
+
throw new Error(`Unexpected argument: ${args[0]}
|
|
1377
|
+
Usage: ${usage}`);
|
|
1378
|
+
}
|
|
1379
|
+
function parseCsv(value) {
|
|
1380
|
+
return value?.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) ?? [];
|
|
1381
|
+
}
|
|
1382
|
+
function driftSummary(report) {
|
|
1383
|
+
const highConfidence = highConfidenceDriftFindings(report).length;
|
|
1384
|
+
return { total: report.findings.length, highConfidence, degraded: report.degraded };
|
|
1385
|
+
}
|
|
1386
|
+
async function executeDrift(context, args, options = {}) {
|
|
1387
|
+
const json = takeFlag(args, "--json");
|
|
1388
|
+
const docs = takeOption(json.rest, "--docs");
|
|
1389
|
+
const ignore = takeOption(docs.rest, "--ignore");
|
|
1390
|
+
const failOnDrift = takeFlag(ignore.rest, "--fail-on-drift");
|
|
1391
|
+
requireNoExtraArgs(failOnDrift.rest, "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]");
|
|
1392
|
+
const docsGlobs = parseCsv(docs.value);
|
|
1393
|
+
const ignoreGlobs = parseCsv(ignore.value);
|
|
1394
|
+
const effectiveDocsGlobs = docsGlobs.length > 0 ? docsGlobs : options.docsGlobs;
|
|
1395
|
+
const effectiveIgnoreGlobs = ignoreGlobs.length > 0 ? ignoreGlobs : options.ignoreGlobs;
|
|
1396
|
+
const effectiveFailOnDrift = failOnDrift.value || options.failOnDrift === true;
|
|
1397
|
+
const report = await detectDrift({
|
|
1398
|
+
projectRoot: context.projectRoot,
|
|
1399
|
+
...effectiveDocsGlobs !== undefined ? { docsGlobs: effectiveDocsGlobs } : {},
|
|
1400
|
+
...effectiveIgnoreGlobs !== undefined ? { ignoreGlobs: effectiveIgnoreGlobs } : {}
|
|
1401
|
+
});
|
|
1402
|
+
const failed = effectiveFailOnDrift && highConfidenceDriftFindings(report).length > 0;
|
|
1403
|
+
const details = { report, summary: driftSummary(report), failOnDrift: effectiveFailOnDrift, failed };
|
|
1404
|
+
if (context.outputMode === "text") {
|
|
1405
|
+
if (json.value)
|
|
1406
|
+
console.log(JSON.stringify(details, null, 2));
|
|
1407
|
+
else
|
|
1408
|
+
console.log(report.findings.length === 0 ? `No drift findings across ${report.scanned} documents.` : report.findings.map((finding) => `${finding.docPath}:${finding.line ?? "?"} ${finding.kind} ${finding.confidence} ${finding.detail}`).join(`
|
|
1409
|
+
`));
|
|
1410
|
+
}
|
|
1411
|
+
return { ok: !failed, group: "drift", command: "scan", details };
|
|
1412
|
+
}
|
|
1413
|
+
function createDocsDriftRuntimeCliCommand(options = {}) {
|
|
1414
|
+
return {
|
|
1415
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
1416
|
+
family: "drift",
|
|
1417
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
1418
|
+
description: "Scan documentation for stale code references.",
|
|
1419
|
+
usage: DOCS_DRIFT_CLI_COMMAND,
|
|
1420
|
+
projectRequired: true,
|
|
1421
|
+
run: (context, args) => executeDrift(context, args, options)
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
var DOCS_DRIFT_RUNTIME_CLI_COMMAND = createDocsDriftRuntimeCliCommand();
|
|
1425
|
+
async function runDriftCli(args, options = {}) {
|
|
1426
|
+
const docsGlobs = [];
|
|
1427
|
+
const ignoreGlobs = [];
|
|
1428
|
+
let json = false;
|
|
1429
|
+
let failOnDrift = false;
|
|
1430
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
1431
|
+
const arg = args[index];
|
|
1432
|
+
if (arg === "--json") {
|
|
1433
|
+
json = true;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (arg === "--fail-on-drift") {
|
|
1437
|
+
failOnDrift = true;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (arg === "--docs") {
|
|
1441
|
+
docsGlobs.push(takeOptionValue(args, index, arg));
|
|
1442
|
+
index += 1;
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (arg === "--ignore") {
|
|
1446
|
+
ignoreGlobs.push(takeOptionValue(args, index, arg));
|
|
1447
|
+
index += 1;
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
throw new Error(`Unknown rig drift argument: ${arg}`);
|
|
1451
|
+
}
|
|
1452
|
+
const report = await detectDrift({
|
|
1453
|
+
projectRoot: options.projectRoot ?? process.cwd(),
|
|
1454
|
+
...docsGlobs.length > 0 ? { docsGlobs } : {},
|
|
1455
|
+
...ignoreGlobs.length > 0 ? { ignoreGlobs } : {}
|
|
1456
|
+
});
|
|
1457
|
+
const write = options.write ?? ((message) => console.log(message));
|
|
1458
|
+
if (json) {
|
|
1459
|
+
write(JSON.stringify(report));
|
|
1460
|
+
} else {
|
|
1461
|
+
write(`Scanned ${report.scanned} doc(s); ${report.findings.length} drift finding(s).`);
|
|
1462
|
+
for (const finding of report.findings) {
|
|
1463
|
+
write(`${finding.confidence.toUpperCase()} ${finding.kind} ${finding.docPath}${finding.line ? `:${finding.line}` : ""} ${finding.reference ?? ""} \u2014 ${finding.detail}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const high = highConfidenceDriftFindings(report);
|
|
1467
|
+
if (failOnDrift && high.length > 0) {
|
|
1468
|
+
options.writeError?.(`${high.length} high-confidence drift finding(s).`);
|
|
1469
|
+
return 2;
|
|
1470
|
+
}
|
|
1471
|
+
return 0;
|
|
1472
|
+
}
|
|
1473
|
+
// packages/standard-plugin/src/drift/judge.ts
|
|
1474
|
+
async function judgeDocumentationDrift(provider, input) {
|
|
1475
|
+
const result = await provider.judge(input);
|
|
1476
|
+
return result.mismatches.map((mismatch) => ({
|
|
1477
|
+
kind: "semantic-mismatch",
|
|
1478
|
+
docPath: input.docPath,
|
|
1479
|
+
line: mismatch.line ?? null,
|
|
1480
|
+
reference: mismatch.reference ?? input.reference ?? null,
|
|
1481
|
+
detail: mismatch.detail,
|
|
1482
|
+
confidence: mismatch.confidence ?? "medium"
|
|
1483
|
+
}));
|
|
1484
|
+
}
|
|
1485
|
+
// packages/standard-plugin/src/plugin.ts
|
|
1486
|
+
var DOCS_HEALTH_PANEL_ID = "docs-health";
|
|
581
1487
|
function requireStringField(config, field, kind) {
|
|
582
1488
|
const value = config[field];
|
|
583
1489
|
if (!value) {
|
|
@@ -585,11 +1491,78 @@ function requireStringField(config, field, kind) {
|
|
|
585
1491
|
}
|
|
586
1492
|
return value;
|
|
587
1493
|
}
|
|
1494
|
+
function isRecord(value) {
|
|
1495
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1496
|
+
}
|
|
1497
|
+
function optionalString(value) {
|
|
1498
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
1499
|
+
}
|
|
1500
|
+
function parseGitHubProjectsOptions(value) {
|
|
1501
|
+
if (!isRecord(value))
|
|
1502
|
+
return;
|
|
1503
|
+
const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
|
|
1504
|
+
const statuses = {};
|
|
1505
|
+
for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
|
|
1506
|
+
const status = optionalString(statusesSource?.[key]);
|
|
1507
|
+
if (status)
|
|
1508
|
+
statuses[key] = status;
|
|
1509
|
+
}
|
|
1510
|
+
const parsed = {};
|
|
1511
|
+
if (typeof value.enabled === "boolean")
|
|
1512
|
+
parsed.enabled = value.enabled;
|
|
1513
|
+
const projectId = optionalString(value.projectId);
|
|
1514
|
+
if (projectId)
|
|
1515
|
+
parsed.projectId = projectId;
|
|
1516
|
+
const statusFieldId = optionalString(value.statusFieldId);
|
|
1517
|
+
if (statusFieldId)
|
|
1518
|
+
parsed.statusFieldId = statusFieldId;
|
|
1519
|
+
if (Object.keys(statuses).length > 0)
|
|
1520
|
+
parsed.statuses = statuses;
|
|
1521
|
+
return parsed;
|
|
1522
|
+
}
|
|
1523
|
+
function githubProjectsOptionsFromConfig(config, context) {
|
|
1524
|
+
const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
|
|
1525
|
+
const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
|
|
1526
|
+
return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
|
|
1527
|
+
}
|
|
1528
|
+
function booleanOption(value) {
|
|
1529
|
+
return typeof value === "boolean" ? value : undefined;
|
|
1530
|
+
}
|
|
1531
|
+
function panelProjectRoot(context) {
|
|
1532
|
+
return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
1533
|
+
}
|
|
1534
|
+
function driftFindingPanelId(finding, index) {
|
|
1535
|
+
return `${finding.docPath}:${finding.line ?? index}:${finding.kind}`;
|
|
1536
|
+
}
|
|
1537
|
+
function createDocsHealthPanelProducer(options = {}) {
|
|
1538
|
+
return async (context) => {
|
|
1539
|
+
const projectRoot = panelProjectRoot(context);
|
|
1540
|
+
if (!projectRoot)
|
|
1541
|
+
return;
|
|
1542
|
+
const report = await detectDrift({
|
|
1543
|
+
projectRoot,
|
|
1544
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
1545
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
1546
|
+
});
|
|
1547
|
+
return {
|
|
1548
|
+
findings: report.findings.map((finding, index) => ({
|
|
1549
|
+
id: driftFindingPanelId(finding, index),
|
|
1550
|
+
docPath: finding.docPath,
|
|
1551
|
+
kind: finding.kind,
|
|
1552
|
+
confidence: finding.confidence,
|
|
1553
|
+
summary: finding.detail,
|
|
1554
|
+
taskId: null
|
|
1555
|
+
})),
|
|
1556
|
+
degraded: report.degraded ? "drift scan degraded" : null
|
|
1557
|
+
};
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
588
1560
|
function standardPlugin(opts = {}) {
|
|
589
1561
|
return definePlugin({
|
|
590
1562
|
name: "rig-standard",
|
|
591
1563
|
version: "0.1.0",
|
|
592
1564
|
contributes: {
|
|
1565
|
+
validators: [DOCS_DRIFT_VALIDATOR],
|
|
593
1566
|
taskSources: [
|
|
594
1567
|
{
|
|
595
1568
|
id: "std:github-issues",
|
|
@@ -601,21 +1574,46 @@ function standardPlugin(opts = {}) {
|
|
|
601
1574
|
kind: "files",
|
|
602
1575
|
description: "JSON files in a local directory"
|
|
603
1576
|
}
|
|
604
|
-
]
|
|
1577
|
+
],
|
|
1578
|
+
capabilities: [
|
|
1579
|
+
{ id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
|
|
1580
|
+
],
|
|
1581
|
+
panels: [
|
|
1582
|
+
{ id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID }
|
|
1583
|
+
],
|
|
1584
|
+
cliCommands: [
|
|
1585
|
+
{
|
|
1586
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
1587
|
+
family: "drift",
|
|
1588
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
1589
|
+
description: "Scan documentation for stale code references.",
|
|
1590
|
+
projectRequired: true
|
|
1591
|
+
}
|
|
1592
|
+
],
|
|
1593
|
+
stageMutations: [DOCS_DRIFT_STAGE_MUTATION]
|
|
605
1594
|
}
|
|
606
1595
|
}, {
|
|
1596
|
+
validators: [createDocsDriftValidator(opts.drift)],
|
|
1597
|
+
stages: { [DOCS_DRIFT_STAGE_ID]: createDocsDriftGateStage(opts.drift) },
|
|
1598
|
+
featureCapabilities: [
|
|
1599
|
+
{ id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
|
|
1600
|
+
],
|
|
1601
|
+
panels: [
|
|
1602
|
+
{ id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID, produce: createDocsHealthPanelProducer(opts.drift) }
|
|
1603
|
+
],
|
|
1604
|
+
cliCommands: [createDocsDriftRuntimeCliCommand(opts.drift)],
|
|
607
1605
|
taskSources: [
|
|
608
1606
|
{
|
|
609
1607
|
id: "std:github-issues",
|
|
610
1608
|
kind: "github-issues",
|
|
611
1609
|
description: "GitHub Issues via gh CLI",
|
|
612
|
-
factory(config) {
|
|
1610
|
+
factory(config, context) {
|
|
613
1611
|
const options = {
|
|
614
1612
|
owner: requireStringField(config, "owner", "github-issues"),
|
|
615
1613
|
repo: requireStringField(config, "repo", "github-issues")
|
|
616
1614
|
};
|
|
617
|
-
|
|
618
|
-
|
|
1615
|
+
const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve4(context.projectRoot, ".rig", "state") } : {};
|
|
1616
|
+
options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
|
|
619
1617
|
if (opts.githubWorkspaceId)
|
|
620
1618
|
options.workspaceId = opts.githubWorkspaceId;
|
|
621
1619
|
if (opts.githubUserId)
|
|
@@ -637,6 +1635,12 @@ function standardPlugin(opts = {}) {
|
|
|
637
1635
|
const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
|
|
638
1636
|
if (listLimit !== undefined)
|
|
639
1637
|
options.listLimit = listLimit;
|
|
1638
|
+
const projects = githubProjectsOptionsFromConfig(config, context);
|
|
1639
|
+
if (projects)
|
|
1640
|
+
options.projects = projects;
|
|
1641
|
+
const useNativeDependencies = booleanOption(config.options?.useNativeDependencies);
|
|
1642
|
+
if (useNativeDependencies !== undefined)
|
|
1643
|
+
options.useNativeDependencies = useNativeDependencies;
|
|
640
1644
|
return createGitHubIssuesTaskSource(options);
|
|
641
1645
|
}
|
|
642
1646
|
},
|
|
@@ -644,19 +1648,55 @@ function standardPlugin(opts = {}) {
|
|
|
644
1648
|
id: "std:files",
|
|
645
1649
|
kind: "files",
|
|
646
1650
|
description: "JSON files in a local directory",
|
|
647
|
-
factory(config) {
|
|
1651
|
+
factory(config, context) {
|
|
648
1652
|
return createFilesTaskSource({
|
|
649
|
-
path: requireStringField(config, "path", "files")
|
|
1653
|
+
path: requireStringField(config, "path", "files"),
|
|
1654
|
+
...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
|
|
650
1655
|
});
|
|
651
1656
|
}
|
|
652
1657
|
}
|
|
653
1658
|
]
|
|
654
1659
|
});
|
|
655
1660
|
}
|
|
1661
|
+
// packages/standard-plugin/src/bundle.ts
|
|
1662
|
+
import { createBlockerClassifierPlugin } from "@rig/blocker-classifier-plugin";
|
|
1663
|
+
import { createDefaultLifecyclePlugin } from "@rig/bundle-default-lifecycle";
|
|
1664
|
+
import { createDependencyGraphPlugin } from "@rig/dependency-graph-plugin";
|
|
1665
|
+
import { createPlanningPlugin } from "@rig/planning-plugin";
|
|
1666
|
+
import { createSupervisorPlugin } from "@rig/supervisor-plugin";
|
|
1667
|
+
function standardProjectPlugins(options = {}) {
|
|
1668
|
+
return [
|
|
1669
|
+
createDefaultLifecyclePlugin(),
|
|
1670
|
+
createDependencyGraphPlugin(),
|
|
1671
|
+
createBlockerClassifierPlugin(),
|
|
1672
|
+
createPlanningPlugin(),
|
|
1673
|
+
createSupervisorPlugin(),
|
|
1674
|
+
standardPlugin(options.standard)
|
|
1675
|
+
];
|
|
1676
|
+
}
|
|
656
1677
|
export {
|
|
1678
|
+
standardProjectPlugins,
|
|
1679
|
+
runDriftCli,
|
|
1680
|
+
runDocsDriftValidation,
|
|
1681
|
+
makeDriftGit,
|
|
1682
|
+
judgeDocumentationDrift,
|
|
1683
|
+
extractDriftReferences,
|
|
1684
|
+
executeDrift,
|
|
1685
|
+
driftGateResult,
|
|
1686
|
+
detectStaleAnchors,
|
|
1687
|
+
detectDrift,
|
|
1688
|
+
detectDeletedReferences,
|
|
657
1689
|
standardPlugin as default,
|
|
658
1690
|
createStateGitHubCredentialProvider,
|
|
659
1691
|
createGitHubIssuesTaskSource,
|
|
660
1692
|
createFilesTaskSource,
|
|
661
|
-
createEnvGitHubCredentialProvider
|
|
1693
|
+
createEnvGitHubCredentialProvider,
|
|
1694
|
+
createDocsDriftValidator,
|
|
1695
|
+
createDocsDriftRuntimeCliCommand,
|
|
1696
|
+
DOCS_HEALTH_PANEL_ID,
|
|
1697
|
+
DOCS_DRIFT_VALIDATOR_ID,
|
|
1698
|
+
DOCS_DRIFT_STAGE_ID,
|
|
1699
|
+
DOCS_DRIFT_RUNTIME_CLI_COMMAND,
|
|
1700
|
+
DOCS_DRIFT_CLI_ID,
|
|
1701
|
+
DOCS_DRIFT_CAPABILITY_ID
|
|
662
1702
|
};
|