artifact-graph 0.8.3 → 0.8.5

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/cli.js CHANGED
@@ -316,6 +316,41 @@ var init_glob_matcher = __esm({
316
316
  }
317
317
  });
318
318
 
319
+ // src/file-walker.ts
320
+ import { readdir } from "fs/promises";
321
+ import { join, relative } from "path";
322
+ async function walkFiles(root, current = root, readDirectory = defaultReadDirectory) {
323
+ let entries;
324
+ try {
325
+ entries = await readDirectory(current, { withFileTypes: true });
326
+ } catch (error) {
327
+ if (current !== root && error.code === "ENOENT") {
328
+ return [];
329
+ }
330
+ throw error;
331
+ }
332
+ const files = [];
333
+ for (const entry of entries) {
334
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === ".artifact-graph") {
335
+ continue;
336
+ }
337
+ const fullPath = join(current, entry.name);
338
+ if (entry.isDirectory()) {
339
+ files.push(...await walkFiles(root, fullPath, readDirectory));
340
+ } else {
341
+ files.push(relative(root, fullPath).split("\\").join("/"));
342
+ }
343
+ }
344
+ return files;
345
+ }
346
+ var defaultReadDirectory;
347
+ var init_file_walker = __esm({
348
+ "src/file-walker.ts"() {
349
+ "use strict";
350
+ defaultReadDirectory = readdir;
351
+ }
352
+ });
353
+
319
354
  // src/target-selector.ts
