@h-rig/docs-drift-plugin 0.0.6-alpha.156
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/README.md +1 -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/metadata.d.ts +13 -0
- package/dist/src/drift/metadata.js +35 -0
- package/dist/src/drift/plugin.d.ts +53 -0
- package/dist/src/drift/plugin.js +509 -0
- package/dist/src/drift/summary.d.ts +24 -0
- package/dist/src/drift/summary.js +42 -0
- package/dist/src/plugin.d.ts +9 -0
- package/dist/src/plugin.js +665 -0
- package/package.json +33 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
|
|
18
|
+
// packages/docs-drift-plugin/src/drift/metadata.ts
|
|
19
|
+
import { Schema } from "effect";
|
|
20
|
+
import { StageMutation as StageMutationSchema } from "@rig/contracts";
|
|
21
|
+
var DOCS_DRIFT_VALIDATOR_ID = "std:docs-drift", DOCS_DRIFT_CLI_ID = "std:drift", DOCS_DRIFT_STAGE_ID = "docs-drift", DOCS_DRIFT_CAPABILITY_ID = "std:docs-drift-capability", DOCS_DRIFT_VALIDATOR, DOCS_DRIFT_STAGE_MUTATION, DOCS_DRIFT_CLI_COMMAND = "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]";
|
|
22
|
+
var init_metadata = __esm(() => {
|
|
23
|
+
DOCS_DRIFT_VALIDATOR = {
|
|
24
|
+
id: DOCS_DRIFT_VALIDATOR_ID,
|
|
25
|
+
category: "regression",
|
|
26
|
+
description: "Detect documentation references that drifted from the source tree."
|
|
27
|
+
};
|
|
28
|
+
DOCS_DRIFT_STAGE_MUTATION = Schema.decodeUnknownSync(StageMutationSchema)({
|
|
29
|
+
op: "insert",
|
|
30
|
+
stage: {
|
|
31
|
+
id: DOCS_DRIFT_STAGE_ID,
|
|
32
|
+
kind: "gate",
|
|
33
|
+
before: ["merge-gate"],
|
|
34
|
+
after: ["open-pr"],
|
|
35
|
+
priority: 0,
|
|
36
|
+
protected: false
|
|
37
|
+
},
|
|
38
|
+
contributedBy: DOCS_DRIFT_STAGE_ID
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// packages/docs-drift-plugin/src/drift/extract-refs.ts
|
|
43
|
+
function stripFenceLines(markdown) {
|
|
44
|
+
const lines = markdown.split(/\r?\n/);
|
|
45
|
+
let fenced = false;
|
|
46
|
+
return lines.map((line) => {
|
|
47
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
48
|
+
fenced = !fenced;
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
return fenced ? "" : line;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function normalizeToken(raw) {
|
|
55
|
+
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[),.;:]+$/g, "").replace(/#L\d+(?:-L\d+)?$/i, "");
|
|
56
|
+
}
|
|
57
|
+
function classifyReference(raw) {
|
|
58
|
+
if (raw.startsWith("@"))
|
|
59
|
+
return null;
|
|
60
|
+
if (PATH_REF.test(raw))
|
|
61
|
+
return "path";
|
|
62
|
+
if (SYMBOL_REF.test(raw))
|
|
63
|
+
return "symbol";
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function pushReference(refs, seen, raw, line) {
|
|
67
|
+
const value = normalizeToken(raw);
|
|
68
|
+
if (!value)
|
|
69
|
+
return;
|
|
70
|
+
const kind = classifyReference(value);
|
|
71
|
+
if (!kind)
|
|
72
|
+
return;
|
|
73
|
+
const key = `${kind}:${value}:${line}`;
|
|
74
|
+
if (seen.has(key))
|
|
75
|
+
return;
|
|
76
|
+
seen.add(key);
|
|
77
|
+
refs.push({ kind, value, line });
|
|
78
|
+
}
|
|
79
|
+
function extractDriftReferences(markdown) {
|
|
80
|
+
const refs = [];
|
|
81
|
+
const seen = new Set;
|
|
82
|
+
const lines = stripFenceLines(markdown);
|
|
83
|
+
for (const [index, line] of lines.entries()) {
|
|
84
|
+
const lineNumber = index + 1;
|
|
85
|
+
for (const match of line.matchAll(INLINE_CODE)) {
|
|
86
|
+
pushReference(refs, seen, match[1] ?? "", lineNumber);
|
|
87
|
+
}
|
|
88
|
+
for (const match of line.matchAll(MARKDOWN_LINK)) {
|
|
89
|
+
pushReference(refs, seen, match[1] ?? "", lineNumber);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return refs;
|
|
93
|
+
}
|
|
94
|
+
var INLINE_CODE, MARKDOWN_LINK, SYMBOL_REF, PATH_REF;
|
|
95
|
+
var init_extract_refs = __esm(() => {
|
|
96
|
+
INLINE_CODE = /`([^`\n]+)`/g;
|
|
97
|
+
MARKDOWN_LINK = /\[[^\]]+\]\(([^)\s]+)\)/g;
|
|
98
|
+
SYMBOL_REF = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/;
|
|
99
|
+
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)$/;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// packages/docs-drift-plugin/src/drift/git-adapter.ts
|
|
103
|
+
import { execFile } from "child_process";
|
|
104
|
+
import { promisify } from "util";
|
|
105
|
+
function processError(value) {
|
|
106
|
+
return value && typeof value === "object" ? value : null;
|
|
107
|
+
}
|
|
108
|
+
function lineCount(output) {
|
|
109
|
+
const trimmed = output.trim();
|
|
110
|
+
return trimmed ? trimmed.split(/\r?\n/).length : 0;
|
|
111
|
+
}
|
|
112
|
+
function makeDriftGit(projectRoot) {
|
|
113
|
+
async function git(args) {
|
|
114
|
+
const result = await execFileAsync("git", [...args], {
|
|
115
|
+
cwd: projectRoot,
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
maxBuffer: 10 * 1024 * 1024
|
|
118
|
+
});
|
|
119
|
+
return String(result.stdout);
|
|
120
|
+
}
|
|
121
|
+
async function grepCountAt(symbolOrPath, commit) {
|
|
122
|
+
try {
|
|
123
|
+
return lineCount(await git(["grep", "-F", "-n", "-e", symbolOrPath, commit, "--"]));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const detail = processError(error);
|
|
126
|
+
if (detail?.code === 1)
|
|
127
|
+
return 0;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
async lastCommitTouching(path) {
|
|
133
|
+
const commit = (await git(["log", "-n", "1", "--format=%H", "--", path])).trim();
|
|
134
|
+
return commit || "HEAD";
|
|
135
|
+
},
|
|
136
|
+
async grepCount(symbolOrPath) {
|
|
137
|
+
return grepCountAt(symbolOrPath, "HEAD");
|
|
138
|
+
},
|
|
139
|
+
async grepCountAtCommit(symbolOrPath, commit) {
|
|
140
|
+
return grepCountAt(symbolOrPath, commit);
|
|
141
|
+
},
|
|
142
|
+
async wasRenamed(symbolOrPath, sinceCommit) {
|
|
143
|
+
if (!symbolOrPath.includes("/") && !symbolOrPath.includes("."))
|
|
144
|
+
return false;
|
|
145
|
+
try {
|
|
146
|
+
const output = await git(["log", "--name-status", "--format=", `${sinceCommit}..HEAD`]);
|
|
147
|
+
return output.split(/\r?\n/).some((line) => {
|
|
148
|
+
const match = line.match(/^R\d*\s+(.+?)\s+(.+)$/);
|
|
149
|
+
return Boolean(match && (match[1] === symbolOrPath || match[2] === symbolOrPath));
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const detail = processError(error);
|
|
153
|
+
if (detail?.code === 128)
|
|
154
|
+
return false;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var execFileAsync;
|
|
161
|
+
var init_git_adapter = __esm(() => {
|
|
162
|
+
execFileAsync = promisify(execFile);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// packages/docs-drift-plugin/src/drift/detect.ts
|
|
166
|
+
var exports_detect = {};
|
|
167
|
+
__export(exports_detect, {
|
|
168
|
+
detectStaleAnchors: () => detectStaleAnchors,
|
|
169
|
+
detectDrift: () => detectDrift,
|
|
170
|
+
detectDeletedReferences: () => detectDeletedReferences
|
|
171
|
+
});
|
|
172
|
+
import { existsSync } from "fs";
|
|
173
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
174
|
+
import { basename, extname, relative, resolve } from "path";
|
|
175
|
+
function globLikeMatch(path, pattern) {
|
|
176
|
+
if (pattern === path)
|
|
177
|
+
return true;
|
|
178
|
+
if (pattern.startsWith("**/*"))
|
|
179
|
+
return path.endsWith(pattern.slice(4));
|
|
180
|
+
if (pattern.endsWith("/**"))
|
|
181
|
+
return path.startsWith(pattern.slice(0, -3));
|
|
182
|
+
if (pattern.includes("*")) {
|
|
183
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
184
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
185
|
+
}
|
|
186
|
+
return path.startsWith(pattern);
|
|
187
|
+
}
|
|
188
|
+
function isDefaultDoc(path) {
|
|
189
|
+
const lower = basename(path).toLowerCase();
|
|
190
|
+
return (path.endsWith(".md") || path.endsWith(".mdx")) && !lower.startsWith("changelog") && !lower.includes("generated");
|
|
191
|
+
}
|
|
192
|
+
function isIgnored(path, patterns) {
|
|
193
|
+
return (patterns ?? []).some((pattern) => globLikeMatch(path, pattern));
|
|
194
|
+
}
|
|
195
|
+
async function collectFiles(root, options) {
|
|
196
|
+
const files = [];
|
|
197
|
+
async function visit(dir) {
|
|
198
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
199
|
+
if (entry.isDirectory() && DEFAULT_IGNORED_DIRS[entry.name])
|
|
200
|
+
continue;
|
|
201
|
+
const absolute = resolve(dir, entry.name);
|
|
202
|
+
const rel = relative(root, absolute).replace(/\\/g, "/");
|
|
203
|
+
if (isIgnored(rel, options.ignore))
|
|
204
|
+
continue;
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
await visit(absolute);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!entry.isFile())
|
|
210
|
+
continue;
|
|
211
|
+
if (options.docs) {
|
|
212
|
+
const matchesConfigured = options.patterns && options.patterns.length > 0 ? options.patterns.some((pattern) => globLikeMatch(rel, pattern)) : isDefaultDoc(rel);
|
|
213
|
+
if (matchesConfigured)
|
|
214
|
+
files.push(rel);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (SOURCE_EXTENSIONS[extname(entry.name)])
|
|
218
|
+
files.push(rel);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await visit(root);
|
|
222
|
+
return files.sort();
|
|
223
|
+
}
|
|
224
|
+
async function sourceReferenceCount(projectRoot, reference, docPath) {
|
|
225
|
+
if (reference.kind === "path")
|
|
226
|
+
return existsSync(resolve(projectRoot, reference.value)) ? 1 : 0;
|
|
227
|
+
let count = 0;
|
|
228
|
+
const sourceFiles = await collectFiles(projectRoot, { docs: false });
|
|
229
|
+
for (const sourceFile of sourceFiles) {
|
|
230
|
+
if (sourceFile === docPath)
|
|
231
|
+
continue;
|
|
232
|
+
const text = await readFile(resolve(projectRoot, sourceFile), "utf8").catch(() => "");
|
|
233
|
+
if (text.includes(reference.value))
|
|
234
|
+
count += 1;
|
|
235
|
+
}
|
|
236
|
+
return count;
|
|
237
|
+
}
|
|
238
|
+
function deletedReferenceFinding(docPath, reference) {
|
|
239
|
+
return {
|
|
240
|
+
kind: "deleted-reference",
|
|
241
|
+
docPath,
|
|
242
|
+
line: reference.line,
|
|
243
|
+
reference: reference.value,
|
|
244
|
+
detail: `Documented reference "${reference.value}" no longer exists in the source tree.`,
|
|
245
|
+
confidence: "high"
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function staleAnchorFinding(docPath, reference) {
|
|
249
|
+
return {
|
|
250
|
+
kind: "stale-anchor",
|
|
251
|
+
docPath,
|
|
252
|
+
line: reference.line,
|
|
253
|
+
reference: reference.value,
|
|
254
|
+
detail: `Documented path "${reference.value}" changed after this doc was last updated.`,
|
|
255
|
+
confidence: "medium"
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function detectDeletedReferences(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
|
|
259
|
+
const markdown = await readFile(resolve(projectRoot, docPath), "utf8");
|
|
260
|
+
const docCommit = await git.lastCommitTouching(docPath);
|
|
261
|
+
const findings = [];
|
|
262
|
+
for (const reference of extractDriftReferences(markdown)) {
|
|
263
|
+
if (await sourceReferenceCount(projectRoot, reference, docPath) > 0)
|
|
264
|
+
continue;
|
|
265
|
+
if (await git.wasRenamed(reference.value, docCommit))
|
|
266
|
+
continue;
|
|
267
|
+
findings.push(deletedReferenceFinding(docPath, reference));
|
|
268
|
+
}
|
|
269
|
+
return findings;
|
|
270
|
+
}
|
|
271
|
+
async function detectStaleAnchors(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
|
|
272
|
+
const markdown = await readFile(resolve(projectRoot, docPath), "utf8");
|
|
273
|
+
const docCommit = await git.lastCommitTouching(docPath);
|
|
274
|
+
const findings = [];
|
|
275
|
+
for (const reference of extractDriftReferences(markdown).filter((ref) => ref.kind === "path")) {
|
|
276
|
+
if (!existsSync(resolve(projectRoot, reference.value)))
|
|
277
|
+
continue;
|
|
278
|
+
const sourceStat = await stat(resolve(projectRoot, reference.value)).catch(() => null);
|
|
279
|
+
if (!sourceStat?.isFile())
|
|
280
|
+
continue;
|
|
281
|
+
const sourceCommit = await git.lastCommitTouching(reference.value);
|
|
282
|
+
if (sourceCommit !== docCommit && !await git.wasRenamed(reference.value, docCommit)) {
|
|
283
|
+
findings.push(staleAnchorFinding(docPath, reference));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return findings;
|
|
287
|
+
}
|
|
288
|
+
async function detectDrift(options) {
|
|
289
|
+
const git = options.git ?? makeDriftGit(options.projectRoot);
|
|
290
|
+
const docs = await collectFiles(options.projectRoot, {
|
|
291
|
+
docs: true,
|
|
292
|
+
...options.docsGlobs !== undefined ? { patterns: options.docsGlobs } : {},
|
|
293
|
+
...options.ignoreGlobs !== undefined ? { ignore: options.ignoreGlobs } : {}
|
|
294
|
+
});
|
|
295
|
+
const findings = [];
|
|
296
|
+
let degraded = false;
|
|
297
|
+
for (const docPath of docs) {
|
|
298
|
+
try {
|
|
299
|
+
findings.push(...await detectDeletedReferences(options.projectRoot, docPath, git));
|
|
300
|
+
findings.push(...await detectStaleAnchors(options.projectRoot, docPath, git));
|
|
301
|
+
} catch {
|
|
302
|
+
degraded = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
generatedAt: new Date().toISOString(),
|
|
307
|
+
scanned: docs.length,
|
|
308
|
+
degraded,
|
|
309
|
+
findings
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
var DEFAULT_IGNORED_DIRS, SOURCE_EXTENSIONS;
|
|
313
|
+
var init_detect = __esm(() => {
|
|
314
|
+
init_extract_refs();
|
|
315
|
+
init_git_adapter();
|
|
316
|
+
DEFAULT_IGNORED_DIRS = {
|
|
317
|
+
".git": true,
|
|
318
|
+
node_modules: true,
|
|
319
|
+
dist: true,
|
|
320
|
+
build: true,
|
|
321
|
+
coverage: true,
|
|
322
|
+
".next": true,
|
|
323
|
+
vendor: true
|
|
324
|
+
};
|
|
325
|
+
SOURCE_EXTENSIONS = {
|
|
326
|
+
".ts": true,
|
|
327
|
+
".tsx": true,
|
|
328
|
+
".js": true,
|
|
329
|
+
".jsx": true,
|
|
330
|
+
".mjs": true,
|
|
331
|
+
".cjs": true,
|
|
332
|
+
".rs": true,
|
|
333
|
+
".go": true,
|
|
334
|
+
".py": true,
|
|
335
|
+
".rb": true,
|
|
336
|
+
".java": true,
|
|
337
|
+
".kt": true,
|
|
338
|
+
".swift": true,
|
|
339
|
+
".c": true,
|
|
340
|
+
".cc": true,
|
|
341
|
+
".cpp": true,
|
|
342
|
+
".h": true,
|
|
343
|
+
".hpp": true,
|
|
344
|
+
".json": true,
|
|
345
|
+
".toml": true,
|
|
346
|
+
".yml": true,
|
|
347
|
+
".yaml": true
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// packages/docs-drift-plugin/src/drift/plugin.ts
|
|
352
|
+
var exports_plugin = {};
|
|
353
|
+
__export(exports_plugin, {
|
|
354
|
+
runDriftCli: () => runDriftCli,
|
|
355
|
+
runDocsDriftValidation: () => runDocsDriftValidation,
|
|
356
|
+
highConfidenceDriftFindings: () => highConfidenceDriftFindings,
|
|
357
|
+
executeDrift: () => executeDrift,
|
|
358
|
+
driftGateResult: () => driftGateResult,
|
|
359
|
+
createDocsDriftValidator: () => createDocsDriftValidator,
|
|
360
|
+
createDocsDriftRuntimeCliCommand: () => createDocsDriftRuntimeCliCommand,
|
|
361
|
+
createDocsDriftGateStage: () => createDocsDriftGateStage,
|
|
362
|
+
DOCS_DRIFT_VALIDATOR_ID: () => DOCS_DRIFT_VALIDATOR_ID,
|
|
363
|
+
DOCS_DRIFT_VALIDATOR: () => DOCS_DRIFT_VALIDATOR,
|
|
364
|
+
DOCS_DRIFT_STAGE_MUTATION: () => DOCS_DRIFT_STAGE_MUTATION,
|
|
365
|
+
DOCS_DRIFT_STAGE_ID: () => DOCS_DRIFT_STAGE_ID,
|
|
366
|
+
DOCS_DRIFT_RUNTIME_CLI_COMMAND: () => DOCS_DRIFT_RUNTIME_CLI_COMMAND,
|
|
367
|
+
DOCS_DRIFT_CLI_ID: () => DOCS_DRIFT_CLI_ID,
|
|
368
|
+
DOCS_DRIFT_CLI_COMMAND: () => DOCS_DRIFT_CLI_COMMAND,
|
|
369
|
+
DOCS_DRIFT_CAPABILITY_ID: () => DOCS_DRIFT_CAPABILITY_ID
|
|
370
|
+
});
|
|
371
|
+
function highConfidenceDriftFindings(report) {
|
|
372
|
+
return report.findings.filter((finding) => finding.confidence === "high");
|
|
373
|
+
}
|
|
374
|
+
function driftGateResult(report, mode = "enforce") {
|
|
375
|
+
const high = highConfidenceDriftFindings(report);
|
|
376
|
+
if (mode === "enforce" && high.length > 0) {
|
|
377
|
+
return { kind: "block", reason: `${high.length} high-confidence documentation drift finding(s).` };
|
|
378
|
+
}
|
|
379
|
+
return { kind: "allow" };
|
|
380
|
+
}
|
|
381
|
+
function createDocsDriftGateStage(options = {}) {
|
|
382
|
+
return async (ctx) => {
|
|
383
|
+
const projectRoot = typeof ctx.metadata?.projectRoot === "string" ? ctx.metadata.projectRoot : process.cwd();
|
|
384
|
+
const report = await detectDrift({
|
|
385
|
+
projectRoot,
|
|
386
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
387
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
388
|
+
});
|
|
389
|
+
return driftGateResult(report, options.failOnDrift ? "enforce" : "observe");
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function runDocsDriftValidation(options) {
|
|
393
|
+
const report = await detectDrift(options);
|
|
394
|
+
const high = highConfidenceDriftFindings(report);
|
|
395
|
+
const passed = options.failOnDrift ? high.length === 0 : true;
|
|
396
|
+
const findingWord = report.findings.length === 1 ? "finding" : "findings";
|
|
397
|
+
return {
|
|
398
|
+
id: DOCS_DRIFT_VALIDATOR_ID,
|
|
399
|
+
passed,
|
|
400
|
+
summary: `docs drift scanned ${report.scanned} doc(s), ${report.findings.length} ${findingWord}`,
|
|
401
|
+
details: JSON.stringify(report)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function createDocsDriftValidator(options = {}) {
|
|
405
|
+
return {
|
|
406
|
+
...DOCS_DRIFT_VALIDATOR,
|
|
407
|
+
async run(ctx) {
|
|
408
|
+
return runDocsDriftValidation({
|
|
409
|
+
projectRoot: ctx.workspaceRoot,
|
|
410
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
411
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
|
|
412
|
+
...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function takeOptionValue(args, index, flag) {
|
|
418
|
+
const value = args[index + 1];
|
|
419
|
+
if (!value)
|
|
420
|
+
throw new Error(`${flag} requires a value`);
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
function takeFlag(args, flag) {
|
|
424
|
+
const rest = [...args];
|
|
425
|
+
const index = rest.indexOf(flag);
|
|
426
|
+
if (index < 0)
|
|
427
|
+
return { value: false, rest };
|
|
428
|
+
rest.splice(index, 1);
|
|
429
|
+
return { value: true, rest };
|
|
430
|
+
}
|
|
431
|
+
function takeOption(args, flag) {
|
|
432
|
+
const rest = [...args];
|
|
433
|
+
const index = rest.indexOf(flag);
|
|
434
|
+
if (index < 0)
|
|
435
|
+
return { rest };
|
|
436
|
+
const value = rest[index + 1];
|
|
437
|
+
if (!value || value.startsWith("-"))
|
|
438
|
+
throw new Error(`${flag} requires a value.`);
|
|
439
|
+
rest.splice(index, 2);
|
|
440
|
+
return { value, rest };
|
|
441
|
+
}
|
|
442
|
+
function requireNoExtraArgs(args, usage) {
|
|
443
|
+
if (args.length > 0)
|
|
444
|
+
throw new Error(`Unexpected argument: ${args[0]}
|
|
445
|
+
Usage: ${usage}`);
|
|
446
|
+
}
|
|
447
|
+
function parseCsv(value) {
|
|
448
|
+
return value?.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) ?? [];
|
|
449
|
+
}
|
|
450
|
+
function driftSummary(report) {
|
|
451
|
+
const highConfidence = highConfidenceDriftFindings(report).length;
|
|
452
|
+
return { total: report.findings.length, highConfidence, degraded: report.degraded };
|
|
453
|
+
}
|
|
454
|
+
async function executeDrift(context, args, options = {}) {
|
|
455
|
+
const json = takeFlag(args, "--json");
|
|
456
|
+
const docs = takeOption(json.rest, "--docs");
|
|
457
|
+
const ignore = takeOption(docs.rest, "--ignore");
|
|
458
|
+
const failOnDrift = takeFlag(ignore.rest, "--fail-on-drift");
|
|
459
|
+
requireNoExtraArgs(failOnDrift.rest, "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]");
|
|
460
|
+
const docsGlobs = parseCsv(docs.value);
|
|
461
|
+
const ignoreGlobs = parseCsv(ignore.value);
|
|
462
|
+
const effectiveDocsGlobs = docsGlobs.length > 0 ? docsGlobs : options.docsGlobs;
|
|
463
|
+
const effectiveIgnoreGlobs = ignoreGlobs.length > 0 ? ignoreGlobs : options.ignoreGlobs;
|
|
464
|
+
const effectiveFailOnDrift = failOnDrift.value || options.failOnDrift === true;
|
|
465
|
+
const report = await detectDrift({
|
|
466
|
+
projectRoot: context.projectRoot,
|
|
467
|
+
...effectiveDocsGlobs !== undefined ? { docsGlobs: effectiveDocsGlobs } : {},
|
|
468
|
+
...effectiveIgnoreGlobs !== undefined ? { ignoreGlobs: effectiveIgnoreGlobs } : {}
|
|
469
|
+
});
|
|
470
|
+
const failed = effectiveFailOnDrift && highConfidenceDriftFindings(report).length > 0;
|
|
471
|
+
const details = { report, summary: driftSummary(report), failOnDrift: effectiveFailOnDrift, failed };
|
|
472
|
+
if (context.outputMode === "text") {
|
|
473
|
+
if (json.value)
|
|
474
|
+
console.log(JSON.stringify(details, null, 2));
|
|
475
|
+
else
|
|
476
|
+
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(`
|
|
477
|
+
`));
|
|
478
|
+
}
|
|
479
|
+
return { ok: !failed, group: "drift", command: "scan", details };
|
|
480
|
+
}
|
|
481
|
+
function createDocsDriftRuntimeCliCommand(options = {}) {
|
|
482
|
+
return {
|
|
483
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
484
|
+
family: "drift",
|
|
485
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
486
|
+
description: "Scan documentation for stale code references.",
|
|
487
|
+
usage: DOCS_DRIFT_CLI_COMMAND,
|
|
488
|
+
projectRequired: true,
|
|
489
|
+
run: (context, args) => executeDrift(context, args, options)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
async function runDriftCli(args, options = {}) {
|
|
493
|
+
const docsGlobs = [];
|
|
494
|
+
const ignoreGlobs = [];
|
|
495
|
+
let json = false;
|
|
496
|
+
let failOnDrift = false;
|
|
497
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
498
|
+
const arg = args[index];
|
|
499
|
+
if (arg === "--json") {
|
|
500
|
+
json = true;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (arg === "--fail-on-drift") {
|
|
504
|
+
failOnDrift = true;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (arg === "--docs") {
|
|
508
|
+
docsGlobs.push(takeOptionValue(args, index, arg));
|
|
509
|
+
index += 1;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (arg === "--ignore") {
|
|
513
|
+
ignoreGlobs.push(takeOptionValue(args, index, arg));
|
|
514
|
+
index += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
throw new Error(`Unknown rig drift argument: ${arg}`);
|
|
518
|
+
}
|
|
519
|
+
const report = await detectDrift({
|
|
520
|
+
projectRoot: options.projectRoot ?? process.cwd(),
|
|
521
|
+
...docsGlobs.length > 0 ? { docsGlobs } : {},
|
|
522
|
+
...ignoreGlobs.length > 0 ? { ignoreGlobs } : {}
|
|
523
|
+
});
|
|
524
|
+
const write = options.write ?? ((message) => console.log(message));
|
|
525
|
+
if (json) {
|
|
526
|
+
write(JSON.stringify(report));
|
|
527
|
+
} else {
|
|
528
|
+
write(`Scanned ${report.scanned} doc(s); ${report.findings.length} drift finding(s).`);
|
|
529
|
+
for (const finding of report.findings) {
|
|
530
|
+
write(`${finding.confidence.toUpperCase()} ${finding.kind} ${finding.docPath}${finding.line ? `:${finding.line}` : ""} ${finding.reference ?? ""} \u2014 ${finding.detail}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const high = highConfidenceDriftFindings(report);
|
|
534
|
+
if (failOnDrift && high.length > 0) {
|
|
535
|
+
options.writeError?.(`${high.length} high-confidence drift finding(s).`);
|
|
536
|
+
return 2;
|
|
537
|
+
}
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
var DOCS_DRIFT_RUNTIME_CLI_COMMAND;
|
|
541
|
+
var init_plugin = __esm(() => {
|
|
542
|
+
init_detect();
|
|
543
|
+
init_metadata();
|
|
544
|
+
init_metadata();
|
|
545
|
+
DOCS_DRIFT_RUNTIME_CLI_COMMAND = createDocsDriftRuntimeCliCommand();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// packages/docs-drift-plugin/src/plugin.ts
|
|
549
|
+
init_metadata();
|
|
550
|
+
init_metadata();
|
|
551
|
+
import { definePlugin } from "@rig/core/config";
|
|
552
|
+
var DOCS_HEALTH_PANEL_ID = "docs-health";
|
|
553
|
+
function isRecord(value) {
|
|
554
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
555
|
+
}
|
|
556
|
+
function panelProjectRoot(context) {
|
|
557
|
+
return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
558
|
+
}
|
|
559
|
+
function driftFindingPanelId(finding, index) {
|
|
560
|
+
return `${finding.docPath}:${finding.line ?? index}:${finding.kind}`;
|
|
561
|
+
}
|
|
562
|
+
function createDocsHealthPanelProducer(options = {}) {
|
|
563
|
+
return async (context) => {
|
|
564
|
+
const projectRoot = panelProjectRoot(context);
|
|
565
|
+
if (!projectRoot)
|
|
566
|
+
return;
|
|
567
|
+
const { detectDrift: detectDrift2 } = await Promise.resolve().then(() => (init_detect(), exports_detect));
|
|
568
|
+
const report = await detectDrift2({
|
|
569
|
+
projectRoot,
|
|
570
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
571
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
findings: report.findings.map((finding, index) => ({
|
|
575
|
+
id: driftFindingPanelId(finding, index),
|
|
576
|
+
docPath: finding.docPath,
|
|
577
|
+
kind: finding.kind,
|
|
578
|
+
confidence: finding.confidence,
|
|
579
|
+
summary: finding.detail,
|
|
580
|
+
taskId: null
|
|
581
|
+
})),
|
|
582
|
+
degraded: report.degraded ? "drift scan degraded" : null
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function createLazyDocsDriftValidator(options = {}) {
|
|
587
|
+
return {
|
|
588
|
+
...DOCS_DRIFT_VALIDATOR,
|
|
589
|
+
async run(ctx) {
|
|
590
|
+
const { runDocsDriftValidation: runDocsDriftValidation2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
591
|
+
return runDocsDriftValidation2({
|
|
592
|
+
projectRoot: ctx.workspaceRoot,
|
|
593
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
594
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
|
|
595
|
+
...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function createLazyDocsDriftGateStage(options = {}) {
|
|
601
|
+
return async (ctx) => {
|
|
602
|
+
const { createDocsDriftGateStage: createDocsDriftGateStage2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
603
|
+
return createDocsDriftGateStage2(options)(ctx);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function createLazyDocsDriftRuntimeCliCommand(options = {}) {
|
|
607
|
+
return {
|
|
608
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
609
|
+
family: "drift",
|
|
610
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
611
|
+
description: "Scan documentation for stale code references.",
|
|
612
|
+
usage: DOCS_DRIFT_CLI_COMMAND,
|
|
613
|
+
projectRequired: true,
|
|
614
|
+
run: async (context, args) => {
|
|
615
|
+
const { executeDrift: executeDrift2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
616
|
+
return executeDrift2(context, args, options);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function createStandardDocsDriftPlugin(opts = {}) {
|
|
621
|
+
return definePlugin({
|
|
622
|
+
name: "@rig/standard-plugin:docs-drift",
|
|
623
|
+
version: "0.1.0",
|
|
624
|
+
contributes: {
|
|
625
|
+
validators: [DOCS_DRIFT_VALIDATOR],
|
|
626
|
+
capabilities: [
|
|
627
|
+
{ id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
|
|
628
|
+
],
|
|
629
|
+
panels: [
|
|
630
|
+
{ id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID }
|
|
631
|
+
],
|
|
632
|
+
cliCommands: [
|
|
633
|
+
{
|
|
634
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
635
|
+
family: "drift",
|
|
636
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
637
|
+
description: "Scan documentation for stale code references.",
|
|
638
|
+
projectRequired: true
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
stageMutations: [DOCS_DRIFT_STAGE_MUTATION]
|
|
642
|
+
}
|
|
643
|
+
}, {
|
|
644
|
+
validators: [createLazyDocsDriftValidator(opts)],
|
|
645
|
+
stages: { [DOCS_DRIFT_STAGE_ID]: createLazyDocsDriftGateStage(opts) },
|
|
646
|
+
featureCapabilities: [
|
|
647
|
+
{ id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
|
|
648
|
+
],
|
|
649
|
+
panels: [
|
|
650
|
+
{ id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID, produce: createDocsHealthPanelProducer(opts) }
|
|
651
|
+
],
|
|
652
|
+
cliCommands: [createLazyDocsDriftRuntimeCliCommand(opts)]
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
export {
|
|
656
|
+
createStandardDocsDriftPlugin,
|
|
657
|
+
DOCS_HEALTH_PANEL_ID,
|
|
658
|
+
DOCS_DRIFT_VALIDATOR_ID,
|
|
659
|
+
DOCS_DRIFT_VALIDATOR,
|
|
660
|
+
DOCS_DRIFT_STAGE_MUTATION,
|
|
661
|
+
DOCS_DRIFT_STAGE_ID,
|
|
662
|
+
DOCS_DRIFT_CLI_ID,
|
|
663
|
+
DOCS_DRIFT_CLI_COMMAND,
|
|
664
|
+
DOCS_DRIFT_CAPABILITY_ID
|
|
665
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@h-rig/docs-drift-plugin",
|
|
3
|
+
"version": "0.0.6-alpha.156",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "First-party documentation drift detection capability plugin for Rig.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/plugin.d.ts",
|
|
14
|
+
"import": "./dist/src/plugin.js"
|
|
15
|
+
},
|
|
16
|
+
"./plugin": {
|
|
17
|
+
"types": "./dist/src/plugin.d.ts",
|
|
18
|
+
"import": "./dist/src/plugin.js"
|
|
19
|
+
},
|
|
20
|
+
"./drift/*": {
|
|
21
|
+
"types": "./dist/src/drift/*.d.ts",
|
|
22
|
+
"import": "./dist/src/drift/*.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.3.11"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.156",
|
|
30
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.156",
|
|
31
|
+
"effect": "4.0.0-beta.90"
|
|
32
|
+
}
|
|
33
|
+
}
|