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/CHANGELOG.md +30 -0
- package/INSTALL.md +21 -0
- package/README.md +22 -0
- package/README.zh-CN.md +20 -0
- package/dist/cli.js +300 -116
- package/dist/index.cjs +330 -151
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.js +277 -101
- package/package.json +1 -1
- package/templates/git-hooks/pre-commit.sh +5 -2
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
"#
|
|
2102
|
+
"# \u7248\u672C\u9501\u5BA1\u8BA1\uFF08version-lock audit\uFF09",
|
|
1957
2103
|
"",
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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("
|
|
2113
|
+
lines.push("\u672A\u53D1\u73B0\u7248\u672C\u9501\u95EE\u9898\u3002");
|
|
1966
2114
|
return `${lines.join("\n")}
|
|
1967
2115
|
`;
|
|
1968
2116
|
}
|
|
1969
|
-
|
|
1970
|
-
lines.push(
|
|
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
|
-
"#
|
|
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
|
-
|
|
1980
|
-
|
|
1981
|
-
`
|
|
1982
|
-
`
|
|
1983
|
-
`
|
|
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, "
|
|
1990
|
-
appendList(lines, "
|
|
1991
|
-
appendList(lines, "
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
appendList(lines, "
|
|
1997
|
-
appendList(lines, "
|
|
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
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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("/") ?
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
2516
|
-
const lockPath =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
4107
|
+
const entries = await readdir2(contractsDir, { withFileTypes: true });
|
|
3926
4108
|
for (const entry of entries) {
|
|
3927
4109
|
if (entry.isDirectory()) {
|
|
3928
|
-
const schemaPath =
|
|
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
|
|
4421
|
-
import { basename as basename3, dirname as dirname3, extname, isAbsolute as isAbsolute3, join as
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
5324
|
+
const cacheDir = join7(root, ".artifact-graph");
|
|
5143
5325
|
await mkdir4(cacheDir, { recursive: true });
|
|
5144
|
-
await writeFile3(
|
|
5326
|
+
await writeFile3(join7(cacheDir, "index.json"), `${JSON.stringify(graph, null, 2)}
|
|
5145
5327
|
`);
|
|
5146
|
-
const db = new Database(
|
|
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 =
|
|
7011
|
+
const e2eDir = join7(root, "artifacts", "tests", "e2e");
|
|
6830
7012
|
let e2eFiles;
|
|
6831
7013
|
try {
|
|
6832
|
-
e2eFiles = (await
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
7514
|
+
const e2eDir = join7(root, "artifacts", "tests", "e2e");
|
|
7331
7515
|
let files;
|
|
7332
7516
|
try {
|
|
7333
|
-
files = (await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
7608
|
-
const
|
|
7609
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
|
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 :
|
|
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 :
|
|
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"] :
|
|
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) =>
|
|
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
|
|
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 =
|
|
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>
|