320
355
  function parseTargetSelector(value) {
321
356
  const separator = value.indexOf(":");
@@ -830,7 +865,7 @@ var init_packet_assembler = __esm({
830
865
 
831
866
  // src/packet-audit.ts
832
867
  import { mkdir, writeFile } from "fs/promises";
833
- import { join } from "path";
868
+ import { join as join2 } from "path";
834
869
  function parseTargetsFile(content, schema) {
835
870
  const validTypes = schema ? new Set(getTargetArtifactTypes(schema)) : VALID_TYPES;
836
871
  const validTypesLabel = [...validTypes].join(", ");
@@ -921,7 +956,7 @@ async function auditSingleTarget(target, graph, options) {
921
956
  const fmt = options.format ?? "markdown";
922
957
  const ext = fmt === "json" ? "json" : "md";
923
958
  const filename = `${target.type}-${target.id}.packet.${ext}`;
924
- const outPath = join(options.outDir, filename);
959
+ const outPath = join2(options.outDir, filename);
925
960
  let content;
926
961
  if (fmt === "json") {
927
962
  content = JSON.stringify(packet, null, 2) + "\n";
@@ -1001,7 +1036,7 @@ async function auditPackets(root, targets, options, graph) {
1001
1036
  ...isCompact ? { summaryDetail: "compact", countsByType } : {}
1002
1037
  };
1003
1038
  if (options.outDir) {
1004
- const summaryPath = join(options.outDir, "summary.json");
1039
+ const summaryPath = join2(options.outDir, "summary.json");
1005
1040
  await writeFile(summaryPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
1006
1041
  }
1007
1042
  return summary;
@@ -1373,7 +1408,7 @@ var init_packet_prompt_validator = __esm({
1373
1408
  import { createHash } from "crypto";
1374
1409
  import { existsSync } from "fs";
1375
1410
  import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
1376
- import { dirname, join as join2, relative } from "path";
1411
+ import { dirname, join as join3, relative as relative2 } from "path";
1377
1412
  async function buildVersionIndex(root, graph) {
1378
1413
  const scannedGraph = graph ?? await scanArtifacts(root);
1379
1414
  const hashCache = /* @__PURE__ */ new Map();
@@ -1509,7 +1544,7 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, confi
1509
1544
  for (const entry of lock.locks) {
1510
1545
  if (entry.kind !== "verifies") continue;
1511
1546
  const sourcePath = entry.source.path;
1512
- const fullSourcePath = join2(root, sourcePath);
1547
+ const fullSourcePath = join3(root, sourcePath);
1513
1548
  if (!existsSync(fullSourcePath)) continue;
1514
1549
  let liveness = livenessCache.get(sourcePath);
1515
1550
  if (liveness === void 0) {
@@ -1521,6 +1556,7 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, confi
1521
1556
  status: "orphan_lock",
1522
1557
  edgeId: entry.edgeId,
1523
1558
  message: `Liveness: ${sourcePath} is not active in any configured runner \u2014 locked verifies edges may reference dead tests`,
1559
+ severity: "warning",
1524
1560
  artifact: entry.artifact,
1525
1561
  source: entry.source
1526
1562
  });
@@ -1603,14 +1639,14 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, confi
1603
1639
  currentArtifactHash: targetNode.contentHash
1604
1640
  });
1605
1641
  }
1606
- if (!existsSync(join2(root, sourceNode.path))) {
1642
+ if (!existsSync(join3(root, sourceNode.path))) {
1607
1643
  entryIssues.push({
1608
1644
  status: "orphan_lock",
1609
1645
  edgeId: relEdgeId,
1610
1646
  message: `Artifact relation source file ${sourceNode.path} no longer exists`
1611
1647
  });
1612
1648
  }
1613
- if (!existsSync(join2(root, targetNode.path))) {
1649
+ if (!existsSync(join3(root, targetNode.path))) {
1614
1650
  entryIssues.push({
1615
1651
  status: "orphan_lock",
1616
1652
  edgeId: relEdgeId,
@@ -1650,7 +1686,84 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, confi
1650
1686
  fresh,
1651
1687
  totalArtifactRelationLocks: artifactRelationLocks.length,
1652
1688
  artifactRelationFresh,
1653
- issues: sortBy(issues, (issue2) => `${issue2.status} ${issue2.edgeId} ${issue2.verifiedByPath ?? ""}`)
1689
+ issues: sortBy(issues.map(enrichVersionLockIssue), (issue2) => `${issue2.status} ${issue2.edgeId} ${issue2.verifiedByPath ?? ""}`)
1690
+ };
1691
+ }
1692
+ function isLivenessIssue(issue2) {
1693
+ return issue2.status === "orphan_lock" && issue2.message.startsWith(LIVENESS_MESSAGE_PREFIX);
1694
+ }
1695
+ function versionLockIssueSeverity(issue2) {
1696
+ if (issue2.severity) {
1697
+ return issue2.severity;
1698
+ }
1699
+ if (issue2.status === "missing_lock") {
1700
+ return "warning";
1701
+ }
1702
+ if (issue2.status === "orphan_lock" && issue2.message.startsWith(LIVENESS_MESSAGE_PREFIX)) {
1703
+ return "warning";
1704
+ }
1705
+ return "error";
1706
+ }
1707
+ function isVersionLockIssueBlocking(issue2, strictMissingLock) {
1708
+ if (issue2.status === "missing_lock") {
1709
+ return strictMissingLock;
1710
+ }
1711
+ return versionLockIssueSeverity(issue2) === "error";
1712
+ }
1713
+ function versionLockIssueRemediation(issue2) {
1714
+ switch (issue2.status) {
1715
+ case "artifact_changed":
1716
+ return [
1717
+ "\u786E\u8BA4\u8FD9\u6B21\u5236\u54C1\u53D8\u66F4\uFF08\u5185\u5BB9\u6216\u8DEF\u5F84\uFF09\u662F\u6709\u610F\u7684\u3002",
1718
+ "\u8FD0\u884C `artifact-graph version-lock refresh --changed-only --worktree --format markdown`\uFF08\u65E5\u5E38\u5F00\u53D1\uFF09\u6216 `artifact-graph version-lock refresh --changed-only --staged --format markdown`\uFF08\u63D0\u4EA4\u524D\uFF09\u5237\u65B0\u53D7\u5F71\u54CD\u7684\u9501\u3002",
1719
+ "\u8FD0\u884C `git diff artifacts/traceability-version-lock.json` \u5BA1\u67E5\u9501\u53D8\u66F4\uFF0C\u786E\u8BA4\u540E `git add artifacts/traceability-version-lock.json` \u5E76\u91CD\u65B0\u63D0\u4EA4\u3002"
1720
+ ];
1721
+ case "source_changed":
1722
+ return [
1723
+ "\u786E\u8BA4\u8FD9\u6B21\u6E90\u7801/\u6D4B\u8BD5\u53D8\u66F4\uFF08\u5185\u5BB9\u6216\u8DEF\u5F84\uFF09\u662F\u6709\u610F\u7684\u3002",
1724
+ "\u8FD0\u884C `artifact-graph version-lock refresh --changed-only --worktree --format markdown`\uFF08\u65E5\u5E38\u5F00\u53D1\uFF09\u6216 `artifact-graph version-lock refresh --changed-only --staged --format markdown`\uFF08\u63D0\u4EA4\u524D\uFF09\u5237\u65B0\u53D7\u5F71\u54CD\u7684\u9501\u3002",
1725
+ "\u8FD0\u884C `git diff artifacts/traceability-version-lock.json` \u5BA1\u67E5\u9501\u53D8\u66F4\uFF0C\u786E\u8BA4\u540E `git add artifacts/traceability-version-lock.json` \u5E76\u91CD\u65B0\u63D0\u4EA4\u3002"
1726
+ ];
1727
+ case "verified_by_changed":
1728
+ return [
1729
+ "\u786E\u8BA4\u8FD9\u6B21\u9A8C\u8BC1\u6587\u4EF6\u53D8\u66F4\u662F\u6709\u610F\u7684\u3002",
1730
+ "\u8FD0\u884C `artifact-graph version-lock refresh --changed-only --worktree --format markdown`\uFF08\u65E5\u5E38\u5F00\u53D1\uFF09\u6216 `artifact-graph version-lock refresh --changed-only --staged --format markdown`\uFF08\u63D0\u4EA4\u524D\uFF09\u5237\u65B0\u53D7\u5F71\u54CD\u7684\u9501\u3002",
1731
+ "\u8FD0\u884C `git diff artifacts/traceability-version-lock.json` \u5BA1\u67E5\u9501\u53D8\u66F4\uFF0C\u786E\u8BA4\u540E `git add artifacts/traceability-version-lock.json` \u5E76\u91CD\u65B0\u63D0\u4EA4\u3002"
1732
+ ];
1733
+ case "missing_lock":
1734
+ return [
1735
+ "\u65B0\u589E\u8FFD\u6EAF\u8FB9\u8FD8\u6CA1\u6709\u5BF9\u5E94\u7248\u672C\u9501\uFF1B\u9ED8\u8BA4\u4E0D\u963B\u65AD\uFF0C`--strict-missing-lock` \u4E0B\u4F1A\u963B\u65AD\u3002",
1736
+ "\u8FD0\u884C `artifact-graph version-lock refresh --changed-only --worktree --format markdown` \u4E3A\u53D7\u5F71\u54CD\u8FB9\u8865\u9501\uFF1B\u9996\u6B21\u5EFA\u7ACB\u57FA\u7EBF\u6216\u914D\u7F6E\u53D8\u66F4\u65F6\u7528 `artifact-graph version-lock refresh --all --format markdown`\u3002",
1737
+ "\u8FD0\u884C `git diff artifacts/traceability-version-lock.json` \u5BA1\u67E5\u540E `git add artifacts/traceability-version-lock.json` \u6682\u5B58\u3002"
1738
+ ];
1739
+ case "orphan_lock":
1740
+ if (isLivenessIssue(issue2)) {
1741
+ return [
1742
+ "\u8FD9\u662F liveness \u8B66\u544A\uFF0C\u4E0D\u662F\u6821\u9A8C\u5931\u8D25\uFF1A\u9501\u4ECD\u7136\u4FDD\u7559\uFF0C\u9ED8\u8BA4\u4E0D\u963B\u65AD\u3002",
1743
+ "\u5982\u679C\u6D4B\u8BD5\u5DF2\u5E9F\u5F03\uFF1A\u5220\u9664\u8BE5\u6587\u4EF6\u6216\u79FB\u9664\u5176\u4E2D\u7684\u8FFD\u6EAF\u6CE8\u91CA\uFF0C\u7136\u540E\u8FD0\u884C `artifact-graph version-lock refresh --all --remove-orphans --format markdown` \u6E05\u7406\u5BF9\u5E94\u7684\u9501\u3002",
1744
+ "\u5982\u679C\u6D4B\u8BD5\u4ECD\u7136\u6709\u6548\uFF1A\u628A\u5B83\u52A0\u5165 artifact-graph.config.yaml \u4E2D\u67D0\u4E2A e2e runner \u7684 include\uFF0C\u6216\u68C0\u67E5 exclude/testIgnore \u662F\u5426\u8BEF\u4F24\u3002"
1745
+ ];
1746
+ }
1747
+ return [
1748
+ "\u9501\u5F15\u7528\u7684\u5236\u54C1\u3001\u6E90\u7801\u6216\u8FFD\u6EAF\u8FB9\u5728\u5F53\u524D\u56FE\u4E2D\u5DF2\u4E0D\u5B58\u5728\uFF0C\u5E38\u89C1\u4E8E\u5236\u54C1\u5220\u9664\u6216\u62C6\u5206\u4E4B\u540E\u3002",
1749
+ "\u786E\u8BA4\u5220\u9664\u662F\u6709\u610F\u7684\u540E\uFF0C\u8FD0\u884C `artifact-graph version-lock refresh --all --remove-orphans --format markdown` \u6E05\u7406\u5B64\u7ACB\u9501\u3002",
1750
+ "\u8FD0\u884C `git diff artifacts/traceability-version-lock.json` \u5BA1\u67E5\u88AB\u79FB\u9664\u7684\u9501\u6761\u76EE\uFF0C\u786E\u8BA4\u540E `git add artifacts/traceability-version-lock.json` \u6682\u5B58\u3002",
1751
+ "\u5982\u679C\u5220\u9664\u662F\u8BEF\u64CD\u4F5C\uFF0C\u5148\u6062\u590D\u5BF9\u5E94\u5236\u54C1\u6216\u6E90\u7801\u6587\u4EF6\uFF0C\u518D\u8FD0\u884C\u4E0D\u5E26 `--remove-orphans` \u7684 refresh \u5237\u65B0\u54C8\u5E0C\u3002"
1752
+ ];
1753
+ case "target_not_found":
1754
+ return [
1755
+ "\u68C0\u67E5 `--target` \u7684 `type:id` \u662F\u5426\u62FC\u5199\u6B63\u786E\u3002",
1756
+ "\u5982\u679C\u8BE5\u5236\u54C1\u5DF2\u88AB\u5220\u9664\u6216\u6539\u540D\uFF0C\u6539\u7528\u5F53\u524D\u5B58\u5728\u7684\u5236\u54C1\u6807\u8BC6\u91CD\u8BD5\u3002"
1757
+ ];
1758
+ default:
1759
+ return [];
1760
+ }
1761
+ }
1762
+ function enrichVersionLockIssue(issue2) {
1763
+ return {
1764
+ ...issue2,
1765
+ severity: versionLockIssueSeverity(issue2),
1766
+ remediation: issue2.remediation ?? versionLockIssueRemediation(issue2)
1654
1767
  };
1655
1768
  }
1656
1769
  async function updateVersionLock(root, options) {
@@ -1948,59 +2061,118 @@ async function traceVersion(root, target, lockPath = VERSION_LOCK_PATH) {
1948
2061
  currentEdges: index.edges.filter((edge2) => edge2.from === targetUid || edge2.to === targetUid),
1949
2062
  locks: lock.locks.filter((entry) => `${entry.artifact.type}:${entry.artifact.id}` === targetUid),
1950
2063
  artifactRelations: targetArtifactRelations,
1951
- issues: [...targetIssues, ...audit.issues.filter((issue2) => `${issue2.artifact?.type}:${issue2.artifact?.id}` === targetUid || issue2.edgeId.includes(`#${targetUid}`))]
2064
+ issues: [...targetIssues.map(enrichVersionLockIssue), ...audit.issues.filter((issue2) => `${issue2.artifact?.type}:${issue2.artifact?.id}` === targetUid || issue2.edgeId.includes(`#${targetUid}`))]
1952
2065
  };
1953
2066
  }
1954
- function renderVersionLockAuditMarkdown(result) {
2067
+ function renderIssueBlockingTag(issue2, strictMissingLock) {
2068
+ return isVersionLockIssueBlocking(issue2, strictMissingLock) ? "\u963B\u65AD" : "\u8B66\u544A";
2069
+ }
2070
+ function renderIssueBlockingExplanation(issue2, strictMissingLock) {
2071
+ if (!isVersionLockIssueBlocking(issue2, strictMissingLock)) {
2072
+ return "\u5426\uFF08\u4EC5\u63D0\u9192\uFF0C\u4E0D\u963B\u65AD\uFF09";
2073
+ }
2074
+ if (issue2.status === "missing_lock" && versionLockIssueSeverity(issue2) !== "error") {
2075
+ return "\u662F\uFF08\u5DF2\u7531 --strict-missing-lock \u5347\u7EA7\u4E3A\u963B\u65AD\uFF09";
2076
+ }
2077
+ return "\u662F\uFF08\u4F1A\u4F7F\u547D\u4EE4\u4EE5\u975E\u96F6\u9000\u51FA\u7801\u7ED3\u675F\uFF09";
2078
+ }
2079
+ function appendIssueDetails(lines, issues, strictMissingLock) {
2080
+ issues.forEach((issue2, index) => {
2081
+ const label = VERSION_LOCK_STATUS_LABELS[issue2.status] ?? issue2.status;
2082
+ lines.push(`### ${index + 1}. [${renderIssueBlockingTag(issue2, strictMissingLock)}] ${label}\uFF08\`${issue2.status}\`\uFF09`);
2083
+ lines.push("");
2084
+ lines.push(`- \u8FB9: \`${issue2.edgeId}\``);
2085
+ lines.push(`- \u8BE6\u60C5: ${issue2.message}`);
2086
+ lines.push(`- \u662F\u5426\u963B\u65AD: ${renderIssueBlockingExplanation(issue2, strictMissingLock)}`);
2087
+ const remediation = issue2.remediation ?? [];
2088
+ if (remediation.length > 0) {
2089
+ lines.push("- \u89E3\u51B3\u6B65\u9AA4:");
2090
+ remediation.forEach((step, stepIndex) => {
2091
+ lines.push(` ${stepIndex + 1}. ${step}`);
2092
+ });
2093
+ }
2094
+ lines.push("");
2095
+ });
2096
+ }
2097
+ function renderVersionLockAuditMarkdown(result, options = {}) {
2098
+ const strictMissingLock = options.strictMissingLock === true;
2099
+ const blockingCount = result.issues.filter((issue2) => isVersionLockIssueBlocking(issue2, strictMissingLock)).length;
2100
+ const nonBlockingCount = result.issues.length - blockingCount;
1955
2101
  const lines = [
1956
- "# Version Lock Audit",
2102
+ "# \u7248\u672C\u9501\u5BA1\u8BA1\uFF08version-lock audit\uFF09",
1957
2103
  "",
1958
- `Root: \`${result.root}\``,
1959
- `Lock: \`${result.lockPath}\``,
1960
- `Locks: ${result.totalLocks} | Fresh: ${result.fresh} | Issues: ${result.issues.length}`,
1961
- `Artifact Relations: ${result.totalArtifactRelationLocks} | Fresh: ${result.artifactRelationFresh}`,
2104
+ `- \u6839\u76EE\u5F55: \`${result.root}\``,
2105
+ `- \u9501\u6587\u4EF6: \`${result.lockPath}\``,
2106
+ `- \u5B9E\u73B0/\u9A8C\u8BC1\u9501: ${result.totalLocks}\uFF08\u65B0\u9C9C ${result.fresh}\uFF09`,
2107
+ `- \u5236\u54C1\u5173\u7CFB\u9501: ${result.totalArtifactRelationLocks}\uFF08\u65B0\u9C9C ${result.artifactRelationFresh}\uFF09`,
2108
+ `- \u963B\u65AD\u7B56\u7565: ${strictMissingLock ? "--strict-missing-lock\uFF08missing_lock \u5347\u7EA7\u4E3A\u963B\u65AD\uFF09" : "\u9ED8\u8BA4\uFF08missing_lock \u4E0D\u963B\u65AD\uFF09"}`,
2109
+ `- \u95EE\u9898: ${result.issues.length}\uFF08\u963B\u65AD ${blockingCount}\uFF0C\u4E0D\u963B\u65AD ${nonBlockingCount}\uFF09`,
1962
2110
  ""
1963
2111
  ];
1964
2112
  if (result.issues.length === 0) {
1965
- lines.push("No version lock issues.");
2113
+ lines.push("\u672A\u53D1\u73B0\u7248\u672C\u9501\u95EE\u9898\u3002");
1966
2114
  return `${lines.join("\n")}
1967
2115
  `;
1968
2116
  }
1969
- for (const issue2 of result.issues) {
1970
- lines.push(`- [${issue2.status}] \`${issue2.edgeId}\` \u2014 ${issue2.message}`);
2117
+ if (blockingCount > 0) {
2118
+ lines.push(`\u5F53\u524D\u7B56\u7565\u4E0B\u5B58\u5728 ${blockingCount} \u4E2A\u963B\u65AD\u95EE\u9898\uFF0C\u547D\u4EE4\u5C06\u4EE5\u975E\u96F6\u9000\u51FA\u7801\u7ED3\u675F\uFF1B\u8BF7\u6309\u4E0B\u65B9\u6B65\u9AA4\u9010\u9879\u5904\u7406\u3002`);
2119
+ } else {
2120
+ lines.push("\u5F53\u524D\u7B56\u7565\u4E0B\u6CA1\u6709\u963B\u65AD\u95EE\u9898\uFF0C\u4EE5\u4E0B\u4EC5\u4E3A\u63D0\u9192\uFF0C\u547D\u4EE4\u4EE5\u96F6\u9000\u51FA\u7801\u7ED3\u675F\u3002");
1971
2121
  }
2122
+ lines.push("");
2123
+ lines.push("## \u95EE\u9898\u6E05\u5355");
2124
+ lines.push("");
2125
+ appendIssueDetails(lines, result.issues, strictMissingLock);
1972
2126
  return `${lines.join("\n")}
1973
2127
  `;
1974
2128
  }
1975
2129
  function renderVersionLockRefreshMarkdown(result) {
2130
+ const strictMissingLock = true;
2131
+ const blockingCount = result.postAudit.issues.filter((issue2) => isVersionLockIssueBlocking(issue2, strictMissingLock)).length;
2132
+ const nonBlockingCount = result.postAudit.issues.length - blockingCount;
1976
2133
  const lines = [
1977
- "# Version Lock Refresh",
2134
+ "# \u7248\u672C\u9501\u5237\u65B0\uFF08version-lock refresh\uFF09",
2135
+ "",
2136
+ `- \u6839\u76EE\u5F55: \`${result.root}\``,
2137
+ `- \u9501\u6587\u4EF6: \`${result.lockPath}\``,
2138
+ `- \u6A21\u5F0F: \`${result.mode}\`\uFF08\u53D8\u66F4\u8DEF\u5F84 ${result.changedPaths.length} \u4E2A\uFF0C\u53D7\u5F71\u54CD\u8FB9 ${result.affectedEdges.length} \u6761\uFF09`,
2139
+ `- \u5B9E\u73B0/\u9A8C\u8BC1\u9501: \u65B0\u589E ${result.addedLocks.length} | \u66F4\u65B0 ${result.updatedLocks.length} | \u4FDD\u7559\u5B64\u7ACB ${result.retainedOrphans.length} | \u5220\u9664\u5B64\u7ACB ${result.removedOrphans.length}`,
2140
+ `- \u5236\u54C1\u5173\u7CFB\u9501: \u65B0\u589E ${result.addedArtifactRelationLocks.length} | \u66F4\u65B0 ${result.updatedArtifactRelationLocks.length} | \u4FDD\u7559\u5B64\u7ACB ${result.retainedArtifactRelationOrphans.length} | \u5220\u9664 ${result.removedArtifactRelationLocks.length}`,
2141
+ `- \u963B\u65AD\u7B56\u7565: refresh \u56FA\u5B9A\u6309 --strict-missing-lock \u5224\u5B9A\uFF0Cmissing_lock \u4E5F\u4F1A\u963B\u65AD`,
2142
+ `- \u5237\u65B0\u540E\u5BA1\u8BA1\u95EE\u9898: ${result.postAudit.issues.length}\uFF08\u963B\u65AD ${blockingCount}\uFF0C\u4E0D\u963B\u65AD ${nonBlockingCount}\uFF09`,
1978
2143
  "",
1979
- `Root: \`${result.root}\``,
1980
- `Lock: \`${result.lockPath}\``,
1981
- `Mode: \`${result.mode}\``,
1982
- `Changed paths: ${result.changedPaths.length}`,
1983
- `Affected edges: ${result.affectedEdges.length}`,
1984
- `Added: ${result.addedLocks.length} | Updated: ${result.updatedLocks.length} | Retained orphans: ${result.retainedOrphans.length} | Removed orphans: ${result.removedOrphans.length}`,
1985
- `Artifact Relations \u2014 Added: ${result.addedArtifactRelationLocks.length} | Updated: ${result.updatedArtifactRelationLocks.length} | Retained orphans: ${result.retainedArtifactRelationOrphans.length} | Removed: ${result.removedArtifactRelationLocks.length}`,
1986
- `Post-audit issues: ${result.postAudit.issues.length}`,
2144
+ "\u9501\u6587\u4EF6\u5DF2\u5199\u5165\u3002\u82E5\u9501\u6587\u4EF6\u76F8\u5BF9\u6682\u5B58\u533A\u6709\u53D8\u5316\uFF08\u4F8B\u5982 pre-commit \u573A\u666F\uFF09\uFF0C\u8BF7\u5148\u5BA1\u67E5\u5E76\u6682\u5B58\uFF1A",
2145
+ "",
2146
+ "1. `git diff artifacts/traceability-version-lock.json`",
2147
+ "2. `git add artifacts/traceability-version-lock.json`",
2148
+ "3. \u91CD\u65B0\u6267\u884C `git commit`",
1987
2149
  ""
1988
2150
  ];
1989
- appendList(lines, "Added Locks", result.addedLocks);
1990
- appendList(lines, "Updated Locks", result.updatedLocks);
1991
- appendList(lines, "Retained Orphans", result.retainedOrphans);
1992
- appendList(lines, "Removed Orphans", result.removedOrphans);
1993
- appendList(lines, "Added Artifact Relation Locks", result.addedArtifactRelationLocks);
1994
- appendList(lines, "Updated Artifact Relation Locks", result.updatedArtifactRelationLocks);
1995
- appendList(lines, "Retained Artifact Relation Orphans", result.retainedArtifactRelationOrphans);
1996
- appendList(lines, "Removed Artifact Relation Locks", result.removedArtifactRelationLocks);
1997
- appendList(lines, "Warnings", result.warnings);
2151
+ appendList(lines, "\u65B0\u589E\u7684\u9501", result.addedLocks);
2152
+ appendList(lines, "\u66F4\u65B0\u7684\u9501", result.updatedLocks);
2153
+ appendList(lines, "\u4FDD\u7559\u7684\u5B64\u7ACB\u9501", result.retainedOrphans);
2154
+ if (result.retainedOrphans.length > 0) {
2155
+ lines.push("> \u4EE5\u4E0A\u662F\u5DF2\u5220\u9664/\u62C6\u5206\u5236\u54C1\u6216\u5931\u6548\u8FFD\u6EAF\u8FB9\u9057\u7559\u7684\u9501\u3002\u786E\u8BA4\u4E0D\u518D\u9700\u8981\u540E\u8FD0\u884C `artifact-graph version-lock refresh --all --remove-orphans --format markdown` \u6E05\u7406\uFF0C\u5BA1\u67E5 `git diff artifacts/traceability-version-lock.json` \u540E\u518D\u6682\u5B58\u3002");
2156
+ lines.push("");
2157
+ }
2158
+ appendList(lines, "\u5220\u9664\u7684\u5B64\u7ACB\u9501", result.removedOrphans);
2159
+ appendList(lines, "\u65B0\u589E\u7684\u5236\u54C1\u5173\u7CFB\u9501", result.addedArtifactRelationLocks);
2160
+ appendList(lines, "\u66F4\u65B0\u7684\u5236\u54C1\u5173\u7CFB\u9501", result.updatedArtifactRelationLocks);
2161
+ appendList(lines, "\u4FDD\u7559\u7684\u5B64\u7ACB\u5236\u54C1\u5173\u7CFB\u9501", result.retainedArtifactRelationOrphans);
2162
+ if (result.retainedArtifactRelationOrphans.length > 0) {
2163
+ lines.push("> \u4EE5\u4E0A\u662F\u5931\u6548\u5236\u54C1\u5173\u7CFB\u9057\u7559\u7684\u9501\u3002\u786E\u8BA4\u540E\u8FD0\u884C `artifact-graph version-lock refresh --all --remove-orphans --format markdown` \u6E05\u7406\uFF0C\u5BA1\u67E5\u9501 diff \u540E\u518D\u6682\u5B58\u3002");
2164
+ lines.push("");
2165
+ }
2166
+ appendList(lines, "\u5220\u9664\u7684\u5236\u54C1\u5173\u7CFB\u9501", result.removedArtifactRelationLocks);
2167
+ appendList(lines, "\u63D0\u9192", result.warnings);
1998
2168
  if (result.postAudit.issues.length > 0) {
1999
- lines.push("## Post-Audit Issues");
2000
- for (const issue2 of result.postAudit.issues) {
2001
- lines.push(`- [${issue2.status}] \`${issue2.edgeId}\` \u2014 ${issue2.message}`);
2169
+ if (blockingCount > 0) {
2170
+ lines.push(`## \u5237\u65B0\u540E\u5BA1\u8BA1\u95EE\u9898\uFF08\u963B\u65AD ${blockingCount}\uFF0C\u4E0D\u963B\u65AD ${nonBlockingCount}\uFF09`);
2171
+ } else {
2172
+ lines.push(`## \u5237\u65B0\u540E\u5BA1\u8BA1\u95EE\u9898\uFF08\u5F53\u524D\u7B56\u7565\u4E0B\u5168\u90E8\u4E0D\u963B\u65AD\uFF09`);
2002
2173
  }
2003
2174
  lines.push("");
2175
+ appendIssueDetails(lines, result.postAudit.issues, strictMissingLock);
2004
2176
  }
2005
2177
  return `${lines.join("\n")}
2006
2178
  `;
@@ -2047,7 +2219,7 @@ function renderTraceVersionMarkdown(result) {
2047
2219
  async function readVersionLock(root, lockPath) {
2048
2220
  const safeLockPath = normalizeRelativePath(root, lockPath);
2049
2221
  try {
2050
- const raw = await readFile(join2(root, safeLockPath), "utf-8");
2222
+ const raw = await readFile(join3(root, safeLockPath), "utf-8");
2051
2223
  let parsed;
2052
2224
  try {
2053
2225
  parsed = JSON.parse(raw);
@@ -2206,7 +2378,7 @@ function requireSafeRelativePath(value, path) {
2206
2378
  }
2207
2379
  async function writeVersionLock(root, lockPath, lock) {
2208
2380
  const safeLockPath = normalizeRelativePath(root, lockPath);
2209
- const fullPath = join2(root, safeLockPath);
2381
+ const fullPath = join3(root, safeLockPath);
2210
2382
  await mkdir2(dirname(fullPath), { recursive: true });
2211
2383
  await writeFile2(fullPath, `${JSON.stringify(lock, null, 2)}
2212
2384
  `);
@@ -2389,14 +2561,14 @@ async function hashRelativePath(root, path, cache) {
2389
2561
  const normalized = normalizeRelativePath(root, path);
2390
2562
  const cached = cache.get(normalized);
2391
2563
  if (cached) return cached;
2392
- const content = await readFile(join2(root, normalized));
2564
+ const content = await readFile(join3(root, normalized));
2393
2565
  const hash = `sha256:${createHash("sha256").update(content).digest("hex")}`;
2394
2566
  cache.set(normalized, hash);
2395
2567
  return hash;
2396
2568
  }
2397
2569
  function normalizeRelativePath(root, path) {
2398
2570
  const normalized = path.replace(/\\/g, "/");
2399
- const relativePath = normalized.startsWith("/") ? relative(root, normalized).replace(/\\/g, "/") : normalized.replace(/^\.\//, "");
2571
+ const relativePath = normalized.startsWith("/") ? relative2(root, normalized).replace(/\\/g, "/") : normalized.replace(/^\.\//, "");
2400
2572
  if (relativePath === ".." || relativePath.startsWith("../")) {
2401
2573
  throw new Error(`Path is outside root: ${path}`);
2402
2574
  }
@@ -2412,7 +2584,7 @@ async function getTestFileRunnerLiveness(root, filePath, config) {
2412
2584
  if (!/e2e/i.test(filePath) && !/\.e2e\./i.test(filePath)) {
2413
2585
  return "active";
2414
2586
  }
2415
- const fullSourcePath = join2(root, filePath);
2587
+ const fullSourcePath = join3(root, filePath);
2416
2588
  if (!existsSync(fullSourcePath)) return "inactive";
2417
2589
  try {
2418
2590
  const content = await readFile(fullSourcePath, "utf-8");
@@ -2455,7 +2627,7 @@ async function isFileActiveInRunner(root, filePath, runner) {
2455
2627
  function sortUnique(items) {
2456
2628
  return [...new Set(items)].sort((left, right) => left.localeCompare(right));
2457
2629
  }
2458
- var VERSION_LOCK_PATH, VERSION_INDEX_SCHEMA_VERSION, VERSION_LOCK_SCHEMA_VERSION;
2630
+ var VERSION_LOCK_PATH, VERSION_INDEX_SCHEMA_VERSION, VERSION_LOCK_SCHEMA_VERSION, LIVENESS_MESSAGE_PREFIX, VERSION_LOCK_STATUS_LABELS;
2459
2631
  var init_versioned_traceability = __esm({
2460
2632
  "src/versioned-traceability.ts"() {
2461
2633
  "use strict";
@@ -2464,6 +2636,16 @@ var init_versioned_traceability = __esm({
2464
2636
  VERSION_LOCK_PATH = "artifacts/traceability-version-lock.json";
2465
2637
  VERSION_INDEX_SCHEMA_VERSION = "1.0";
2466
2638
  VERSION_LOCK_SCHEMA_VERSION = "1.0";
2639
+ LIVENESS_MESSAGE_PREFIX = "Liveness:";
2640
+ VERSION_LOCK_STATUS_LABELS = {
2641
+ fresh: "\u65B0\u9C9C",
2642
+ target_not_found: "\u76EE\u6807\u5236\u54C1\u4E0D\u5B58\u5728",
2643
+ artifact_changed: "\u5236\u54C1\u5DF2\u53D8\u5316",
2644
+ source_changed: "\u6E90\u7801/\u6D4B\u8BD5\u5DF2\u53D8\u5316",
2645
+ verified_by_changed: "\u9A8C\u8BC1\u6587\u4EF6\u5DF2\u53D8\u5316",
2646
+ missing_lock: "\u7F3A\u5C11\u7248\u672C\u9501",
2647
+ orphan_lock: "\u5B64\u7ACB\u9501"
2648
+ };
2467
2649
  }
2468
2650
  });
2469
2651
 
@@ -2471,7 +2653,7 @@ var init_versioned_traceability = __esm({
2471
2653
  import { execFile } from "child_process";
2472
2654
  import { constants } from "fs";
2473
2655
  import { access } from "fs/promises";
2474
- import { isAbsolute, join as join3, resolve } from "path";
2656
+ import { isAbsolute, join as join4, resolve } from "path";
2475
2657
  import { promisify } from "util";
2476
2658
  async function resolveArtifactGraphCli(root, options = {}) {
2477
2659
  const pathCli = await findCommandOnPath("artifact-graph");
@@ -2479,7 +2661,7 @@ async function resolveArtifactGraphCli(root, options = {}) {
2479
2661
  const candidates = [
2480
2662
  {
2481
2663
  source: "node_modules",
2482
- path: join3(root, "node_modules/.bin/artifact-graph"),
2664
+ path: join4(root, "node_modules/.bin/artifact-graph"),
2483
2665
  exists: false
2484
2666
  },
2485
2667
  {
@@ -2512,8 +2694,8 @@ async function resolveArtifactGraphCli(root, options = {}) {
2512
2694
  }
2513
2695
  async function doctorArtifactChain(root, options = {}) {
2514
2696
  const cli = await resolveArtifactGraphCli(root, options);
2515
- const configPath = join3(root, "artifact-graph.config.yaml");
2516
- const lockPath = join3(root, VERSION_LOCK_PATH);
2697
+ const configPath = join4(root, "artifact-graph.config.yaml");
2698
+ const lockPath = join4(root, VERSION_LOCK_PATH);
2517
2699
  const supportedCommands = cli.path ? await detectSupportedCommands(cli.path) : [];
2518
2700
  const nodeCompatible = isNodeCompatible(process.versions.node);
2519
2701
  const warnings = [
@@ -2738,7 +2920,7 @@ var init_git_hook_path = __esm({
2738
2920
  import { constants as constants2 } from "fs";
2739
2921
  import { randomUUID } from "crypto";
2740
2922
  import { lstat, mkdir as mkdir3, open, readlink, rename, unlink } from "fs/promises";
2741
- import { basename, dirname as dirname2, join as join4 } from "path";
2923
+ import { basename, dirname as dirname2, join as join5 } from "path";
2742
2924
  function detectHookInterpreter(content) {
2743
2925
  if (content.trim().length === 0) {
2744
2926
  return "empty";
@@ -3046,7 +3228,7 @@ async function removeHookAtomically(hookPath, snapshot) {
3046
3228
  }
3047
3229
  async function writeHookAtomically(hookPath, content, snapshot, mode) {
3048
3230
  await mkdir3(dirname2(hookPath), { recursive: true });
3049
- const temporaryPath = join4(dirname2(hookPath), `.${basename(hookPath)}.${randomUUID()}.tmp`);
3231
+ const temporaryPath = join5(dirname2(hookPath), `.${basename(hookPath)}.${randomUUID()}.tmp`);
3050
3232
  let temporaryExists = false;
3051
3233
  try {
3052
3234
  const temporary = await open(temporaryPath, "wx", mode);
@@ -3511,8 +3693,8 @@ __export(contract_kernel_exports, {
3511
3693
  verifyDigest: () => verifyDigest
3512
3694
  });
3513
3695
  import { createHash as createHash2 } from "crypto";
3514
- import { readFile as readFile2, readdir } from "fs/promises";
3515
- import { join as join5 } from "path";
3696
+ import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
3697
+ import { join as join6 } from "path";
3516
3698
  import _AjvModule from "ajv";
3517
3699
  function isOfficialNamespace(namespace) {
3518
3700
  return OFFICIAL_NAMESPACE_PATTERN.test(namespace);
@@ -3922,10 +4104,10 @@ async function loadContract(contractPath, options) {
3922
4104
  }
3923
4105
  async function loadContractsFromDirectory(contractsDir, options) {
3924
4106
  const contracts = [];
3925
- const entries = await readdir(contractsDir, { withFileTypes: true });
4107
+ const entries = await readdir2(contractsDir, { withFileTypes: true });
3926
4108
  for (const entry of entries) {
3927
4109
  if (entry.isDirectory()) {
3928
- const schemaPath = join5(contractsDir, entry.name, "schema.json");
4110
+ const schemaPath = join6(contractsDir, entry.name, "schema.json");
3929
4111
  const contract = await loadContract(schemaPath, options);
3930
4112
  contracts.push(contract);
3931
4113
  }
@@ -4417,8 +4599,8 @@ import Database from "better-sqlite3";
4417
4599
  import matter from "gray-matter";
4418
4600
  import yaml from "js-yaml";
4419
4601
  import { accessSync, constants as fsConstants, existsSync as existsSync2, statSync } from "fs";
4420
- import { mkdir as mkdir4, readFile as readFile3, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
4421
- import { basename as basename3, dirname as dirname3, extname, isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve3 } from "path";
4602
+ import { mkdir as mkdir4, readFile as readFile3, readdir as readdir3, writeFile as writeFile3 } from "fs/promises";
4603
+ import { basename as basename3, dirname as dirname3, extname, isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3 } from "path";
4422
4604
  function isTargetArtifactType(type) {
4423
4605
  return isPacketTargetType(type);
4424
4606
  }
@@ -4460,7 +4642,7 @@ function resolveArtifactTypeName(schema, token) {
4460
4642
  return void 0;
4461
4643
  }
4462
4644
  async function loadConfig(root) {
4463
- const configPath = join6(root, "artifact-graph.config.yaml");
4645
+ const configPath = join7(root, "artifact-graph.config.yaml");
4464
4646
  let parsed = {};
4465
4647
  try {
4466
4648
  const raw = await readFile3(configPath, "utf-8");
@@ -4637,7 +4819,7 @@ async function scanArtifacts(root, schema) {
4637
4819
  continue;
4638
4820
  }
4639
4821
  scannedFiles.set(file, type);
4640
- const raw = await readFile3(join6(root, file), "utf-8");
4822
+ const raw = await readFile3(join7(root, file), "utf-8");
4641
4823
  const parsed = parseFile(type, file, raw, config);
4642
4824
  nodes.push(...parsed.nodes);
4643
4825
  edges.push(...parsed.edges);
@@ -4949,7 +5131,7 @@ async function validateScenarioPrdLinkIndex(root, graph) {
4949
5131
  const indexPath = "artifacts/prd/feature-index.md";
4950
5132
  let raw = "";
4951
5133
  try {
4952
- raw = await readFile3(join6(root, indexPath), "utf-8");
5134
+ raw = await readFile3(join7(root, indexPath), "utf-8");
4953
5135
  } catch (error) {
4954
5136
  if (error.code === "ENOENT") {
4955
5137
  return [];
@@ -5139,11 +5321,11 @@ function nextId(graph, schema, type, rangeName) {
5139
5321
  throw new Error(`ID range ${type}.${rangeName} is exhausted`);
5140
5322
  }
5141
5323
  async function writeGraphCache(root, graph) {
5142
- const cacheDir = join6(root, ".artifact-graph");
5324
+ const cacheDir = join7(root, ".artifact-graph");
5143
5325
  await mkdir4(cacheDir, { recursive: true });
5144
- await writeFile3(join6(cacheDir, "index.json"), `${JSON.stringify(graph, null, 2)}
5326
+ await writeFile3(join7(cacheDir, "index.json"), `${JSON.stringify(graph, null, 2)}
5145
5327
  `);
5146
- const db = new Database(join6(cacheDir, "graph.sqlite"));
5328
+ const db = new Database(join7(cacheDir, "graph.sqlite"));
5147
5329
  try {
5148
5330
  db.exec(`
5149
5331
  DROP TABLE IF EXISTS nodes;
@@ -6826,10 +7008,10 @@ function validateE2eRegistry(graph) {
6826
7008
  async function validateExecutableTraceability(root, config) {
6827
7009
  const issues = [];
6828
7010
  const schema = config ?? await loadConfig(root);
6829
- const e2eDir = join6(root, "artifacts", "tests", "e2e");
7011
+ const e2eDir = join7(root, "artifacts", "tests", "e2e");
6830
7012
  let e2eFiles;
6831
7013
  try {
6832
- e2eFiles = (await readdir2(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join6(e2eDir, name));
7014
+ e2eFiles = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join7(e2eDir, name));
6833
7015
  } catch {
6834
7016
  return [];
6835
7017
  }
@@ -6839,7 +7021,7 @@ async function validateExecutableTraceability(root, config) {
6839
7021
  const mdBatches = /* @__PURE__ */ new Set();
6840
7022
  for (const filePath of e2eFiles) {
6841
7023
  const raw = await readFile3(filePath, "utf-8");
6842
- const relPath = relative2(root, filePath).split("\\").join("/");
7024
+ const relPath = relative3(root, filePath).split("\\").join("/");
6843
7025
  const parsed = matter(raw);
6844
7026
  const data = parsed.data;
6845
7027
  const batch = String(data.test_batch ?? basename3(filePath, extname(filePath))).trim();
@@ -6867,7 +7049,7 @@ async function validateExecutableTraceability(root, config) {
6867
7049
  }
6868
7050
  }
6869
7051
  }
6870
- const allFiles = await walk(root);
7052
+ const allFiles = await walkFiles(root);
6871
7053
  const specFiles = /* @__PURE__ */ new Set();
6872
7054
  const configuredRunners = schema.e2e?.runners ?? [];
6873
7055
  if (configuredRunners.length > 0) {
@@ -6887,7 +7069,7 @@ async function validateExecutableTraceability(root, config) {
6887
7069
  const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
6888
7070
  const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
6889
7071
  for (const specFile of specFiles) {
6890
- const fullSpecPath = join6(root, specFile);
7072
+ const fullSpecPath = join7(root, specFile);
6891
7073
  let content;
6892
7074
  try {
6893
7075
  content = await readFile3(fullSpecPath, "utf-8");
@@ -6896,14 +7078,16 @@ async function validateExecutableTraceability(root, config) {
6896
7078
  }
6897
7079
  const level = detectTestLevel(specFile, content);
6898
7080
  const specLines = content.split(/\r?\n/);
6899
- for (let lineIndex = 0; lineIndex < specLines.length; lineIndex += 1) {
6900
- const line = specLines[lineIndex];
6901
- let match = tcAnnotationRegex.exec(line);
7081
+ const lineComments = scanCodeComments(content).filter((comment) => comment.kind === "line" && comment.standalone);
7082
+ for (const comment of lineComments) {
7083
+ const lineIndex = comment.lineNumber - 1;
7084
+ const commentText = comment.text.replace(/^!/, "");
7085
+ let match = tcAnnotationRegex.exec(`//${commentText}`);
6902
7086
  let annotatedLevel = "";
6903
7087
  if (match) {
6904
7088
  annotatedLevel = match[2];
6905
7089
  } else {
6906
- match = tcAnnotationNoLevelRegex.exec(line);
7090
+ match = tcAnnotationNoLevelRegex.exec(`//${commentText}`);
6907
7091
  }
6908
7092
  if (!match) {
6909
7093
  continue;
@@ -6930,7 +7114,7 @@ async function validateExecutableTraceability(root, config) {
6930
7114
  level: effectiveLevel,
6931
7115
  file: specFile,
6932
7116
  testName,
6933
- line: lineIndex + 1
7117
+ line: comment.lineNumber
6934
7118
  };
6935
7119
  const key = `${batch}:${tcId}`;
6936
7120
  const existing = refToSource.get(key) ?? [];
@@ -6967,7 +7151,7 @@ async function validateExecutableTraceability(root, config) {
6967
7151
  if (entry.testId) {
6968
7152
  let content;
6969
7153
  try {
6970
- content = await readFile3(join6(root, normalizedRefFile), "utf-8");
7154
+ content = await readFile3(join7(root, normalizedRefFile), "utf-8");
6971
7155
  } catch {
6972
7156
  continue;
6973
7157
  }
@@ -7144,11 +7328,11 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
7144
7328
  let withExecutableRef = 0;
7145
7329
  const statusBreakdown = {};
7146
7330
  const chainTypeBreakdown = {};
7147
- const e2eDir = join6(root, "artifacts", "tests", "e2e");
7331
+ const e2eDir = join7(root, "artifacts", "tests", "e2e");
7148
7332
  const tcFieldsMap = /* @__PURE__ */ new Map();
7149
7333
  let e2eFiles;
7150
7334
  try {
7151
- e2eFiles = (await readdir2(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join6(e2eDir, name));
7335
+ e2eFiles = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join7(e2eDir, name));
7152
7336
  } catch {
7153
7337
  e2eFiles = [];
7154
7338
  }
@@ -7215,7 +7399,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
7215
7399
  }
7216
7400
  }
7217
7401
  const runners = (await loadConfig(root)).e2e?.runners ?? [];
7218
- const allProjectFiles = await walk(root);
7402
+ const allProjectFiles = await walkFiles(root);
7219
7403
  for (const node of e2eNodes) {
7220
7404
  const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
7221
7405
  const status = String(fields["status"] ?? "").trim().toLowerCase();
@@ -7226,7 +7410,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
7226
7410
  let hasActiveE2eRef = false;
7227
7411
  for (const entry of parseExecutableRefLines(execRef)) {
7228
7412
  const normalized = resolveExecutableRefFile(entry.file, allProjectFiles);
7229
- if (!normalized || !existsSync2(join6(root, normalized))) continue;
7413
+ if (!normalized || !existsSync2(join7(root, normalized))) continue;
7230
7414
  const accepting = await getAcceptingRunners(root, normalized, runners);
7231
7415
  if (accepting.some((runner) => runner.kind === "e2e")) {
7232
7416
  hasActiveE2eRef = true;
@@ -7285,7 +7469,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
7285
7469
  const acCoverageRateByFeature = {};
7286
7470
  const featureAcMap = /* @__PURE__ */ new Map();
7287
7471
  for (const node of featureNodes) {
7288
- const acs = parseAcceptanceCriteria(await readFile3(join6(root, node.path), "utf-8"));
7472
+ const acs = parseAcceptanceCriteria(await readFile3(join7(root, node.path), "utf-8"));
7289
7473
  featureAcMap.set(node.code, new Set(acs));
7290
7474
  }
7291
7475
  const coveredAcByFeature = /* @__PURE__ */ new Map();
@@ -7327,10 +7511,10 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
7327
7511
  };
7328
7512
  }
7329
7513
  async function generateE2eRegistry(root, opts) {
7330
- const e2eDir = join6(root, "artifacts", "tests", "e2e");
7514
+ const e2eDir = join7(root, "artifacts", "tests", "e2e");
7331
7515
  let files;
7332
7516
  try {
7333
- files = (await readdir2(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).sort();
7517
+ files = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).sort();
7334
7518
  } catch {
7335
7519
  return {
7336
7520
  registry_version: "1.0",
@@ -7343,7 +7527,7 @@ async function generateE2eRegistry(root, opts) {
7343
7527
  const batches = [];
7344
7528
  let totalTestCases = 0;
7345
7529
  for (const file of files) {
7346
- const filePath = join6(e2eDir, file);
7530
+ const filePath = join7(e2eDir, file);
7347
7531
  const raw = await readFile3(filePath, "utf-8");
7348
7532
  const parsed = matter(raw);
7349
7533
  const data = parsed.data;
@@ -7471,7 +7655,7 @@ async function validatePartialRustEvidence(tcFields, tcKey, root, allFiles) {
7471
7655
  if (!normalizedPath) {
7472
7656
  return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
7473
7657
  }
7474
- const fullPath = join6(root, normalizedPath);
7658
+ const fullPath = join7(root, normalizedPath);
7475
7659
  let content;
7476
7660
  try {
7477
7661
  content = await readFile3(fullPath, "utf-8");
@@ -7583,7 +7767,7 @@ function escapeRegExp(value) {
7583
7767
  }
7584
7768
  async function hasMarkdownTc(tcKey, e2eDir) {
7585
7769
  const [batch, tcId] = tcKey.split(":");
7586
- const filePath = join6(e2eDir, `${batch}.md`);
7770
+ const filePath = join7(e2eDir, `${batch}.md`);
7587
7771
  try {
7588
7772
  const raw = await readFile3(filePath, "utf-8");
7589
7773
  const tcRegex = new RegExp(`^#{2,3}\\s+${escapeRegExp(tcId)}\\s*[:\uFF1A]?`, "m");
@@ -7593,7 +7777,7 @@ async function hasMarkdownTc(tcKey, e2eDir) {
7593
7777
  }
7594
7778
  }
7595
7779
  async function findFiles(root, patterns) {
7596
- const all = await walk(root);
7780
+ const all = await walkFiles(root);
7597
7781
  const matched = /* @__PURE__ */ new Set();
7598
7782
  for (const pattern of patterns) {
7599
7783
  for (const file of all) {
@@ -7604,21 +7788,9 @@ async function findFiles(root, patterns) {
7604
7788
  }
7605
7789
  return [...matched].sort();
7606
7790
  }
7607
- async function walk(root, current = root) {
7608
- const entries = await readdir2(current, { withFileTypes: true });
7609
- const files = [];
7610
- for (const entry of entries) {
7611
- if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === ".artifact-graph") {
7612
- continue;
7613
- }
7614
- const fullPath = join6(current, entry.name);
7615
- if (entry.isDirectory()) {
7616
- files.push(...await walk(root, fullPath));
7617
- } else {
7618
- files.push(relative2(root, fullPath).split("\\").join("/"));
7619
- }
7620
- }
7621
- return files;
7791
+ function matchesConfiguredArtifactPath(path, schema) {
7792
+ const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
7793
+ return Object.values(schema.types).some((definition) => definition.paths.some((pattern) => matchesPattern(normalizedPath, pattern)));
7622
7794
  }
7623
7795
  function matchesPattern(file, pattern) {
7624
7796
  if (!pattern.includes("*")) {
@@ -8095,7 +8267,7 @@ function resolveArtifactContext(graph, opts) {
8095
8267
  }
8096
8268
  if (root) {
8097
8269
  for (const ap of ALWAYS_PRESENT_ITEMS) {
8098
- const fullPath = join6(root, ap.path);
8270
+ const fullPath = join7(root, ap.path);
8099
8271
  let stat;
8100
8272
  try {
8101
8273
  stat = statSync(fullPath);
@@ -8332,6 +8504,7 @@ var init_index = __esm({
8332
8504
  init_packet_constants();
8333
8505
  init_packet_validator();
8334
8506
  init_glob_matcher();
8507
+ init_file_walker();
8335
8508
  init_packet_constants();
8336
8509
  init_target_selector();
8337
8510
  init_packet_assembler();
@@ -8436,7 +8609,7 @@ var init_index = __esm({
8436
8609
 
8437
8610
  // src/packet-prompt-audit.ts
8438
8611
  import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
8439
- import { join as join7 } from "path";
8612
+ import { join as join8 } from "path";
8440
8613
  function promptFilename(target) {
8441
8614
  return `prompt-${target.type}-${target.id}.md`;
8442
8615
  }
@@ -8488,7 +8661,7 @@ async function auditSinglePromptTarget(target, graph, options) {
8488
8661
  }
8489
8662
  if (options.outDir) {
8490
8663
  const filename = promptFilename(target);
8491
- const outPath = join7(options.outDir, filename);
8664
+ const outPath = join8(options.outDir, filename);
8492
8665
  await writeFile4(outPath, prompt, "utf-8");
8493
8666
  entry.outputPath = outPath;
8494
8667
  }
@@ -8594,9 +8767,9 @@ async function auditPromptBatch(root, targets, options, graph) {
8594
8767
  };
8595
8768
  if (options.outDir) {
8596
8769
  await mkdir5(options.outDir, { recursive: true });
8597
- const jsonPath = join7(options.outDir, "prompt-audit-summary.json");
8770
+ const jsonPath = join8(options.outDir, "prompt-audit-summary.json");
8598
8771
  await writeFile4(jsonPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
8599
- const mdPath = join7(options.outDir, "prompt-audit-summary.md");
8772
+ const mdPath = join8(options.outDir, "prompt-audit-summary.md");
8600
8773
  await writeFile4(mdPath, renderPromptAuditSummaryMarkdown(summary), "utf-8");
8601
8774
  }
8602
8775
  return summary;
@@ -8638,7 +8811,7 @@ __export(cli_exports, {
8638
8811
  import yaml2 from "js-yaml";
8639
8812
  import { realpathSync } from "fs";
8640
8813
  import { access as access2, mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
8641
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join8 } from "path";
8814
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join9 } from "path";
8642
8815
  import { fileURLToPath } from "url";
8643
8816
  async function runCli(argv, io = {}) {
8644
8817
  const parsed = parseArgs(argv);
@@ -8655,7 +8828,7 @@ async function runCli(argv, io = {}) {
8655
8828
  switch (parsed.command) {
8656
8829
  case "init": {
8657
8830
  await initConfig(root);
8658
- out(`Created ${join8(root, "artifact-graph.config.yaml")}
8831
+ out(`Created ${join9(root, "artifact-graph.config.yaml")}
8659
8832
  `);
8660
8833
  return 0;
8661
8834
  }
@@ -9408,7 +9581,7 @@ async function runCli(argv, io = {}) {
9408
9581
  out(`${JSON.stringify(result, null, 2)}
9409
9582
  `);
9410
9583
  } else {
9411
- out(renderVersionLockAuditMarkdown(result));
9584
+ out(renderVersionLockAuditMarkdown(result, { strictMissingLock: parsed.flags["strict-missing-lock"] === true }));
9412
9585
  }
9413
9586
  return hasBlockingVersionIssues(result.issues, parsed.flags["strict-missing-lock"] === true) && !parsed.flags["warning-only"] ? 1 : 0;
9414
9587
  }
@@ -9484,7 +9657,8 @@ async function runCli(argv, io = {}) {
9484
9657
  err("Stage or stash the unstaged changes before running changed-only staged refresh.\n");
9485
9658
  return 1;
9486
9659
  }
9487
- const unstagedGraphPaths = changeResult.unstagedPaths.filter(isGraphRelevantPath);
9660
+ const schema = await loadConfig(root);
9661
+ const unstagedGraphPaths = changeResult.unstagedPaths.filter((path) => isGraphRelevantPath(path, schema));
9488
9662
  if (unstagedGraphPaths.length > 0) {
9489
9663
  err("Cannot refresh staged version locks because graph-relevant unstaged changes may affect working-tree hashes:\n");
9490
9664
  for (const conflictPath of unstagedGraphPaths) {
@@ -9593,7 +9767,7 @@ async function runCli(argv, io = {}) {
9593
9767
  err("Usage: artifact-graph validate-review-result --file <path> [--format json]\n");
9594
9768
  return 1;
9595
9769
  }
9596
- const resolvedPath = isAbsolute4(filePath) ? filePath : join8(root, filePath);
9770
+ const resolvedPath = isAbsolute4(filePath) ? filePath : join9(root, filePath);
9597
9771
  let content;
9598
9772
  try {
9599
9773
  content = await readFile4(resolvedPath, "utf-8");
@@ -9631,7 +9805,7 @@ async function runCli(argv, io = {}) {
9631
9805
  const deterministic = checkMode || parsed.flags.deterministic === true;
9632
9806
  const registry = await generateE2eRegistry(root, { deterministic });
9633
9807
  const output = JSON.stringify(registry, null, 2) + "\n";
9634
- const outPath = typeof parsed.flags.out === "string" ? parsed.flags.out : join8(root, "artifacts/tests/e2e/e2e-test-registry.json");
9808
+ const outPath = typeof parsed.flags.out === "string" ? parsed.flags.out : join9(root, "artifacts/tests/e2e/e2e-test-registry.json");
9635
9809
  if (checkMode) {
9636
9810
  let existing = "";
9637
9811
  try {
@@ -9668,7 +9842,7 @@ async function runCli(argv, io = {}) {
9668
9842
  return 1;
9669
9843
  }
9670
9844
  const packageDir = dirname4(fileURLToPath(import.meta.url));
9671
- const contractsDir = typeof parsed.flags["contracts-dir"] === "string" ? parsed.flags["contracts-dir"] : join8(packageDir, "..", "contracts");
9845
+ const contractsDir = typeof parsed.flags["contracts-dir"] === "string" ? parsed.flags["contracts-dir"] : join9(packageDir, "..", "contracts");
9672
9846
  const revisionDigest = typeof parsed.flags["revision-digest"] === "string" ? parsed.flags["revision-digest"] : void 0;
9673
9847
  async function resolveContract(contractId) {
9674
9848
  const catalog = await loadContractCatalog(contractsDir);
@@ -9925,13 +10099,17 @@ async function runCli(argv, io = {}) {
9925
10099
  }
9926
10100
  }
9927
10101
  function hasBlockingVersionIssues(issues, strictMissingLock) {
9928
- return issues.some((issue2) => issue2.status !== "missing_lock" || strictMissingLock);
10102
+ return issues.some((issue2) => isVersionLockIssueBlocking({
10103
+ status: issue2.status,
10104
+ message: issue2.message ?? "",
10105
+ severity: issue2.severity
10106
+ }, strictMissingLock));
9929
10107
  }
9930
- function isGraphRelevantPath(path) {
9931
- return path === "artifact-graph.config.yaml" || path === VERSION_LOCK_PATH || path.startsWith("artifacts/") || /\.(md|mdx|json|ya?ml|ts|tsx|js|jsx|mts|cts|rs|py|go)$/.test(path);
10108
+ function isGraphRelevantPath(path, schema) {
10109
+ return path === "artifact-graph.config.yaml" || path === VERSION_LOCK_PATH || matchesConfiguredArtifactPath(path, schema);
9932
10110
  }
9933
10111
  async function initConfig(root) {
9934
- const configPath = join8(root, "artifact-graph.config.yaml");
10112
+ const configPath = join9(root, "artifact-graph.config.yaml");
9935
10113
  try {
9936
10114
  await access2(configPath);
9937
10115
  throw new Error(`Config already exists: ${configPath}`);
@@ -9988,6 +10166,12 @@ Commands:
9988
10166
  version-lock update --target <type:id> --source <path> [--verified-by <path,path>] [--lock-path <path>]
9989
10167
  version-lock bootstrap [--force] [--lock-path <path>]
9990
10168
  version-lock refresh (--all | --changed-only (--staged | --worktree | --base <ref>)) [--remove-orphans] [--format json|markdown] [--lock-path <path>]
10169
+ --remove-orphans delete locks whose artifact, source, or traceability edge no longer
10170
+ exists (default: retain them). Typical cleanup after deleting or
10171
+ splitting artifacts:
10172
+ artifact-graph version-lock refresh --all --remove-orphans --format markdown
10173
+ git diff artifacts/traceability-version-lock.json # review
10174
+ git add artifacts/traceability-version-lock.json # stage, then commit
9991
10175
  trace-version --target <type:id> [--format json|markdown] [--warning-only] [--strict-missing-lock] [--lock-path <path>]
9992
10176
  hooks install-git [--hook pre-commit|pre-push|all] [--uninstall]
9993
10177
  next-id <type> --range <name>