artifact-graph 0.5.0 → 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 +41 -0
- package/dist/cli.js +803 -42
- package/dist/index.cjs +676 -41
- package/dist/index.d.cts +121 -3
- package/dist/index.d.ts +121 -3
- package/dist/index.js +668 -35
- package/package.json +1 -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,7 +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);
|
|
102
|
-
var
|
|
104
|
+
var import_node_fs4 = require("fs");
|
|
103
105
|
var import_promises5 = require("fs/promises");
|
|
104
106
|
var import_node_path6 = require("path");
|
|
105
107
|
|
|
@@ -356,6 +358,45 @@ function validatePacketMarkdown(markdown) {
|
|
|
356
358
|
return { ok: !hasError, issues };
|
|
357
359
|
}
|
|
358
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
|
+
|
|
359
400
|
// src/target-selector.ts
|
|
360
401
|
function parseTargetSelector(value) {
|
|
361
402
|
const separator = value.indexOf(":");
|
|
@@ -1373,6 +1414,7 @@ function validatePacketPrompt(prompt) {
|
|
|
1373
1414
|
|
|
1374
1415
|
// src/versioned-traceability.ts
|
|
1375
1416
|
var import_node_crypto = require("crypto");
|
|
1417
|
+
var import_node_fs = require("fs");
|
|
1376
1418
|
var import_promises2 = require("fs/promises");
|
|
1377
1419
|
var import_node_path2 = require("path");
|
|
1378
1420
|
var VERSION_LOCK_PATH = "artifacts/traceability-version-lock.json";
|
|
@@ -1413,13 +1455,15 @@ async function buildVersionIndex(root, graph) {
|
|
|
1413
1455
|
edges: sortBy(edges, (edge2) => `${edge2.from} ${edge2.to} ${edge2.kind} ${edge2.sourcePath} ${edge2.sourceLine}`)
|
|
1414
1456
|
};
|
|
1415
1457
|
}
|
|
1416
|
-
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
1458
|
+
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, config) {
|
|
1417
1459
|
const index = await buildVersionIndex(root, graph);
|
|
1460
|
+
const schema = config ?? await loadConfig(root);
|
|
1418
1461
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1419
1462
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1420
1463
|
const nodeByArtifact = new Map(index.nodes.map((node) => [`${node.type}:${node.id}`, node]));
|
|
1421
1464
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1422
1465
|
const currentEdges = implementationEdges(index);
|
|
1466
|
+
const lockableEdges = await lockableImplementationEdges(root, index, schema);
|
|
1423
1467
|
const currentEdgeIds = new Set(currentEdges.map((edge2) => edge2.edgeId));
|
|
1424
1468
|
const issues = [];
|
|
1425
1469
|
let fresh = 0;
|
|
@@ -1507,8 +1551,29 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
|
1507
1551
|
issues.push(...entryIssues);
|
|
1508
1552
|
}
|
|
1509
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
|
+
}
|
|
1510
1575
|
const reportedMissingLocks = /* @__PURE__ */ new Set();
|
|
1511
|
-
for (const edge2 of
|
|
1576
|
+
for (const edge2 of lockableEdges) {
|
|
1512
1577
|
const edgeId = edge2.edgeId;
|
|
1513
1578
|
if (!lock.locks.some((entry) => entry.edgeId === edgeId)) {
|
|
1514
1579
|
if (reportedMissingLocks.has(edgeId)) {
|
|
@@ -1590,6 +1655,7 @@ async function updateVersionLock(root, options) {
|
|
|
1590
1655
|
}
|
|
1591
1656
|
async function bootstrapVersionLock(root, options = {}) {
|
|
1592
1657
|
const index = await buildVersionIndex(root);
|
|
1658
|
+
const config = await loadConfig(root);
|
|
1593
1659
|
const lockPath = normalizeRelativePath(root, options.lockPath ?? VERSION_LOCK_PATH);
|
|
1594
1660
|
if (!options.force) {
|
|
1595
1661
|
const existing = await readVersionLock(root, lockPath);
|
|
@@ -1599,7 +1665,7 @@ async function bootstrapVersionLock(root, options = {}) {
|
|
|
1599
1665
|
}
|
|
1600
1666
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1601
1667
|
const entries = /* @__PURE__ */ new Map();
|
|
1602
|
-
for (const edge2 of
|
|
1668
|
+
for (const edge2 of await lockableImplementationEdges(root, index, config)) {
|
|
1603
1669
|
const source = nodeByUid.get(edge2.from);
|
|
1604
1670
|
const artifact = nodeByUid.get(edge2.to);
|
|
1605
1671
|
if (!source || !artifact) {
|
|
@@ -1634,10 +1700,11 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1634
1700
|
throw new Error("Changed-only version-lock refresh includes artifact-graph.config.yaml and requires --all");
|
|
1635
1701
|
}
|
|
1636
1702
|
const index = await buildVersionIndex(root);
|
|
1703
|
+
const config = await loadConfig(root);
|
|
1637
1704
|
const lock = await readVersionLock(root, lockPath);
|
|
1638
1705
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1639
1706
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1640
|
-
const currentImplementationEdges =
|
|
1707
|
+
const currentImplementationEdges = await lockableImplementationEdges(root, index, config);
|
|
1641
1708
|
const currentEdgePairs = new Set(currentImplementationEdges.map((edge2) => `${edge2.from} ${edge2.to}`));
|
|
1642
1709
|
const currentEntries = /* @__PURE__ */ new Map();
|
|
1643
1710
|
const changedPathSet = new Set(changedPaths);
|
|
@@ -1712,7 +1779,7 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1712
1779
|
locks: sortBy([...nextLocks.values()], (item) => item.edgeId)
|
|
1713
1780
|
};
|
|
1714
1781
|
await writeVersionLock(root, lockPath, next);
|
|
1715
|
-
const postAudit = await auditVersionLock(root, lockPath);
|
|
1782
|
+
const postAudit = await auditVersionLock(root, lockPath, void 0, config);
|
|
1716
1783
|
return {
|
|
1717
1784
|
schemaVersion: "1.0",
|
|
1718
1785
|
root,
|
|
@@ -1731,7 +1798,8 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1731
1798
|
async function traceVersion(root, target, lockPath = VERSION_LOCK_PATH) {
|
|
1732
1799
|
const index = await buildVersionIndex(root);
|
|
1733
1800
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1734
|
-
const
|
|
1801
|
+
const config = await loadConfig(root);
|
|
1802
|
+
const audit = await auditVersionLock(root, safeLockPath, void 0, config);
|
|
1735
1803
|
const targetUid = parseTarget(target);
|
|
1736
1804
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1737
1805
|
const targetNode = index.nodes.find((node) => node.uid === targetUid);
|
|
@@ -1967,6 +2035,28 @@ function implementationEdges(index) {
|
|
|
1967
2035
|
};
|
|
1968
2036
|
});
|
|
1969
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
|
+
}
|
|
1970
2060
|
function lockRefFromNode(node) {
|
|
1971
2061
|
return {
|
|
1972
2062
|
type: node.type,
|
|
@@ -2079,13 +2169,60 @@ function normalizeRelativePath(root, path) {
|
|
|
2079
2169
|
function sortBy(items, keyFn) {
|
|
2080
2170
|
return [...items].sort((left, right) => keyFn(left).localeCompare(keyFn(right)));
|
|
2081
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
|
+
}
|
|
2082
2219
|
function sortUnique(items) {
|
|
2083
2220
|
return [...new Set(items)].sort((left, right) => left.localeCompare(right));
|
|
2084
2221
|
}
|
|
2085
2222
|
|
|
2086
2223
|
// src/cli-resolver.ts
|
|
2087
2224
|
var import_node_child_process = require("child_process");
|
|
2088
|
-
var
|
|
2225
|
+
var import_node_fs2 = require("fs");
|
|
2089
2226
|
var import_promises3 = require("fs/promises");
|
|
2090
2227
|
var import_node_path3 = require("path");
|
|
2091
2228
|
var import_node_util = require("util");
|
|
@@ -2223,7 +2360,7 @@ ${maybe.stderr ?? ""}`;
|
|
|
2223
2360
|
}
|
|
2224
2361
|
async function pathExists(path) {
|
|
2225
2362
|
try {
|
|
2226
|
-
await (0, import_promises3.access)(path,
|
|
2363
|
+
await (0, import_promises3.access)(path, import_node_fs2.constants.R_OK);
|
|
2227
2364
|
return true;
|
|
2228
2365
|
} catch {
|
|
2229
2366
|
return false;
|
|
@@ -2322,7 +2459,7 @@ async function resolveGitHookPath(root, hookName) {
|
|
|
2322
2459
|
}
|
|
2323
2460
|
|
|
2324
2461
|
// src/hook-installer.ts
|
|
2325
|
-
var
|
|
2462
|
+
var import_node_fs3 = require("fs");
|
|
2326
2463
|
var import_node_crypto2 = require("crypto");
|
|
2327
2464
|
var import_promises4 = require("fs/promises");
|
|
2328
2465
|
var import_node_path5 = require("path");
|
|
@@ -2636,7 +2773,7 @@ async function readHookSnapshot(hookPath) {
|
|
|
2636
2773
|
}
|
|
2637
2774
|
let handle;
|
|
2638
2775
|
try {
|
|
2639
|
-
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));
|
|
2640
2777
|
const opened = await handle.stat({ bigint: true });
|
|
2641
2778
|
if (opened.dev !== metadata.dev || opened.ino !== metadata.ino) {
|
|
2642
2779
|
await handle.close();
|
|
@@ -3142,7 +3279,12 @@ var DEFAULT_SCHEMA = {
|
|
|
3142
3279
|
allowedEdges: [],
|
|
3143
3280
|
forbiddenEdges: [{ from: "scenario", to: "entity", kind: "references" }],
|
|
3144
3281
|
statuses: ["planned", "active", "done", "deprecated"],
|
|
3145
|
-
idRanges: {}
|
|
3282
|
+
idRanges: {},
|
|
3283
|
+
e2e: {
|
|
3284
|
+
report_uncovered_scenarios: true,
|
|
3285
|
+
report_uncovered_features: true,
|
|
3286
|
+
runners: []
|
|
3287
|
+
}
|
|
3146
3288
|
};
|
|
3147
3289
|
async function loadConfig(root) {
|
|
3148
3290
|
const configPath = (0, import_node_path6.join)(root, "artifact-graph.config.yaml");
|
|
@@ -3161,7 +3303,21 @@ async function loadConfig(root) {
|
|
|
3161
3303
|
`Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
|
|
3162
3304
|
);
|
|
3163
3305
|
}
|
|
3164
|
-
|
|
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 = {
|
|
3165
3321
|
...DEFAULT_SCHEMA,
|
|
3166
3322
|
...parsed,
|
|
3167
3323
|
types: mergeArtifactTypes(DEFAULT_SCHEMA.types, parsed.types),
|
|
@@ -3170,8 +3326,101 @@ async function loadConfig(root) {
|
|
|
3170
3326
|
allowedEdges: parsed.allowedEdges ?? DEFAULT_SCHEMA.allowedEdges,
|
|
3171
3327
|
forbiddenEdges: parsed.forbiddenEdges ?? DEFAULT_SCHEMA.forbiddenEdges,
|
|
3172
3328
|
statuses: parsed.statuses ?? DEFAULT_SCHEMA.statuses,
|
|
3173
|
-
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
|
|
3329
|
+
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges),
|
|
3330
|
+
e2e: mergedE2e
|
|
3331
|
+
};
|
|
3332
|
+
return merged;
|
|
3333
|
+
}
|
|
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
|
+
}
|
|
3174
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
|
+
}
|
|
3175
3424
|
}
|
|
3176
3425
|
function buildGraph(nodes, edges, diagnostics = [], root) {
|
|
3177
3426
|
const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
|
|
@@ -5320,6 +5569,29 @@ function validateE2eTests(graph) {
|
|
|
5320
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" }));
|
|
5321
5570
|
}
|
|
5322
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
|
+
}
|
|
5323
5595
|
if (needsDesktopChainWarning(node)) {
|
|
5324
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" }));
|
|
5325
5597
|
}
|
|
@@ -5378,10 +5650,10 @@ function validateE2eRegistry(graph) {
|
|
|
5378
5650
|
}
|
|
5379
5651
|
return issues;
|
|
5380
5652
|
}
|
|
5381
|
-
async function validateExecutableTraceability(root) {
|
|
5653
|
+
async function validateExecutableTraceability(root, config) {
|
|
5382
5654
|
const issues = [];
|
|
5655
|
+
const schema = config ?? await loadConfig(root);
|
|
5383
5656
|
const e2eDir = (0, import_node_path6.join)(root, "artifacts", "tests", "e2e");
|
|
5384
|
-
const specPatterns = ["heimdall/**/*.spec.ts", "heimdall/**/*.e2e.spec.ts"];
|
|
5385
5657
|
let e2eFiles;
|
|
5386
5658
|
try {
|
|
5387
5659
|
e2eFiles = (await (0, import_promises5.readdir)(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => (0, import_node_path6.join)(e2eDir, name));
|
|
@@ -5424,9 +5696,16 @@ async function validateExecutableTraceability(root) {
|
|
|
5424
5696
|
}
|
|
5425
5697
|
const allFiles = await walk(root);
|
|
5426
5698
|
const specFiles = /* @__PURE__ */ new Set();
|
|
5427
|
-
|
|
5699
|
+
const configuredRunners = schema.e2e?.runners ?? [];
|
|
5700
|
+
if (configuredRunners.length > 0) {
|
|
5428
5701
|
for (const file of allFiles) {
|
|
5429
|
-
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)) {
|
|
5430
5709
|
specFiles.add(file);
|
|
5431
5710
|
}
|
|
5432
5711
|
}
|
|
@@ -5497,12 +5776,21 @@ async function validateExecutableTraceability(root) {
|
|
|
5497
5776
|
const refEntries = parseExecutableRefLines(ref);
|
|
5498
5777
|
const validFiles = [];
|
|
5499
5778
|
for (const entry of refEntries) {
|
|
5500
|
-
const normalizedRefFile =
|
|
5501
|
-
const fileExists = specFiles.has(normalizedRefFile);
|
|
5779
|
+
const normalizedRefFile = resolveExecutableRefFile(entry.file, allFiles);
|
|
5780
|
+
const fileExists = normalizedRefFile !== void 0 && specFiles.has(normalizedRefFile);
|
|
5502
5781
|
if (!fileExists) {
|
|
5503
5782
|
issues.push(issue("E2E-TRACE-001", `executable_ref target file not found: ${entry.file}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5504
5783
|
continue;
|
|
5505
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
|
+
}
|
|
5506
5794
|
if (entry.testId) {
|
|
5507
5795
|
let content;
|
|
5508
5796
|
try {
|
|
@@ -5517,10 +5805,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5517
5805
|
}
|
|
5518
5806
|
}
|
|
5519
5807
|
const annotationsForTc = refToSource.get(tcKey);
|
|
5520
|
-
const hasAnnotationInFile = annotationsForTc?.some((ann) =>
|
|
5521
|
-
const normalizedAnnFile = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5522
|
-
return normalizedAnnFile === normalizedRefFile;
|
|
5523
|
-
}) ?? false;
|
|
5808
|
+
const hasAnnotationInFile = annotationsForTc?.some((ann) => ann.file === normalizedRefFile) ?? false;
|
|
5524
5809
|
if (!hasAnnotationInFile) {
|
|
5525
5810
|
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5526
5811
|
continue;
|
|
@@ -5558,24 +5843,37 @@ async function validateExecutableTraceability(root) {
|
|
|
5558
5843
|
if (matchesAnyRef) {
|
|
5559
5844
|
continue;
|
|
5560
5845
|
}
|
|
5561
|
-
const primaryRef = refEntries[0];
|
|
5562
|
-
const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
|
|
5563
5846
|
const detail = `file: MD refs=[${refEntries.map((e) => e.file).join(", ")}] vs source=${ann.file}`;
|
|
5564
5847
|
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5565
5848
|
}
|
|
5566
5849
|
}
|
|
5567
|
-
for (const [tcKey, { chainType, path, line }] of
|
|
5568
|
-
|
|
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") {
|
|
5569
5854
|
continue;
|
|
5570
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;
|
|
5571
5864
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5572
5865
|
const sourceAnnotations = refToSource.get(tcKey);
|
|
5573
5866
|
if (!sourceAnnotations) {
|
|
5574
5867
|
continue;
|
|
5575
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
|
+
}
|
|
5576
5875
|
for (const ann of sourceAnnotations) {
|
|
5577
|
-
|
|
5578
|
-
if (!validFiles.has(normalizedAnnFile)) {
|
|
5876
|
+
if (!validFiles.has(ann.file)) {
|
|
5579
5877
|
continue;
|
|
5580
5878
|
}
|
|
5581
5879
|
if (ann.level === "mock_playwright") {
|
|
@@ -5601,10 +5899,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5601
5899
|
continue;
|
|
5602
5900
|
}
|
|
5603
5901
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5604
|
-
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) =>
|
|
5605
|
-
const normalized = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5606
|
-
return validFiles.has(normalized);
|
|
5607
|
-
});
|
|
5902
|
+
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) => validFiles.has(ann.file));
|
|
5608
5903
|
const hasDesktopChain = sourceAnnotations?.some((ann) => ann.level === "desktop_chain") ?? false;
|
|
5609
5904
|
const hasBridge = sourceAnnotations?.some((ann) => ann.level === "ui_sidecar_bridge") ?? false;
|
|
5610
5905
|
if (isComplete) {
|
|
@@ -5613,7 +5908,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5613
5908
|
if (hasDesktopChain) {
|
|
5614
5909
|
hasValidEvidence = true;
|
|
5615
5910
|
} else if (hasBridge) {
|
|
5616
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5911
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5617
5912
|
if (partialResult.hasValidPartialRust) {
|
|
5618
5913
|
hasValidEvidence = true;
|
|
5619
5914
|
} else {
|
|
@@ -5653,7 +5948,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5653
5948
|
}
|
|
5654
5949
|
let partialDetail = "";
|
|
5655
5950
|
if (hasBridge) {
|
|
5656
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5951
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5657
5952
|
if (partialResult.hasValidPartialRust) {
|
|
5658
5953
|
continue;
|
|
5659
5954
|
}
|
|
@@ -5670,6 +5965,287 @@ async function validateExecutableTraceability(root) {
|
|
|
5670
5965
|
}
|
|
5671
5966
|
return issues;
|
|
5672
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
|
+
}
|
|
5673
6249
|
function isPendingExecutableRef(ref) {
|
|
5674
6250
|
const stripped = ref.replace(/^[\s-*()]+/, "").trim();
|
|
5675
6251
|
return /^pending\b/i.test(stripped);
|
|
@@ -5687,6 +6263,20 @@ function parseExecutableRefLines(ref) {
|
|
|
5687
6263
|
}
|
|
5688
6264
|
return results;
|
|
5689
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
|
+
};
|
|
5690
6280
|
function isDesktopChainType(chainType) {
|
|
5691
6281
|
const normalizedChainType = chainType.trim().toLowerCase();
|
|
5692
6282
|
return normalizedChainType === "desktop_chain" || normalizedChainType === "";
|
|
@@ -5694,7 +6284,7 @@ function isDesktopChainType(chainType) {
|
|
|
5694
6284
|
function parseChainCoverageStatus(chainCoverage) {
|
|
5695
6285
|
return chainCoverage.trim().toLowerCase().match(/^[a-z_]+/)?.[0] ?? "";
|
|
5696
6286
|
}
|
|
5697
|
-
async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
6287
|
+
async function validatePartialRustEvidence(tcFields, tcKey, root, allFiles) {
|
|
5698
6288
|
const partialEvidence = String(tcFields["partial_evidence"] ?? "");
|
|
5699
6289
|
if (!partialEvidence.trim()) {
|
|
5700
6290
|
return { hasValidPartialRust: false, detail: "no partial_evidence field" };
|
|
@@ -5718,7 +6308,10 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5718
6308
|
return { hasValidPartialRust: false, detail: "no .rs file in partial_evidence" };
|
|
5719
6309
|
}
|
|
5720
6310
|
for (const ref of rustRefs) {
|
|
5721
|
-
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
|
+
}
|
|
5722
6315
|
const fullPath = (0, import_node_path6.join)(root, normalizedPath);
|
|
5723
6316
|
let content;
|
|
5724
6317
|
try {
|
|
@@ -5754,6 +6347,46 @@ function detectTestLevel(specFile, content) {
|
|
|
5754
6347
|
}
|
|
5755
6348
|
return "desktop_chain";
|
|
5756
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
|
+
}
|
|
5757
6390
|
function splitMarkdownCells(line) {
|
|
5758
6391
|
const cells = [];
|
|
5759
6392
|
let current = "";
|
|
@@ -5907,7 +6540,7 @@ function flattenAcCoverage(value) {
|
|
|
5907
6540
|
function needsDesktopChainWarning(node) {
|
|
5908
6541
|
const tcFields = asRecord(node.attrs?.tcFields);
|
|
5909
6542
|
const chainType = String(tcFields["chain_type"] ?? "").trim().toLowerCase();
|
|
5910
|
-
if (chainType
|
|
6543
|
+
if (chainType && VALID_CHAIN_TYPES.has(chainType) && chainType !== "desktop_chain" || chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5911
6544
|
return false;
|
|
5912
6545
|
}
|
|
5913
6546
|
const chainCoverage = String(tcFields["chain_coverage"] ?? "").trim().toLowerCase();
|
|
@@ -6330,7 +6963,7 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6330
6963
|
const fullPath = (0, import_node_path6.join)(root, ap.path);
|
|
6331
6964
|
let stat;
|
|
6332
6965
|
try {
|
|
6333
|
-
stat = (0,
|
|
6966
|
+
stat = (0, import_node_fs4.statSync)(fullPath);
|
|
6334
6967
|
} catch {
|
|
6335
6968
|
stat = null;
|
|
6336
6969
|
}
|
|
@@ -6360,7 +6993,7 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6360
6993
|
}
|
|
6361
6994
|
} else {
|
|
6362
6995
|
try {
|
|
6363
|
-
(0,
|
|
6996
|
+
(0, import_node_fs4.accessSync)(fullPath, import_node_fs4.constants.R_OK);
|
|
6364
6997
|
} catch {
|
|
6365
6998
|
const msg = `Required baseline artifact is not readable: ${ap.path}`;
|
|
6366
6999
|
if (!missing.includes(msg)) {
|
|
@@ -6579,12 +7212,14 @@ function formatContextMarkdown(manifest) {
|
|
|
6579
7212
|
buildGraph,
|
|
6580
7213
|
buildVersionIndex,
|
|
6581
7214
|
collectChangedPaths,
|
|
7215
|
+
computeE2eCoverageStats,
|
|
6582
7216
|
dirname,
|
|
6583
7217
|
discoverAndAuditPackets,
|
|
6584
7218
|
discoverTargets,
|
|
6585
7219
|
doctorArtifactChain,
|
|
6586
7220
|
extname,
|
|
6587
7221
|
formatContextMarkdown,
|
|
7222
|
+
generateE2eRegistry,
|
|
6588
7223
|
getArtifactTypeMetadata,
|
|
6589
7224
|
getTargetArtifactTypes,
|
|
6590
7225
|
installManagedHookBlock,
|