artifact-graph 0.4.1 → 0.6.0
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 +85 -0
- package/INSTALL.md +110 -16
- package/README.md +22 -6
- package/README.zh-CN.md +21 -6
- package/dist/cli.js +1158 -118
- package/dist/index.cjs +988 -101
- package/dist/index.d.cts +162 -6
- package/dist/index.d.ts +162 -6
- package/dist/index.js +983 -98
- package/package.json +2 -1
- package/schemas/review-result.schema.json +74 -1
- package/templates/git-hooks/pre-commit.sh +18 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync2, statSync } from "fs";
|
|
5
6
|
import { mkdir as mkdir4, readFile as readFile2, readdir, writeFile as writeFile3 } from "fs/promises";
|
|
6
|
-
import { basename as basename2, dirname as dirname3, extname, join as join5, relative as relative2 } from "path";
|
|
7
|
+
import { basename as basename2, dirname as dirname3, extname, isAbsolute as isAbsolute3, join as join5, relative as relative2, resolve as resolve3 } from "path";
|
|
7
8
|
|
|
8
9
|
// src/packet-constants.ts
|
|
9
10
|
var ALWAYS_PRESENT_ITEMS = [
|
|
@@ -82,13 +83,76 @@ function validatePacket(packet, schema) {
|
|
|
82
83
|
path: "target.id"
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
const isBaselineExplicitlyDisabled = packet.baselinePolicy === false;
|
|
87
|
+
const expectedPaths = new Set(ALWAYS_PRESENT_ITEMS.map((item) => item.path));
|
|
88
|
+
if (isBaselineExplicitlyDisabled) {
|
|
89
|
+
if (packet.requiredBaseline.total !== 0) {
|
|
90
|
+
issues.push({
|
|
91
|
+
severity: "error",
|
|
92
|
+
code: "PKT-004",
|
|
93
|
+
message: `baselinePolicy=false requires requiredBaseline.total=0, got ${packet.requiredBaseline.total}`,
|
|
94
|
+
path: "requiredBaseline.total"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (packet.requiredBaseline.items.length !== 0) {
|
|
98
|
+
issues.push({
|
|
99
|
+
severity: "error",
|
|
100
|
+
code: "PKT-004",
|
|
101
|
+
message: `baselinePolicy=false requires requiredBaseline.items=[], got ${packet.requiredBaseline.items.length} item(s)`,
|
|
102
|
+
path: "requiredBaseline.items"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
|
|
107
|
+
issues.push({
|
|
108
|
+
severity: "error",
|
|
109
|
+
code: "PKT-004",
|
|
110
|
+
message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
|
|
111
|
+
path: "requiredBaseline.total"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (packet.requiredBaseline.items.length !== BASELINE_ITEMS_COUNT) {
|
|
115
|
+
issues.push({
|
|
116
|
+
severity: "error",
|
|
117
|
+
code: "PKT-004",
|
|
118
|
+
message: `requiredBaseline.items.length must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.items.length}`,
|
|
119
|
+
path: "requiredBaseline.items"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const actualPaths = packet.requiredBaseline.items.map((item) => item.path);
|
|
123
|
+
const actualPathSet = new Set(actualPaths);
|
|
124
|
+
if (actualPathSet.size !== actualPaths.length) {
|
|
125
|
+
issues.push({
|
|
126
|
+
severity: "error",
|
|
127
|
+
code: "PKT-004",
|
|
128
|
+
message: `requiredBaseline.items contains duplicate paths (${actualPaths.length} items, ${actualPathSet.size} unique)`,
|
|
129
|
+
path: "requiredBaseline.items"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const missingPaths = [];
|
|
133
|
+
for (const ep of expectedPaths) {
|
|
134
|
+
if (!actualPathSet.has(ep)) missingPaths.push(ep);
|
|
135
|
+
}
|
|
136
|
+
if (missingPaths.length > 0) {
|
|
137
|
+
issues.push({
|
|
138
|
+
severity: "error",
|
|
139
|
+
code: "PKT-004",
|
|
140
|
+
message: `requiredBaseline.items missing expected path(s): ${missingPaths.join(", ")}`,
|
|
141
|
+
path: "requiredBaseline.items"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const extraPaths = [];
|
|
145
|
+
for (const ap of actualPathSet) {
|
|
146
|
+
if (!expectedPaths.has(ap)) extraPaths.push(ap);
|
|
147
|
+
}
|
|
148
|
+
if (extraPaths.length > 0) {
|
|
149
|
+
issues.push({
|
|
150
|
+
severity: "error",
|
|
151
|
+
code: "PKT-004",
|
|
152
|
+
message: `requiredBaseline.items contains unexpected path(s): ${extraPaths.join(", ")}`,
|
|
153
|
+
path: "requiredBaseline.items"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
92
156
|
}
|
|
93
157
|
const constraints = packet.implementationBlueprintDraft.constraints;
|
|
94
158
|
if (constraints.length !== BASELINE_CONSTRAINTS_COUNT) {
|
|
@@ -195,6 +259,45 @@ function validatePacketMarkdown(markdown) {
|
|
|
195
259
|
return { ok: !hasError, issues };
|
|
196
260
|
}
|
|
197
261
|
|
|
262
|
+
// src/glob-matcher.ts
|
|
263
|
+
function matchesRunnerGlob(filePath, pattern) {
|
|
264
|
+
const normalizedPath = normalizeGlobValue(filePath);
|
|
265
|
+
const normalizedPattern = normalizeGlobValue(pattern);
|
|
266
|
+
let expression = "^";
|
|
267
|
+
for (let index = 0; index < normalizedPattern.length; index += 1) {
|
|
268
|
+
const character = normalizedPattern[index];
|
|
269
|
+
if (character === "*") {
|
|
270
|
+
if (normalizedPattern[index + 1] === "*") {
|
|
271
|
+
while (normalizedPattern[index + 1] === "*") {
|
|
272
|
+
index += 1;
|
|
273
|
+
}
|
|
274
|
+
if (normalizedPattern[index + 1] === "/") {
|
|
275
|
+
index += 1;
|
|
276
|
+
expression += "(?:[^/]+/)*";
|
|
277
|
+
} else {
|
|
278
|
+
expression += ".*";
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
expression += "[^/]*";
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (character === "?") {
|
|
286
|
+
expression += "[^/]";
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
expression += escapeRegexCharacter(character);
|
|
290
|
+
}
|
|
291
|
+
expression += "$";
|
|
292
|
+
return new RegExp(expression).test(normalizedPath);
|
|
293
|
+
}
|
|
294
|
+
function normalizeGlobValue(value) {
|
|
295
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
296
|
+
}
|
|
297
|
+
function escapeRegexCharacter(character) {
|
|
298
|
+
return "\\^$+?.()|{}[]".includes(character) ? String.fromCharCode(92) + character : character;
|
|
299
|
+
}
|
|
300
|
+
|
|
198
301
|
// src/target-selector.ts
|
|
199
302
|
function parseTargetSelector(value) {
|
|
200
303
|
const separator = value.indexOf(":");
|
|
@@ -515,7 +618,10 @@ function assemblePacket(manifest, options) {
|
|
|
515
618
|
missing: [...manifest.missing],
|
|
516
619
|
missingDetails: manifest.missingDetails ? [...manifest.missingDetails] : void 0,
|
|
517
620
|
implementationBlueprintDraft: blueprintDraft,
|
|
518
|
-
validationCommands
|
|
621
|
+
validationCommands,
|
|
622
|
+
// @feature ACA17
|
|
623
|
+
// @decision D-ACA-17
|
|
624
|
+
baselinePolicy: manifest.baselinePolicy
|
|
519
625
|
};
|
|
520
626
|
return packet;
|
|
521
627
|
}
|
|
@@ -736,7 +842,9 @@ async function auditSingleTarget(target, graph, options) {
|
|
|
736
842
|
const manifest = resolveArtifactContext(graph, {
|
|
737
843
|
target: { type: target.type, id: target.id },
|
|
738
844
|
mode: options.mode,
|
|
739
|
-
maxPerCategory: options.maxPerCategory
|
|
845
|
+
maxPerCategory: options.maxPerCategory,
|
|
846
|
+
universalBaseline: options.universalBaseline,
|
|
847
|
+
root: options.root
|
|
740
848
|
});
|
|
741
849
|
const packet = assemblePacket(manifest, {
|
|
742
850
|
mode: options.mode,
|
|
@@ -873,6 +981,7 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
873
981
|
type: d.type,
|
|
874
982
|
id: d.id
|
|
875
983
|
}));
|
|
984
|
+
const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
|
|
876
985
|
return auditPackets(root, targets, {
|
|
877
986
|
root,
|
|
878
987
|
outDir: options.outDir,
|
|
@@ -882,7 +991,8 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
882
991
|
summaryOnly: options.summaryOnly,
|
|
883
992
|
sampleTargets: options.sampleTargets,
|
|
884
993
|
summaryDetail: options.summaryDetail,
|
|
885
|
-
schema: config
|
|
994
|
+
schema: config,
|
|
995
|
+
universalBaseline: effectiveBaseline
|
|
886
996
|
}, graph);
|
|
887
997
|
}
|
|
888
998
|
|
|
@@ -1205,6 +1315,7 @@ function validatePacketPrompt(prompt) {
|
|
|
1205
1315
|
|
|
1206
1316
|
// src/versioned-traceability.ts
|
|
1207
1317
|
import { createHash } from "crypto";
|
|
1318
|
+
import { existsSync } from "fs";
|
|
1208
1319
|
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
1209
1320
|
import { dirname, join as join2, relative } from "path";
|
|
1210
1321
|
var VERSION_LOCK_PATH = "artifacts/traceability-version-lock.json";
|
|
@@ -1245,13 +1356,15 @@ async function buildVersionIndex(root, graph) {
|
|
|
1245
1356
|
edges: sortBy(edges, (edge2) => `${edge2.from} ${edge2.to} ${edge2.kind} ${edge2.sourcePath} ${edge2.sourceLine}`)
|
|
1246
1357
|
};
|
|
1247
1358
|
}
|
|
1248
|
-
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
1359
|
+
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, config) {
|
|
1249
1360
|
const index = await buildVersionIndex(root, graph);
|
|
1361
|
+
const schema = config ?? await loadConfig(root);
|
|
1250
1362
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1251
1363
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1252
1364
|
const nodeByArtifact = new Map(index.nodes.map((node) => [`${node.type}:${node.id}`, node]));
|
|
1253
1365
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1254
1366
|
const currentEdges = implementationEdges(index);
|
|
1367
|
+
const lockableEdges = await lockableImplementationEdges(root, index, schema);
|
|
1255
1368
|
const currentEdgeIds = new Set(currentEdges.map((edge2) => edge2.edgeId));
|
|
1256
1369
|
const issues = [];
|
|
1257
1370
|
let fresh = 0;
|
|
@@ -1339,8 +1452,29 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
|
1339
1452
|
issues.push(...entryIssues);
|
|
1340
1453
|
}
|
|
1341
1454
|
}
|
|
1455
|
+
const livenessCache = /* @__PURE__ */ new Map();
|
|
1456
|
+
for (const entry of lock.locks) {
|
|
1457
|
+
if (entry.kind !== "verifies") continue;
|
|
1458
|
+
const sourcePath = entry.source.path;
|
|
1459
|
+
const fullSourcePath = join2(root, sourcePath);
|
|
1460
|
+
if (!existsSync(fullSourcePath)) continue;
|
|
1461
|
+
let liveness = livenessCache.get(sourcePath);
|
|
1462
|
+
if (liveness === void 0) {
|
|
1463
|
+
liveness = await getTestFileRunnerLiveness(root, sourcePath, schema);
|
|
1464
|
+
livenessCache.set(sourcePath, liveness);
|
|
1465
|
+
}
|
|
1466
|
+
if (liveness === "inactive") {
|
|
1467
|
+
issues.push({
|
|
1468
|
+
status: "orphan_lock",
|
|
1469
|
+
edgeId: entry.edgeId,
|
|
1470
|
+
message: `Liveness: ${sourcePath} is not active in any configured runner \u2014 locked verifies edges may reference dead tests`,
|
|
1471
|
+
artifact: entry.artifact,
|
|
1472
|
+
source: entry.source
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1342
1476
|
const reportedMissingLocks = /* @__PURE__ */ new Set();
|
|
1343
|
-
for (const edge2 of
|
|
1477
|
+
for (const edge2 of lockableEdges) {
|
|
1344
1478
|
const edgeId = edge2.edgeId;
|
|
1345
1479
|
if (!lock.locks.some((entry) => entry.edgeId === edgeId)) {
|
|
1346
1480
|
if (reportedMissingLocks.has(edgeId)) {
|
|
@@ -1422,6 +1556,7 @@ async function updateVersionLock(root, options) {
|
|
|
1422
1556
|
}
|
|
1423
1557
|
async function bootstrapVersionLock(root, options = {}) {
|
|
1424
1558
|
const index = await buildVersionIndex(root);
|
|
1559
|
+
const config = await loadConfig(root);
|
|
1425
1560
|
const lockPath = normalizeRelativePath(root, options.lockPath ?? VERSION_LOCK_PATH);
|
|
1426
1561
|
if (!options.force) {
|
|
1427
1562
|
const existing = await readVersionLock(root, lockPath);
|
|
@@ -1431,7 +1566,7 @@ async function bootstrapVersionLock(root, options = {}) {
|
|
|
1431
1566
|
}
|
|
1432
1567
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1433
1568
|
const entries = /* @__PURE__ */ new Map();
|
|
1434
|
-
for (const edge2 of
|
|
1569
|
+
for (const edge2 of await lockableImplementationEdges(root, index, config)) {
|
|
1435
1570
|
const source = nodeByUid.get(edge2.from);
|
|
1436
1571
|
const artifact = nodeByUid.get(edge2.to);
|
|
1437
1572
|
if (!source || !artifact) {
|
|
@@ -1466,10 +1601,11 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1466
1601
|
throw new Error("Changed-only version-lock refresh includes artifact-graph.config.yaml and requires --all");
|
|
1467
1602
|
}
|
|
1468
1603
|
const index = await buildVersionIndex(root);
|
|
1604
|
+
const config = await loadConfig(root);
|
|
1469
1605
|
const lock = await readVersionLock(root, lockPath);
|
|
1470
1606
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1471
1607
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1472
|
-
const currentImplementationEdges =
|
|
1608
|
+
const currentImplementationEdges = await lockableImplementationEdges(root, index, config);
|
|
1473
1609
|
const currentEdgePairs = new Set(currentImplementationEdges.map((edge2) => `${edge2.from} ${edge2.to}`));
|
|
1474
1610
|
const currentEntries = /* @__PURE__ */ new Map();
|
|
1475
1611
|
const changedPathSet = new Set(changedPaths);
|
|
@@ -1544,7 +1680,7 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1544
1680
|
locks: sortBy([...nextLocks.values()], (item) => item.edgeId)
|
|
1545
1681
|
};
|
|
1546
1682
|
await writeVersionLock(root, lockPath, next);
|
|
1547
|
-
const postAudit = await auditVersionLock(root, lockPath);
|
|
1683
|
+
const postAudit = await auditVersionLock(root, lockPath, void 0, config);
|
|
1548
1684
|
return {
|
|
1549
1685
|
schemaVersion: "1.0",
|
|
1550
1686
|
root,
|
|
@@ -1563,7 +1699,8 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1563
1699
|
async function traceVersion(root, target, lockPath = VERSION_LOCK_PATH) {
|
|
1564
1700
|
const index = await buildVersionIndex(root);
|
|
1565
1701
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1566
|
-
const
|
|
1702
|
+
const config = await loadConfig(root);
|
|
1703
|
+
const audit = await auditVersionLock(root, safeLockPath, void 0, config);
|
|
1567
1704
|
const targetUid = parseTarget(target);
|
|
1568
1705
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1569
1706
|
const targetNode = index.nodes.find((node) => node.uid === targetUid);
|
|
@@ -1799,6 +1936,28 @@ function implementationEdges(index) {
|
|
|
1799
1936
|
};
|
|
1800
1937
|
});
|
|
1801
1938
|
}
|
|
1939
|
+
async function lockableImplementationEdges(root, index, config) {
|
|
1940
|
+
const edges = implementationEdges(index);
|
|
1941
|
+
const nodesByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1942
|
+
const livenessByPath = /* @__PURE__ */ new Map();
|
|
1943
|
+
const result = [];
|
|
1944
|
+
for (const edge2 of edges) {
|
|
1945
|
+
const source = nodesByUid.get(edge2.from);
|
|
1946
|
+
if (source?.sourceKind !== "test") {
|
|
1947
|
+
result.push(edge2);
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
let liveness = livenessByPath.get(source.path);
|
|
1951
|
+
if (liveness === void 0) {
|
|
1952
|
+
liveness = await getTestFileRunnerLiveness(root, source.path, config);
|
|
1953
|
+
livenessByPath.set(source.path, liveness);
|
|
1954
|
+
}
|
|
1955
|
+
if (liveness !== "inactive") {
|
|
1956
|
+
result.push(edge2);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return result;
|
|
1960
|
+
}
|
|
1802
1961
|
function lockRefFromNode(node) {
|
|
1803
1962
|
return {
|
|
1804
1963
|
type: node.type,
|
|
@@ -1911,6 +2070,53 @@ function normalizeRelativePath(root, path) {
|
|
|
1911
2070
|
function sortBy(items, keyFn) {
|
|
1912
2071
|
return [...items].sort((left, right) => keyFn(left).localeCompare(keyFn(right)));
|
|
1913
2072
|
}
|
|
2073
|
+
async function getTestFileRunnerLiveness(root, filePath, config) {
|
|
2074
|
+
const schema = config ?? await loadConfig(root);
|
|
2075
|
+
const runners = schema.e2e?.runners ?? [];
|
|
2076
|
+
if (runners.length === 0) {
|
|
2077
|
+
if (!/e2e/i.test(filePath) && !/\.e2e\./i.test(filePath)) {
|
|
2078
|
+
return "active";
|
|
2079
|
+
}
|
|
2080
|
+
const fullSourcePath = join2(root, filePath);
|
|
2081
|
+
if (!existsSync(fullSourcePath)) return "inactive";
|
|
2082
|
+
try {
|
|
2083
|
+
const content = await readFile(fullSourcePath, "utf-8");
|
|
2084
|
+
return /\/\/!?\s*@(?:e2e_test|tc)\s+/.test(content) ? "active" : "inactive";
|
|
2085
|
+
} catch {
|
|
2086
|
+
return "inactive";
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
let inRunnerScope = false;
|
|
2090
|
+
for (const runner of runners) {
|
|
2091
|
+
if (!isFileIncludedByRunner(filePath, runner)) continue;
|
|
2092
|
+
inRunnerScope = true;
|
|
2093
|
+
const isActive = await isFileActiveInRunner(root, filePath, runner);
|
|
2094
|
+
if (isActive) return "active";
|
|
2095
|
+
}
|
|
2096
|
+
return inRunnerScope ? "inactive" : "unscoped";
|
|
2097
|
+
}
|
|
2098
|
+
function isFileIncludedByRunner(filePath, runner) {
|
|
2099
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2100
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2101
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) return false;
|
|
2102
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length + 1);
|
|
2103
|
+
return runner.include.some((pattern) => matchesRunnerGlob(relativePath, pattern));
|
|
2104
|
+
}
|
|
2105
|
+
async function isFileActiveInRunner(root, filePath, runner) {
|
|
2106
|
+
if (!isFileIncludedByRunner(filePath, runner)) return false;
|
|
2107
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2108
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2109
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length).replace(/^\//, "");
|
|
2110
|
+
const matchesExclude = (runner.exclude ?? []).some(
|
|
2111
|
+
(pattern) => matchesRunnerGlob(relativePath, pattern)
|
|
2112
|
+
);
|
|
2113
|
+
if (matchesExclude) return false;
|
|
2114
|
+
const matchesTestIgnore = (runner.testIgnore ?? []).some(
|
|
2115
|
+
(pattern) => matchesRunnerGlob(relativePath, pattern)
|
|
2116
|
+
);
|
|
2117
|
+
if (matchesTestIgnore) return false;
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
1914
2120
|
function sortUnique(items) {
|
|
1915
2121
|
return [...new Set(items)].sort((left, right) => left.localeCompare(right));
|
|
1916
2122
|
}
|
|
@@ -2597,12 +2803,35 @@ var VALID_DECISIONS = /* @__PURE__ */ new Set([
|
|
|
2597
2803
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["block", "warn", "info"]);
|
|
2598
2804
|
var VALID_FINDING_STATUSES = /* @__PURE__ */ new Set(["open", "resolved", "accepted", "superseded"]);
|
|
2599
2805
|
var VALID_EXECUTORS = /* @__PURE__ */ new Set(["script", "worker", "agent", "manual", "cli"]);
|
|
2806
|
+
var TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
|
|
2807
|
+
"schema_version",
|
|
2808
|
+
"run_id",
|
|
2809
|
+
"stage_id",
|
|
2810
|
+
"attempt",
|
|
2811
|
+
"status",
|
|
2812
|
+
"decision",
|
|
2813
|
+
"summary",
|
|
2814
|
+
"outputs",
|
|
2815
|
+
"warnings",
|
|
2816
|
+
"blocking_reason",
|
|
2817
|
+
"degradation",
|
|
2818
|
+
"producer",
|
|
2819
|
+
"acceptance",
|
|
2820
|
+
"evidence",
|
|
2821
|
+
"review",
|
|
2822
|
+
"repair"
|
|
2823
|
+
]);
|
|
2600
2824
|
function validateReviewResult(input) {
|
|
2601
2825
|
const errors = [];
|
|
2602
2826
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
2603
2827
|
return [{ path: "$", message: "Root must be a non-null object" }];
|
|
2604
2828
|
}
|
|
2605
2829
|
const obj = input;
|
|
2830
|
+
for (const key of Object.keys(obj)) {
|
|
2831
|
+
if (!TOP_LEVEL_FIELDS.has(key)) {
|
|
2832
|
+
errors.push({ path: `$.${key}`, message: "Unknown top-level property" });
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2606
2835
|
if (obj.schema_version !== "1.0") {
|
|
2607
2836
|
errors.push({ path: "$.schema_version", message: `Must be "1.0", got ${JSON.stringify(obj.schema_version)}` });
|
|
2608
2837
|
}
|
|
@@ -2621,8 +2850,8 @@ function validateReviewResult(input) {
|
|
|
2621
2850
|
if (obj.stage_id !== void 0 && typeof obj.stage_id !== "string") {
|
|
2622
2851
|
errors.push({ path: "$.stage_id", message: "Must be a string if present" });
|
|
2623
2852
|
}
|
|
2624
|
-
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1)) {
|
|
2625
|
-
errors.push({ path: "$.attempt", message: "Must be
|
|
2853
|
+
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1 || obj.attempt > 3)) {
|
|
2854
|
+
errors.push({ path: "$.attempt", message: "Must be an integer from 1 through 3 if present" });
|
|
2626
2855
|
}
|
|
2627
2856
|
if (obj.outputs !== void 0) {
|
|
2628
2857
|
checkStringArray(obj.outputs, "$.outputs", errors);
|
|
@@ -2633,20 +2862,14 @@ function validateReviewResult(input) {
|
|
|
2633
2862
|
checkOptionalNullableString(obj.blocking_reason, "$.blocking_reason", errors);
|
|
2634
2863
|
checkOptionalNullableString(obj.degradation, "$.degradation", errors);
|
|
2635
2864
|
if (obj.producer !== void 0) {
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
}
|
|
2645
|
-
if (typeof p.name !== "string") {
|
|
2646
|
-
errors.push({ path: "$.producer.name", message: "Must be a string" });
|
|
2647
|
-
}
|
|
2648
|
-
checkOptionalString(p.skill, "$.producer.skill", errors);
|
|
2649
|
-
}
|
|
2865
|
+
validateProducer(obj.producer, "$.producer", errors);
|
|
2866
|
+
}
|
|
2867
|
+
const successfulAcceptance = obj.status === "SUCCEEDED" && (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR");
|
|
2868
|
+
if (successfulAcceptance && obj.producer === void 0) {
|
|
2869
|
+
errors.push({ path: "$.producer", message: "Successful acceptance requires producer identity" });
|
|
2870
|
+
}
|
|
2871
|
+
if (obj.acceptance !== void 0) {
|
|
2872
|
+
validateAcceptance(obj.acceptance, obj.producer, "$.acceptance", errors);
|
|
2650
2873
|
}
|
|
2651
2874
|
if (obj.evidence !== void 0) {
|
|
2652
2875
|
if (!Array.isArray(obj.evidence)) {
|
|
@@ -2672,6 +2895,17 @@ function validateReviewResult(input) {
|
|
|
2672
2895
|
}
|
|
2673
2896
|
if (obj.review !== void 0) {
|
|
2674
2897
|
validateReviewData(obj.review, "$.review", errors);
|
|
2898
|
+
if (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR") {
|
|
2899
|
+
const findings = isPlainObject(obj.review) && Array.isArray(obj.review.findings) ? obj.review.findings : [];
|
|
2900
|
+
findings.forEach((finding, index) => {
|
|
2901
|
+
if (isPlainObject(finding) && finding.severity === "block" && (finding.status === void 0 || finding.status === "open")) {
|
|
2902
|
+
errors.push({
|
|
2903
|
+
path: `$.review.findings[${index}]`,
|
|
2904
|
+
message: `${obj.decision} cannot contain an open block finding`
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2675
2909
|
}
|
|
2676
2910
|
if (obj.repair !== void 0) {
|
|
2677
2911
|
if (!isPlainObject(obj.repair)) {
|
|
@@ -2707,6 +2941,47 @@ function checkStringArray(val, path, errors) {
|
|
|
2707
2941
|
function isPlainObject(val) {
|
|
2708
2942
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
2709
2943
|
}
|
|
2944
|
+
function validateProducer(val, path, errors) {
|
|
2945
|
+
if (!isPlainObject(val)) {
|
|
2946
|
+
errors.push({ path, message: "Must be an object" });
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
if (typeof val.executor !== "string") {
|
|
2950
|
+
errors.push({ path: `${path}.executor`, message: "Must be a string" });
|
|
2951
|
+
} else if (!VALID_EXECUTORS.has(val.executor)) {
|
|
2952
|
+
errors.push({ path: `${path}.executor`, message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(val.executor)}` });
|
|
2953
|
+
}
|
|
2954
|
+
if (typeof val.name !== "string" || val.name.length === 0) {
|
|
2955
|
+
errors.push({ path: `${path}.name`, message: "Must be a non-empty string" });
|
|
2956
|
+
}
|
|
2957
|
+
checkOptionalString(val.skill, `${path}.skill`, errors);
|
|
2958
|
+
}
|
|
2959
|
+
function producerIdentity(val) {
|
|
2960
|
+
return JSON.stringify([val.executor, val.name]);
|
|
2961
|
+
}
|
|
2962
|
+
function validateAcceptance(val, resultProducer, path, errors) {
|
|
2963
|
+
if (!isPlainObject(val)) {
|
|
2964
|
+
errors.push({ path, message: "Must be an object" });
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
validateProducer(val.reviewer, `${path}.reviewer`, errors);
|
|
2968
|
+
if (!isPlainObject(val.source_result)) {
|
|
2969
|
+
errors.push({ path: `${path}.source_result`, message: "Must be an object" });
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
const source = val.source_result;
|
|
2973
|
+
if (typeof source.run_id !== "string" || source.run_id.length === 0) {
|
|
2974
|
+
errors.push({ path: `${path}.source_result.run_id`, message: "Must be a non-empty string" });
|
|
2975
|
+
}
|
|
2976
|
+
checkOptionalString(source.stage_id, `${path}.source_result.stage_id`, errors);
|
|
2977
|
+
validateProducer(source.producer, `${path}.source_result.producer`, errors);
|
|
2978
|
+
if (isPlainObject(val.reviewer) && isPlainObject(resultProducer) && producerIdentity(val.reviewer) !== producerIdentity(resultProducer)) {
|
|
2979
|
+
errors.push({ path: `${path}.reviewer`, message: "Acceptance reviewer must match the result producer" });
|
|
2980
|
+
}
|
|
2981
|
+
if (isPlainObject(val.reviewer) && isPlainObject(source.producer) && producerIdentity(val.reviewer) === producerIdentity(source.producer)) {
|
|
2982
|
+
errors.push({ path: `${path}.reviewer`, message: "Repair producer cannot accept its own result" });
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2710
2985
|
function checkOptionalString(val, path, errors) {
|
|
2711
2986
|
if (val !== void 0 && typeof val !== "string") {
|
|
2712
2987
|
errors.push({ path, message: "Must be a string if present" });
|
|
@@ -2875,7 +3150,7 @@ var DEFAULT_SCHEMA = {
|
|
|
2875
3150
|
scenario: { paths: ["artifacts/scenarios/**/*.md"], displayName: "\u573A\u666F\u5267\u672C", role: "scenario", layer: "scenario", aliases: ["scenarios", "scenario-script"] },
|
|
2876
3151
|
design: { paths: ["artifacts/design/**/*.md"], displayName: "\u8BBE\u8BA1\u89C4\u683C", role: "design", layer: "design", aliases: ["design-spec", "design_docs"] },
|
|
2877
3152
|
test: { paths: ["heimdall/packages/**/*.test.ts"], displayName: "\u4EE3\u7801\u6CE8\u91CA\u8FFD\u6EAF", role: "context", layer: "implementation", aliases: ["code-test", "code-trace", "unit-test"] },
|
|
2878
|
-
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests"] },
|
|
3153
|
+
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests", "tc"] },
|
|
2879
3154
|
e2e_registry: { paths: ["artifacts/tests/e2e/e2e-test-registry.json"], displayName: "E2E \u6D4B\u8BD5\u6CE8\u518C\u8868", role: "context", layer: "verification", aliases: ["e2e-registry"] },
|
|
2880
3155
|
"rule-golden-cases": { paths: ["artifacts/tests/rule-golden-cases.md"], displayName: "\u89C4\u5219\u9EC4\u91D1\u6D4B\u8BD5\u7528\u4F8B", role: "context", layer: "verification", aliases: ["rule_golden_cases"] },
|
|
2881
3156
|
"test-strategy": { paths: ["artifacts/design/test-strategy.md"], displayName: "\u6D4B\u8BD5\u7B56\u7565", role: "context", layer: "verification", aliases: ["test_strategy"] },
|
|
@@ -2905,7 +3180,12 @@ var DEFAULT_SCHEMA = {
|
|
|
2905
3180
|
allowedEdges: [],
|
|
2906
3181
|
forbiddenEdges: [{ from: "scenario", to: "entity", kind: "references" }],
|
|
2907
3182
|
statuses: ["planned", "active", "done", "deprecated"],
|
|
2908
|
-
idRanges: {}
|
|
3183
|
+
idRanges: {},
|
|
3184
|
+
e2e: {
|
|
3185
|
+
report_uncovered_scenarios: true,
|
|
3186
|
+
report_uncovered_features: true,
|
|
3187
|
+
runners: []
|
|
3188
|
+
}
|
|
2909
3189
|
};
|
|
2910
3190
|
async function loadConfig(root) {
|
|
2911
3191
|
const configPath = join5(root, "artifact-graph.config.yaml");
|
|
@@ -2918,7 +3198,27 @@ async function loadConfig(root) {
|
|
|
2918
3198
|
throw error;
|
|
2919
3199
|
}
|
|
2920
3200
|
}
|
|
2921
|
-
|
|
3201
|
+
const ub = parsed.context?.universal_baseline;
|
|
3202
|
+
if (ub !== void 0 && typeof ub !== "boolean") {
|
|
3203
|
+
throw new Error(
|
|
3204
|
+
`Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
if (parsed.e2e !== void 0) {
|
|
3208
|
+
if (typeof parsed.e2e !== "object" || parsed.e2e === null || Array.isArray(parsed.e2e)) {
|
|
3209
|
+
throw new Error("Invalid e2e: must be an object.");
|
|
3210
|
+
}
|
|
3211
|
+
validateE2eConfig(parsed.e2e);
|
|
3212
|
+
}
|
|
3213
|
+
const mergedE2e = parsed.e2e === void 0 ? DEFAULT_SCHEMA.e2e : {
|
|
3214
|
+
...DEFAULT_SCHEMA.e2e,
|
|
3215
|
+
...parsed.e2e,
|
|
3216
|
+
runners: (parsed.e2e.runners ?? DEFAULT_SCHEMA.e2e?.runners ?? []).map((runner) => ({
|
|
3217
|
+
kind: "e2e",
|
|
3218
|
+
...runner
|
|
3219
|
+
}))
|
|
3220
|
+
};
|
|
3221
|
+
const merged = {
|
|
2922
3222
|
...DEFAULT_SCHEMA,
|
|
2923
3223
|
...parsed,
|
|
2924
3224
|
types: mergeArtifactTypes(DEFAULT_SCHEMA.types, parsed.types),
|
|
@@ -2927,10 +3227,103 @@ async function loadConfig(root) {
|
|
|
2927
3227
|
allowedEdges: parsed.allowedEdges ?? DEFAULT_SCHEMA.allowedEdges,
|
|
2928
3228
|
forbiddenEdges: parsed.forbiddenEdges ?? DEFAULT_SCHEMA.forbiddenEdges,
|
|
2929
3229
|
statuses: parsed.statuses ?? DEFAULT_SCHEMA.statuses,
|
|
2930
|
-
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
|
|
3230
|
+
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges),
|
|
3231
|
+
e2e: mergedE2e
|
|
2931
3232
|
};
|
|
3233
|
+
return merged;
|
|
2932
3234
|
}
|
|
2933
|
-
function
|
|
3235
|
+
function validateE2eConfig(e2e) {
|
|
3236
|
+
for (const field of ["report_uncovered_scenarios", "report_uncovered_features"]) {
|
|
3237
|
+
if (e2e[field] !== void 0 && typeof e2e[field] !== "boolean") {
|
|
3238
|
+
throw new Error(`Invalid e2e.${field}: must be boolean.`);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
if (e2e.executable_ref_warning !== void 0) {
|
|
3242
|
+
if (typeof e2e.executable_ref_warning !== "number" || e2e.executable_ref_warning < 0 || e2e.executable_ref_warning > 1) {
|
|
3243
|
+
throw new Error(`Invalid e2e.executable_ref_warning: ${JSON.stringify(e2e.executable_ref_warning)}. Must be a number between 0 and 1.`);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
if (e2e.executable_ref_error !== void 0) {
|
|
3247
|
+
if (typeof e2e.executable_ref_error !== "number" || e2e.executable_ref_error < 0 || e2e.executable_ref_error > 1) {
|
|
3248
|
+
throw new Error(`Invalid e2e.executable_ref_error: ${JSON.stringify(e2e.executable_ref_error)}. Must be a number between 0 and 1.`);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
const validateWaivers = (waivers, field) => {
|
|
3252
|
+
if (waivers === void 0) return;
|
|
3253
|
+
if (!Array.isArray(waivers)) {
|
|
3254
|
+
throw new Error(`Invalid ${field}: must be an array of {id, reason} objects.`);
|
|
3255
|
+
}
|
|
3256
|
+
for (const w of waivers) {
|
|
3257
|
+
if (typeof w !== "object" || w === null || !("id" in w) || !("reason" in w)) {
|
|
3258
|
+
throw new Error(`Invalid ${field} entry: ${JSON.stringify(w)}. Must be {id, reason} object.`);
|
|
3259
|
+
}
|
|
3260
|
+
if (typeof w.id !== "string" || !w.id.trim()) {
|
|
3261
|
+
throw new Error(`Invalid ${field} entry: id must be a non-empty string. Got: ${JSON.stringify(w.id)}`);
|
|
3262
|
+
}
|
|
3263
|
+
if (typeof w.reason !== "string" || !w.reason.trim()) {
|
|
3264
|
+
throw new Error(`Invalid ${field} entry: reason must be a non-empty string. Got: ${JSON.stringify(w.reason)}`);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
validateWaivers(e2e.scenario_waivers, "e2e.scenario_waivers");
|
|
3269
|
+
validateWaivers(e2e.feature_waivers, "e2e.feature_waivers");
|
|
3270
|
+
if (e2e.runners !== void 0) {
|
|
3271
|
+
if (!Array.isArray(e2e.runners)) {
|
|
3272
|
+
throw new Error(`Invalid e2e.runners: must be an array.`);
|
|
3273
|
+
}
|
|
3274
|
+
for (const runner of e2e.runners) {
|
|
3275
|
+
if (typeof runner !== "object" || runner === null) {
|
|
3276
|
+
throw new Error(`Invalid e2e.runners entry: must be an object.`);
|
|
3277
|
+
}
|
|
3278
|
+
if (typeof runner.name !== "string" || !runner.name.trim()) {
|
|
3279
|
+
throw new Error(`Invalid e2e.runners entry: name must be a non-empty string.`);
|
|
3280
|
+
}
|
|
3281
|
+
if (typeof runner.root !== "string" || !runner.root.trim()) {
|
|
3282
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: must be a non-empty string.`);
|
|
3283
|
+
}
|
|
3284
|
+
if (isAbsolute3(runner.root)) {
|
|
3285
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: "${runner.root}" must not be an absolute path.`);
|
|
3286
|
+
}
|
|
3287
|
+
if (runner.root.replace(/\\/g, "/").split("/").includes("..")) {
|
|
3288
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: "${runner.root}" must not contain ".." segments.`);
|
|
3289
|
+
}
|
|
3290
|
+
if (!Array.isArray(runner.include) || runner.include.length === 0) {
|
|
3291
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].include: must be a non-empty array of glob patterns.`);
|
|
3292
|
+
}
|
|
3293
|
+
for (const pattern of runner.include) {
|
|
3294
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3295
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].include: pattern must be a non-empty string.`);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
if (runner.exclude !== void 0) {
|
|
3299
|
+
if (!Array.isArray(runner.exclude)) {
|
|
3300
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].exclude: must be an array.`);
|
|
3301
|
+
}
|
|
3302
|
+
for (const pattern of runner.exclude) {
|
|
3303
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3304
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].exclude: pattern must be a non-empty string.`);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (runner.testIgnore !== void 0) {
|
|
3309
|
+
if (!Array.isArray(runner.testIgnore)) {
|
|
3310
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].testIgnore: must be an array.`);
|
|
3311
|
+
}
|
|
3312
|
+
for (const pattern of runner.testIgnore) {
|
|
3313
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3314
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].testIgnore: pattern must be a non-empty string.`);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
if (runner.kind !== void 0) {
|
|
3319
|
+
if (!["unit", "integration", "e2e"].includes(runner.kind)) {
|
|
3320
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].kind: "${runner.kind}". Must be unit, integration, or e2e.`);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
function buildGraph(nodes, edges, diagnostics = [], root) {
|
|
2934
3327
|
const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
|
|
2935
3328
|
graphNodes.sort(compareNode);
|
|
2936
3329
|
edges.sort(compareEdge);
|
|
@@ -2947,6 +3340,7 @@ function buildGraph(nodes, edges, diagnostics = []) {
|
|
|
2947
3340
|
nodes: graphNodes,
|
|
2948
3341
|
edges: dedupedEdges,
|
|
2949
3342
|
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3343
|
+
...root ? { root } : {},
|
|
2950
3344
|
diagnostics: diagnostics.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line)
|
|
2951
3345
|
};
|
|
2952
3346
|
}
|
|
@@ -2978,7 +3372,8 @@ async function scanArtifacts(root, schema) {
|
|
|
2978
3372
|
scanDiagnostics.push(...parsed.diagnostics);
|
|
2979
3373
|
}
|
|
2980
3374
|
}
|
|
2981
|
-
const
|
|
3375
|
+
const absoluteRoot = isAbsolute3(root) ? root : resolve3(root);
|
|
3376
|
+
const graph = buildGraph(nodes, edges, scanDiagnostics, absoluteRoot);
|
|
2982
3377
|
return resolveMatrixEdges(graph);
|
|
2983
3378
|
}
|
|
2984
3379
|
function artifactTypeEntriesBySpecificity(schema) {
|
|
@@ -3318,23 +3713,34 @@ async function validateScenarioPrdLinkIndex(root, graph) {
|
|
|
3318
3713
|
function validateCodeCommentTraceabilityFormat(graph) {
|
|
3319
3714
|
const issues = [];
|
|
3320
3715
|
for (const node of graph.nodes) {
|
|
3321
|
-
if (node.type !== "test") {
|
|
3716
|
+
if (node.type !== "test" && node.type !== "implementation") {
|
|
3322
3717
|
continue;
|
|
3323
3718
|
}
|
|
3324
3719
|
const invalidComments = node.attrs?.invalidTraceabilityComments;
|
|
3325
|
-
if (
|
|
3326
|
-
|
|
3720
|
+
if (Array.isArray(invalidComments)) {
|
|
3721
|
+
for (const invalid of invalidComments) {
|
|
3722
|
+
const line = typeof invalid?.line === "number" ? invalid.line : node.line;
|
|
3723
|
+
const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
|
|
3724
|
+
issues.push(issue(
|
|
3725
|
+
"CODE_COMMENT_TRACEABILITY_FORMAT",
|
|
3726
|
+
`${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
|
|
3727
|
+
node.path,
|
|
3728
|
+
line,
|
|
3729
|
+
{ node: node.uid }
|
|
3730
|
+
));
|
|
3731
|
+
}
|
|
3327
3732
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3733
|
+
const deprecatedComments = node.attrs?.deprecatedTraceabilityComments;
|
|
3734
|
+
if (Array.isArray(deprecatedComments)) {
|
|
3735
|
+
for (const deprecated of deprecatedComments) {
|
|
3736
|
+
issues.push(issue(
|
|
3737
|
+
"E2E-TRACE-007",
|
|
3738
|
+
"@tc is deprecated; use @e2e_test instead",
|
|
3739
|
+
node.path,
|
|
3740
|
+
typeof deprecated?.line === "number" ? deprecated.line : node.line,
|
|
3741
|
+
{ node: node.uid, severity: "warning" }
|
|
3742
|
+
));
|
|
3743
|
+
}
|
|
3338
3744
|
}
|
|
3339
3745
|
}
|
|
3340
3746
|
return issues;
|
|
@@ -3816,13 +4222,16 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3816
4222
|
const isTest = isTestFile(path);
|
|
3817
4223
|
const nodeType = isTest ? "test" : "implementation";
|
|
3818
4224
|
const edgeKind = isTest ? "verifies" : "implements";
|
|
4225
|
+
const attrs = {};
|
|
4226
|
+
if (traceabilityComments.invalid.length > 0) attrs.invalidTraceabilityComments = traceabilityComments.invalid;
|
|
4227
|
+
if (traceabilityComments.deprecated.length > 0) attrs.deprecatedTraceabilityComments = traceabilityComments.deprecated;
|
|
3819
4228
|
const node = {
|
|
3820
4229
|
type: nodeType,
|
|
3821
4230
|
code: path,
|
|
3822
4231
|
title: path.split("/").at(-1) ?? path,
|
|
3823
4232
|
path,
|
|
3824
4233
|
line: 1,
|
|
3825
|
-
attrs
|
|
4234
|
+
attrs
|
|
3826
4235
|
};
|
|
3827
4236
|
let hasTags = false;
|
|
3828
4237
|
for (const { tags, lineNumber } of traceabilityComments.canonical) {
|
|
@@ -3833,7 +4242,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3833
4242
|
}
|
|
3834
4243
|
}
|
|
3835
4244
|
}
|
|
3836
|
-
if (hasTags || traceabilityComments.invalid.length > 0) {
|
|
4245
|
+
if (hasTags || traceabilityComments.invalid.length > 0 || traceabilityComments.deprecated.length > 0) {
|
|
3837
4246
|
nodes.push(node);
|
|
3838
4247
|
}
|
|
3839
4248
|
return { nodes, edges };
|
|
@@ -3841,6 +4250,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3841
4250
|
function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
3842
4251
|
const canonical = [];
|
|
3843
4252
|
const invalid = [];
|
|
4253
|
+
const deprecated = [];
|
|
3844
4254
|
for (const comment of scanCodeComments(raw)) {
|
|
3845
4255
|
if (!containsTraceabilityTag(comment.text, schema)) {
|
|
3846
4256
|
continue;
|
|
@@ -3856,6 +4266,9 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3856
4266
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3857
4267
|
if (parsed.valid) {
|
|
3858
4268
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4269
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4270
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4271
|
+
}
|
|
3859
4272
|
} else {
|
|
3860
4273
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3861
4274
|
}
|
|
@@ -3871,11 +4284,14 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3871
4284
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3872
4285
|
if (parsed.valid) {
|
|
3873
4286
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4287
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4288
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4289
|
+
}
|
|
3874
4290
|
} else {
|
|
3875
4291
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3876
4292
|
}
|
|
3877
4293
|
}
|
|
3878
|
-
return { canonical, invalid };
|
|
4294
|
+
return { canonical, invalid, deprecated };
|
|
3879
4295
|
}
|
|
3880
4296
|
function scanCodeComments(raw) {
|
|
3881
4297
|
const comments = [];
|
|
@@ -4042,7 +4458,14 @@ function expandCodeRange(value) {
|
|
|
4042
4458
|
return Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${String(start + index).padStart(width, "0")}`);
|
|
4043
4459
|
}
|
|
4044
4460
|
function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
4045
|
-
|
|
4461
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4462
|
+
for (const [type, definition] of Object.entries(schema.types)) {
|
|
4463
|
+
tokens.add(type);
|
|
4464
|
+
for (const alias of definition.aliases ?? []) tokens.add(alias);
|
|
4465
|
+
}
|
|
4466
|
+
if (tokens.size === 0) return false;
|
|
4467
|
+
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
4468
|
+
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
4046
4469
|
}
|
|
4047
4470
|
function parseDesign(path, raw) {
|
|
4048
4471
|
const parsed = matter(raw);
|
|
@@ -5047,6 +5470,29 @@ function validateE2eTests(graph) {
|
|
|
5047
5470
|
issues.push(issue("E2E_AC_UNKNOWN", `${node.uid} references unknown AC ${reference.feature}(${reference.ac})`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5048
5471
|
}
|
|
5049
5472
|
}
|
|
5473
|
+
const tcStatus = String(fields["status"] ?? "").trim().toLowerCase();
|
|
5474
|
+
if (tcStatus && !VALID_TC_STATUSES.has(tcStatus)) {
|
|
5475
|
+
issues.push(issue("E2E_INVALID_TC_STATUS", `${node.uid} has invalid TC status "${tcStatus}"; allowed: ${[...VALID_TC_STATUSES].join(", ")}`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5476
|
+
}
|
|
5477
|
+
if (tcStatus === "waived") {
|
|
5478
|
+
const reason = String(fields["waived_reason"] ?? "").trim();
|
|
5479
|
+
if (!reason) {
|
|
5480
|
+
issues.push(issue("E2E_WAIVED_NO_REASON", `${node.uid} has status "waived" but no waived_reason`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
const rawChainType = String(fields["chain_type"] ?? "").trim();
|
|
5484
|
+
const chainType = rawChainType.toLowerCase();
|
|
5485
|
+
if (rawChainType) {
|
|
5486
|
+
if (!VALID_CHAIN_TYPES.has(chainType) && !(chainType in DEPRECATED_CHAIN_TYPE_ALIASES)) {
|
|
5487
|
+
issues.push(issue("E2E_INVALID_CHAIN_TYPE", `${node.uid} has invalid chain_type "${rawChainType}"; allowed: ${[...VALID_CHAIN_TYPES].join(", ")}`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5488
|
+
} else if (chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5489
|
+
issues.push(issue("E2E_DEPRECATED_CHAIN_TYPE", `${node.uid} uses deprecated chain_type "${rawChainType}"; migrate to "${DEPRECATED_CHAIN_TYPE_ALIASES[chainType]}"`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
const rawAcCoverageRate = String(fields["ac_coverage_rate"] ?? "").trim();
|
|
5493
|
+
if (rawAcCoverageRate) {
|
|
5494
|
+
issues.push(issue("E2E_AC_COVERAGE_RATE_FREETEXT", `${node.uid} has handwritten ac_coverage_rate "${rawAcCoverageRate}"; this field must be derived from ac_coverage and the feature acceptance-criteria inventory`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5495
|
+
}
|
|
5050
5496
|
if (needsDesktopChainWarning(node)) {
|
|
5051
5497
|
issues.push(issue("E2E_DESKTOP_CHAIN_WARNING", `${node.uid} appears desktop-related but does not cover the full React/UI -> Tauri/IPC -> Node sidecar/JSON Lines -> core/engine -> SQLite/report data \u771F\u5B9E\u684C\u9762\u94FE\u8DEF`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5052
5498
|
}
|
|
@@ -5105,10 +5551,10 @@ function validateE2eRegistry(graph) {
|
|
|
5105
5551
|
}
|
|
5106
5552
|
return issues;
|
|
5107
5553
|
}
|
|
5108
|
-
async function validateExecutableTraceability(root) {
|
|
5554
|
+
async function validateExecutableTraceability(root, config) {
|
|
5109
5555
|
const issues = [];
|
|
5556
|
+
const schema = config ?? await loadConfig(root);
|
|
5110
5557
|
const e2eDir = join5(root, "artifacts", "tests", "e2e");
|
|
5111
|
-
const specPatterns = ["heimdall/**/*.spec.ts", "heimdall/**/*.e2e.spec.ts"];
|
|
5112
5558
|
let e2eFiles;
|
|
5113
5559
|
try {
|
|
5114
5560
|
e2eFiles = (await readdir(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join5(e2eDir, name));
|
|
@@ -5151,16 +5597,23 @@ async function validateExecutableTraceability(root) {
|
|
|
5151
5597
|
}
|
|
5152
5598
|
const allFiles = await walk(root);
|
|
5153
5599
|
const specFiles = /* @__PURE__ */ new Set();
|
|
5154
|
-
|
|
5600
|
+
const configuredRunners = schema.e2e?.runners ?? [];
|
|
5601
|
+
if (configuredRunners.length > 0) {
|
|
5155
5602
|
for (const file of allFiles) {
|
|
5156
|
-
if (
|
|
5603
|
+
if (configuredRunners.some((runner) => isRunnerIncludeCandidate(file, runner))) {
|
|
5604
|
+
specFiles.add(file);
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
} else {
|
|
5608
|
+
for (const file of allFiles) {
|
|
5609
|
+
if (/\.(?:e2e\.)?spec\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(file)) {
|
|
5157
5610
|
specFiles.add(file);
|
|
5158
5611
|
}
|
|
5159
5612
|
}
|
|
5160
5613
|
}
|
|
5161
5614
|
const refToSource = /* @__PURE__ */ new Map();
|
|
5162
|
-
const tcAnnotationRegex = /\/\/!?\s*@tc\s+(\S+?)\s+\[(\w+)\]/;
|
|
5163
|
-
const tcAnnotationNoLevelRegex = /\/\/!?\s*@tc\s+(\S+)/;
|
|
5615
|
+
const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
|
|
5616
|
+
const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
|
|
5164
5617
|
for (const specFile of specFiles) {
|
|
5165
5618
|
const fullSpecPath = join5(root, specFile);
|
|
5166
5619
|
let content;
|
|
@@ -5224,12 +5677,21 @@ async function validateExecutableTraceability(root) {
|
|
|
5224
5677
|
const refEntries = parseExecutableRefLines(ref);
|
|
5225
5678
|
const validFiles = [];
|
|
5226
5679
|
for (const entry of refEntries) {
|
|
5227
|
-
const normalizedRefFile =
|
|
5228
|
-
const fileExists = specFiles.has(normalizedRefFile);
|
|
5680
|
+
const normalizedRefFile = resolveExecutableRefFile(entry.file, allFiles);
|
|
5681
|
+
const fileExists = normalizedRefFile !== void 0 && specFiles.has(normalizedRefFile);
|
|
5229
5682
|
if (!fileExists) {
|
|
5230
5683
|
issues.push(issue("E2E-TRACE-001", `executable_ref target file not found: ${entry.file}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5231
5684
|
continue;
|
|
5232
5685
|
}
|
|
5686
|
+
const runners = schema.e2e?.runners ?? [];
|
|
5687
|
+
if (runners.length > 0) {
|
|
5688
|
+
const acceptingRunners = await getAcceptingRunners(root, normalizedRefFile, runners);
|
|
5689
|
+
const hasE2eRunner = acceptingRunners.some((r) => r.kind === "e2e" || r.kind === "integration");
|
|
5690
|
+
const hasUnitRunner = acceptingRunners.some((r) => r.kind === "unit");
|
|
5691
|
+
if (hasUnitRunner && !hasE2eRunner && acceptingRunners.length > 0) {
|
|
5692
|
+
issues.push(issue("E2E-UNIT-TEST-NOT-E2E", `executable_ref target ${entry.file} is only accepted by unit runner(s) [${acceptingRunners.map((r) => r.name).join(", ")}], not by any e2e/integration runner`, path, line, { node: tcKey, severity: "warning" }));
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5233
5695
|
if (entry.testId) {
|
|
5234
5696
|
let content;
|
|
5235
5697
|
try {
|
|
@@ -5244,12 +5706,9 @@ async function validateExecutableTraceability(root) {
|
|
|
5244
5706
|
}
|
|
5245
5707
|
}
|
|
5246
5708
|
const annotationsForTc = refToSource.get(tcKey);
|
|
5247
|
-
const hasAnnotationInFile = annotationsForTc?.some((ann) =>
|
|
5248
|
-
const normalizedAnnFile = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5249
|
-
return normalizedAnnFile === normalizedRefFile;
|
|
5250
|
-
}) ?? false;
|
|
5709
|
+
const hasAnnotationInFile = annotationsForTc?.some((ann) => ann.file === normalizedRefFile) ?? false;
|
|
5251
5710
|
if (!hasAnnotationInFile) {
|
|
5252
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no
|
|
5711
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5253
5712
|
continue;
|
|
5254
5713
|
}
|
|
5255
5714
|
validFiles.push(normalizedRefFile);
|
|
@@ -5260,13 +5719,13 @@ async function validateExecutableTraceability(root) {
|
|
|
5260
5719
|
const batch = tcKey.split(":")[0];
|
|
5261
5720
|
if (!mdBatches.has(batch)) {
|
|
5262
5721
|
for (const ann of annotations) {
|
|
5263
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5722
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5264
5723
|
}
|
|
5265
5724
|
continue;
|
|
5266
5725
|
}
|
|
5267
5726
|
if (!mdToRef.has(tcKey) && !await hasMarkdownTc(tcKey, e2eDir)) {
|
|
5268
5727
|
for (const ann of annotations) {
|
|
5269
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5728
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5270
5729
|
}
|
|
5271
5730
|
}
|
|
5272
5731
|
}
|
|
@@ -5285,24 +5744,37 @@ async function validateExecutableTraceability(root) {
|
|
|
5285
5744
|
if (matchesAnyRef) {
|
|
5286
5745
|
continue;
|
|
5287
5746
|
}
|
|
5288
|
-
const primaryRef = refEntries[0];
|
|
5289
|
-
const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
|
|
5290
5747
|
const detail = `file: MD refs=[${refEntries.map((e) => e.file).join(", ")}] vs source=${ann.file}`;
|
|
5291
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194
|
|
5748
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5292
5749
|
}
|
|
5293
5750
|
}
|
|
5294
|
-
for (const [tcKey, { chainType, path, line }] of
|
|
5295
|
-
|
|
5751
|
+
for (const [tcKey, { chainType, path, line }] of allMdTcInfo) {
|
|
5752
|
+
const tcFields = tcKeyToFields.get(tcKey);
|
|
5753
|
+
const explicitChainType = String(tcFields?.["chain_type"] ?? "").trim().toLowerCase();
|
|
5754
|
+
if (explicitChainType !== "desktop_chain") {
|
|
5296
5755
|
continue;
|
|
5297
5756
|
}
|
|
5757
|
+
if (!mdToRef.has(tcKey)) {
|
|
5758
|
+
issues.push(issue("E2E-DESKTOP-CHAIN-MISSING", `desktop_chain TC ${tcKey} has no executable_ref`, path, line, { node: tcKey, severity: "warning" }));
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
for (const [tcKey, { chainType, path, line }] of mdToRef) {
|
|
5762
|
+
const normalizedDeclaredChainType = chainType.trim().toLowerCase();
|
|
5763
|
+
const hasLegalNonDesktopDeclaration = normalizedDeclaredChainType.length > 0 && VALID_CHAIN_TYPES.has(normalizedDeclaredChainType) && normalizedDeclaredChainType !== "desktop_chain";
|
|
5764
|
+
if (hasLegalNonDesktopDeclaration) continue;
|
|
5298
5765
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5299
5766
|
const sourceAnnotations = refToSource.get(tcKey);
|
|
5300
5767
|
if (!sourceAnnotations) {
|
|
5301
5768
|
continue;
|
|
5302
5769
|
}
|
|
5770
|
+
const tcFields = tcKeyToFields.get(tcKey);
|
|
5771
|
+
const explicitChainType = String(tcFields?.["chain_type"] ?? "").trim().toLowerCase();
|
|
5772
|
+
const hasExplicitDesktopChain = explicitChainType === "desktop_chain";
|
|
5773
|
+
if (hasExplicitDesktopChain) {
|
|
5774
|
+
continue;
|
|
5775
|
+
}
|
|
5303
5776
|
for (const ann of sourceAnnotations) {
|
|
5304
|
-
|
|
5305
|
-
if (!validFiles.has(normalizedAnnFile)) {
|
|
5777
|
+
if (!validFiles.has(ann.file)) {
|
|
5306
5778
|
continue;
|
|
5307
5779
|
}
|
|
5308
5780
|
if (ann.level === "mock_playwright") {
|
|
@@ -5328,10 +5800,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5328
5800
|
continue;
|
|
5329
5801
|
}
|
|
5330
5802
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5331
|
-
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) =>
|
|
5332
|
-
const normalized = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5333
|
-
return validFiles.has(normalized);
|
|
5334
|
-
});
|
|
5803
|
+
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) => validFiles.has(ann.file));
|
|
5335
5804
|
const hasDesktopChain = sourceAnnotations?.some((ann) => ann.level === "desktop_chain") ?? false;
|
|
5336
5805
|
const hasBridge = sourceAnnotations?.some((ann) => ann.level === "ui_sidecar_bridge") ?? false;
|
|
5337
5806
|
if (isComplete) {
|
|
@@ -5340,7 +5809,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5340
5809
|
if (hasDesktopChain) {
|
|
5341
5810
|
hasValidEvidence = true;
|
|
5342
5811
|
} else if (hasBridge) {
|
|
5343
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5812
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5344
5813
|
if (partialResult.hasValidPartialRust) {
|
|
5345
5814
|
hasValidEvidence = true;
|
|
5346
5815
|
} else {
|
|
@@ -5380,7 +5849,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5380
5849
|
}
|
|
5381
5850
|
let partialDetail = "";
|
|
5382
5851
|
if (hasBridge) {
|
|
5383
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5852
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5384
5853
|
if (partialResult.hasValidPartialRust) {
|
|
5385
5854
|
continue;
|
|
5386
5855
|
}
|
|
@@ -5397,6 +5866,287 @@ async function validateExecutableTraceability(root) {
|
|
|
5397
5866
|
}
|
|
5398
5867
|
return issues;
|
|
5399
5868
|
}
|
|
5869
|
+
async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
5870
|
+
const e2eNodes = graph.nodes.filter((n) => n.type === "e2e_test" && n.attrs?.fileLevelOnly !== true);
|
|
5871
|
+
const totalTestCases = e2eNodes.length;
|
|
5872
|
+
let withExecutableRef = 0;
|
|
5873
|
+
const statusBreakdown = {};
|
|
5874
|
+
const chainTypeBreakdown = {};
|
|
5875
|
+
const e2eDir = join5(root, "artifacts", "tests", "e2e");
|
|
5876
|
+
const tcFieldsMap = /* @__PURE__ */ new Map();
|
|
5877
|
+
let e2eFiles;
|
|
5878
|
+
try {
|
|
5879
|
+
e2eFiles = (await readdir(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join5(e2eDir, name));
|
|
5880
|
+
} catch {
|
|
5881
|
+
e2eFiles = [];
|
|
5882
|
+
}
|
|
5883
|
+
for (const filePath of e2eFiles) {
|
|
5884
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
5885
|
+
const lines = raw.split(/\r?\n/);
|
|
5886
|
+
const tcStarts = [];
|
|
5887
|
+
lines.forEach((line, index) => {
|
|
5888
|
+
const match = /^#{2,3}\s+(TC-\d+[a-z]?)\s*[::]?\s*.*$/.exec(line);
|
|
5889
|
+
if (match) {
|
|
5890
|
+
tcStarts.push({ id: match[1], index });
|
|
5891
|
+
}
|
|
5892
|
+
});
|
|
5893
|
+
const parsed = matter(raw);
|
|
5894
|
+
const batch = String(parsed.data.test_batch ?? basename2(filePath, extname(filePath))).trim();
|
|
5895
|
+
for (let i = 0; i < tcStarts.length; i++) {
|
|
5896
|
+
const start = tcStarts[i];
|
|
5897
|
+
const end = tcStarts[i + 1]?.index ?? lines.length;
|
|
5898
|
+
const block = lines.slice(start.index, end);
|
|
5899
|
+
const fields = extractE2eTcFields(block);
|
|
5900
|
+
tcFieldsMap.set(`${batch}:${start.id}`, fields);
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
for (const node of e2eNodes) {
|
|
5904
|
+
const tcKey = node.code;
|
|
5905
|
+
const fields = tcFieldsMap.get(tcKey) ?? asRecord(node.attrs?.tcFields);
|
|
5906
|
+
const execRef = String(fields["executable_ref"] ?? "").trim();
|
|
5907
|
+
if (execRef && !isPendingExecutableRef(execRef)) {
|
|
5908
|
+
withExecutableRef++;
|
|
5909
|
+
}
|
|
5910
|
+
const status = String(fields["status"] ?? "created").trim().toLowerCase();
|
|
5911
|
+
statusBreakdown[status] = (statusBreakdown[status] ?? 0) + 1;
|
|
5912
|
+
const chainType = String(fields["chain_type"] ?? "").trim().toLowerCase() || "unspecified";
|
|
5913
|
+
chainTypeBreakdown[chainType] = (chainTypeBreakdown[chainType] ?? 0) + 1;
|
|
5914
|
+
}
|
|
5915
|
+
const executableRefRate = totalTestCases > 0 ? `${withExecutableRef}/${totalTestCases} (${(withExecutableRef / totalTestCases * 100).toFixed(1)}%)` : "0/0";
|
|
5916
|
+
const scenarioNodes = graph.nodes.filter((n) => n.type === "scenario");
|
|
5917
|
+
const featureNodes = graph.nodes.filter((n) => n.type === "feature");
|
|
5918
|
+
const linkedScenarios = /* @__PURE__ */ new Set();
|
|
5919
|
+
const linkedFeatures = /* @__PURE__ */ new Set();
|
|
5920
|
+
const acCoveredScenarios = /* @__PURE__ */ new Set();
|
|
5921
|
+
const acCoveredFeatures = /* @__PURE__ */ new Set();
|
|
5922
|
+
const verifiedScenarios = /* @__PURE__ */ new Set();
|
|
5923
|
+
const verifiedFeatures = /* @__PURE__ */ new Set();
|
|
5924
|
+
for (const edge2 of graph.edges) {
|
|
5925
|
+
if (edge2.kind === "verifies" && edge2.from.startsWith("e2e_test:")) {
|
|
5926
|
+
if (edge2.to.startsWith("scenario:")) {
|
|
5927
|
+
linkedScenarios.add(edge2.to.replace("scenario:", ""));
|
|
5928
|
+
}
|
|
5929
|
+
if (edge2.to.startsWith("feature:")) {
|
|
5930
|
+
linkedFeatures.add(edge2.to.replace("feature:", ""));
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
for (const node of e2eNodes) {
|
|
5935
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
5936
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
5937
|
+
for (const feature of Object.keys(acCoverage)) {
|
|
5938
|
+
acCoveredFeatures.add(feature);
|
|
5939
|
+
}
|
|
5940
|
+
const relatedScenarios = toArray(fields["related_scenarios"] ?? node.attrs?.related_scenarios).map(String);
|
|
5941
|
+
for (const scenario of relatedScenarios) {
|
|
5942
|
+
acCoveredScenarios.add(scenario);
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
const runners = (await loadConfig(root)).e2e?.runners ?? [];
|
|
5946
|
+
const allProjectFiles = await walk(root);
|
|
5947
|
+
for (const node of e2eNodes) {
|
|
5948
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
5949
|
+
const status = String(fields["status"] ?? "").trim().toLowerCase();
|
|
5950
|
+
const execRef = String(fields["executable_ref"] ?? "").trim();
|
|
5951
|
+
if (status !== "verified") continue;
|
|
5952
|
+
if (!execRef || isPendingExecutableRef(execRef)) continue;
|
|
5953
|
+
if (runners.length === 0) continue;
|
|
5954
|
+
let hasActiveE2eRef = false;
|
|
5955
|
+
for (const entry of parseExecutableRefLines(execRef)) {
|
|
5956
|
+
const normalized = resolveExecutableRefFile(entry.file, allProjectFiles);
|
|
5957
|
+
if (!normalized || !existsSync2(join5(root, normalized))) continue;
|
|
5958
|
+
const accepting = await getAcceptingRunners(root, normalized, runners);
|
|
5959
|
+
if (accepting.some((runner) => runner.kind === "e2e")) {
|
|
5960
|
+
hasActiveE2eRef = true;
|
|
5961
|
+
break;
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
if (!hasActiveE2eRef) continue;
|
|
5965
|
+
const relatedScenarios = toArray(fields["related_scenarios"] ?? node.attrs?.related_scenarios).map(String);
|
|
5966
|
+
for (const scenario of relatedScenarios) {
|
|
5967
|
+
verifiedScenarios.add(scenario);
|
|
5968
|
+
}
|
|
5969
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
5970
|
+
for (const feature of Object.keys(acCoverage)) {
|
|
5971
|
+
verifiedFeatures.add(feature);
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5974
|
+
const scenarioWaivers = new Set((thresholds.scenarioWaivers ?? []).map((w) => w.id));
|
|
5975
|
+
const featureWaivers = new Set((thresholds.featureWaivers ?? []).map((w) => w.id));
|
|
5976
|
+
const uncoveredScenarios = scenarioNodes.map((n) => n.code).filter((code) => !linkedScenarios.has(code) && !scenarioWaivers.has(code));
|
|
5977
|
+
const uncoveredFeatures = featureNodes.map((n) => n.code).filter((code) => !acCoveredFeatures.has(code) && !featureWaivers.has(code));
|
|
5978
|
+
const scenarioCoverage = {};
|
|
5979
|
+
for (const node of scenarioNodes) {
|
|
5980
|
+
scenarioCoverage[node.code] = {
|
|
5981
|
+
linked: linkedScenarios.has(node.code),
|
|
5982
|
+
acCovered: acCoveredScenarios.has(node.code),
|
|
5983
|
+
waived: scenarioWaivers.has(node.code),
|
|
5984
|
+
verified: !scenarioWaivers.has(node.code) && verifiedScenarios.has(node.code)
|
|
5985
|
+
};
|
|
5986
|
+
}
|
|
5987
|
+
const featureCoverage = {};
|
|
5988
|
+
for (const node of featureNodes) {
|
|
5989
|
+
featureCoverage[node.code] = {
|
|
5990
|
+
linked: linkedFeatures.has(node.code),
|
|
5991
|
+
acCovered: acCoveredFeatures.has(node.code),
|
|
5992
|
+
waived: featureWaivers.has(node.code),
|
|
5993
|
+
verified: !featureWaivers.has(node.code) && verifiedFeatures.has(node.code)
|
|
5994
|
+
};
|
|
5995
|
+
}
|
|
5996
|
+
const thresholdWarnings = [];
|
|
5997
|
+
const thresholdErrors = [];
|
|
5998
|
+
const warningRate = thresholds.executableRefWarning;
|
|
5999
|
+
const errorRate = thresholds.executableRefError;
|
|
6000
|
+
const actualRate = totalTestCases > 0 ? withExecutableRef / totalTestCases : 1;
|
|
6001
|
+
if (warningRate !== void 0 && actualRate < warningRate) {
|
|
6002
|
+
thresholdWarnings.push(`executable_ref coverage ${executableRefRate} < warning threshold ${(warningRate * 100).toFixed(0)}%`);
|
|
6003
|
+
}
|
|
6004
|
+
if (errorRate !== void 0 && actualRate < errorRate) {
|
|
6005
|
+
thresholdErrors.push(`executable_ref coverage ${executableRefRate} < error threshold ${(errorRate * 100).toFixed(0)}%`);
|
|
6006
|
+
}
|
|
6007
|
+
if (thresholds.reportUncoveredScenarios !== false && uncoveredScenarios.length > 0) {
|
|
6008
|
+
thresholdWarnings.push(`${uncoveredScenarios.length} scenario(s) have no E2E coverage: ${uncoveredScenarios.join(", ")}`);
|
|
6009
|
+
}
|
|
6010
|
+
if (thresholds.reportUncoveredFeatures !== false && uncoveredFeatures.length > 0) {
|
|
6011
|
+
thresholdWarnings.push(`${uncoveredFeatures.length} feature(s) have no E2E coverage: ${uncoveredFeatures.join(", ")}`);
|
|
6012
|
+
}
|
|
6013
|
+
const acCoverageRateByFeature = {};
|
|
6014
|
+
const featureAcMap = /* @__PURE__ */ new Map();
|
|
6015
|
+
for (const node of featureNodes) {
|
|
6016
|
+
const acs = parseAcceptanceCriteria(await readFile2(join5(root, node.path), "utf-8"));
|
|
6017
|
+
featureAcMap.set(node.code, new Set(acs));
|
|
6018
|
+
}
|
|
6019
|
+
const coveredAcByFeature = /* @__PURE__ */ new Map();
|
|
6020
|
+
for (const node of e2eNodes) {
|
|
6021
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
6022
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
6023
|
+
for (const [feature, acs] of Object.entries(acCoverage)) {
|
|
6024
|
+
const existing = coveredAcByFeature.get(feature) ?? /* @__PURE__ */ new Set();
|
|
6025
|
+
for (const ac of toArray(acs)) {
|
|
6026
|
+
existing.add(String(ac));
|
|
6027
|
+
}
|
|
6028
|
+
coveredAcByFeature.set(feature, existing);
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
for (const [feature, allAcs] of featureAcMap) {
|
|
6032
|
+
const denominator = allAcs.size;
|
|
6033
|
+
if (denominator === 0) continue;
|
|
6034
|
+
const coveredAcs = coveredAcByFeature.get(feature) ?? /* @__PURE__ */ new Set();
|
|
6035
|
+
const numerator = [...coveredAcs].filter((ac) => allAcs.has(ac)).length;
|
|
6036
|
+
acCoverageRateByFeature[feature] = {
|
|
6037
|
+
numerator,
|
|
6038
|
+
denominator,
|
|
6039
|
+
rate: denominator > 0 ? numerator / denominator : 0
|
|
6040
|
+
};
|
|
6041
|
+
}
|
|
6042
|
+
return {
|
|
6043
|
+
totalTestCases,
|
|
6044
|
+
withExecutableRef,
|
|
6045
|
+
executableRefRate,
|
|
6046
|
+
statusBreakdown,
|
|
6047
|
+
chainTypeBreakdown,
|
|
6048
|
+
uncoveredScenarios,
|
|
6049
|
+
uncoveredFeatures,
|
|
6050
|
+
thresholdWarnings,
|
|
6051
|
+
thresholdErrors,
|
|
6052
|
+
acCoverageRateByFeature,
|
|
6053
|
+
scenarioCoverage,
|
|
6054
|
+
featureCoverage
|
|
6055
|
+
};
|
|
6056
|
+
}
|
|
6057
|
+
async function generateE2eRegistry(root, opts) {
|
|
6058
|
+
const e2eDir = join5(root, "artifacts", "tests", "e2e");
|
|
6059
|
+
let files;
|
|
6060
|
+
try {
|
|
6061
|
+
files = (await readdir(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).sort();
|
|
6062
|
+
} catch {
|
|
6063
|
+
return {
|
|
6064
|
+
registry_version: "1.0",
|
|
6065
|
+
generated_at: opts?.deterministic ? "1970-01-01T00:00:00.000Z" : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6066
|
+
total_batches: 0,
|
|
6067
|
+
total_test_cases: 0,
|
|
6068
|
+
batches: []
|
|
6069
|
+
};
|
|
6070
|
+
}
|
|
6071
|
+
const batches = [];
|
|
6072
|
+
let totalTestCases = 0;
|
|
6073
|
+
for (const file of files) {
|
|
6074
|
+
const filePath = join5(e2eDir, file);
|
|
6075
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
6076
|
+
const parsed = matter(raw);
|
|
6077
|
+
const data = parsed.data;
|
|
6078
|
+
const batch = String(data.test_batch ?? basename2(file, extname(file))).trim();
|
|
6079
|
+
const relPath = `artifacts/tests/e2e/${file}`;
|
|
6080
|
+
const scope = String(data.scope ?? "").trim();
|
|
6081
|
+
const acCoverage = normalizeAcCoverageForRegistry(data.ac_coverage);
|
|
6082
|
+
const relatedScenarios = toArray(data.related_scenarios).map(String).filter(Boolean);
|
|
6083
|
+
const lines = raw.split(/\r?\n/);
|
|
6084
|
+
const tcStarts = [];
|
|
6085
|
+
lines.forEach((line, index) => {
|
|
6086
|
+
const match = /^#{2,3}\s+(TC-\d+[a-z]?)\s*[::]?\s*.*$/.exec(line);
|
|
6087
|
+
if (match) {
|
|
6088
|
+
tcStarts.push({ id: match[1], index });
|
|
6089
|
+
}
|
|
6090
|
+
});
|
|
6091
|
+
const statusSummary = {};
|
|
6092
|
+
const blockingReasons = {};
|
|
6093
|
+
const frontmatterFixesBlock = String(data.fixes_block ?? "").trim();
|
|
6094
|
+
if (frontmatterFixesBlock && /no\s+(test\s+file|e2e)/i.test(frontmatterFixesBlock)) {
|
|
6095
|
+
for (const tc of tcStarts) {
|
|
6096
|
+
blockingReasons[tc.id] = frontmatterFixesBlock;
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
for (const start of tcStarts) {
|
|
6100
|
+
const end = tcStarts[tcStarts.indexOf(start) + 1]?.index ?? lines.length;
|
|
6101
|
+
const block = lines.slice(start.index, end);
|
|
6102
|
+
const fields = extractE2eTcFields(block);
|
|
6103
|
+
const status = String(fields["status"] ?? "created").trim().toLowerCase() || "created";
|
|
6104
|
+
statusSummary[status] = (statusSummary[status] ?? 0) + 1;
|
|
6105
|
+
if (status === "created" && !blockingReasons[start.id]) {
|
|
6106
|
+
const executableRef = String(fields["executable_ref"] ?? "").trim();
|
|
6107
|
+
const chainType = String(fields["chain_type"] ?? "").trim().toLowerCase();
|
|
6108
|
+
if (!executableRef && chainType === "desktop_chain") {
|
|
6109
|
+
blockingReasons[start.id] = "desktop_chain TC requires executable_ref";
|
|
6110
|
+
} else if (isPendingExecutableRef(executableRef)) {
|
|
6111
|
+
blockingReasons[start.id] = `pending: ${executableRef}`;
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
const testCaseCount = tcStarts.length;
|
|
6116
|
+
totalTestCases += testCaseCount;
|
|
6117
|
+
const batchStatus = Object.keys(blockingReasons).length > 0 ? "blocked" : void 0;
|
|
6118
|
+
batches.push({
|
|
6119
|
+
batch_id: batch,
|
|
6120
|
+
file: relPath,
|
|
6121
|
+
scope,
|
|
6122
|
+
ac_coverage: acCoverage,
|
|
6123
|
+
related_scenarios: relatedScenarios,
|
|
6124
|
+
test_case_count: testCaseCount,
|
|
6125
|
+
status_summary: statusSummary,
|
|
6126
|
+
status: batchStatus,
|
|
6127
|
+
blocking_reasons: Object.keys(blockingReasons).length > 0 ? blockingReasons : void 0
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
6130
|
+
return {
|
|
6131
|
+
registry_version: "1.0",
|
|
6132
|
+
generated_at: opts?.deterministic ? "1970-01-01T00:00:00.000Z" : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6133
|
+
total_batches: batches.length,
|
|
6134
|
+
total_test_cases: totalTestCases,
|
|
6135
|
+
batches
|
|
6136
|
+
};
|
|
6137
|
+
}
|
|
6138
|
+
function normalizeAcCoverageForRegistry(value) {
|
|
6139
|
+
if (!value || typeof value !== "object") return {};
|
|
6140
|
+
const result = {};
|
|
6141
|
+
for (const [key, val] of Object.entries(value)) {
|
|
6142
|
+
if (Array.isArray(val)) {
|
|
6143
|
+
result[key] = val.map(String);
|
|
6144
|
+
} else if (typeof val === "string") {
|
|
6145
|
+
result[key] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
return result;
|
|
6149
|
+
}
|
|
5400
6150
|
function isPendingExecutableRef(ref) {
|
|
5401
6151
|
const stripped = ref.replace(/^[\s-*()]+/, "").trim();
|
|
5402
6152
|
return /^pending\b/i.test(stripped);
|
|
@@ -5414,6 +6164,20 @@ function parseExecutableRefLines(ref) {
|
|
|
5414
6164
|
}
|
|
5415
6165
|
return results;
|
|
5416
6166
|
}
|
|
6167
|
+
var VALID_TC_STATUSES = /* @__PURE__ */ new Set(["created", "automated", "verified", "waived"]);
|
|
6168
|
+
var VALID_CHAIN_TYPES = /* @__PURE__ */ new Set([
|
|
6169
|
+
"desktop_chain",
|
|
6170
|
+
"mock_playwright",
|
|
6171
|
+
"core_e2e",
|
|
6172
|
+
"cli_e2e",
|
|
6173
|
+
"ui_sidecar_bridge",
|
|
6174
|
+
"partial_sidecar",
|
|
6175
|
+
"partial_rust"
|
|
6176
|
+
]);
|
|
6177
|
+
var DEPRECATED_CHAIN_TYPE_ALIASES = {
|
|
6178
|
+
core_only: "core_e2e",
|
|
6179
|
+
frontend_only: "mock_playwright"
|
|
6180
|
+
};
|
|
5417
6181
|
function isDesktopChainType(chainType) {
|
|
5418
6182
|
const normalizedChainType = chainType.trim().toLowerCase();
|
|
5419
6183
|
return normalizedChainType === "desktop_chain" || normalizedChainType === "";
|
|
@@ -5421,7 +6185,7 @@ function isDesktopChainType(chainType) {
|
|
|
5421
6185
|
function parseChainCoverageStatus(chainCoverage) {
|
|
5422
6186
|
return chainCoverage.trim().toLowerCase().match(/^[a-z_]+/)?.[0] ?? "";
|
|
5423
6187
|
}
|
|
5424
|
-
async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
6188
|
+
async function validatePartialRustEvidence(tcFields, tcKey, root, allFiles) {
|
|
5425
6189
|
const partialEvidence = String(tcFields["partial_evidence"] ?? "");
|
|
5426
6190
|
if (!partialEvidence.trim()) {
|
|
5427
6191
|
return { hasValidPartialRust: false, detail: "no partial_evidence field" };
|
|
@@ -5445,7 +6209,10 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5445
6209
|
return { hasValidPartialRust: false, detail: "no .rs file in partial_evidence" };
|
|
5446
6210
|
}
|
|
5447
6211
|
for (const ref of rustRefs) {
|
|
5448
|
-
const normalizedPath =
|
|
6212
|
+
const normalizedPath = resolveExecutableRefFile(ref.file, allFiles);
|
|
6213
|
+
if (!normalizedPath) {
|
|
6214
|
+
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
6215
|
+
}
|
|
5449
6216
|
const fullPath = join5(root, normalizedPath);
|
|
5450
6217
|
let content;
|
|
5451
6218
|
try {
|
|
@@ -5454,14 +6221,14 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5454
6221
|
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
5455
6222
|
}
|
|
5456
6223
|
const tcAnnotationPattern = new RegExp(
|
|
5457
|
-
`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
6224
|
+
`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
5458
6225
|
);
|
|
5459
6226
|
if (!tcAnnotationPattern.test(content)) {
|
|
5460
|
-
const noLevelPattern = new RegExp(`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\b`);
|
|
6227
|
+
const noLevelPattern = new RegExp(`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\b`);
|
|
5461
6228
|
if (noLevelPattern.test(content)) {
|
|
5462
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has
|
|
6229
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has E2E trace annotation ${tcKey} but not tagged [partial_rust]` };
|
|
5463
6230
|
}
|
|
5464
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no
|
|
6231
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no E2E trace annotation ${tcKey}` };
|
|
5465
6232
|
}
|
|
5466
6233
|
}
|
|
5467
6234
|
return { hasValidPartialRust: true, detail: "ok" };
|
|
@@ -5481,6 +6248,46 @@ function detectTestLevel(specFile, content) {
|
|
|
5481
6248
|
}
|
|
5482
6249
|
return "desktop_chain";
|
|
5483
6250
|
}
|
|
6251
|
+
function resolveExecutableRefFile(refFile, allFiles) {
|
|
6252
|
+
const normalized = refFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6253
|
+
if (!normalized || isAbsolute3(refFile) || normalized.split("/").includes("..")) {
|
|
6254
|
+
return void 0;
|
|
6255
|
+
}
|
|
6256
|
+
if (allFiles.includes(normalized)) {
|
|
6257
|
+
return normalized;
|
|
6258
|
+
}
|
|
6259
|
+
const suffix = `/${normalized}`;
|
|
6260
|
+
const matches = allFiles.filter((file) => file.endsWith(suffix));
|
|
6261
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
6262
|
+
}
|
|
6263
|
+
function isRunnerIncludeCandidate(filePath, runner) {
|
|
6264
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
6265
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
6266
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) {
|
|
6267
|
+
return false;
|
|
6268
|
+
}
|
|
6269
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length + 1);
|
|
6270
|
+
return runner.include.some((pattern) => matchesRunnerGlob(relativePath, pattern));
|
|
6271
|
+
}
|
|
6272
|
+
async function getAcceptingRunners(root, filePath, runners) {
|
|
6273
|
+
const accepting = [];
|
|
6274
|
+
for (const runner of runners) {
|
|
6275
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
6276
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
6277
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.startsWith(runnerRoot + "/") ? normalizedPath.slice(runnerRoot.length + 1) : normalizedPath;
|
|
6278
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) {
|
|
6279
|
+
continue;
|
|
6280
|
+
}
|
|
6281
|
+
const matchesInclude = runner.include.some((p) => matchesRunnerGlob(relativePath, p));
|
|
6282
|
+
if (!matchesInclude) continue;
|
|
6283
|
+
const matchesExclude = (runner.exclude ?? []).some((p) => matchesRunnerGlob(relativePath, p));
|
|
6284
|
+
if (matchesExclude) continue;
|
|
6285
|
+
const matchesTestIgnore = (runner.testIgnore ?? []).some((p) => matchesRunnerGlob(relativePath, p));
|
|
6286
|
+
if (matchesTestIgnore) continue;
|
|
6287
|
+
accepting.push(runner);
|
|
6288
|
+
}
|
|
6289
|
+
return accepting;
|
|
6290
|
+
}
|
|
5484
6291
|
function splitMarkdownCells(line) {
|
|
5485
6292
|
const cells = [];
|
|
5486
6293
|
let current = "";
|
|
@@ -5634,7 +6441,7 @@ function flattenAcCoverage(value) {
|
|
|
5634
6441
|
function needsDesktopChainWarning(node) {
|
|
5635
6442
|
const tcFields = asRecord(node.attrs?.tcFields);
|
|
5636
6443
|
const chainType = String(tcFields["chain_type"] ?? "").trim().toLowerCase();
|
|
5637
|
-
if (chainType
|
|
6444
|
+
if (chainType && VALID_CHAIN_TYPES.has(chainType) && chainType !== "desktop_chain" || chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5638
6445
|
return false;
|
|
5639
6446
|
}
|
|
5640
6447
|
const chainCoverage = String(tcFields["chain_coverage"] ?? "").trim().toLowerCase();
|
|
@@ -5788,9 +6595,14 @@ function mergeRecord(base, override) {
|
|
|
5788
6595
|
function mergeArtifactTypes(base, override) {
|
|
5789
6596
|
const result = { ...base };
|
|
5790
6597
|
for (const [type, definition] of Object.entries(override ?? {})) {
|
|
6598
|
+
const aliases = [
|
|
6599
|
+
...base[type]?.aliases ?? [],
|
|
6600
|
+
...definition.aliases ?? []
|
|
6601
|
+
].filter((alias, index, all) => all.indexOf(alias) === index);
|
|
5791
6602
|
result[type] = {
|
|
5792
6603
|
...base[type] ?? {},
|
|
5793
|
-
...definition
|
|
6604
|
+
...definition,
|
|
6605
|
+
...aliases.length > 0 ? { aliases } : {}
|
|
5794
6606
|
};
|
|
5795
6607
|
}
|
|
5796
6608
|
return result;
|
|
@@ -5884,6 +6696,8 @@ var TIER_ORDER = ["baseline", "target", "direct", "matrix", "transitive"];
|
|
|
5884
6696
|
function resolveArtifactContext(graph, opts) {
|
|
5885
6697
|
const mode = opts.mode ?? "full";
|
|
5886
6698
|
const maxPerCategory = opts.maxPerCategory ?? 20;
|
|
6699
|
+
const universalBaseline = opts.universalBaseline ?? true;
|
|
6700
|
+
const root = opts.root ?? graph.root;
|
|
5887
6701
|
const legacyCount = [opts.feature, opts.scenario, opts.decision, opts.design, opts.e2e_test].filter(Boolean).length;
|
|
5888
6702
|
if (opts.target && legacyCount > 0) {
|
|
5889
6703
|
return {
|
|
@@ -6036,12 +6850,80 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6036
6850
|
return "direct";
|
|
6037
6851
|
}
|
|
6038
6852
|
const pathMap = /* @__PURE__ */ new Map();
|
|
6039
|
-
|
|
6040
|
-
const
|
|
6041
|
-
|
|
6042
|
-
if (
|
|
6853
|
+
if (universalBaseline) {
|
|
6854
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6855
|
+
const existing = pathMap.get(ap.path);
|
|
6856
|
+
if (existing) {
|
|
6857
|
+
if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
|
|
6858
|
+
} else {
|
|
6859
|
+
pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
if (root) {
|
|
6863
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6864
|
+
const fullPath = join5(root, ap.path);
|
|
6865
|
+
let stat;
|
|
6866
|
+
try {
|
|
6867
|
+
stat = statSync(fullPath);
|
|
6868
|
+
} catch {
|
|
6869
|
+
stat = null;
|
|
6870
|
+
}
|
|
6871
|
+
if (!stat) {
|
|
6872
|
+
const msg = `Required baseline artifact not found: ${ap.path}`;
|
|
6873
|
+
if (!missing.includes(msg)) {
|
|
6874
|
+
missing.push(msg);
|
|
6875
|
+
missingDetails.push({
|
|
6876
|
+
ref: ap.path,
|
|
6877
|
+
from: "baseline",
|
|
6878
|
+
kind: "missing-baseline",
|
|
6879
|
+
message: msg,
|
|
6880
|
+
suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6881
|
+
});
|
|
6882
|
+
}
|
|
6883
|
+
} else if (!stat.isFile()) {
|
|
6884
|
+
const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
|
|
6885
|
+
if (!missing.includes(msg)) {
|
|
6886
|
+
missing.push(msg);
|
|
6887
|
+
missingDetails.push({
|
|
6888
|
+
ref: ap.path,
|
|
6889
|
+
from: "baseline",
|
|
6890
|
+
kind: "missing-baseline",
|
|
6891
|
+
message: msg,
|
|
6892
|
+
suggestedAction: `\u5C06 ${ap.path} \u4ECE\u76EE\u5F55\u6539\u4E3A\u6587\u4EF6\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6893
|
+
});
|
|
6894
|
+
}
|
|
6895
|
+
} else {
|
|
6896
|
+
try {
|
|
6897
|
+
accessSync(fullPath, fsConstants.R_OK);
|
|
6898
|
+
} catch {
|
|
6899
|
+
const msg = `Required baseline artifact is not readable: ${ap.path}`;
|
|
6900
|
+
if (!missing.includes(msg)) {
|
|
6901
|
+
missing.push(msg);
|
|
6902
|
+
missingDetails.push({
|
|
6903
|
+
ref: ap.path,
|
|
6904
|
+
from: "baseline",
|
|
6905
|
+
kind: "missing-baseline",
|
|
6906
|
+
message: msg,
|
|
6907
|
+
suggestedAction: `\u4FEE\u590D ${ap.path} \u7684\u6587\u4EF6\u6743\u9650\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6908
|
+
});
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
}
|
|
6043
6913
|
} else {
|
|
6044
|
-
|
|
6914
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6915
|
+
const msg = `Cannot verify baseline without root: ${ap.path}`;
|
|
6916
|
+
if (!missing.includes(msg)) {
|
|
6917
|
+
missing.push(msg);
|
|
6918
|
+
missingDetails.push({
|
|
6919
|
+
ref: ap.path,
|
|
6920
|
+
from: "baseline",
|
|
6921
|
+
kind: "missing-baseline",
|
|
6922
|
+
message: msg,
|
|
6923
|
+
suggestedAction: `\u4F20\u9012 root \u53C2\u6570\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6924
|
+
});
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6045
6927
|
}
|
|
6046
6928
|
}
|
|
6047
6929
|
pathMap.set(targetNode.path, {
|
|
@@ -6142,7 +7024,8 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6142
7024
|
context,
|
|
6143
7025
|
missing,
|
|
6144
7026
|
missingDetails,
|
|
6145
|
-
omitted
|
|
7027
|
+
omitted,
|
|
7028
|
+
baselinePolicy: universalBaseline
|
|
6146
7029
|
};
|
|
6147
7030
|
}
|
|
6148
7031
|
function formatContextMarkdown(manifest) {
|
|
@@ -6229,12 +7112,14 @@ export {
|
|
|
6229
7112
|
buildGraph,
|
|
6230
7113
|
buildVersionIndex,
|
|
6231
7114
|
collectChangedPaths,
|
|
7115
|
+
computeE2eCoverageStats,
|
|
6232
7116
|
dirname3 as dirname,
|
|
6233
7117
|
discoverAndAuditPackets,
|
|
6234
7118
|
discoverTargets,
|
|
6235
7119
|
doctorArtifactChain,
|
|
6236
7120
|
extname,
|
|
6237
7121
|
formatContextMarkdown,
|
|
7122
|
+
generateE2eRegistry,
|
|
6238
7123
|
getArtifactTypeMetadata,
|
|
6239
7124
|
getTargetArtifactTypes,
|
|
6240
7125
|
installManagedHookBlock,
|