@h-rig/standard-plugin 0.0.6-alpha.132 → 0.0.6-alpha.133

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // packages/standard-plugin/src/index.ts
3
- import { resolve as resolve3 } from "path";
3
+ import { resolve as resolve4 } from "path";
4
4
  import { definePlugin } from "@rig/core";
5
5
 
6
6
  // packages/standard-plugin/src/github-issues-source.ts
@@ -93,17 +93,42 @@ function statusFor(issue) {
93
93
  return "cancelled";
94
94
  return "open";
95
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
+ }
96
123
  function parseDeps(body) {
97
124
  const match = body.match(/^depends-on:\s*([^\n]+)/im);
98
- if (!match)
99
- return [];
100
- 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")])];
101
127
  }
102
128
  function parseParents(body) {
103
129
  const match = body.match(/^parents?:\s*([^\n]+)/im);
104
- if (!match)
105
- return [];
106
- 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")])];
107
132
  }
108
133
  function issueTypeFor(issue) {
109
134
  const labels = labelNamesFor(issue);
@@ -114,7 +139,7 @@ function issueTypeFor(issue) {
114
139
  return "epic";
115
140
  return "task";
116
141
  }
117
- function issueToTask(issue, repo) {
142
+ function issueToTask(issue, repo, nativeDependencies) {
118
143
  const labelNames = labelNamesFor(issue);
119
144
  const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
120
145
  const roleLabel = labelNames.find((l) => l.startsWith("role:"));
@@ -122,10 +147,12 @@ function issueToTask(issue, repo) {
122
147
  const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
123
148
  const body = issue.body ?? "";
124
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;
125
152
  return {
126
153
  id: String(issue.number),
127
154
  ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
128
- deps: parseDeps(body),
155
+ deps,
129
156
  status: statusFor(issue),
130
157
  title: issue.title,
131
158
  body,
@@ -137,6 +164,7 @@ function issueToTask(issue, repo) {
137
164
  sourceIssueId: `${repo}#${issue.number}`,
138
165
  parentChildDeps: parseParents(body),
139
166
  labels: labelNames,
167
+ ...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
140
168
  raw: issue
141
169
  };
142
170
  }
@@ -310,6 +338,86 @@ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
310
338
  return asProjectRecord(response)?.data ?? response;
311
339
  };
312
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
+ }
313
421
  function projectStatusFieldFrom(data, projectId) {
314
422
  const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
315
423
  for (const node of Array.isArray(fields) ? fields : []) {
@@ -645,6 +753,16 @@ function createGitHubIssuesTaskSource(opts) {
645
753
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
646
754
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
647
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
+ }
648
766
  return {
649
767
  id: "std:github-issues",
650
768
  kind: "github-issues",
@@ -671,7 +789,7 @@ function createGitHubIssuesTaskSource(opts) {
671
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.`);
672
790
  }
673
791
  const issues = rawIssues.filter((issue) => !issue.pull_request);
674
- return issues.map((i) => issueToTask(i, repo));
792
+ return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
675
793
  },
676
794
  async get(id) {
677
795
  const env = await resolveCredentialEnv(opts, "selected-repo");
@@ -688,12 +806,12 @@ function createGitHubIssuesTaskSource(opts) {
688
806
  ], spawnFn, env, timeoutMs);
689
807
  } catch (error) {
690
808
  const detail = error instanceof Error ? error.message : String(error);
691
- if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found/i.test(detail)) {
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)) {
692
810
  return;
693
811
  }
694
812
  throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
695
813
  }
696
- return issueToTask(issue, repo);
814
+ return issueToTaskWithOptionalNativeDependencies(issue, env);
697
815
  },
698
816
  async updateStatus(id, status) {
699
817
  const env = await resolveCredentialEnv(opts, "selected-repo");
@@ -717,6 +835,7 @@ function createGitHubIssuesTaskSource(opts) {
717
835
  },
718
836
  async createIssue(input) {
719
837
  const env = await resolveCredentialEnv(opts, "selected-repo");
838
+ const body = input.body ?? "";
720
839
  const args = [
721
840
  "api",
722
841
  "-X",
@@ -725,12 +844,31 @@ function createGitHubIssuesTaskSource(opts) {
725
844
  "-f",
726
845
  `title=${input.title}`,
727
846
  "-f",
728
- `body=${input.body ?? ""}`,
847
+ `body=${body}`,
729
848
  ...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
730
849
  ];
731
850
  const issue = runGh(bin, args, spawnFn, env, timeoutMs);
732
851
  notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
733
- 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);
734
872
  },
735
873
  async getIssueBody(id) {
736
874
  const env = await resolveCredentialEnv(opts, "selected-repo");
@@ -843,6 +981,425 @@ function createFilesTaskSource(opts) {
843
981
  };
844
982
  }
845
983
 
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_VALIDATOR = {
1287
+ id: DOCS_DRIFT_VALIDATOR_ID,
1288
+ category: "regression",
1289
+ description: "Detect documentation references that drifted from the source tree."
1290
+ };
1291
+ var DOCS_DRIFT_STAGE_MUTATION = Schema.decodeUnknownSync(StageMutationSchema)({
1292
+ op: "insert",
1293
+ stage: {
1294
+ id: DOCS_DRIFT_STAGE_ID,
1295
+ kind: "gate",
1296
+ before: ["merge-gate"],
1297
+ after: ["verify"]
1298
+ },
1299
+ contributedBy: DOCS_DRIFT_STAGE_ID
1300
+ });
1301
+ var DOCS_DRIFT_CLI_COMMAND = `bun -e 'import { runDriftCli } from "@rig/standard-plugin/drift"; process.exitCode = await runDriftCli(process.argv.slice(1), { projectRoot: process.cwd() });' --`;
1302
+ function highConfidenceDriftFindings(report) {
1303
+ return report.findings.filter((finding) => finding.confidence === "high");
1304
+ }
1305
+ function driftGateResult(report, mode = "enforce") {
1306
+ const high = highConfidenceDriftFindings(report);
1307
+ if (mode === "enforce" && high.length > 0) {
1308
+ return { kind: "block", reason: `${high.length} high-confidence documentation drift finding(s).` };
1309
+ }
1310
+ return { kind: "allow" };
1311
+ }
1312
+ async function runDocsDriftValidation(options) {
1313
+ const report = await detectDrift(options);
1314
+ const high = highConfidenceDriftFindings(report);
1315
+ const passed = options.failOnDrift ? high.length === 0 : true;
1316
+ const findingWord = report.findings.length === 1 ? "finding" : "findings";
1317
+ return {
1318
+ id: DOCS_DRIFT_VALIDATOR_ID,
1319
+ passed,
1320
+ summary: `docs drift scanned ${report.scanned} doc(s), ${report.findings.length} ${findingWord}`,
1321
+ details: JSON.stringify(report)
1322
+ };
1323
+ }
1324
+ function createDocsDriftValidator(options = {}) {
1325
+ return {
1326
+ ...DOCS_DRIFT_VALIDATOR,
1327
+ async run(ctx) {
1328
+ return runDocsDriftValidation({
1329
+ projectRoot: ctx.workspaceRoot,
1330
+ ...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
1331
+ ...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
1332
+ ...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
1333
+ });
1334
+ }
1335
+ };
1336
+ }
1337
+ function takeOptionValue(args, index, flag) {
1338
+ const value = args[index + 1];
1339
+ if (!value)
1340
+ throw new Error(`${flag} requires a value`);
1341
+ return value;
1342
+ }
1343
+ async function runDriftCli(args, options = {}) {
1344
+ const docsGlobs = [];
1345
+ const ignoreGlobs = [];
1346
+ let json = false;
1347
+ let failOnDrift = false;
1348
+ for (let index = 0;index < args.length; index += 1) {
1349
+ const arg = args[index];
1350
+ if (arg === "--json") {
1351
+ json = true;
1352
+ continue;
1353
+ }
1354
+ if (arg === "--fail-on-drift") {
1355
+ failOnDrift = true;
1356
+ continue;
1357
+ }
1358
+ if (arg === "--docs") {
1359
+ docsGlobs.push(takeOptionValue(args, index, arg));
1360
+ index += 1;
1361
+ continue;
1362
+ }
1363
+ if (arg === "--ignore") {
1364
+ ignoreGlobs.push(takeOptionValue(args, index, arg));
1365
+ index += 1;
1366
+ continue;
1367
+ }
1368
+ throw new Error(`Unknown rig drift argument: ${arg}`);
1369
+ }
1370
+ const report = await detectDrift({
1371
+ projectRoot: options.projectRoot ?? process.cwd(),
1372
+ ...docsGlobs.length > 0 ? { docsGlobs } : {},
1373
+ ...ignoreGlobs.length > 0 ? { ignoreGlobs } : {}
1374
+ });
1375
+ const write = options.write ?? ((message) => console.log(message));
1376
+ if (json) {
1377
+ write(JSON.stringify(report));
1378
+ } else {
1379
+ write(`Scanned ${report.scanned} doc(s); ${report.findings.length} drift finding(s).`);
1380
+ for (const finding of report.findings) {
1381
+ write(`${finding.confidence.toUpperCase()} ${finding.kind} ${finding.docPath}${finding.line ? `:${finding.line}` : ""} ${finding.reference ?? ""} \u2014 ${finding.detail}`);
1382
+ }
1383
+ }
1384
+ const high = highConfidenceDriftFindings(report);
1385
+ if (failOnDrift && high.length > 0) {
1386
+ options.writeError?.(`${high.length} high-confidence drift finding(s).`);
1387
+ return 2;
1388
+ }
1389
+ return 0;
1390
+ }
1391
+ // packages/standard-plugin/src/drift/judge.ts
1392
+ async function judgeDocumentationDrift(provider, input) {
1393
+ const result = await provider.judge(input);
1394
+ return result.mismatches.map((mismatch) => ({
1395
+ kind: "semantic-mismatch",
1396
+ docPath: input.docPath,
1397
+ line: mismatch.line ?? null,
1398
+ reference: mismatch.reference ?? input.reference ?? null,
1399
+ detail: mismatch.detail,
1400
+ confidence: mismatch.confidence ?? "medium"
1401
+ }));
1402
+ }
846
1403
  // packages/standard-plugin/src/index.ts
847
1404
  function requireStringField(config, field, kind) {
848
1405
  const value = config[field];
@@ -885,11 +1442,15 @@ function githubProjectsOptionsFromConfig(config, context) {
885
1442
  const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
886
1443
  return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
887
1444
  }
1445
+ function booleanOption(value) {
1446
+ return typeof value === "boolean" ? value : undefined;
1447
+ }
888
1448
  function standardPlugin(opts = {}) {
889
1449
  return definePlugin({
890
1450
  name: "rig-standard",
891
1451
  version: "0.1.0",
892
1452
  contributes: {
1453
+ validators: [DOCS_DRIFT_VALIDATOR],
893
1454
  taskSources: [
894
1455
  {
895
1456
  id: "std:github-issues",
@@ -901,9 +1462,18 @@ function standardPlugin(opts = {}) {
901
1462
  kind: "files",
902
1463
  description: "JSON files in a local directory"
903
1464
  }
904
- ]
1465
+ ],
1466
+ cliCommands: [
1467
+ {
1468
+ id: DOCS_DRIFT_CLI_ID,
1469
+ command: DOCS_DRIFT_CLI_COMMAND,
1470
+ description: "Scan documentation for stale code references."
1471
+ }
1472
+ ],
1473
+ stageMutations: [DOCS_DRIFT_STAGE_MUTATION]
905
1474
  }
906
1475
  }, {
1476
+ validators: [createDocsDriftValidator(opts.drift)],
907
1477
  taskSources: [
908
1478
  {
909
1479
  id: "std:github-issues",
@@ -914,7 +1484,7 @@ function standardPlugin(opts = {}) {
914
1484
  owner: requireStringField(config, "owner", "github-issues"),
915
1485
  repo: requireStringField(config, "repo", "github-issues")
916
1486
  };
917
- const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve3(context.projectRoot, ".rig", "state") } : {};
1487
+ const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve4(context.projectRoot, ".rig", "state") } : {};
918
1488
  options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
919
1489
  if (opts.githubWorkspaceId)
920
1490
  options.workspaceId = opts.githubWorkspaceId;
@@ -940,6 +1510,9 @@ function standardPlugin(opts = {}) {
940
1510
  const projects = githubProjectsOptionsFromConfig(config, context);
941
1511
  if (projects)
942
1512
  options.projects = projects;
1513
+ const useNativeDependencies = booleanOption(config.options?.useNativeDependencies);
1514
+ if (useNativeDependencies !== undefined)
1515
+ options.useNativeDependencies = useNativeDependencies;
943
1516
  return createGitHubIssuesTaskSource(options);
944
1517
  }
945
1518
  },
@@ -958,9 +1531,22 @@ function standardPlugin(opts = {}) {
958
1531
  });
959
1532
  }
960
1533
  export {
1534
+ runDriftCli,
1535
+ runDocsDriftValidation,
1536
+ makeDriftGit,
1537
+ judgeDocumentationDrift,
1538
+ extractDriftReferences,
1539
+ driftGateResult,
1540
+ detectStaleAnchors,
1541
+ detectDrift,
1542
+ detectDeletedReferences,
961
1543
  standardPlugin as default,
962
1544
  createStateGitHubCredentialProvider,
963
1545
  createGitHubIssuesTaskSource,
964
1546
  createFilesTaskSource,
965
- createEnvGitHubCredentialProvider
1547
+ createEnvGitHubCredentialProvider,
1548
+ createDocsDriftValidator,
1549
+ DOCS_DRIFT_VALIDATOR_ID,
1550
+ DOCS_DRIFT_STAGE_ID,
1551
+ DOCS_DRIFT_CLI_ID
966
1552
  };