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.cjs
CHANGED
|
@@ -50,12 +50,14 @@ __export(index_exports, {
|
|
|
50
50
|
buildGraph: () => buildGraph,
|
|
51
51
|
buildVersionIndex: () => buildVersionIndex,
|
|
52
52
|
collectChangedPaths: () => collectChangedPaths,
|
|
53
|
+
computeE2eCoverageStats: () => computeE2eCoverageStats,
|
|
53
54
|
dirname: () => import_node_path6.dirname,
|
|
54
55
|
discoverAndAuditPackets: () => discoverAndAuditPackets,
|
|
55
56
|
discoverTargets: () => discoverTargets,
|
|
56
57
|
doctorArtifactChain: () => doctorArtifactChain,
|
|
57
58
|
extname: () => import_node_path6.extname,
|
|
58
59
|
formatContextMarkdown: () => formatContextMarkdown,
|
|
60
|
+
generateE2eRegistry: () => generateE2eRegistry,
|
|
59
61
|
getArtifactTypeMetadata: () => getArtifactTypeMetadata,
|
|
60
62
|
getTargetArtifactTypes: () => getTargetArtifactTypes,
|
|
61
63
|
installManagedHookBlock: () => installManagedHookBlock,
|
|
@@ -99,6 +101,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
99
101
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
100
102
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
101
103
|
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
104
|
+
var import_node_fs4 = require("fs");
|
|
102
105
|
var import_promises5 = require("fs/promises");
|
|
103
106
|
var import_node_path6 = require("path");
|
|
104
107
|
|
|
@@ -179,13 +182,76 @@ function validatePacket(packet, schema) {
|
|
|
179
182
|
path: "target.id"
|
|
180
183
|
});
|
|
181
184
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
const isBaselineExplicitlyDisabled = packet.baselinePolicy === false;
|
|
186
|
+
const expectedPaths = new Set(ALWAYS_PRESENT_ITEMS.map((item) => item.path));
|
|
187
|
+
if (isBaselineExplicitlyDisabled) {
|
|
188
|
+
if (packet.requiredBaseline.total !== 0) {
|
|
189
|
+
issues.push({
|
|
190
|
+
severity: "error",
|
|
191
|
+
code: "PKT-004",
|
|
192
|
+
message: `baselinePolicy=false requires requiredBaseline.total=0, got ${packet.requiredBaseline.total}`,
|
|
193
|
+
path: "requiredBaseline.total"
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (packet.requiredBaseline.items.length !== 0) {
|
|
197
|
+
issues.push({
|
|
198
|
+
severity: "error",
|
|
199
|
+
code: "PKT-004",
|
|
200
|
+
message: `baselinePolicy=false requires requiredBaseline.items=[], got ${packet.requiredBaseline.items.length} item(s)`,
|
|
201
|
+
path: "requiredBaseline.items"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
|
|
206
|
+
issues.push({
|
|
207
|
+
severity: "error",
|
|
208
|
+
code: "PKT-004",
|
|
209
|
+
message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
|
|
210
|
+
path: "requiredBaseline.total"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (packet.requiredBaseline.items.length !== BASELINE_ITEMS_COUNT) {
|
|
214
|
+
issues.push({
|
|
215
|
+
severity: "error",
|
|
216
|
+
code: "PKT-004",
|
|
217
|
+
message: `requiredBaseline.items.length must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.items.length}`,
|
|
218
|
+
path: "requiredBaseline.items"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const actualPaths = packet.requiredBaseline.items.map((item) => item.path);
|
|
222
|
+
const actualPathSet = new Set(actualPaths);
|
|
223
|
+
if (actualPathSet.size !== actualPaths.length) {
|
|
224
|
+
issues.push({
|
|
225
|
+
severity: "error",
|
|
226
|
+
code: "PKT-004",
|
|
227
|
+
message: `requiredBaseline.items contains duplicate paths (${actualPaths.length} items, ${actualPathSet.size} unique)`,
|
|
228
|
+
path: "requiredBaseline.items"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const missingPaths = [];
|
|
232
|
+
for (const ep of expectedPaths) {
|
|
233
|
+
if (!actualPathSet.has(ep)) missingPaths.push(ep);
|
|
234
|
+
}
|
|
235
|
+
if (missingPaths.length > 0) {
|
|
236
|
+
issues.push({
|
|
237
|
+
severity: "error",
|
|
238
|
+
code: "PKT-004",
|
|
239
|
+
message: `requiredBaseline.items missing expected path(s): ${missingPaths.join(", ")}`,
|
|
240
|
+
path: "requiredBaseline.items"
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const extraPaths = [];
|
|
244
|
+
for (const ap of actualPathSet) {
|
|
245
|
+
if (!expectedPaths.has(ap)) extraPaths.push(ap);
|
|
246
|
+
}
|
|
247
|
+
if (extraPaths.length > 0) {
|
|
248
|
+
issues.push({
|
|
249
|
+
severity: "error",
|
|
250
|
+
code: "PKT-004",
|
|
251
|
+
message: `requiredBaseline.items contains unexpected path(s): ${extraPaths.join(", ")}`,
|
|
252
|
+
path: "requiredBaseline.items"
|
|
253
|
+
});
|
|
254
|
+
}
|
|
189
255
|
}
|
|
190
256
|
const constraints = packet.implementationBlueprintDraft.constraints;
|
|
191
257
|
if (constraints.length !== BASELINE_CONSTRAINTS_COUNT) {
|
|
@@ -292,6 +358,45 @@ function validatePacketMarkdown(markdown) {
|
|
|
292
358
|
return { ok: !hasError, issues };
|
|
293
359
|
}
|
|
294
360
|
|
|
361
|
+
// src/glob-matcher.ts
|
|
362
|
+
function matchesRunnerGlob(filePath, pattern) {
|
|
363
|
+
const normalizedPath = normalizeGlobValue(filePath);
|
|
364
|
+
const normalizedPattern = normalizeGlobValue(pattern);
|
|
365
|
+
let expression = "^";
|
|
366
|
+
for (let index = 0; index < normalizedPattern.length; index += 1) {
|
|
367
|
+
const character = normalizedPattern[index];
|
|
368
|
+
if (character === "*") {
|
|
369
|
+
if (normalizedPattern[index + 1] === "*") {
|
|
370
|
+
while (normalizedPattern[index + 1] === "*") {
|
|
371
|
+
index += 1;
|
|
372
|
+
}
|
|
373
|
+
if (normalizedPattern[index + 1] === "/") {
|
|
374
|
+
index += 1;
|
|
375
|
+
expression += "(?:[^/]+/)*";
|
|
376
|
+
} else {
|
|
377
|
+
expression += ".*";
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
expression += "[^/]*";
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (character === "?") {
|
|
385
|
+
expression += "[^/]";
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
expression += escapeRegexCharacter(character);
|
|
389
|
+
}
|
|
390
|
+
expression += "$";
|
|
391
|
+
return new RegExp(expression).test(normalizedPath);
|
|
392
|
+
}
|
|
393
|
+
function normalizeGlobValue(value) {
|
|
394
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
395
|
+
}
|
|
396
|
+
function escapeRegexCharacter(character) {
|
|
397
|
+
return "\\^$+?.()|{}[]".includes(character) ? String.fromCharCode(92) + character : character;
|
|
398
|
+
}
|
|
399
|
+
|
|
295
400
|
// src/target-selector.ts
|
|
296
401
|
function parseTargetSelector(value) {
|
|
297
402
|
const separator = value.indexOf(":");
|
|
@@ -612,7 +717,10 @@ function assemblePacket(manifest, options) {
|
|
|
612
717
|
missing: [...manifest.missing],
|
|
613
718
|
missingDetails: manifest.missingDetails ? [...manifest.missingDetails] : void 0,
|
|
614
719
|
implementationBlueprintDraft: blueprintDraft,
|
|
615
|
-
validationCommands
|
|
720
|
+
validationCommands,
|
|
721
|
+
// @feature ACA17
|
|
722
|
+
// @decision D-ACA-17
|
|
723
|
+
baselinePolicy: manifest.baselinePolicy
|
|
616
724
|
};
|
|
617
725
|
return packet;
|
|
618
726
|
}
|
|
@@ -833,7 +941,9 @@ async function auditSingleTarget(target, graph, options) {
|
|
|
833
941
|
const manifest = resolveArtifactContext(graph, {
|
|
834
942
|
target: { type: target.type, id: target.id },
|
|
835
943
|
mode: options.mode,
|
|
836
|
-
maxPerCategory: options.maxPerCategory
|
|
944
|
+
maxPerCategory: options.maxPerCategory,
|
|
945
|
+
universalBaseline: options.universalBaseline,
|
|
946
|
+
root: options.root
|
|
837
947
|
});
|
|
838
948
|
const packet = assemblePacket(manifest, {
|
|
839
949
|
mode: options.mode,
|
|
@@ -970,6 +1080,7 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
970
1080
|
type: d.type,
|
|
971
1081
|
id: d.id
|
|
972
1082
|
}));
|
|
1083
|
+
const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
|
|
973
1084
|
return auditPackets(root, targets, {
|
|
974
1085
|
root,
|
|
975
1086
|
outDir: options.outDir,
|
|
@@ -979,7 +1090,8 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
979
1090
|
summaryOnly: options.summaryOnly,
|
|
980
1091
|
sampleTargets: options.sampleTargets,
|
|
981
1092
|
summaryDetail: options.summaryDetail,
|
|
982
|
-
schema: config
|
|
1093
|
+
schema: config,
|
|
1094
|
+
universalBaseline: effectiveBaseline
|
|
983
1095
|
}, graph);
|
|
984
1096
|
}
|
|
985
1097
|
|
|
@@ -1302,6 +1414,7 @@ function validatePacketPrompt(prompt) {
|
|
|
1302
1414
|
|
|
1303
1415
|
// src/versioned-traceability.ts
|
|
1304
1416
|
var import_node_crypto = require("crypto");
|
|
1417
|
+
var import_node_fs = require("fs");
|
|
1305
1418
|
var import_promises2 = require("fs/promises");
|
|
1306
1419
|
var import_node_path2 = require("path");
|
|
1307
1420
|
var VERSION_LOCK_PATH = "artifacts/traceability-version-lock.json";
|
|
@@ -1342,13 +1455,15 @@ async function buildVersionIndex(root, graph) {
|
|
|
1342
1455
|
edges: sortBy(edges, (edge2) => `${edge2.from} ${edge2.to} ${edge2.kind} ${edge2.sourcePath} ${edge2.sourceLine}`)
|
|
1343
1456
|
};
|
|
1344
1457
|
}
|
|
1345
|
-
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
1458
|
+
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, config) {
|
|
1346
1459
|
const index = await buildVersionIndex(root, graph);
|
|
1460
|
+
const schema = config ?? await loadConfig(root);
|
|
1347
1461
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1348
1462
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1349
1463
|
const nodeByArtifact = new Map(index.nodes.map((node) => [`${node.type}:${node.id}`, node]));
|
|
1350
1464
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1351
1465
|
const currentEdges = implementationEdges(index);
|
|
1466
|
+
const lockableEdges = await lockableImplementationEdges(root, index, schema);
|
|
1352
1467
|
const currentEdgeIds = new Set(currentEdges.map((edge2) => edge2.edgeId));
|
|
1353
1468
|
const issues = [];
|
|
1354
1469
|
let fresh = 0;
|
|
@@ -1436,8 +1551,29 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
|
1436
1551
|
issues.push(...entryIssues);
|
|
1437
1552
|
}
|
|
1438
1553
|
}
|
|
1554
|
+
const livenessCache = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const entry of lock.locks) {
|
|
1556
|
+
if (entry.kind !== "verifies") continue;
|
|
1557
|
+
const sourcePath = entry.source.path;
|
|
1558
|
+
const fullSourcePath = (0, import_node_path2.join)(root, sourcePath);
|
|
1559
|
+
if (!(0, import_node_fs.existsSync)(fullSourcePath)) continue;
|
|
1560
|
+
let liveness = livenessCache.get(sourcePath);
|
|
1561
|
+
if (liveness === void 0) {
|
|
1562
|
+
liveness = await getTestFileRunnerLiveness(root, sourcePath, schema);
|
|
1563
|
+
livenessCache.set(sourcePath, liveness);
|
|
1564
|
+
}
|
|
1565
|
+
if (liveness === "inactive") {
|
|
1566
|
+
issues.push({
|
|
1567
|
+
status: "orphan_lock",
|
|
1568
|
+
edgeId: entry.edgeId,
|
|
1569
|
+
message: `Liveness: ${sourcePath} is not active in any configured runner \u2014 locked verifies edges may reference dead tests`,
|
|
1570
|
+
artifact: entry.artifact,
|
|
1571
|
+
source: entry.source
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1439
1575
|
const reportedMissingLocks = /* @__PURE__ */ new Set();
|
|
1440
|
-
for (const edge2 of
|
|
1576
|
+
for (const edge2 of lockableEdges) {
|
|
1441
1577
|
const edgeId = edge2.edgeId;
|
|
1442
1578
|
if (!lock.locks.some((entry) => entry.edgeId === edgeId)) {
|
|
1443
1579
|
if (reportedMissingLocks.has(edgeId)) {
|
|
@@ -1519,6 +1655,7 @@ async function updateVersionLock(root, options) {
|
|
|
1519
1655
|
}
|
|
1520
1656
|
async function bootstrapVersionLock(root, options = {}) {
|
|
1521
1657
|
const index = await buildVersionIndex(root);
|
|
1658
|
+
const config = await loadConfig(root);
|
|
1522
1659
|
const lockPath = normalizeRelativePath(root, options.lockPath ?? VERSION_LOCK_PATH);
|
|
1523
1660
|
if (!options.force) {
|
|
1524
1661
|
const existing = await readVersionLock(root, lockPath);
|
|
@@ -1528,7 +1665,7 @@ async function bootstrapVersionLock(root, options = {}) {
|
|
|
1528
1665
|
}
|
|
1529
1666
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1530
1667
|
const entries = /* @__PURE__ */ new Map();
|
|
1531
|
-
for (const edge2 of
|
|
1668
|
+
for (const edge2 of await lockableImplementationEdges(root, index, config)) {
|
|
1532
1669
|
const source = nodeByUid.get(edge2.from);
|
|
1533
1670
|
const artifact = nodeByUid.get(edge2.to);
|
|
1534
1671
|
if (!source || !artifact) {
|
|
@@ -1563,10 +1700,11 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1563
1700
|
throw new Error("Changed-only version-lock refresh includes artifact-graph.config.yaml and requires --all");
|
|
1564
1701
|
}
|
|
1565
1702
|
const index = await buildVersionIndex(root);
|
|
1703
|
+
const config = await loadConfig(root);
|
|
1566
1704
|
const lock = await readVersionLock(root, lockPath);
|
|
1567
1705
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1568
1706
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1569
|
-
const currentImplementationEdges =
|
|
1707
|
+
const currentImplementationEdges = await lockableImplementationEdges(root, index, config);
|
|
1570
1708
|
const currentEdgePairs = new Set(currentImplementationEdges.map((edge2) => `${edge2.from} ${edge2.to}`));
|
|
1571
1709
|
const currentEntries = /* @__PURE__ */ new Map();
|
|
1572
1710
|
const changedPathSet = new Set(changedPaths);
|
|
@@ -1641,7 +1779,7 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1641
1779
|
locks: sortBy([...nextLocks.values()], (item) => item.edgeId)
|
|
1642
1780
|
};
|
|
1643
1781
|
await writeVersionLock(root, lockPath, next);
|
|
1644
|
-
const postAudit = await auditVersionLock(root, lockPath);
|
|
1782
|
+
const postAudit = await auditVersionLock(root, lockPath, void 0, config);
|
|
1645
1783
|
return {
|
|
1646
1784
|
schemaVersion: "1.0",
|
|
1647
1785
|
root,
|
|
@@ -1660,7 +1798,8 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1660
1798
|
async function traceVersion(root, target, lockPath = VERSION_LOCK_PATH) {
|
|
1661
1799
|
const index = await buildVersionIndex(root);
|
|
1662
1800
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1663
|
-
const
|
|
1801
|
+
const config = await loadConfig(root);
|
|
1802
|
+
const audit = await auditVersionLock(root, safeLockPath, void 0, config);
|
|
1664
1803
|
const targetUid = parseTarget(target);
|
|
1665
1804
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1666
1805
|
const targetNode = index.nodes.find((node) => node.uid === targetUid);
|
|
@@ -1896,6 +2035,28 @@ function implementationEdges(index) {
|
|
|
1896
2035
|
};
|
|
1897
2036
|
});
|
|
1898
2037
|
}
|
|
2038
|
+
async function lockableImplementationEdges(root, index, config) {
|
|
2039
|
+
const edges = implementationEdges(index);
|
|
2040
|
+
const nodesByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
2041
|
+
const livenessByPath = /* @__PURE__ */ new Map();
|
|
2042
|
+
const result = [];
|
|
2043
|
+
for (const edge2 of edges) {
|
|
2044
|
+
const source = nodesByUid.get(edge2.from);
|
|
2045
|
+
if (source?.sourceKind !== "test") {
|
|
2046
|
+
result.push(edge2);
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
let liveness = livenessByPath.get(source.path);
|
|
2050
|
+
if (liveness === void 0) {
|
|
2051
|
+
liveness = await getTestFileRunnerLiveness(root, source.path, config);
|
|
2052
|
+
livenessByPath.set(source.path, liveness);
|
|
2053
|
+
}
|
|
2054
|
+
if (liveness !== "inactive") {
|
|
2055
|
+
result.push(edge2);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return result;
|
|
2059
|
+
}
|
|
1899
2060
|
function lockRefFromNode(node) {
|
|
1900
2061
|
return {
|
|
1901
2062
|
type: node.type,
|
|
@@ -2008,13 +2169,60 @@ function normalizeRelativePath(root, path) {
|
|
|
2008
2169
|
function sortBy(items, keyFn) {
|
|
2009
2170
|
return [...items].sort((left, right) => keyFn(left).localeCompare(keyFn(right)));
|
|
2010
2171
|
}
|
|
2172
|
+
async function getTestFileRunnerLiveness(root, filePath, config) {
|
|
2173
|
+
const schema = config ?? await loadConfig(root);
|
|
2174
|
+
const runners = schema.e2e?.runners ?? [];
|
|
2175
|
+
if (runners.length === 0) {
|
|
2176
|
+
if (!/e2e/i.test(filePath) && !/\.e2e\./i.test(filePath)) {
|
|
2177
|
+
return "active";
|
|
2178
|
+
}
|
|
2179
|
+
const fullSourcePath = (0, import_node_path2.join)(root, filePath);
|
|
2180
|
+
if (!(0, import_node_fs.existsSync)(fullSourcePath)) return "inactive";
|
|
2181
|
+
try {
|
|
2182
|
+
const content = await (0, import_promises2.readFile)(fullSourcePath, "utf-8");
|
|
2183
|
+
return /\/\/!?\s*@(?:e2e_test|tc)\s+/.test(content) ? "active" : "inactive";
|
|
2184
|
+
} catch {
|
|
2185
|
+
return "inactive";
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
let inRunnerScope = false;
|
|
2189
|
+
for (const runner of runners) {
|
|
2190
|
+
if (!isFileIncludedByRunner(filePath, runner)) continue;
|
|
2191
|
+
inRunnerScope = true;
|
|
2192
|
+
const isActive = await isFileActiveInRunner(root, filePath, runner);
|
|
2193
|
+
if (isActive) return "active";
|
|
2194
|
+
}
|
|
2195
|
+
return inRunnerScope ? "inactive" : "unscoped";
|
|
2196
|
+
}
|
|
2197
|
+
function isFileIncludedByRunner(filePath, runner) {
|
|
2198
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2199
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2200
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) return false;
|
|
2201
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length + 1);
|
|
2202
|
+
return runner.include.some((pattern) => matchesRunnerGlob(relativePath, pattern));
|
|
2203
|
+
}
|
|
2204
|
+
async function isFileActiveInRunner(root, filePath, runner) {
|
|
2205
|
+
if (!isFileIncludedByRunner(filePath, runner)) return false;
|
|
2206
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2207
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2208
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length).replace(/^\//, "");
|
|
2209
|
+
const matchesExclude = (runner.exclude ?? []).some(
|
|
2210
|
+
(pattern) => matchesRunnerGlob(relativePath, pattern)
|
|
2211
|
+
);
|
|
2212
|
+
if (matchesExclude) return false;
|
|
2213
|
+
const matchesTestIgnore = (runner.testIgnore ?? []).some(
|
|
2214
|
+
(pattern) => matchesRunnerGlob(relativePath, pattern)
|
|
2215
|
+
);
|
|
2216
|
+
if (matchesTestIgnore) return false;
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2011
2219
|
function sortUnique(items) {
|
|
2012
2220
|
return [...new Set(items)].sort((left, right) => left.localeCompare(right));
|
|
2013
2221
|
}
|
|
2014
2222
|
|
|
2015
2223
|
// src/cli-resolver.ts
|
|
2016
2224
|
var import_node_child_process = require("child_process");
|
|
2017
|
-
var
|
|
2225
|
+
var import_node_fs2 = require("fs");
|
|
2018
2226
|
var import_promises3 = require("fs/promises");
|
|
2019
2227
|
var import_node_path3 = require("path");
|
|
2020
2228
|
var import_node_util = require("util");
|
|
@@ -2152,7 +2360,7 @@ ${maybe.stderr ?? ""}`;
|
|
|
2152
2360
|
}
|
|
2153
2361
|
async function pathExists(path) {
|
|
2154
2362
|
try {
|
|
2155
|
-
await (0, import_promises3.access)(path,
|
|
2363
|
+
await (0, import_promises3.access)(path, import_node_fs2.constants.R_OK);
|
|
2156
2364
|
return true;
|
|
2157
2365
|
} catch {
|
|
2158
2366
|
return false;
|
|
@@ -2251,7 +2459,7 @@ async function resolveGitHookPath(root, hookName) {
|
|
|
2251
2459
|
}
|
|
2252
2460
|
|
|
2253
2461
|
// src/hook-installer.ts
|
|
2254
|
-
var
|
|
2462
|
+
var import_node_fs3 = require("fs");
|
|
2255
2463
|
var import_node_crypto2 = require("crypto");
|
|
2256
2464
|
var import_promises4 = require("fs/promises");
|
|
2257
2465
|
var import_node_path5 = require("path");
|
|
@@ -2565,7 +2773,7 @@ async function readHookSnapshot(hookPath) {
|
|
|
2565
2773
|
}
|
|
2566
2774
|
let handle;
|
|
2567
2775
|
try {
|
|
2568
|
-
handle = await (0, import_promises4.open)(hookPath,
|
|
2776
|
+
handle = await (0, import_promises4.open)(hookPath, import_node_fs3.constants.O_RDONLY | (import_node_fs3.constants.O_NOFOLLOW ?? 0));
|
|
2569
2777
|
const opened = await handle.stat({ bigint: true });
|
|
2570
2778
|
if (opened.dev !== metadata.dev || opened.ino !== metadata.ino) {
|
|
2571
2779
|
await handle.close();
|
|
@@ -2694,12 +2902,35 @@ var VALID_DECISIONS = /* @__PURE__ */ new Set([
|
|
|
2694
2902
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["block", "warn", "info"]);
|
|
2695
2903
|
var VALID_FINDING_STATUSES = /* @__PURE__ */ new Set(["open", "resolved", "accepted", "superseded"]);
|
|
2696
2904
|
var VALID_EXECUTORS = /* @__PURE__ */ new Set(["script", "worker", "agent", "manual", "cli"]);
|
|
2905
|
+
var TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
|
|
2906
|
+
"schema_version",
|
|
2907
|
+
"run_id",
|
|
2908
|
+
"stage_id",
|
|
2909
|
+
"attempt",
|
|
2910
|
+
"status",
|
|
2911
|
+
"decision",
|
|
2912
|
+
"summary",
|
|
2913
|
+
"outputs",
|
|
2914
|
+
"warnings",
|
|
2915
|
+
"blocking_reason",
|
|
2916
|
+
"degradation",
|
|
2917
|
+
"producer",
|
|
2918
|
+
"acceptance",
|
|
2919
|
+
"evidence",
|
|
2920
|
+
"review",
|
|
2921
|
+
"repair"
|
|
2922
|
+
]);
|
|
2697
2923
|
function validateReviewResult(input) {
|
|
2698
2924
|
const errors = [];
|
|
2699
2925
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
2700
2926
|
return [{ path: "$", message: "Root must be a non-null object" }];
|
|
2701
2927
|
}
|
|
2702
2928
|
const obj = input;
|
|
2929
|
+
for (const key of Object.keys(obj)) {
|
|
2930
|
+
if (!TOP_LEVEL_FIELDS.has(key)) {
|
|
2931
|
+
errors.push({ path: `$.${key}`, message: "Unknown top-level property" });
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2703
2934
|
if (obj.schema_version !== "1.0") {
|
|
2704
2935
|
errors.push({ path: "$.schema_version", message: `Must be "1.0", got ${JSON.stringify(obj.schema_version)}` });
|
|
2705
2936
|
}
|
|
@@ -2718,8 +2949,8 @@ function validateReviewResult(input) {
|
|
|
2718
2949
|
if (obj.stage_id !== void 0 && typeof obj.stage_id !== "string") {
|
|
2719
2950
|
errors.push({ path: "$.stage_id", message: "Must be a string if present" });
|
|
2720
2951
|
}
|
|
2721
|
-
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1)) {
|
|
2722
|
-
errors.push({ path: "$.attempt", message: "Must be
|
|
2952
|
+
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1 || obj.attempt > 3)) {
|
|
2953
|
+
errors.push({ path: "$.attempt", message: "Must be an integer from 1 through 3 if present" });
|
|
2723
2954
|
}
|
|
2724
2955
|
if (obj.outputs !== void 0) {
|
|
2725
2956
|
checkStringArray(obj.outputs, "$.outputs", errors);
|
|
@@ -2730,20 +2961,14 @@ function validateReviewResult(input) {
|
|
|
2730
2961
|
checkOptionalNullableString(obj.blocking_reason, "$.blocking_reason", errors);
|
|
2731
2962
|
checkOptionalNullableString(obj.degradation, "$.degradation", errors);
|
|
2732
2963
|
if (obj.producer !== void 0) {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2742
|
-
if (typeof p.name !== "string") {
|
|
2743
|
-
errors.push({ path: "$.producer.name", message: "Must be a string" });
|
|
2744
|
-
}
|
|
2745
|
-
checkOptionalString(p.skill, "$.producer.skill", errors);
|
|
2746
|
-
}
|
|
2964
|
+
validateProducer(obj.producer, "$.producer", errors);
|
|
2965
|
+
}
|
|
2966
|
+
const successfulAcceptance = obj.status === "SUCCEEDED" && (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR");
|
|
2967
|
+
if (successfulAcceptance && obj.producer === void 0) {
|
|
2968
|
+
errors.push({ path: "$.producer", message: "Successful acceptance requires producer identity" });
|
|
2969
|
+
}
|
|
2970
|
+
if (obj.acceptance !== void 0) {
|
|
2971
|
+
validateAcceptance(obj.acceptance, obj.producer, "$.acceptance", errors);
|
|
2747
2972
|
}
|
|
2748
2973
|
if (obj.evidence !== void 0) {
|
|
2749
2974
|
if (!Array.isArray(obj.evidence)) {
|
|
@@ -2769,6 +2994,17 @@ function validateReviewResult(input) {
|
|
|
2769
2994
|
}
|
|
2770
2995
|
if (obj.review !== void 0) {
|
|
2771
2996
|
validateReviewData(obj.review, "$.review", errors);
|
|
2997
|
+
if (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR") {
|
|
2998
|
+
const findings = isPlainObject(obj.review) && Array.isArray(obj.review.findings) ? obj.review.findings : [];
|
|
2999
|
+
findings.forEach((finding, index) => {
|
|
3000
|
+
if (isPlainObject(finding) && finding.severity === "block" && (finding.status === void 0 || finding.status === "open")) {
|
|
3001
|
+
errors.push({
|
|
3002
|
+
path: `$.review.findings[${index}]`,
|
|
3003
|
+
message: `${obj.decision} cannot contain an open block finding`
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
2772
3008
|
}
|
|
2773
3009
|
if (obj.repair !== void 0) {
|
|
2774
3010
|
if (!isPlainObject(obj.repair)) {
|
|
@@ -2804,6 +3040,47 @@ function checkStringArray(val, path, errors) {
|
|
|
2804
3040
|
function isPlainObject(val) {
|
|
2805
3041
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
2806
3042
|
}
|
|
3043
|
+
function validateProducer(val, path, errors) {
|
|
3044
|
+
if (!isPlainObject(val)) {
|
|
3045
|
+
errors.push({ path, message: "Must be an object" });
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
if (typeof val.executor !== "string") {
|
|
3049
|
+
errors.push({ path: `${path}.executor`, message: "Must be a string" });
|
|
3050
|
+
} else if (!VALID_EXECUTORS.has(val.executor)) {
|
|
3051
|
+
errors.push({ path: `${path}.executor`, message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(val.executor)}` });
|
|
3052
|
+
}
|
|
3053
|
+
if (typeof val.name !== "string" || val.name.length === 0) {
|
|
3054
|
+
errors.push({ path: `${path}.name`, message: "Must be a non-empty string" });
|
|
3055
|
+
}
|
|
3056
|
+
checkOptionalString(val.skill, `${path}.skill`, errors);
|
|
3057
|
+
}
|
|
3058
|
+
function producerIdentity(val) {
|
|
3059
|
+
return JSON.stringify([val.executor, val.name]);
|
|
3060
|
+
}
|
|
3061
|
+
function validateAcceptance(val, resultProducer, path, errors) {
|
|
3062
|
+
if (!isPlainObject(val)) {
|
|
3063
|
+
errors.push({ path, message: "Must be an object" });
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
validateProducer(val.reviewer, `${path}.reviewer`, errors);
|
|
3067
|
+
if (!isPlainObject(val.source_result)) {
|
|
3068
|
+
errors.push({ path: `${path}.source_result`, message: "Must be an object" });
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
const source = val.source_result;
|
|
3072
|
+
if (typeof source.run_id !== "string" || source.run_id.length === 0) {
|
|
3073
|
+
errors.push({ path: `${path}.source_result.run_id`, message: "Must be a non-empty string" });
|
|
3074
|
+
}
|
|
3075
|
+
checkOptionalString(source.stage_id, `${path}.source_result.stage_id`, errors);
|
|
3076
|
+
validateProducer(source.producer, `${path}.source_result.producer`, errors);
|
|
3077
|
+
if (isPlainObject(val.reviewer) && isPlainObject(resultProducer) && producerIdentity(val.reviewer) !== producerIdentity(resultProducer)) {
|
|
3078
|
+
errors.push({ path: `${path}.reviewer`, message: "Acceptance reviewer must match the result producer" });
|
|
3079
|
+
}
|
|
3080
|
+
if (isPlainObject(val.reviewer) && isPlainObject(source.producer) && producerIdentity(val.reviewer) === producerIdentity(source.producer)) {
|
|
3081
|
+
errors.push({ path: `${path}.reviewer`, message: "Repair producer cannot accept its own result" });
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
2807
3084
|
function checkOptionalString(val, path, errors) {
|
|
2808
3085
|
if (val !== void 0 && typeof val !== "string") {
|
|
2809
3086
|
errors.push({ path, message: "Must be a string if present" });
|
|
@@ -2972,7 +3249,7 @@ var DEFAULT_SCHEMA = {
|
|
|
2972
3249
|
scenario: { paths: ["artifacts/scenarios/**/*.md"], displayName: "\u573A\u666F\u5267\u672C", role: "scenario", layer: "scenario", aliases: ["scenarios", "scenario-script"] },
|
|
2973
3250
|
design: { paths: ["artifacts/design/**/*.md"], displayName: "\u8BBE\u8BA1\u89C4\u683C", role: "design", layer: "design", aliases: ["design-spec", "design_docs"] },
|
|
2974
3251
|
test: { paths: ["heimdall/packages/**/*.test.ts"], displayName: "\u4EE3\u7801\u6CE8\u91CA\u8FFD\u6EAF", role: "context", layer: "implementation", aliases: ["code-test", "code-trace", "unit-test"] },
|
|
2975
|
-
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests"] },
|
|
3252
|
+
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests", "tc"] },
|
|
2976
3253
|
e2e_registry: { paths: ["artifacts/tests/e2e/e2e-test-registry.json"], displayName: "E2E \u6D4B\u8BD5\u6CE8\u518C\u8868", role: "context", layer: "verification", aliases: ["e2e-registry"] },
|
|
2977
3254
|
"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"] },
|
|
2978
3255
|
"test-strategy": { paths: ["artifacts/design/test-strategy.md"], displayName: "\u6D4B\u8BD5\u7B56\u7565", role: "context", layer: "verification", aliases: ["test_strategy"] },
|
|
@@ -3002,7 +3279,12 @@ var DEFAULT_SCHEMA = {
|
|
|
3002
3279
|
allowedEdges: [],
|
|
3003
3280
|
forbiddenEdges: [{ from: "scenario", to: "entity", kind: "references" }],
|
|
3004
3281
|
statuses: ["planned", "active", "done", "deprecated"],
|
|
3005
|
-
idRanges: {}
|
|
3282
|
+
idRanges: {},
|
|
3283
|
+
e2e: {
|
|
3284
|
+
report_uncovered_scenarios: true,
|
|
3285
|
+
report_uncovered_features: true,
|
|
3286
|
+
runners: []
|
|
3287
|
+
}
|
|
3006
3288
|
};
|
|
3007
3289
|
async function loadConfig(root) {
|
|
3008
3290
|
const configPath = (0, import_node_path6.join)(root, "artifact-graph.config.yaml");
|
|
@@ -3015,7 +3297,27 @@ async function loadConfig(root) {
|
|
|
3015
3297
|
throw error;
|
|
3016
3298
|
}
|
|
3017
3299
|
}
|
|
3018
|
-
|
|
3300
|
+
const ub = parsed.context?.universal_baseline;
|
|
3301
|
+
if (ub !== void 0 && typeof ub !== "boolean") {
|
|
3302
|
+
throw new Error(
|
|
3303
|
+
`Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
if (parsed.e2e !== void 0) {
|
|
3307
|
+
if (typeof parsed.e2e !== "object" || parsed.e2e === null || Array.isArray(parsed.e2e)) {
|
|
3308
|
+
throw new Error("Invalid e2e: must be an object.");
|
|
3309
|
+
}
|
|
3310
|
+
validateE2eConfig(parsed.e2e);
|
|
3311
|
+
}
|
|
3312
|
+
const mergedE2e = parsed.e2e === void 0 ? DEFAULT_SCHEMA.e2e : {
|
|
3313
|
+
...DEFAULT_SCHEMA.e2e,
|
|
3314
|
+
...parsed.e2e,
|
|
3315
|
+
runners: (parsed.e2e.runners ?? DEFAULT_SCHEMA.e2e?.runners ?? []).map((runner) => ({
|
|
3316
|
+
kind: "e2e",
|
|
3317
|
+
...runner
|
|
3318
|
+
}))
|
|
3319
|
+
};
|
|
3320
|
+
const merged = {
|
|
3019
3321
|
...DEFAULT_SCHEMA,
|
|
3020
3322
|
...parsed,
|
|
3021
3323
|
types: mergeArtifactTypes(DEFAULT_SCHEMA.types, parsed.types),
|
|
@@ -3024,10 +3326,103 @@ async function loadConfig(root) {
|
|
|
3024
3326
|
allowedEdges: parsed.allowedEdges ?? DEFAULT_SCHEMA.allowedEdges,
|
|
3025
3327
|
forbiddenEdges: parsed.forbiddenEdges ?? DEFAULT_SCHEMA.forbiddenEdges,
|
|
3026
3328
|
statuses: parsed.statuses ?? DEFAULT_SCHEMA.statuses,
|
|
3027
|
-
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
|
|
3329
|
+
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges),
|
|
3330
|
+
e2e: mergedE2e
|
|
3028
3331
|
};
|
|
3332
|
+
return merged;
|
|
3029
3333
|
}
|
|
3030
|
-
function
|
|
3334
|
+
function validateE2eConfig(e2e) {
|
|
3335
|
+
for (const field of ["report_uncovered_scenarios", "report_uncovered_features"]) {
|
|
3336
|
+
if (e2e[field] !== void 0 && typeof e2e[field] !== "boolean") {
|
|
3337
|
+
throw new Error(`Invalid e2e.${field}: must be boolean.`);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
if (e2e.executable_ref_warning !== void 0) {
|
|
3341
|
+
if (typeof e2e.executable_ref_warning !== "number" || e2e.executable_ref_warning < 0 || e2e.executable_ref_warning > 1) {
|
|
3342
|
+
throw new Error(`Invalid e2e.executable_ref_warning: ${JSON.stringify(e2e.executable_ref_warning)}. Must be a number between 0 and 1.`);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
if (e2e.executable_ref_error !== void 0) {
|
|
3346
|
+
if (typeof e2e.executable_ref_error !== "number" || e2e.executable_ref_error < 0 || e2e.executable_ref_error > 1) {
|
|
3347
|
+
throw new Error(`Invalid e2e.executable_ref_error: ${JSON.stringify(e2e.executable_ref_error)}. Must be a number between 0 and 1.`);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
const validateWaivers = (waivers, field) => {
|
|
3351
|
+
if (waivers === void 0) return;
|
|
3352
|
+
if (!Array.isArray(waivers)) {
|
|
3353
|
+
throw new Error(`Invalid ${field}: must be an array of {id, reason} objects.`);
|
|
3354
|
+
}
|
|
3355
|
+
for (const w of waivers) {
|
|
3356
|
+
if (typeof w !== "object" || w === null || !("id" in w) || !("reason" in w)) {
|
|
3357
|
+
throw new Error(`Invalid ${field} entry: ${JSON.stringify(w)}. Must be {id, reason} object.`);
|
|
3358
|
+
}
|
|
3359
|
+
if (typeof w.id !== "string" || !w.id.trim()) {
|
|
3360
|
+
throw new Error(`Invalid ${field} entry: id must be a non-empty string. Got: ${JSON.stringify(w.id)}`);
|
|
3361
|
+
}
|
|
3362
|
+
if (typeof w.reason !== "string" || !w.reason.trim()) {
|
|
3363
|
+
throw new Error(`Invalid ${field} entry: reason must be a non-empty string. Got: ${JSON.stringify(w.reason)}`);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
validateWaivers(e2e.scenario_waivers, "e2e.scenario_waivers");
|
|
3368
|
+
validateWaivers(e2e.feature_waivers, "e2e.feature_waivers");
|
|
3369
|
+
if (e2e.runners !== void 0) {
|
|
3370
|
+
if (!Array.isArray(e2e.runners)) {
|
|
3371
|
+
throw new Error(`Invalid e2e.runners: must be an array.`);
|
|
3372
|
+
}
|
|
3373
|
+
for (const runner of e2e.runners) {
|
|
3374
|
+
if (typeof runner !== "object" || runner === null) {
|
|
3375
|
+
throw new Error(`Invalid e2e.runners entry: must be an object.`);
|
|
3376
|
+
}
|
|
3377
|
+
if (typeof runner.name !== "string" || !runner.name.trim()) {
|
|
3378
|
+
throw new Error(`Invalid e2e.runners entry: name must be a non-empty string.`);
|
|
3379
|
+
}
|
|
3380
|
+
if (typeof runner.root !== "string" || !runner.root.trim()) {
|
|
3381
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: must be a non-empty string.`);
|
|
3382
|
+
}
|
|
3383
|
+
if ((0, import_node_path6.isAbsolute)(runner.root)) {
|
|
3384
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: "${runner.root}" must not be an absolute path.`);
|
|
3385
|
+
}
|
|
3386
|
+
if (runner.root.replace(/\\/g, "/").split("/").includes("..")) {
|
|
3387
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: "${runner.root}" must not contain ".." segments.`);
|
|
3388
|
+
}
|
|
3389
|
+
if (!Array.isArray(runner.include) || runner.include.length === 0) {
|
|
3390
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].include: must be a non-empty array of glob patterns.`);
|
|
3391
|
+
}
|
|
3392
|
+
for (const pattern of runner.include) {
|
|
3393
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3394
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].include: pattern must be a non-empty string.`);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (runner.exclude !== void 0) {
|
|
3398
|
+
if (!Array.isArray(runner.exclude)) {
|
|
3399
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].exclude: must be an array.`);
|
|
3400
|
+
}
|
|
3401
|
+
for (const pattern of runner.exclude) {
|
|
3402
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3403
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].exclude: pattern must be a non-empty string.`);
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
if (runner.testIgnore !== void 0) {
|
|
3408
|
+
if (!Array.isArray(runner.testIgnore)) {
|
|
3409
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].testIgnore: must be an array.`);
|
|
3410
|
+
}
|
|
3411
|
+
for (const pattern of runner.testIgnore) {
|
|
3412
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3413
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].testIgnore: pattern must be a non-empty string.`);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
if (runner.kind !== void 0) {
|
|
3418
|
+
if (!["unit", "integration", "e2e"].includes(runner.kind)) {
|
|
3419
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].kind: "${runner.kind}". Must be unit, integration, or e2e.`);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function buildGraph(nodes, edges, diagnostics = [], root) {
|
|
3031
3426
|
const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
|
|
3032
3427
|
graphNodes.sort(compareNode);
|
|
3033
3428
|
edges.sort(compareEdge);
|
|
@@ -3044,6 +3439,7 @@ function buildGraph(nodes, edges, diagnostics = []) {
|
|
|
3044
3439
|
nodes: graphNodes,
|
|
3045
3440
|
edges: dedupedEdges,
|
|
3046
3441
|
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3442
|
+
...root ? { root } : {},
|
|
3047
3443
|
diagnostics: diagnostics.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line)
|
|
3048
3444
|
};
|
|
3049
3445
|
}
|
|
@@ -3075,7 +3471,8 @@ async function scanArtifacts(root, schema) {
|
|
|
3075
3471
|
scanDiagnostics.push(...parsed.diagnostics);
|
|
3076
3472
|
}
|
|
3077
3473
|
}
|
|
3078
|
-
const
|
|
3474
|
+
const absoluteRoot = (0, import_node_path6.isAbsolute)(root) ? root : (0, import_node_path6.resolve)(root);
|
|
3475
|
+
const graph = buildGraph(nodes, edges, scanDiagnostics, absoluteRoot);
|
|
3079
3476
|
return resolveMatrixEdges(graph);
|
|
3080
3477
|
}
|
|
3081
3478
|
function artifactTypeEntriesBySpecificity(schema) {
|
|
@@ -3415,23 +3812,34 @@ async function validateScenarioPrdLinkIndex(root, graph) {
|
|
|
3415
3812
|
function validateCodeCommentTraceabilityFormat(graph) {
|
|
3416
3813
|
const issues = [];
|
|
3417
3814
|
for (const node of graph.nodes) {
|
|
3418
|
-
if (node.type !== "test") {
|
|
3815
|
+
if (node.type !== "test" && node.type !== "implementation") {
|
|
3419
3816
|
continue;
|
|
3420
3817
|
}
|
|
3421
3818
|
const invalidComments = node.attrs?.invalidTraceabilityComments;
|
|
3422
|
-
if (
|
|
3423
|
-
|
|
3819
|
+
if (Array.isArray(invalidComments)) {
|
|
3820
|
+
for (const invalid of invalidComments) {
|
|
3821
|
+
const line = typeof invalid?.line === "number" ? invalid.line : node.line;
|
|
3822
|
+
const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
|
|
3823
|
+
issues.push(issue(
|
|
3824
|
+
"CODE_COMMENT_TRACEABILITY_FORMAT",
|
|
3825
|
+
`${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
|
|
3826
|
+
node.path,
|
|
3827
|
+
line,
|
|
3828
|
+
{ node: node.uid }
|
|
3829
|
+
));
|
|
3830
|
+
}
|
|
3424
3831
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
const
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3832
|
+
const deprecatedComments = node.attrs?.deprecatedTraceabilityComments;
|
|
3833
|
+
if (Array.isArray(deprecatedComments)) {
|
|
3834
|
+
for (const deprecated of deprecatedComments) {
|
|
3835
|
+
issues.push(issue(
|
|
3836
|
+
"E2E-TRACE-007",
|
|
3837
|
+
"@tc is deprecated; use @e2e_test instead",
|
|
3838
|
+
node.path,
|
|
3839
|
+
typeof deprecated?.line === "number" ? deprecated.line : node.line,
|
|
3840
|
+
{ node: node.uid, severity: "warning" }
|
|
3841
|
+
));
|
|
3842
|
+
}
|
|
3435
3843
|
}
|
|
3436
3844
|
}
|
|
3437
3845
|
return issues;
|
|
@@ -3913,13 +4321,16 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3913
4321
|
const isTest = isTestFile(path);
|
|
3914
4322
|
const nodeType = isTest ? "test" : "implementation";
|
|
3915
4323
|
const edgeKind = isTest ? "verifies" : "implements";
|
|
4324
|
+
const attrs = {};
|
|
4325
|
+
if (traceabilityComments.invalid.length > 0) attrs.invalidTraceabilityComments = traceabilityComments.invalid;
|
|
4326
|
+
if (traceabilityComments.deprecated.length > 0) attrs.deprecatedTraceabilityComments = traceabilityComments.deprecated;
|
|
3916
4327
|
const node = {
|
|
3917
4328
|
type: nodeType,
|
|
3918
4329
|
code: path,
|
|
3919
4330
|
title: path.split("/").at(-1) ?? path,
|
|
3920
4331
|
path,
|
|
3921
4332
|
line: 1,
|
|
3922
|
-
attrs
|
|
4333
|
+
attrs
|
|
3923
4334
|
};
|
|
3924
4335
|
let hasTags = false;
|
|
3925
4336
|
for (const { tags, lineNumber } of traceabilityComments.canonical) {
|
|
@@ -3930,7 +4341,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3930
4341
|
}
|
|
3931
4342
|
}
|
|
3932
4343
|
}
|
|
3933
|
-
if (hasTags || traceabilityComments.invalid.length > 0) {
|
|
4344
|
+
if (hasTags || traceabilityComments.invalid.length > 0 || traceabilityComments.deprecated.length > 0) {
|
|
3934
4345
|
nodes.push(node);
|
|
3935
4346
|
}
|
|
3936
4347
|
return { nodes, edges };
|
|
@@ -3938,6 +4349,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3938
4349
|
function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
3939
4350
|
const canonical = [];
|
|
3940
4351
|
const invalid = [];
|
|
4352
|
+
const deprecated = [];
|
|
3941
4353
|
for (const comment of scanCodeComments(raw)) {
|
|
3942
4354
|
if (!containsTraceabilityTag(comment.text, schema)) {
|
|
3943
4355
|
continue;
|
|
@@ -3953,6 +4365,9 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3953
4365
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3954
4366
|
if (parsed.valid) {
|
|
3955
4367
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4368
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4369
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4370
|
+
}
|
|
3956
4371
|
} else {
|
|
3957
4372
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3958
4373
|
}
|
|
@@ -3968,11 +4383,14 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3968
4383
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3969
4384
|
if (parsed.valid) {
|
|
3970
4385
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4386
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4387
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4388
|
+
}
|
|
3971
4389
|
} else {
|
|
3972
4390
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3973
4391
|
}
|
|
3974
4392
|
}
|
|
3975
|
-
return { canonical, invalid };
|
|
4393
|
+
return { canonical, invalid, deprecated };
|
|
3976
4394
|
}
|
|
3977
4395
|
function scanCodeComments(raw) {
|
|
3978
4396
|
const comments = [];
|
|
@@ -4139,7 +4557,14 @@ function expandCodeRange(value) {
|
|
|
4139
4557
|
return Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${String(start + index).padStart(width, "0")}`);
|
|
4140
4558
|
}
|
|
4141
4559
|
function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
4142
|
-
|
|
4560
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4561
|
+
for (const [type, definition] of Object.entries(schema.types)) {
|
|
4562
|
+
tokens.add(type);
|
|
4563
|
+
for (const alias of definition.aliases ?? []) tokens.add(alias);
|
|
4564
|
+
}
|
|
4565
|
+
if (tokens.size === 0) return false;
|
|
4566
|
+
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
4567
|
+
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
4143
4568
|
}
|
|
4144
4569
|
function parseDesign(path, raw) {
|
|
4145
4570
|
const parsed = (0, import_gray_matter.default)(raw);
|
|
@@ -5144,6 +5569,29 @@ function validateE2eTests(graph) {
|
|
|
5144
5569
|
issues.push(issue("E2E_AC_UNKNOWN", `${node.uid} references unknown AC ${reference.feature}(${reference.ac})`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5145
5570
|
}
|
|
5146
5571
|
}
|
|
5572
|
+
const tcStatus = String(fields["status"] ?? "").trim().toLowerCase();
|
|
5573
|
+
if (tcStatus && !VALID_TC_STATUSES.has(tcStatus)) {
|
|
5574
|
+
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" }));
|
|
5575
|
+
}
|
|
5576
|
+
if (tcStatus === "waived") {
|
|
5577
|
+
const reason = String(fields["waived_reason"] ?? "").trim();
|
|
5578
|
+
if (!reason) {
|
|
5579
|
+
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" }));
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
const rawChainType = String(fields["chain_type"] ?? "").trim();
|
|
5583
|
+
const chainType = rawChainType.toLowerCase();
|
|
5584
|
+
if (rawChainType) {
|
|
5585
|
+
if (!VALID_CHAIN_TYPES.has(chainType) && !(chainType in DEPRECATED_CHAIN_TYPE_ALIASES)) {
|
|
5586
|
+
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" }));
|
|
5587
|
+
} else if (chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5588
|
+
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" }));
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
const rawAcCoverageRate = String(fields["ac_coverage_rate"] ?? "").trim();
|
|
5592
|
+
if (rawAcCoverageRate) {
|
|
5593
|
+
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" }));
|
|
5594
|
+
}
|
|
5147
5595
|
if (needsDesktopChainWarning(node)) {
|
|
5148
5596
|
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" }));
|
|
5149
5597
|
}
|
|
@@ -5202,10 +5650,10 @@ function validateE2eRegistry(graph) {
|
|
|
5202
5650
|
}
|
|
5203
5651
|
return issues;
|
|
5204
5652
|
}
|
|
5205
|
-
async function validateExecutableTraceability(root) {
|
|
5653
|
+
async function validateExecutableTraceability(root, config) {
|
|
5206
5654
|
const issues = [];
|
|
5655
|
+
const schema = config ?? await loadConfig(root);
|
|
5207
5656
|
const e2eDir = (0, import_node_path6.join)(root, "artifacts", "tests", "e2e");
|
|
5208
|
-
const specPatterns = ["heimdall/**/*.spec.ts", "heimdall/**/*.e2e.spec.ts"];
|
|
5209
5657
|
let e2eFiles;
|
|
5210
5658
|
try {
|
|
5211
5659
|
e2eFiles = (await (0, import_promises5.readdir)(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => (0, import_node_path6.join)(e2eDir, name));
|
|
@@ -5248,16 +5696,23 @@ async function validateExecutableTraceability(root) {
|
|
|
5248
5696
|
}
|
|
5249
5697
|
const allFiles = await walk(root);
|
|
5250
5698
|
const specFiles = /* @__PURE__ */ new Set();
|
|
5251
|
-
|
|
5699
|
+
const configuredRunners = schema.e2e?.runners ?? [];
|
|
5700
|
+
if (configuredRunners.length > 0) {
|
|
5252
5701
|
for (const file of allFiles) {
|
|
5253
|
-
if (
|
|
5702
|
+
if (configuredRunners.some((runner) => isRunnerIncludeCandidate(file, runner))) {
|
|
5703
|
+
specFiles.add(file);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
} else {
|
|
5707
|
+
for (const file of allFiles) {
|
|
5708
|
+
if (/\.(?:e2e\.)?spec\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(file)) {
|
|
5254
5709
|
specFiles.add(file);
|
|
5255
5710
|
}
|
|
5256
5711
|
}
|
|
5257
5712
|
}
|
|
5258
5713
|
const refToSource = /* @__PURE__ */ new Map();
|
|
5259
|
-
const tcAnnotationRegex = /\/\/!?\s*@tc\s+(\S+?)\s+\[(\w+)\]/;
|
|
5260
|
-
const tcAnnotationNoLevelRegex = /\/\/!?\s*@tc\s+(\S+)/;
|
|
5714
|
+
const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
|
|
5715
|
+
const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
|
|
5261
5716
|
for (const specFile of specFiles) {
|
|
5262
5717
|
const fullSpecPath = (0, import_node_path6.join)(root, specFile);
|
|
5263
5718
|
let content;
|
|
@@ -5321,12 +5776,21 @@ async function validateExecutableTraceability(root) {
|
|
|
5321
5776
|
const refEntries = parseExecutableRefLines(ref);
|
|
5322
5777
|
const validFiles = [];
|
|
5323
5778
|
for (const entry of refEntries) {
|
|
5324
|
-
const normalizedRefFile =
|
|
5325
|
-
const fileExists = specFiles.has(normalizedRefFile);
|
|
5779
|
+
const normalizedRefFile = resolveExecutableRefFile(entry.file, allFiles);
|
|
5780
|
+
const fileExists = normalizedRefFile !== void 0 && specFiles.has(normalizedRefFile);
|
|
5326
5781
|
if (!fileExists) {
|
|
5327
5782
|
issues.push(issue("E2E-TRACE-001", `executable_ref target file not found: ${entry.file}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5328
5783
|
continue;
|
|
5329
5784
|
}
|
|
5785
|
+
const runners = schema.e2e?.runners ?? [];
|
|
5786
|
+
if (runners.length > 0) {
|
|
5787
|
+
const acceptingRunners = await getAcceptingRunners(root, normalizedRefFile, runners);
|
|
5788
|
+
const hasE2eRunner = acceptingRunners.some((r) => r.kind === "e2e" || r.kind === "integration");
|
|
5789
|
+
const hasUnitRunner = acceptingRunners.some((r) => r.kind === "unit");
|
|
5790
|
+
if (hasUnitRunner && !hasE2eRunner && acceptingRunners.length > 0) {
|
|
5791
|
+
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" }));
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5330
5794
|
if (entry.testId) {
|
|
5331
5795
|
let content;
|
|
5332
5796
|
try {
|
|
@@ -5341,12 +5805,9 @@ async function validateExecutableTraceability(root) {
|
|
|
5341
5805
|
}
|
|
5342
5806
|
}
|
|
5343
5807
|
const annotationsForTc = refToSource.get(tcKey);
|
|
5344
|
-
const hasAnnotationInFile = annotationsForTc?.some((ann) =>
|
|
5345
|
-
const normalizedAnnFile = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5346
|
-
return normalizedAnnFile === normalizedRefFile;
|
|
5347
|
-
}) ?? false;
|
|
5808
|
+
const hasAnnotationInFile = annotationsForTc?.some((ann) => ann.file === normalizedRefFile) ?? false;
|
|
5348
5809
|
if (!hasAnnotationInFile) {
|
|
5349
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no
|
|
5810
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5350
5811
|
continue;
|
|
5351
5812
|
}
|
|
5352
5813
|
validFiles.push(normalizedRefFile);
|
|
@@ -5357,13 +5818,13 @@ async function validateExecutableTraceability(root) {
|
|
|
5357
5818
|
const batch = tcKey.split(":")[0];
|
|
5358
5819
|
if (!mdBatches.has(batch)) {
|
|
5359
5820
|
for (const ann of annotations) {
|
|
5360
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5821
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5361
5822
|
}
|
|
5362
5823
|
continue;
|
|
5363
5824
|
}
|
|
5364
5825
|
if (!mdToRef.has(tcKey) && !await hasMarkdownTc(tcKey, e2eDir)) {
|
|
5365
5826
|
for (const ann of annotations) {
|
|
5366
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5827
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5367
5828
|
}
|
|
5368
5829
|
}
|
|
5369
5830
|
}
|
|
@@ -5382,24 +5843,37 @@ async function validateExecutableTraceability(root) {
|
|
|
5382
5843
|
if (matchesAnyRef) {
|
|
5383
5844
|
continue;
|
|
5384
5845
|
}
|
|
5385
|
-
const primaryRef = refEntries[0];
|
|
5386
|
-
const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
|
|
5387
5846
|
const detail = `file: MD refs=[${refEntries.map((e) => e.file).join(", ")}] vs source=${ann.file}`;
|
|
5388
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194
|
|
5847
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5389
5848
|
}
|
|
5390
5849
|
}
|
|
5391
|
-
for (const [tcKey, { chainType, path, line }] of
|
|
5392
|
-
|
|
5850
|
+
for (const [tcKey, { chainType, path, line }] of allMdTcInfo) {
|
|
5851
|
+
const tcFields = tcKeyToFields.get(tcKey);
|
|
5852
|
+
const explicitChainType = String(tcFields?.["chain_type"] ?? "").trim().toLowerCase();
|
|
5853
|
+
if (explicitChainType !== "desktop_chain") {
|
|
5393
5854
|
continue;
|
|
5394
5855
|
}
|
|
5856
|
+
if (!mdToRef.has(tcKey)) {
|
|
5857
|
+
issues.push(issue("E2E-DESKTOP-CHAIN-MISSING", `desktop_chain TC ${tcKey} has no executable_ref`, path, line, { node: tcKey, severity: "warning" }));
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
for (const [tcKey, { chainType, path, line }] of mdToRef) {
|
|
5861
|
+
const normalizedDeclaredChainType = chainType.trim().toLowerCase();
|
|
5862
|
+
const hasLegalNonDesktopDeclaration = normalizedDeclaredChainType.length > 0 && VALID_CHAIN_TYPES.has(normalizedDeclaredChainType) && normalizedDeclaredChainType !== "desktop_chain";
|
|
5863
|
+
if (hasLegalNonDesktopDeclaration) continue;
|
|
5395
5864
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5396
5865
|
const sourceAnnotations = refToSource.get(tcKey);
|
|
5397
5866
|
if (!sourceAnnotations) {
|
|
5398
5867
|
continue;
|
|
5399
5868
|
}
|
|
5869
|
+
const tcFields = tcKeyToFields.get(tcKey);
|
|
5870
|
+
const explicitChainType = String(tcFields?.["chain_type"] ?? "").trim().toLowerCase();
|
|
5871
|
+
const hasExplicitDesktopChain = explicitChainType === "desktop_chain";
|
|
5872
|
+
if (hasExplicitDesktopChain) {
|
|
5873
|
+
continue;
|
|
5874
|
+
}
|
|
5400
5875
|
for (const ann of sourceAnnotations) {
|
|
5401
|
-
|
|
5402
|
-
if (!validFiles.has(normalizedAnnFile)) {
|
|
5876
|
+
if (!validFiles.has(ann.file)) {
|
|
5403
5877
|
continue;
|
|
5404
5878
|
}
|
|
5405
5879
|
if (ann.level === "mock_playwright") {
|
|
@@ -5425,10 +5899,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5425
5899
|
continue;
|
|
5426
5900
|
}
|
|
5427
5901
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5428
|
-
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) =>
|
|
5429
|
-
const normalized = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5430
|
-
return validFiles.has(normalized);
|
|
5431
|
-
});
|
|
5902
|
+
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) => validFiles.has(ann.file));
|
|
5432
5903
|
const hasDesktopChain = sourceAnnotations?.some((ann) => ann.level === "desktop_chain") ?? false;
|
|
5433
5904
|
const hasBridge = sourceAnnotations?.some((ann) => ann.level === "ui_sidecar_bridge") ?? false;
|
|
5434
5905
|
if (isComplete) {
|
|
@@ -5437,7 +5908,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5437
5908
|
if (hasDesktopChain) {
|
|
5438
5909
|
hasValidEvidence = true;
|
|
5439
5910
|
} else if (hasBridge) {
|
|
5440
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5911
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5441
5912
|
if (partialResult.hasValidPartialRust) {
|
|
5442
5913
|
hasValidEvidence = true;
|
|
5443
5914
|
} else {
|
|
@@ -5477,7 +5948,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5477
5948
|
}
|
|
5478
5949
|
let partialDetail = "";
|
|
5479
5950
|
if (hasBridge) {
|
|
5480
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5951
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5481
5952
|
if (partialResult.hasValidPartialRust) {
|
|
5482
5953
|
continue;
|
|
5483
5954
|
}
|
|
@@ -5494,6 +5965,287 @@ async function validateExecutableTraceability(root) {
|
|
|
5494
5965
|
}
|
|
5495
5966
|
return issues;
|
|
5496
5967
|
}
|
|
5968
|
+
async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
5969
|
+
const e2eNodes = graph.nodes.filter((n) => n.type === "e2e_test" && n.attrs?.fileLevelOnly !== true);
|
|
5970
|
+
const totalTestCases = e2eNodes.length;
|
|
5971
|
+
let withExecutableRef = 0;
|
|
5972
|
+
const statusBreakdown = {};
|
|
5973
|
+
const chainTypeBreakdown = {};
|
|
5974
|
+
const e2eDir = (0, import_node_path6.join)(root, "artifacts", "tests", "e2e");
|
|
5975
|
+
const tcFieldsMap = /* @__PURE__ */ new Map();
|
|
5976
|
+
let e2eFiles;
|
|
5977
|
+
try {
|
|
5978
|
+
e2eFiles = (await (0, import_promises5.readdir)(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => (0, import_node_path6.join)(e2eDir, name));
|
|
5979
|
+
} catch {
|
|
5980
|
+
e2eFiles = [];
|
|
5981
|
+
}
|
|
5982
|
+
for (const filePath of e2eFiles) {
|
|
5983
|
+
const raw = await (0, import_promises5.readFile)(filePath, "utf-8");
|
|
5984
|
+
const lines = raw.split(/\r?\n/);
|
|
5985
|
+
const tcStarts = [];
|
|
5986
|
+
lines.forEach((line, index) => {
|
|
5987
|
+
const match = /^#{2,3}\s+(TC-\d+[a-z]?)\s*[::]?\s*.*$/.exec(line);
|
|
5988
|
+
if (match) {
|
|
5989
|
+
tcStarts.push({ id: match[1], index });
|
|
5990
|
+
}
|
|
5991
|
+
});
|
|
5992
|
+
const parsed = (0, import_gray_matter.default)(raw);
|
|
5993
|
+
const batch = String(parsed.data.test_batch ?? (0, import_node_path6.basename)(filePath, (0, import_node_path6.extname)(filePath))).trim();
|
|
5994
|
+
for (let i = 0; i < tcStarts.length; i++) {
|
|
5995
|
+
const start = tcStarts[i];
|
|
5996
|
+
const end = tcStarts[i + 1]?.index ?? lines.length;
|
|
5997
|
+
const block = lines.slice(start.index, end);
|
|
5998
|
+
const fields = extractE2eTcFields(block);
|
|
5999
|
+
tcFieldsMap.set(`${batch}:${start.id}`, fields);
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
for (const node of e2eNodes) {
|
|
6003
|
+
const tcKey = node.code;
|
|
6004
|
+
const fields = tcFieldsMap.get(tcKey) ?? asRecord(node.attrs?.tcFields);
|
|
6005
|
+
const execRef = String(fields["executable_ref"] ?? "").trim();
|
|
6006
|
+
if (execRef && !isPendingExecutableRef(execRef)) {
|
|
6007
|
+
withExecutableRef++;
|
|
6008
|
+
}
|
|
6009
|
+
const status = String(fields["status"] ?? "created").trim().toLowerCase();
|
|
6010
|
+
statusBreakdown[status] = (statusBreakdown[status] ?? 0) + 1;
|
|
6011
|
+
const chainType = String(fields["chain_type"] ?? "").trim().toLowerCase() || "unspecified";
|
|
6012
|
+
chainTypeBreakdown[chainType] = (chainTypeBreakdown[chainType] ?? 0) + 1;
|
|
6013
|
+
}
|
|
6014
|
+
const executableRefRate = totalTestCases > 0 ? `${withExecutableRef}/${totalTestCases} (${(withExecutableRef / totalTestCases * 100).toFixed(1)}%)` : "0/0";
|
|
6015
|
+
const scenarioNodes = graph.nodes.filter((n) => n.type === "scenario");
|
|
6016
|
+
const featureNodes = graph.nodes.filter((n) => n.type === "feature");
|
|
6017
|
+
const linkedScenarios = /* @__PURE__ */ new Set();
|
|
6018
|
+
const linkedFeatures = /* @__PURE__ */ new Set();
|
|
6019
|
+
const acCoveredScenarios = /* @__PURE__ */ new Set();
|
|
6020
|
+
const acCoveredFeatures = /* @__PURE__ */ new Set();
|
|
6021
|
+
const verifiedScenarios = /* @__PURE__ */ new Set();
|
|
6022
|
+
const verifiedFeatures = /* @__PURE__ */ new Set();
|
|
6023
|
+
for (const edge2 of graph.edges) {
|
|
6024
|
+
if (edge2.kind === "verifies" && edge2.from.startsWith("e2e_test:")) {
|
|
6025
|
+
if (edge2.to.startsWith("scenario:")) {
|
|
6026
|
+
linkedScenarios.add(edge2.to.replace("scenario:", ""));
|
|
6027
|
+
}
|
|
6028
|
+
if (edge2.to.startsWith("feature:")) {
|
|
6029
|
+
linkedFeatures.add(edge2.to.replace("feature:", ""));
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
for (const node of e2eNodes) {
|
|
6034
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
6035
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
6036
|
+
for (const feature of Object.keys(acCoverage)) {
|
|
6037
|
+
acCoveredFeatures.add(feature);
|
|
6038
|
+
}
|
|
6039
|
+
const relatedScenarios = toArray(fields["related_scenarios"] ?? node.attrs?.related_scenarios).map(String);
|
|
6040
|
+
for (const scenario of relatedScenarios) {
|
|
6041
|
+
acCoveredScenarios.add(scenario);
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
const runners = (await loadConfig(root)).e2e?.runners ?? [];
|
|
6045
|
+
const allProjectFiles = await walk(root);
|
|
6046
|
+
for (const node of e2eNodes) {
|
|
6047
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
6048
|
+
const status = String(fields["status"] ?? "").trim().toLowerCase();
|
|
6049
|
+
const execRef = String(fields["executable_ref"] ?? "").trim();
|
|
6050
|
+
if (status !== "verified") continue;
|
|
6051
|
+
if (!execRef || isPendingExecutableRef(execRef)) continue;
|
|
6052
|
+
if (runners.length === 0) continue;
|
|
6053
|
+
let hasActiveE2eRef = false;
|
|
6054
|
+
for (const entry of parseExecutableRefLines(execRef)) {
|
|
6055
|
+
const normalized = resolveExecutableRefFile(entry.file, allProjectFiles);
|
|
6056
|
+
if (!normalized || !(0, import_node_fs4.existsSync)((0, import_node_path6.join)(root, normalized))) continue;
|
|
6057
|
+
const accepting = await getAcceptingRunners(root, normalized, runners);
|
|
6058
|
+
if (accepting.some((runner) => runner.kind === "e2e")) {
|
|
6059
|
+
hasActiveE2eRef = true;
|
|
6060
|
+
break;
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
if (!hasActiveE2eRef) continue;
|
|
6064
|
+
const relatedScenarios = toArray(fields["related_scenarios"] ?? node.attrs?.related_scenarios).map(String);
|
|
6065
|
+
for (const scenario of relatedScenarios) {
|
|
6066
|
+
verifiedScenarios.add(scenario);
|
|
6067
|
+
}
|
|
6068
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
6069
|
+
for (const feature of Object.keys(acCoverage)) {
|
|
6070
|
+
verifiedFeatures.add(feature);
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
const scenarioWaivers = new Set((thresholds.scenarioWaivers ?? []).map((w) => w.id));
|
|
6074
|
+
const featureWaivers = new Set((thresholds.featureWaivers ?? []).map((w) => w.id));
|
|
6075
|
+
const uncoveredScenarios = scenarioNodes.map((n) => n.code).filter((code) => !linkedScenarios.has(code) && !scenarioWaivers.has(code));
|
|
6076
|
+
const uncoveredFeatures = featureNodes.map((n) => n.code).filter((code) => !acCoveredFeatures.has(code) && !featureWaivers.has(code));
|
|
6077
|
+
const scenarioCoverage = {};
|
|
6078
|
+
for (const node of scenarioNodes) {
|
|
6079
|
+
scenarioCoverage[node.code] = {
|
|
6080
|
+
linked: linkedScenarios.has(node.code),
|
|
6081
|
+
acCovered: acCoveredScenarios.has(node.code),
|
|
6082
|
+
waived: scenarioWaivers.has(node.code),
|
|
6083
|
+
verified: !scenarioWaivers.has(node.code) && verifiedScenarios.has(node.code)
|
|
6084
|
+
};
|
|
6085
|
+
}
|
|
6086
|
+
const featureCoverage = {};
|
|
6087
|
+
for (const node of featureNodes) {
|
|
6088
|
+
featureCoverage[node.code] = {
|
|
6089
|
+
linked: linkedFeatures.has(node.code),
|
|
6090
|
+
acCovered: acCoveredFeatures.has(node.code),
|
|
6091
|
+
waived: featureWaivers.has(node.code),
|
|
6092
|
+
verified: !featureWaivers.has(node.code) && verifiedFeatures.has(node.code)
|
|
6093
|
+
};
|
|
6094
|
+
}
|
|
6095
|
+
const thresholdWarnings = [];
|
|
6096
|
+
const thresholdErrors = [];
|
|
6097
|
+
const warningRate = thresholds.executableRefWarning;
|
|
6098
|
+
const errorRate = thresholds.executableRefError;
|
|
6099
|
+
const actualRate = totalTestCases > 0 ? withExecutableRef / totalTestCases : 1;
|
|
6100
|
+
if (warningRate !== void 0 && actualRate < warningRate) {
|
|
6101
|
+
thresholdWarnings.push(`executable_ref coverage ${executableRefRate} < warning threshold ${(warningRate * 100).toFixed(0)}%`);
|
|
6102
|
+
}
|
|
6103
|
+
if (errorRate !== void 0 && actualRate < errorRate) {
|
|
6104
|
+
thresholdErrors.push(`executable_ref coverage ${executableRefRate} < error threshold ${(errorRate * 100).toFixed(0)}%`);
|
|
6105
|
+
}
|
|
6106
|
+
if (thresholds.reportUncoveredScenarios !== false && uncoveredScenarios.length > 0) {
|
|
6107
|
+
thresholdWarnings.push(`${uncoveredScenarios.length} scenario(s) have no E2E coverage: ${uncoveredScenarios.join(", ")}`);
|
|
6108
|
+
}
|
|
6109
|
+
if (thresholds.reportUncoveredFeatures !== false && uncoveredFeatures.length > 0) {
|
|
6110
|
+
thresholdWarnings.push(`${uncoveredFeatures.length} feature(s) have no E2E coverage: ${uncoveredFeatures.join(", ")}`);
|
|
6111
|
+
}
|
|
6112
|
+
const acCoverageRateByFeature = {};
|
|
6113
|
+
const featureAcMap = /* @__PURE__ */ new Map();
|
|
6114
|
+
for (const node of featureNodes) {
|
|
6115
|
+
const acs = parseAcceptanceCriteria(await (0, import_promises5.readFile)((0, import_node_path6.join)(root, node.path), "utf-8"));
|
|
6116
|
+
featureAcMap.set(node.code, new Set(acs));
|
|
6117
|
+
}
|
|
6118
|
+
const coveredAcByFeature = /* @__PURE__ */ new Map();
|
|
6119
|
+
for (const node of e2eNodes) {
|
|
6120
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
6121
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
6122
|
+
for (const [feature, acs] of Object.entries(acCoverage)) {
|
|
6123
|
+
const existing = coveredAcByFeature.get(feature) ?? /* @__PURE__ */ new Set();
|
|
6124
|
+
for (const ac of toArray(acs)) {
|
|
6125
|
+
existing.add(String(ac));
|
|
6126
|
+
}
|
|
6127
|
+
coveredAcByFeature.set(feature, existing);
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
for (const [feature, allAcs] of featureAcMap) {
|
|
6131
|
+
const denominator = allAcs.size;
|
|
6132
|
+
if (denominator === 0) continue;
|
|
6133
|
+
const coveredAcs = coveredAcByFeature.get(feature) ?? /* @__PURE__ */ new Set();
|
|
6134
|
+
const numerator = [...coveredAcs].filter((ac) => allAcs.has(ac)).length;
|
|
6135
|
+
acCoverageRateByFeature[feature] = {
|
|
6136
|
+
numerator,
|
|
6137
|
+
denominator,
|
|
6138
|
+
rate: denominator > 0 ? numerator / denominator : 0
|
|
6139
|
+
};
|
|
6140
|
+
}
|
|
6141
|
+
return {
|
|
6142
|
+
totalTestCases,
|
|
6143
|
+
withExecutableRef,
|
|
6144
|
+
executableRefRate,
|
|
6145
|
+
statusBreakdown,
|
|
6146
|
+
chainTypeBreakdown,
|
|
6147
|
+
uncoveredScenarios,
|
|
6148
|
+
uncoveredFeatures,
|
|
6149
|
+
thresholdWarnings,
|
|
6150
|
+
thresholdErrors,
|
|
6151
|
+
acCoverageRateByFeature,
|
|
6152
|
+
scenarioCoverage,
|
|
6153
|
+
featureCoverage
|
|
6154
|
+
};
|
|
6155
|
+
}
|
|
6156
|
+
async function generateE2eRegistry(root, opts) {
|
|
6157
|
+
const e2eDir = (0, import_node_path6.join)(root, "artifacts", "tests", "e2e");
|
|
6158
|
+
let files;
|
|
6159
|
+
try {
|
|
6160
|
+
files = (await (0, import_promises5.readdir)(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).sort();
|
|
6161
|
+
} catch {
|
|
6162
|
+
return {
|
|
6163
|
+
registry_version: "1.0",
|
|
6164
|
+
generated_at: opts?.deterministic ? "1970-01-01T00:00:00.000Z" : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6165
|
+
total_batches: 0,
|
|
6166
|
+
total_test_cases: 0,
|
|
6167
|
+
batches: []
|
|
6168
|
+
};
|
|
6169
|
+
}
|
|
6170
|
+
const batches = [];
|
|
6171
|
+
let totalTestCases = 0;
|
|
6172
|
+
for (const file of files) {
|
|
6173
|
+
const filePath = (0, import_node_path6.join)(e2eDir, file);
|
|
6174
|
+
const raw = await (0, import_promises5.readFile)(filePath, "utf-8");
|
|
6175
|
+
const parsed = (0, import_gray_matter.default)(raw);
|
|
6176
|
+
const data = parsed.data;
|
|
6177
|
+
const batch = String(data.test_batch ?? (0, import_node_path6.basename)(file, (0, import_node_path6.extname)(file))).trim();
|
|
6178
|
+
const relPath = `artifacts/tests/e2e/${file}`;
|
|
6179
|
+
const scope = String(data.scope ?? "").trim();
|
|
6180
|
+
const acCoverage = normalizeAcCoverageForRegistry(data.ac_coverage);
|
|
6181
|
+
const relatedScenarios = toArray(data.related_scenarios).map(String).filter(Boolean);
|
|
6182
|
+
const lines = raw.split(/\r?\n/);
|
|
6183
|
+
const tcStarts = [];
|
|
6184
|
+
lines.forEach((line, index) => {
|
|
6185
|
+
const match = /^#{2,3}\s+(TC-\d+[a-z]?)\s*[::]?\s*.*$/.exec(line);
|
|
6186
|
+
if (match) {
|
|
6187
|
+
tcStarts.push({ id: match[1], index });
|
|
6188
|
+
}
|
|
6189
|
+
});
|
|
6190
|
+
const statusSummary = {};
|
|
6191
|
+
const blockingReasons = {};
|
|
6192
|
+
const frontmatterFixesBlock = String(data.fixes_block ?? "").trim();
|
|
6193
|
+
if (frontmatterFixesBlock && /no\s+(test\s+file|e2e)/i.test(frontmatterFixesBlock)) {
|
|
6194
|
+
for (const tc of tcStarts) {
|
|
6195
|
+
blockingReasons[tc.id] = frontmatterFixesBlock;
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
for (const start of tcStarts) {
|
|
6199
|
+
const end = tcStarts[tcStarts.indexOf(start) + 1]?.index ?? lines.length;
|
|
6200
|
+
const block = lines.slice(start.index, end);
|
|
6201
|
+
const fields = extractE2eTcFields(block);
|
|
6202
|
+
const status = String(fields["status"] ?? "created").trim().toLowerCase() || "created";
|
|
6203
|
+
statusSummary[status] = (statusSummary[status] ?? 0) + 1;
|
|
6204
|
+
if (status === "created" && !blockingReasons[start.id]) {
|
|
6205
|
+
const executableRef = String(fields["executable_ref"] ?? "").trim();
|
|
6206
|
+
const chainType = String(fields["chain_type"] ?? "").trim().toLowerCase();
|
|
6207
|
+
if (!executableRef && chainType === "desktop_chain") {
|
|
6208
|
+
blockingReasons[start.id] = "desktop_chain TC requires executable_ref";
|
|
6209
|
+
} else if (isPendingExecutableRef(executableRef)) {
|
|
6210
|
+
blockingReasons[start.id] = `pending: ${executableRef}`;
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
const testCaseCount = tcStarts.length;
|
|
6215
|
+
totalTestCases += testCaseCount;
|
|
6216
|
+
const batchStatus = Object.keys(blockingReasons).length > 0 ? "blocked" : void 0;
|
|
6217
|
+
batches.push({
|
|
6218
|
+
batch_id: batch,
|
|
6219
|
+
file: relPath,
|
|
6220
|
+
scope,
|
|
6221
|
+
ac_coverage: acCoverage,
|
|
6222
|
+
related_scenarios: relatedScenarios,
|
|
6223
|
+
test_case_count: testCaseCount,
|
|
6224
|
+
status_summary: statusSummary,
|
|
6225
|
+
status: batchStatus,
|
|
6226
|
+
blocking_reasons: Object.keys(blockingReasons).length > 0 ? blockingReasons : void 0
|
|
6227
|
+
});
|
|
6228
|
+
}
|
|
6229
|
+
return {
|
|
6230
|
+
registry_version: "1.0",
|
|
6231
|
+
generated_at: opts?.deterministic ? "1970-01-01T00:00:00.000Z" : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6232
|
+
total_batches: batches.length,
|
|
6233
|
+
total_test_cases: totalTestCases,
|
|
6234
|
+
batches
|
|
6235
|
+
};
|
|
6236
|
+
}
|
|
6237
|
+
function normalizeAcCoverageForRegistry(value) {
|
|
6238
|
+
if (!value || typeof value !== "object") return {};
|
|
6239
|
+
const result = {};
|
|
6240
|
+
for (const [key, val] of Object.entries(value)) {
|
|
6241
|
+
if (Array.isArray(val)) {
|
|
6242
|
+
result[key] = val.map(String);
|
|
6243
|
+
} else if (typeof val === "string") {
|
|
6244
|
+
result[key] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
return result;
|
|
6248
|
+
}
|
|
5497
6249
|
function isPendingExecutableRef(ref) {
|
|
5498
6250
|
const stripped = ref.replace(/^[\s-*()]+/, "").trim();
|
|
5499
6251
|
return /^pending\b/i.test(stripped);
|
|
@@ -5511,6 +6263,20 @@ function parseExecutableRefLines(ref) {
|
|
|
5511
6263
|
}
|
|
5512
6264
|
return results;
|
|
5513
6265
|
}
|
|
6266
|
+
var VALID_TC_STATUSES = /* @__PURE__ */ new Set(["created", "automated", "verified", "waived"]);
|
|
6267
|
+
var VALID_CHAIN_TYPES = /* @__PURE__ */ new Set([
|
|
6268
|
+
"desktop_chain",
|
|
6269
|
+
"mock_playwright",
|
|
6270
|
+
"core_e2e",
|
|
6271
|
+
"cli_e2e",
|
|
6272
|
+
"ui_sidecar_bridge",
|
|
6273
|
+
"partial_sidecar",
|
|
6274
|
+
"partial_rust"
|
|
6275
|
+
]);
|
|
6276
|
+
var DEPRECATED_CHAIN_TYPE_ALIASES = {
|
|
6277
|
+
core_only: "core_e2e",
|
|
6278
|
+
frontend_only: "mock_playwright"
|
|
6279
|
+
};
|
|
5514
6280
|
function isDesktopChainType(chainType) {
|
|
5515
6281
|
const normalizedChainType = chainType.trim().toLowerCase();
|
|
5516
6282
|
return normalizedChainType === "desktop_chain" || normalizedChainType === "";
|
|
@@ -5518,7 +6284,7 @@ function isDesktopChainType(chainType) {
|
|
|
5518
6284
|
function parseChainCoverageStatus(chainCoverage) {
|
|
5519
6285
|
return chainCoverage.trim().toLowerCase().match(/^[a-z_]+/)?.[0] ?? "";
|
|
5520
6286
|
}
|
|
5521
|
-
async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
6287
|
+
async function validatePartialRustEvidence(tcFields, tcKey, root, allFiles) {
|
|
5522
6288
|
const partialEvidence = String(tcFields["partial_evidence"] ?? "");
|
|
5523
6289
|
if (!partialEvidence.trim()) {
|
|
5524
6290
|
return { hasValidPartialRust: false, detail: "no partial_evidence field" };
|
|
@@ -5542,7 +6308,10 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5542
6308
|
return { hasValidPartialRust: false, detail: "no .rs file in partial_evidence" };
|
|
5543
6309
|
}
|
|
5544
6310
|
for (const ref of rustRefs) {
|
|
5545
|
-
const normalizedPath =
|
|
6311
|
+
const normalizedPath = resolveExecutableRefFile(ref.file, allFiles);
|
|
6312
|
+
if (!normalizedPath) {
|
|
6313
|
+
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
6314
|
+
}
|
|
5546
6315
|
const fullPath = (0, import_node_path6.join)(root, normalizedPath);
|
|
5547
6316
|
let content;
|
|
5548
6317
|
try {
|
|
@@ -5551,14 +6320,14 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5551
6320
|
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
5552
6321
|
}
|
|
5553
6322
|
const tcAnnotationPattern = new RegExp(
|
|
5554
|
-
`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
6323
|
+
`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
5555
6324
|
);
|
|
5556
6325
|
if (!tcAnnotationPattern.test(content)) {
|
|
5557
|
-
const noLevelPattern = new RegExp(`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\b`);
|
|
6326
|
+
const noLevelPattern = new RegExp(`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\b`);
|
|
5558
6327
|
if (noLevelPattern.test(content)) {
|
|
5559
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has
|
|
6328
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has E2E trace annotation ${tcKey} but not tagged [partial_rust]` };
|
|
5560
6329
|
}
|
|
5561
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no
|
|
6330
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no E2E trace annotation ${tcKey}` };
|
|
5562
6331
|
}
|
|
5563
6332
|
}
|
|
5564
6333
|
return { hasValidPartialRust: true, detail: "ok" };
|
|
@@ -5578,6 +6347,46 @@ function detectTestLevel(specFile, content) {
|
|
|
5578
6347
|
}
|
|
5579
6348
|
return "desktop_chain";
|
|
5580
6349
|
}
|
|
6350
|
+
function resolveExecutableRefFile(refFile, allFiles) {
|
|
6351
|
+
const normalized = refFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6352
|
+
if (!normalized || (0, import_node_path6.isAbsolute)(refFile) || normalized.split("/").includes("..")) {
|
|
6353
|
+
return void 0;
|
|
6354
|
+
}
|
|
6355
|
+
if (allFiles.includes(normalized)) {
|
|
6356
|
+
return normalized;
|
|
6357
|
+
}
|
|
6358
|
+
const suffix = `/${normalized}`;
|
|
6359
|
+
const matches = allFiles.filter((file) => file.endsWith(suffix));
|
|
6360
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
6361
|
+
}
|
|
6362
|
+
function isRunnerIncludeCandidate(filePath, runner) {
|
|
6363
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
6364
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
6365
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) {
|
|
6366
|
+
return false;
|
|
6367
|
+
}
|
|
6368
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length + 1);
|
|
6369
|
+
return runner.include.some((pattern) => matchesRunnerGlob(relativePath, pattern));
|
|
6370
|
+
}
|
|
6371
|
+
async function getAcceptingRunners(root, filePath, runners) {
|
|
6372
|
+
const accepting = [];
|
|
6373
|
+
for (const runner of runners) {
|
|
6374
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
6375
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
6376
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.startsWith(runnerRoot + "/") ? normalizedPath.slice(runnerRoot.length + 1) : normalizedPath;
|
|
6377
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) {
|
|
6378
|
+
continue;
|
|
6379
|
+
}
|
|
6380
|
+
const matchesInclude = runner.include.some((p) => matchesRunnerGlob(relativePath, p));
|
|
6381
|
+
if (!matchesInclude) continue;
|
|
6382
|
+
const matchesExclude = (runner.exclude ?? []).some((p) => matchesRunnerGlob(relativePath, p));
|
|
6383
|
+
if (matchesExclude) continue;
|
|
6384
|
+
const matchesTestIgnore = (runner.testIgnore ?? []).some((p) => matchesRunnerGlob(relativePath, p));
|
|
6385
|
+
if (matchesTestIgnore) continue;
|
|
6386
|
+
accepting.push(runner);
|
|
6387
|
+
}
|
|
6388
|
+
return accepting;
|
|
6389
|
+
}
|
|
5581
6390
|
function splitMarkdownCells(line) {
|
|
5582
6391
|
const cells = [];
|
|
5583
6392
|
let current = "";
|
|
@@ -5731,7 +6540,7 @@ function flattenAcCoverage(value) {
|
|
|
5731
6540
|
function needsDesktopChainWarning(node) {
|
|
5732
6541
|
const tcFields = asRecord(node.attrs?.tcFields);
|
|
5733
6542
|
const chainType = String(tcFields["chain_type"] ?? "").trim().toLowerCase();
|
|
5734
|
-
if (chainType
|
|
6543
|
+
if (chainType && VALID_CHAIN_TYPES.has(chainType) && chainType !== "desktop_chain" || chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5735
6544
|
return false;
|
|
5736
6545
|
}
|
|
5737
6546
|
const chainCoverage = String(tcFields["chain_coverage"] ?? "").trim().toLowerCase();
|
|
@@ -5885,9 +6694,14 @@ function mergeRecord(base, override) {
|
|
|
5885
6694
|
function mergeArtifactTypes(base, override) {
|
|
5886
6695
|
const result = { ...base };
|
|
5887
6696
|
for (const [type, definition] of Object.entries(override ?? {})) {
|
|
6697
|
+
const aliases = [
|
|
6698
|
+
...base[type]?.aliases ?? [],
|
|
6699
|
+
...definition.aliases ?? []
|
|
6700
|
+
].filter((alias, index, all) => all.indexOf(alias) === index);
|
|
5888
6701
|
result[type] = {
|
|
5889
6702
|
...base[type] ?? {},
|
|
5890
|
-
...definition
|
|
6703
|
+
...definition,
|
|
6704
|
+
...aliases.length > 0 ? { aliases } : {}
|
|
5891
6705
|
};
|
|
5892
6706
|
}
|
|
5893
6707
|
return result;
|
|
@@ -5981,6 +6795,8 @@ var TIER_ORDER = ["baseline", "target", "direct", "matrix", "transitive"];
|
|
|
5981
6795
|
function resolveArtifactContext(graph, opts) {
|
|
5982
6796
|
const mode = opts.mode ?? "full";
|
|
5983
6797
|
const maxPerCategory = opts.maxPerCategory ?? 20;
|
|
6798
|
+
const universalBaseline = opts.universalBaseline ?? true;
|
|
6799
|
+
const root = opts.root ?? graph.root;
|
|
5984
6800
|
const legacyCount = [opts.feature, opts.scenario, opts.decision, opts.design, opts.e2e_test].filter(Boolean).length;
|
|
5985
6801
|
if (opts.target && legacyCount > 0) {
|
|
5986
6802
|
return {
|
|
@@ -6133,12 +6949,80 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6133
6949
|
return "direct";
|
|
6134
6950
|
}
|
|
6135
6951
|
const pathMap = /* @__PURE__ */ new Map();
|
|
6136
|
-
|
|
6137
|
-
const
|
|
6138
|
-
|
|
6139
|
-
if (
|
|
6952
|
+
if (universalBaseline) {
|
|
6953
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6954
|
+
const existing = pathMap.get(ap.path);
|
|
6955
|
+
if (existing) {
|
|
6956
|
+
if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
|
|
6957
|
+
} else {
|
|
6958
|
+
pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
if (root) {
|
|
6962
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6963
|
+
const fullPath = (0, import_node_path6.join)(root, ap.path);
|
|
6964
|
+
let stat;
|
|
6965
|
+
try {
|
|
6966
|
+
stat = (0, import_node_fs4.statSync)(fullPath);
|
|
6967
|
+
} catch {
|
|
6968
|
+
stat = null;
|
|
6969
|
+
}
|
|
6970
|
+
if (!stat) {
|
|
6971
|
+
const msg = `Required baseline artifact not found: ${ap.path}`;
|
|
6972
|
+
if (!missing.includes(msg)) {
|
|
6973
|
+
missing.push(msg);
|
|
6974
|
+
missingDetails.push({
|
|
6975
|
+
ref: ap.path,
|
|
6976
|
+
from: "baseline",
|
|
6977
|
+
kind: "missing-baseline",
|
|
6978
|
+
message: msg,
|
|
6979
|
+
suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6980
|
+
});
|
|
6981
|
+
}
|
|
6982
|
+
} else if (!stat.isFile()) {
|
|
6983
|
+
const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
|
|
6984
|
+
if (!missing.includes(msg)) {
|
|
6985
|
+
missing.push(msg);
|
|
6986
|
+
missingDetails.push({
|
|
6987
|
+
ref: ap.path,
|
|
6988
|
+
from: "baseline",
|
|
6989
|
+
kind: "missing-baseline",
|
|
6990
|
+
message: msg,
|
|
6991
|
+
suggestedAction: `\u5C06 ${ap.path} \u4ECE\u76EE\u5F55\u6539\u4E3A\u6587\u4EF6\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6992
|
+
});
|
|
6993
|
+
}
|
|
6994
|
+
} else {
|
|
6995
|
+
try {
|
|
6996
|
+
(0, import_node_fs4.accessSync)(fullPath, import_node_fs4.constants.R_OK);
|
|
6997
|
+
} catch {
|
|
6998
|
+
const msg = `Required baseline artifact is not readable: ${ap.path}`;
|
|
6999
|
+
if (!missing.includes(msg)) {
|
|
7000
|
+
missing.push(msg);
|
|
7001
|
+
missingDetails.push({
|
|
7002
|
+
ref: ap.path,
|
|
7003
|
+
from: "baseline",
|
|
7004
|
+
kind: "missing-baseline",
|
|
7005
|
+
message: msg,
|
|
7006
|
+
suggestedAction: `\u4FEE\u590D ${ap.path} \u7684\u6587\u4EF6\u6743\u9650\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
7007
|
+
});
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
6140
7012
|
} else {
|
|
6141
|
-
|
|
7013
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
7014
|
+
const msg = `Cannot verify baseline without root: ${ap.path}`;
|
|
7015
|
+
if (!missing.includes(msg)) {
|
|
7016
|
+
missing.push(msg);
|
|
7017
|
+
missingDetails.push({
|
|
7018
|
+
ref: ap.path,
|
|
7019
|
+
from: "baseline",
|
|
7020
|
+
kind: "missing-baseline",
|
|
7021
|
+
message: msg,
|
|
7022
|
+
suggestedAction: `\u4F20\u9012 root \u53C2\u6570\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
7023
|
+
});
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
6142
7026
|
}
|
|
6143
7027
|
}
|
|
6144
7028
|
pathMap.set(targetNode.path, {
|
|
@@ -6239,7 +7123,8 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6239
7123
|
context,
|
|
6240
7124
|
missing,
|
|
6241
7125
|
missingDetails,
|
|
6242
|
-
omitted
|
|
7126
|
+
omitted,
|
|
7127
|
+
baselinePolicy: universalBaseline
|
|
6243
7128
|
};
|
|
6244
7129
|
}
|
|
6245
7130
|
function formatContextMarkdown(manifest) {
|
|
@@ -6327,12 +7212,14 @@ function formatContextMarkdown(manifest) {
|
|
|
6327
7212
|
buildGraph,
|
|
6328
7213
|
buildVersionIndex,
|
|
6329
7214
|
collectChangedPaths,
|
|
7215
|
+
computeE2eCoverageStats,
|
|
6330
7216
|
dirname,
|
|
6331
7217
|
discoverAndAuditPackets,
|
|
6332
7218
|
discoverTargets,
|
|
6333
7219
|
doctorArtifactChain,
|
|
6334
7220
|
extname,
|
|
6335
7221
|
formatContextMarkdown,
|
|
7222
|
+
generateE2eRegistry,
|
|
6336
7223
|
getArtifactTypeMetadata,
|
|
6337
7224
|
getTargetArtifactTypes,
|
|
6338
7225
|
installManagedHookBlock,
|