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/cli.js
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
import yaml2 from "js-yaml";
|
|
5
5
|
import { realpathSync } from "fs";
|
|
6
6
|
import { access as access2, mkdir as mkdir6, readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
|
|
7
|
-
import { isAbsolute as
|
|
7
|
+
import { isAbsolute as isAbsolute4, join as join7 } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
11
|
import Database from "better-sqlite3";
|
|
12
12
|
import matter from "gray-matter";
|
|
13
13
|
import yaml from "js-yaml";
|
|
14
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync2, statSync } from "fs";
|
|
14
15
|
import { mkdir as mkdir4, readFile as readFile2, readdir, writeFile as writeFile3 } from "fs/promises";
|
|
15
|
-
import { basename as basename2, dirname as dirname3, extname, join as join5, relative as relative2 } from "path";
|
|
16
|
+
import { basename as basename2, dirname as dirname3, extname, isAbsolute as isAbsolute3, join as join5, relative as relative2, resolve as resolve3 } from "path";
|
|
16
17
|
|
|
17
18
|
// src/packet-constants.ts
|
|
18
19
|
var ALWAYS_PRESENT_ITEMS = [
|
|
@@ -91,13 +92,76 @@ function validatePacket(packet, schema) {
|
|
|
91
92
|
path: "target.id"
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
const isBaselineExplicitlyDisabled = packet.baselinePolicy === false;
|
|
96
|
+
const expectedPaths = new Set(ALWAYS_PRESENT_ITEMS.map((item) => item.path));
|
|
97
|
+
if (isBaselineExplicitlyDisabled) {
|
|
98
|
+
if (packet.requiredBaseline.total !== 0) {
|
|
99
|
+
issues.push({
|
|
100
|
+
severity: "error",
|
|
101
|
+
code: "PKT-004",
|
|
102
|
+
message: `baselinePolicy=false requires requiredBaseline.total=0, got ${packet.requiredBaseline.total}`,
|
|
103
|
+
path: "requiredBaseline.total"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (packet.requiredBaseline.items.length !== 0) {
|
|
107
|
+
issues.push({
|
|
108
|
+
severity: "error",
|
|
109
|
+
code: "PKT-004",
|
|
110
|
+
message: `baselinePolicy=false requires requiredBaseline.items=[], got ${packet.requiredBaseline.items.length} item(s)`,
|
|
111
|
+
path: "requiredBaseline.items"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
|
|
116
|
+
issues.push({
|
|
117
|
+
severity: "error",
|
|
118
|
+
code: "PKT-004",
|
|
119
|
+
message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
|
|
120
|
+
path: "requiredBaseline.total"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (packet.requiredBaseline.items.length !== BASELINE_ITEMS_COUNT) {
|
|
124
|
+
issues.push({
|
|
125
|
+
severity: "error",
|
|
126
|
+
code: "PKT-004",
|
|
127
|
+
message: `requiredBaseline.items.length must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.items.length}`,
|
|
128
|
+
path: "requiredBaseline.items"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const actualPaths = packet.requiredBaseline.items.map((item) => item.path);
|
|
132
|
+
const actualPathSet = new Set(actualPaths);
|
|
133
|
+
if (actualPathSet.size !== actualPaths.length) {
|
|
134
|
+
issues.push({
|
|
135
|
+
severity: "error",
|
|
136
|
+
code: "PKT-004",
|
|
137
|
+
message: `requiredBaseline.items contains duplicate paths (${actualPaths.length} items, ${actualPathSet.size} unique)`,
|
|
138
|
+
path: "requiredBaseline.items"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const missingPaths = [];
|
|
142
|
+
for (const ep of expectedPaths) {
|
|
143
|
+
if (!actualPathSet.has(ep)) missingPaths.push(ep);
|
|
144
|
+
}
|
|
145
|
+
if (missingPaths.length > 0) {
|
|
146
|
+
issues.push({
|
|
147
|
+
severity: "error",
|
|
148
|
+
code: "PKT-004",
|
|
149
|
+
message: `requiredBaseline.items missing expected path(s): ${missingPaths.join(", ")}`,
|
|
150
|
+
path: "requiredBaseline.items"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const extraPaths = [];
|
|
154
|
+
for (const ap of actualPathSet) {
|
|
155
|
+
if (!expectedPaths.has(ap)) extraPaths.push(ap);
|
|
156
|
+
}
|
|
157
|
+
if (extraPaths.length > 0) {
|
|
158
|
+
issues.push({
|
|
159
|
+
severity: "error",
|
|
160
|
+
code: "PKT-004",
|
|
161
|
+
message: `requiredBaseline.items contains unexpected path(s): ${extraPaths.join(", ")}`,
|
|
162
|
+
path: "requiredBaseline.items"
|
|
163
|
+
});
|
|
164
|
+
}
|
|
101
165
|
}
|
|
102
166
|
const constraints = packet.implementationBlueprintDraft.constraints;
|
|
103
167
|
if (constraints.length !== BASELINE_CONSTRAINTS_COUNT) {
|
|
@@ -179,6 +243,45 @@ function validatePacket(packet, schema) {
|
|
|
179
243
|
return { ok: !hasError, issues };
|
|
180
244
|
}
|
|
181
245
|
|
|
246
|
+
// src/glob-matcher.ts
|
|
247
|
+
function matchesRunnerGlob(filePath, pattern) {
|
|
248
|
+
const normalizedPath = normalizeGlobValue(filePath);
|
|
249
|
+
const normalizedPattern = normalizeGlobValue(pattern);
|
|
250
|
+
let expression = "^";
|
|
251
|
+
for (let index = 0; index < normalizedPattern.length; index += 1) {
|
|
252
|
+
const character = normalizedPattern[index];
|
|
253
|
+
if (character === "*") {
|
|
254
|
+
if (normalizedPattern[index + 1] === "*") {
|
|
255
|
+
while (normalizedPattern[index + 1] === "*") {
|
|
256
|
+
index += 1;
|
|
257
|
+
}
|
|
258
|
+
if (normalizedPattern[index + 1] === "/") {
|
|
259
|
+
index += 1;
|
|
260
|
+
expression += "(?:[^/]+/)*";
|
|
261
|
+
} else {
|
|
262
|
+
expression += ".*";
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
expression += "[^/]*";
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (character === "?") {
|
|
270
|
+
expression += "[^/]";
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
expression += escapeRegexCharacter(character);
|
|
274
|
+
}
|
|
275
|
+
expression += "$";
|
|
276
|
+
return new RegExp(expression).test(normalizedPath);
|
|
277
|
+
}
|
|
278
|
+
function normalizeGlobValue(value) {
|
|
279
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
280
|
+
}
|
|
281
|
+
function escapeRegexCharacter(character) {
|
|
282
|
+
return "\\^$+?.()|{}[]".includes(character) ? String.fromCharCode(92) + character : character;
|
|
283
|
+
}
|
|
284
|
+
|
|
182
285
|
// src/target-selector.ts
|
|
183
286
|
function parseTargetSelector(value) {
|
|
184
287
|
const separator = value.indexOf(":");
|
|
@@ -499,7 +602,10 @@ function assemblePacket(manifest, options) {
|
|
|
499
602
|
missing: [...manifest.missing],
|
|
500
603
|
missingDetails: manifest.missingDetails ? [...manifest.missingDetails] : void 0,
|
|
501
604
|
implementationBlueprintDraft: blueprintDraft,
|
|
502
|
-
validationCommands
|
|
605
|
+
validationCommands,
|
|
606
|
+
// @feature ACA17
|
|
607
|
+
// @decision D-ACA-17
|
|
608
|
+
baselinePolicy: manifest.baselinePolicy
|
|
503
609
|
};
|
|
504
610
|
return packet;
|
|
505
611
|
}
|
|
@@ -720,7 +826,9 @@ async function auditSingleTarget(target, graph, options) {
|
|
|
720
826
|
const manifest = resolveArtifactContext(graph, {
|
|
721
827
|
target: { type: target.type, id: target.id },
|
|
722
828
|
mode: options.mode,
|
|
723
|
-
maxPerCategory: options.maxPerCategory
|
|
829
|
+
maxPerCategory: options.maxPerCategory,
|
|
830
|
+
universalBaseline: options.universalBaseline,
|
|
831
|
+
root: options.root
|
|
724
832
|
});
|
|
725
833
|
const packet = assemblePacket(manifest, {
|
|
726
834
|
mode: options.mode,
|
|
@@ -857,6 +965,7 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
857
965
|
type: d.type,
|
|
858
966
|
id: d.id
|
|
859
967
|
}));
|
|
968
|
+
const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
|
|
860
969
|
return auditPackets(root, targets, {
|
|
861
970
|
root,
|
|
862
971
|
outDir: options.outDir,
|
|
@@ -866,7 +975,8 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
866
975
|
summaryOnly: options.summaryOnly,
|
|
867
976
|
sampleTargets: options.sampleTargets,
|
|
868
977
|
summaryDetail: options.summaryDetail,
|
|
869
|
-
schema: config
|
|
978
|
+
schema: config,
|
|
979
|
+
universalBaseline: effectiveBaseline
|
|
870
980
|
}, graph);
|
|
871
981
|
}
|
|
872
982
|
|
|
@@ -1189,6 +1299,7 @@ function validatePacketPrompt(prompt) {
|
|
|
1189
1299
|
|
|
1190
1300
|
// src/versioned-traceability.ts
|
|
1191
1301
|
import { createHash } from "crypto";
|
|
1302
|
+
import { existsSync } from "fs";
|
|
1192
1303
|
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
1193
1304
|
import { dirname, join as join2, relative } from "path";
|
|
1194
1305
|
var VERSION_LOCK_PATH = "artifacts/traceability-version-lock.json";
|
|
@@ -1229,13 +1340,15 @@ async function buildVersionIndex(root, graph) {
|
|
|
1229
1340
|
edges: sortBy(edges, (edge2) => `${edge2.from} ${edge2.to} ${edge2.kind} ${edge2.sourcePath} ${edge2.sourceLine}`)
|
|
1230
1341
|
};
|
|
1231
1342
|
}
|
|
1232
|
-
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
1343
|
+
async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph, config) {
|
|
1233
1344
|
const index = await buildVersionIndex(root, graph);
|
|
1345
|
+
const schema = config ?? await loadConfig(root);
|
|
1234
1346
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1235
1347
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1236
1348
|
const nodeByArtifact = new Map(index.nodes.map((node) => [`${node.type}:${node.id}`, node]));
|
|
1237
1349
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1238
1350
|
const currentEdges = implementationEdges(index);
|
|
1351
|
+
const lockableEdges = await lockableImplementationEdges(root, index, schema);
|
|
1239
1352
|
const currentEdgeIds = new Set(currentEdges.map((edge2) => edge2.edgeId));
|
|
1240
1353
|
const issues = [];
|
|
1241
1354
|
let fresh = 0;
|
|
@@ -1323,8 +1436,29 @@ async function auditVersionLock(root, lockPath = VERSION_LOCK_PATH, graph) {
|
|
|
1323
1436
|
issues.push(...entryIssues);
|
|
1324
1437
|
}
|
|
1325
1438
|
}
|
|
1439
|
+
const livenessCache = /* @__PURE__ */ new Map();
|
|
1440
|
+
for (const entry of lock.locks) {
|
|
1441
|
+
if (entry.kind !== "verifies") continue;
|
|
1442
|
+
const sourcePath = entry.source.path;
|
|
1443
|
+
const fullSourcePath = join2(root, sourcePath);
|
|
1444
|
+
if (!existsSync(fullSourcePath)) continue;
|
|
1445
|
+
let liveness = livenessCache.get(sourcePath);
|
|
1446
|
+
if (liveness === void 0) {
|
|
1447
|
+
liveness = await getTestFileRunnerLiveness(root, sourcePath, schema);
|
|
1448
|
+
livenessCache.set(sourcePath, liveness);
|
|
1449
|
+
}
|
|
1450
|
+
if (liveness === "inactive") {
|
|
1451
|
+
issues.push({
|
|
1452
|
+
status: "orphan_lock",
|
|
1453
|
+
edgeId: entry.edgeId,
|
|
1454
|
+
message: `Liveness: ${sourcePath} is not active in any configured runner \u2014 locked verifies edges may reference dead tests`,
|
|
1455
|
+
artifact: entry.artifact,
|
|
1456
|
+
source: entry.source
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1326
1460
|
const reportedMissingLocks = /* @__PURE__ */ new Set();
|
|
1327
|
-
for (const edge2 of
|
|
1461
|
+
for (const edge2 of lockableEdges) {
|
|
1328
1462
|
const edgeId = edge2.edgeId;
|
|
1329
1463
|
if (!lock.locks.some((entry) => entry.edgeId === edgeId)) {
|
|
1330
1464
|
if (reportedMissingLocks.has(edgeId)) {
|
|
@@ -1406,6 +1540,7 @@ async function updateVersionLock(root, options) {
|
|
|
1406
1540
|
}
|
|
1407
1541
|
async function bootstrapVersionLock(root, options = {}) {
|
|
1408
1542
|
const index = await buildVersionIndex(root);
|
|
1543
|
+
const config = await loadConfig(root);
|
|
1409
1544
|
const lockPath = normalizeRelativePath(root, options.lockPath ?? VERSION_LOCK_PATH);
|
|
1410
1545
|
if (!options.force) {
|
|
1411
1546
|
const existing = await readVersionLock(root, lockPath);
|
|
@@ -1415,7 +1550,7 @@ async function bootstrapVersionLock(root, options = {}) {
|
|
|
1415
1550
|
}
|
|
1416
1551
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1417
1552
|
const entries = /* @__PURE__ */ new Map();
|
|
1418
|
-
for (const edge2 of
|
|
1553
|
+
for (const edge2 of await lockableImplementationEdges(root, index, config)) {
|
|
1419
1554
|
const source = nodeByUid.get(edge2.from);
|
|
1420
1555
|
const artifact = nodeByUid.get(edge2.to);
|
|
1421
1556
|
if (!source || !artifact) {
|
|
@@ -1450,10 +1585,11 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1450
1585
|
throw new Error("Changed-only version-lock refresh includes artifact-graph.config.yaml and requires --all");
|
|
1451
1586
|
}
|
|
1452
1587
|
const index = await buildVersionIndex(root);
|
|
1588
|
+
const config = await loadConfig(root);
|
|
1453
1589
|
const lock = await readVersionLock(root, lockPath);
|
|
1454
1590
|
const nodeByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1455
1591
|
const nodeByPath = new Map(index.nodes.map((node) => [node.path, node]));
|
|
1456
|
-
const currentImplementationEdges =
|
|
1592
|
+
const currentImplementationEdges = await lockableImplementationEdges(root, index, config);
|
|
1457
1593
|
const currentEdgePairs = new Set(currentImplementationEdges.map((edge2) => `${edge2.from} ${edge2.to}`));
|
|
1458
1594
|
const currentEntries = /* @__PURE__ */ new Map();
|
|
1459
1595
|
const changedPathSet = new Set(changedPaths);
|
|
@@ -1528,7 +1664,7 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1528
1664
|
locks: sortBy([...nextLocks.values()], (item) => item.edgeId)
|
|
1529
1665
|
};
|
|
1530
1666
|
await writeVersionLock(root, lockPath, next);
|
|
1531
|
-
const postAudit = await auditVersionLock(root, lockPath);
|
|
1667
|
+
const postAudit = await auditVersionLock(root, lockPath, void 0, config);
|
|
1532
1668
|
return {
|
|
1533
1669
|
schemaVersion: "1.0",
|
|
1534
1670
|
root,
|
|
@@ -1547,7 +1683,8 @@ async function refreshVersionLock(root, options = {}) {
|
|
|
1547
1683
|
async function traceVersion(root, target, lockPath = VERSION_LOCK_PATH) {
|
|
1548
1684
|
const index = await buildVersionIndex(root);
|
|
1549
1685
|
const safeLockPath = normalizeRelativePath(root, lockPath);
|
|
1550
|
-
const
|
|
1686
|
+
const config = await loadConfig(root);
|
|
1687
|
+
const audit = await auditVersionLock(root, safeLockPath, void 0, config);
|
|
1551
1688
|
const targetUid = parseTarget(target);
|
|
1552
1689
|
const lock = await readVersionLock(root, safeLockPath);
|
|
1553
1690
|
const targetNode = index.nodes.find((node) => node.uid === targetUid);
|
|
@@ -1783,6 +1920,28 @@ function implementationEdges(index) {
|
|
|
1783
1920
|
};
|
|
1784
1921
|
});
|
|
1785
1922
|
}
|
|
1923
|
+
async function lockableImplementationEdges(root, index, config) {
|
|
1924
|
+
const edges = implementationEdges(index);
|
|
1925
|
+
const nodesByUid = new Map(index.nodes.map((node) => [node.uid, node]));
|
|
1926
|
+
const livenessByPath = /* @__PURE__ */ new Map();
|
|
1927
|
+
const result = [];
|
|
1928
|
+
for (const edge2 of edges) {
|
|
1929
|
+
const source = nodesByUid.get(edge2.from);
|
|
1930
|
+
if (source?.sourceKind !== "test") {
|
|
1931
|
+
result.push(edge2);
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
let liveness = livenessByPath.get(source.path);
|
|
1935
|
+
if (liveness === void 0) {
|
|
1936
|
+
liveness = await getTestFileRunnerLiveness(root, source.path, config);
|
|
1937
|
+
livenessByPath.set(source.path, liveness);
|
|
1938
|
+
}
|
|
1939
|
+
if (liveness !== "inactive") {
|
|
1940
|
+
result.push(edge2);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return result;
|
|
1944
|
+
}
|
|
1786
1945
|
function lockRefFromNode(node) {
|
|
1787
1946
|
return {
|
|
1788
1947
|
type: node.type,
|
|
@@ -1895,6 +2054,53 @@ function normalizeRelativePath(root, path) {
|
|
|
1895
2054
|
function sortBy(items, keyFn) {
|
|
1896
2055
|
return [...items].sort((left, right) => keyFn(left).localeCompare(keyFn(right)));
|
|
1897
2056
|
}
|
|
2057
|
+
async function getTestFileRunnerLiveness(root, filePath, config) {
|
|
2058
|
+
const schema = config ?? await loadConfig(root);
|
|
2059
|
+
const runners = schema.e2e?.runners ?? [];
|
|
2060
|
+
if (runners.length === 0) {
|
|
2061
|
+
if (!/e2e/i.test(filePath) && !/\.e2e\./i.test(filePath)) {
|
|
2062
|
+
return "active";
|
|
2063
|
+
}
|
|
2064
|
+
const fullSourcePath = join2(root, filePath);
|
|
2065
|
+
if (!existsSync(fullSourcePath)) return "inactive";
|
|
2066
|
+
try {
|
|
2067
|
+
const content = await readFile(fullSourcePath, "utf-8");
|
|
2068
|
+
return /\/\/!?\s*@(?:e2e_test|tc)\s+/.test(content) ? "active" : "inactive";
|
|
2069
|
+
} catch {
|
|
2070
|
+
return "inactive";
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
let inRunnerScope = false;
|
|
2074
|
+
for (const runner of runners) {
|
|
2075
|
+
if (!isFileIncludedByRunner(filePath, runner)) continue;
|
|
2076
|
+
inRunnerScope = true;
|
|
2077
|
+
const isActive = await isFileActiveInRunner(root, filePath, runner);
|
|
2078
|
+
if (isActive) return "active";
|
|
2079
|
+
}
|
|
2080
|
+
return inRunnerScope ? "inactive" : "unscoped";
|
|
2081
|
+
}
|
|
2082
|
+
function isFileIncludedByRunner(filePath, runner) {
|
|
2083
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2084
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2085
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) return false;
|
|
2086
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length + 1);
|
|
2087
|
+
return runner.include.some((pattern) => matchesRunnerGlob(relativePath, pattern));
|
|
2088
|
+
}
|
|
2089
|
+
async function isFileActiveInRunner(root, filePath, runner) {
|
|
2090
|
+
if (!isFileIncludedByRunner(filePath, runner)) return false;
|
|
2091
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2092
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2093
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length).replace(/^\//, "");
|
|
2094
|
+
const matchesExclude = (runner.exclude ?? []).some(
|
|
2095
|
+
(pattern) => matchesRunnerGlob(relativePath, pattern)
|
|
2096
|
+
);
|
|
2097
|
+
if (matchesExclude) return false;
|
|
2098
|
+
const matchesTestIgnore = (runner.testIgnore ?? []).some(
|
|
2099
|
+
(pattern) => matchesRunnerGlob(relativePath, pattern)
|
|
2100
|
+
);
|
|
2101
|
+
if (matchesTestIgnore) return false;
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
1898
2104
|
function sortUnique(items) {
|
|
1899
2105
|
return [...new Set(items)].sort((left, right) => left.localeCompare(right));
|
|
1900
2106
|
}
|
|
@@ -2577,12 +2783,35 @@ var VALID_DECISIONS = /* @__PURE__ */ new Set([
|
|
|
2577
2783
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["block", "warn", "info"]);
|
|
2578
2784
|
var VALID_FINDING_STATUSES = /* @__PURE__ */ new Set(["open", "resolved", "accepted", "superseded"]);
|
|
2579
2785
|
var VALID_EXECUTORS = /* @__PURE__ */ new Set(["script", "worker", "agent", "manual", "cli"]);
|
|
2786
|
+
var TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
|
|
2787
|
+
"schema_version",
|
|
2788
|
+
"run_id",
|
|
2789
|
+
"stage_id",
|
|
2790
|
+
"attempt",
|
|
2791
|
+
"status",
|
|
2792
|
+
"decision",
|
|
2793
|
+
"summary",
|
|
2794
|
+
"outputs",
|
|
2795
|
+
"warnings",
|
|
2796
|
+
"blocking_reason",
|
|
2797
|
+
"degradation",
|
|
2798
|
+
"producer",
|
|
2799
|
+
"acceptance",
|
|
2800
|
+
"evidence",
|
|
2801
|
+
"review",
|
|
2802
|
+
"repair"
|
|
2803
|
+
]);
|
|
2580
2804
|
function validateReviewResult(input) {
|
|
2581
2805
|
const errors = [];
|
|
2582
2806
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
2583
2807
|
return [{ path: "$", message: "Root must be a non-null object" }];
|
|
2584
2808
|
}
|
|
2585
2809
|
const obj = input;
|
|
2810
|
+
for (const key of Object.keys(obj)) {
|
|
2811
|
+
if (!TOP_LEVEL_FIELDS.has(key)) {
|
|
2812
|
+
errors.push({ path: `$.${key}`, message: "Unknown top-level property" });
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2586
2815
|
if (obj.schema_version !== "1.0") {
|
|
2587
2816
|
errors.push({ path: "$.schema_version", message: `Must be "1.0", got ${JSON.stringify(obj.schema_version)}` });
|
|
2588
2817
|
}
|
|
@@ -2601,8 +2830,8 @@ function validateReviewResult(input) {
|
|
|
2601
2830
|
if (obj.stage_id !== void 0 && typeof obj.stage_id !== "string") {
|
|
2602
2831
|
errors.push({ path: "$.stage_id", message: "Must be a string if present" });
|
|
2603
2832
|
}
|
|
2604
|
-
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1)) {
|
|
2605
|
-
errors.push({ path: "$.attempt", message: "Must be
|
|
2833
|
+
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1 || obj.attempt > 3)) {
|
|
2834
|
+
errors.push({ path: "$.attempt", message: "Must be an integer from 1 through 3 if present" });
|
|
2606
2835
|
}
|
|
2607
2836
|
if (obj.outputs !== void 0) {
|
|
2608
2837
|
checkStringArray(obj.outputs, "$.outputs", errors);
|
|
@@ -2613,20 +2842,14 @@ function validateReviewResult(input) {
|
|
|
2613
2842
|
checkOptionalNullableString(obj.blocking_reason, "$.blocking_reason", errors);
|
|
2614
2843
|
checkOptionalNullableString(obj.degradation, "$.degradation", errors);
|
|
2615
2844
|
if (obj.producer !== void 0) {
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
}
|
|
2625
|
-
if (typeof p.name !== "string") {
|
|
2626
|
-
errors.push({ path: "$.producer.name", message: "Must be a string" });
|
|
2627
|
-
}
|
|
2628
|
-
checkOptionalString(p.skill, "$.producer.skill", errors);
|
|
2629
|
-
}
|
|
2845
|
+
validateProducer(obj.producer, "$.producer", errors);
|
|
2846
|
+
}
|
|
2847
|
+
const successfulAcceptance = obj.status === "SUCCEEDED" && (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR");
|
|
2848
|
+
if (successfulAcceptance && obj.producer === void 0) {
|
|
2849
|
+
errors.push({ path: "$.producer", message: "Successful acceptance requires producer identity" });
|
|
2850
|
+
}
|
|
2851
|
+
if (obj.acceptance !== void 0) {
|
|
2852
|
+
validateAcceptance(obj.acceptance, obj.producer, "$.acceptance", errors);
|
|
2630
2853
|
}
|
|
2631
2854
|
if (obj.evidence !== void 0) {
|
|
2632
2855
|
if (!Array.isArray(obj.evidence)) {
|
|
@@ -2652,6 +2875,17 @@ function validateReviewResult(input) {
|
|
|
2652
2875
|
}
|
|
2653
2876
|
if (obj.review !== void 0) {
|
|
2654
2877
|
validateReviewData(obj.review, "$.review", errors);
|
|
2878
|
+
if (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR") {
|
|
2879
|
+
const findings = isPlainObject(obj.review) && Array.isArray(obj.review.findings) ? obj.review.findings : [];
|
|
2880
|
+
findings.forEach((finding, index) => {
|
|
2881
|
+
if (isPlainObject(finding) && finding.severity === "block" && (finding.status === void 0 || finding.status === "open")) {
|
|
2882
|
+
errors.push({
|
|
2883
|
+
path: `$.review.findings[${index}]`,
|
|
2884
|
+
message: `${obj.decision} cannot contain an open block finding`
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2655
2889
|
}
|
|
2656
2890
|
if (obj.repair !== void 0) {
|
|
2657
2891
|
if (!isPlainObject(obj.repair)) {
|
|
@@ -2687,6 +2921,47 @@ function checkStringArray(val, path, errors) {
|
|
|
2687
2921
|
function isPlainObject(val) {
|
|
2688
2922
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
2689
2923
|
}
|
|
2924
|
+
function validateProducer(val, path, errors) {
|
|
2925
|
+
if (!isPlainObject(val)) {
|
|
2926
|
+
errors.push({ path, message: "Must be an object" });
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (typeof val.executor !== "string") {
|
|
2930
|
+
errors.push({ path: `${path}.executor`, message: "Must be a string" });
|
|
2931
|
+
} else if (!VALID_EXECUTORS.has(val.executor)) {
|
|
2932
|
+
errors.push({ path: `${path}.executor`, message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(val.executor)}` });
|
|
2933
|
+
}
|
|
2934
|
+
if (typeof val.name !== "string" || val.name.length === 0) {
|
|
2935
|
+
errors.push({ path: `${path}.name`, message: "Must be a non-empty string" });
|
|
2936
|
+
}
|
|
2937
|
+
checkOptionalString(val.skill, `${path}.skill`, errors);
|
|
2938
|
+
}
|
|
2939
|
+
function producerIdentity(val) {
|
|
2940
|
+
return JSON.stringify([val.executor, val.name]);
|
|
2941
|
+
}
|
|
2942
|
+
function validateAcceptance(val, resultProducer, path, errors) {
|
|
2943
|
+
if (!isPlainObject(val)) {
|
|
2944
|
+
errors.push({ path, message: "Must be an object" });
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
validateProducer(val.reviewer, `${path}.reviewer`, errors);
|
|
2948
|
+
if (!isPlainObject(val.source_result)) {
|
|
2949
|
+
errors.push({ path: `${path}.source_result`, message: "Must be an object" });
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
const source = val.source_result;
|
|
2953
|
+
if (typeof source.run_id !== "string" || source.run_id.length === 0) {
|
|
2954
|
+
errors.push({ path: `${path}.source_result.run_id`, message: "Must be a non-empty string" });
|
|
2955
|
+
}
|
|
2956
|
+
checkOptionalString(source.stage_id, `${path}.source_result.stage_id`, errors);
|
|
2957
|
+
validateProducer(source.producer, `${path}.source_result.producer`, errors);
|
|
2958
|
+
if (isPlainObject(val.reviewer) && isPlainObject(resultProducer) && producerIdentity(val.reviewer) !== producerIdentity(resultProducer)) {
|
|
2959
|
+
errors.push({ path: `${path}.reviewer`, message: "Acceptance reviewer must match the result producer" });
|
|
2960
|
+
}
|
|
2961
|
+
if (isPlainObject(val.reviewer) && isPlainObject(source.producer) && producerIdentity(val.reviewer) === producerIdentity(source.producer)) {
|
|
2962
|
+
errors.push({ path: `${path}.reviewer`, message: "Repair producer cannot accept its own result" });
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2690
2965
|
function checkOptionalString(val, path, errors) {
|
|
2691
2966
|
if (val !== void 0 && typeof val !== "string") {
|
|
2692
2967
|
errors.push({ path, message: "Must be a string if present" });
|
|
@@ -2855,7 +3130,7 @@ var DEFAULT_SCHEMA = {
|
|
|
2855
3130
|
scenario: { paths: ["artifacts/scenarios/**/*.md"], displayName: "\u573A\u666F\u5267\u672C", role: "scenario", layer: "scenario", aliases: ["scenarios", "scenario-script"] },
|
|
2856
3131
|
design: { paths: ["artifacts/design/**/*.md"], displayName: "\u8BBE\u8BA1\u89C4\u683C", role: "design", layer: "design", aliases: ["design-spec", "design_docs"] },
|
|
2857
3132
|
test: { paths: ["heimdall/packages/**/*.test.ts"], displayName: "\u4EE3\u7801\u6CE8\u91CA\u8FFD\u6EAF", role: "context", layer: "implementation", aliases: ["code-test", "code-trace", "unit-test"] },
|
|
2858
|
-
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests"] },
|
|
3133
|
+
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests", "tc"] },
|
|
2859
3134
|
e2e_registry: { paths: ["artifacts/tests/e2e/e2e-test-registry.json"], displayName: "E2E \u6D4B\u8BD5\u6CE8\u518C\u8868", role: "context", layer: "verification", aliases: ["e2e-registry"] },
|
|
2860
3135
|
"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"] },
|
|
2861
3136
|
"test-strategy": { paths: ["artifacts/design/test-strategy.md"], displayName: "\u6D4B\u8BD5\u7B56\u7565", role: "context", layer: "verification", aliases: ["test_strategy"] },
|
|
@@ -2885,7 +3160,12 @@ var DEFAULT_SCHEMA = {
|
|
|
2885
3160
|
allowedEdges: [],
|
|
2886
3161
|
forbiddenEdges: [{ from: "scenario", to: "entity", kind: "references" }],
|
|
2887
3162
|
statuses: ["planned", "active", "done", "deprecated"],
|
|
2888
|
-
idRanges: {}
|
|
3163
|
+
idRanges: {},
|
|
3164
|
+
e2e: {
|
|
3165
|
+
report_uncovered_scenarios: true,
|
|
3166
|
+
report_uncovered_features: true,
|
|
3167
|
+
runners: []
|
|
3168
|
+
}
|
|
2889
3169
|
};
|
|
2890
3170
|
async function loadConfig(root) {
|
|
2891
3171
|
const configPath = join5(root, "artifact-graph.config.yaml");
|
|
@@ -2898,7 +3178,27 @@ async function loadConfig(root) {
|
|
|
2898
3178
|
throw error;
|
|
2899
3179
|
}
|
|
2900
3180
|
}
|
|
2901
|
-
|
|
3181
|
+
const ub = parsed.context?.universal_baseline;
|
|
3182
|
+
if (ub !== void 0 && typeof ub !== "boolean") {
|
|
3183
|
+
throw new Error(
|
|
3184
|
+
`Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
if (parsed.e2e !== void 0) {
|
|
3188
|
+
if (typeof parsed.e2e !== "object" || parsed.e2e === null || Array.isArray(parsed.e2e)) {
|
|
3189
|
+
throw new Error("Invalid e2e: must be an object.");
|
|
3190
|
+
}
|
|
3191
|
+
validateE2eConfig(parsed.e2e);
|
|
3192
|
+
}
|
|
3193
|
+
const mergedE2e = parsed.e2e === void 0 ? DEFAULT_SCHEMA.e2e : {
|
|
3194
|
+
...DEFAULT_SCHEMA.e2e,
|
|
3195
|
+
...parsed.e2e,
|
|
3196
|
+
runners: (parsed.e2e.runners ?? DEFAULT_SCHEMA.e2e?.runners ?? []).map((runner) => ({
|
|
3197
|
+
kind: "e2e",
|
|
3198
|
+
...runner
|
|
3199
|
+
}))
|
|
3200
|
+
};
|
|
3201
|
+
const merged = {
|
|
2902
3202
|
...DEFAULT_SCHEMA,
|
|
2903
3203
|
...parsed,
|
|
2904
3204
|
types: mergeArtifactTypes(DEFAULT_SCHEMA.types, parsed.types),
|
|
@@ -2907,10 +3207,103 @@ async function loadConfig(root) {
|
|
|
2907
3207
|
allowedEdges: parsed.allowedEdges ?? DEFAULT_SCHEMA.allowedEdges,
|
|
2908
3208
|
forbiddenEdges: parsed.forbiddenEdges ?? DEFAULT_SCHEMA.forbiddenEdges,
|
|
2909
3209
|
statuses: parsed.statuses ?? DEFAULT_SCHEMA.statuses,
|
|
2910
|
-
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
|
|
3210
|
+
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges),
|
|
3211
|
+
e2e: mergedE2e
|
|
2911
3212
|
};
|
|
3213
|
+
return merged;
|
|
2912
3214
|
}
|
|
2913
|
-
function
|
|
3215
|
+
function validateE2eConfig(e2e) {
|
|
3216
|
+
for (const field of ["report_uncovered_scenarios", "report_uncovered_features"]) {
|
|
3217
|
+
if (e2e[field] !== void 0 && typeof e2e[field] !== "boolean") {
|
|
3218
|
+
throw new Error(`Invalid e2e.${field}: must be boolean.`);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
if (e2e.executable_ref_warning !== void 0) {
|
|
3222
|
+
if (typeof e2e.executable_ref_warning !== "number" || e2e.executable_ref_warning < 0 || e2e.executable_ref_warning > 1) {
|
|
3223
|
+
throw new Error(`Invalid e2e.executable_ref_warning: ${JSON.stringify(e2e.executable_ref_warning)}. Must be a number between 0 and 1.`);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
if (e2e.executable_ref_error !== void 0) {
|
|
3227
|
+
if (typeof e2e.executable_ref_error !== "number" || e2e.executable_ref_error < 0 || e2e.executable_ref_error > 1) {
|
|
3228
|
+
throw new Error(`Invalid e2e.executable_ref_error: ${JSON.stringify(e2e.executable_ref_error)}. Must be a number between 0 and 1.`);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
const validateWaivers = (waivers, field) => {
|
|
3232
|
+
if (waivers === void 0) return;
|
|
3233
|
+
if (!Array.isArray(waivers)) {
|
|
3234
|
+
throw new Error(`Invalid ${field}: must be an array of {id, reason} objects.`);
|
|
3235
|
+
}
|
|
3236
|
+
for (const w of waivers) {
|
|
3237
|
+
if (typeof w !== "object" || w === null || !("id" in w) || !("reason" in w)) {
|
|
3238
|
+
throw new Error(`Invalid ${field} entry: ${JSON.stringify(w)}. Must be {id, reason} object.`);
|
|
3239
|
+
}
|
|
3240
|
+
if (typeof w.id !== "string" || !w.id.trim()) {
|
|
3241
|
+
throw new Error(`Invalid ${field} entry: id must be a non-empty string. Got: ${JSON.stringify(w.id)}`);
|
|
3242
|
+
}
|
|
3243
|
+
if (typeof w.reason !== "string" || !w.reason.trim()) {
|
|
3244
|
+
throw new Error(`Invalid ${field} entry: reason must be a non-empty string. Got: ${JSON.stringify(w.reason)}`);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
validateWaivers(e2e.scenario_waivers, "e2e.scenario_waivers");
|
|
3249
|
+
validateWaivers(e2e.feature_waivers, "e2e.feature_waivers");
|
|
3250
|
+
if (e2e.runners !== void 0) {
|
|
3251
|
+
if (!Array.isArray(e2e.runners)) {
|
|
3252
|
+
throw new Error(`Invalid e2e.runners: must be an array.`);
|
|
3253
|
+
}
|
|
3254
|
+
for (const runner of e2e.runners) {
|
|
3255
|
+
if (typeof runner !== "object" || runner === null) {
|
|
3256
|
+
throw new Error(`Invalid e2e.runners entry: must be an object.`);
|
|
3257
|
+
}
|
|
3258
|
+
if (typeof runner.name !== "string" || !runner.name.trim()) {
|
|
3259
|
+
throw new Error(`Invalid e2e.runners entry: name must be a non-empty string.`);
|
|
3260
|
+
}
|
|
3261
|
+
if (typeof runner.root !== "string" || !runner.root.trim()) {
|
|
3262
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: must be a non-empty string.`);
|
|
3263
|
+
}
|
|
3264
|
+
if (isAbsolute3(runner.root)) {
|
|
3265
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: "${runner.root}" must not be an absolute path.`);
|
|
3266
|
+
}
|
|
3267
|
+
if (runner.root.replace(/\\/g, "/").split("/").includes("..")) {
|
|
3268
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].root: "${runner.root}" must not contain ".." segments.`);
|
|
3269
|
+
}
|
|
3270
|
+
if (!Array.isArray(runner.include) || runner.include.length === 0) {
|
|
3271
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].include: must be a non-empty array of glob patterns.`);
|
|
3272
|
+
}
|
|
3273
|
+
for (const pattern of runner.include) {
|
|
3274
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3275
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].include: pattern must be a non-empty string.`);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
if (runner.exclude !== void 0) {
|
|
3279
|
+
if (!Array.isArray(runner.exclude)) {
|
|
3280
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].exclude: must be an array.`);
|
|
3281
|
+
}
|
|
3282
|
+
for (const pattern of runner.exclude) {
|
|
3283
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3284
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].exclude: pattern must be a non-empty string.`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
if (runner.testIgnore !== void 0) {
|
|
3289
|
+
if (!Array.isArray(runner.testIgnore)) {
|
|
3290
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].testIgnore: must be an array.`);
|
|
3291
|
+
}
|
|
3292
|
+
for (const pattern of runner.testIgnore) {
|
|
3293
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
3294
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].testIgnore: pattern must be a non-empty string.`);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
if (runner.kind !== void 0) {
|
|
3299
|
+
if (!["unit", "integration", "e2e"].includes(runner.kind)) {
|
|
3300
|
+
throw new Error(`Invalid e2e.runners[${runner.name}].kind: "${runner.kind}". Must be unit, integration, or e2e.`);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
function buildGraph(nodes, edges, diagnostics = [], root) {
|
|
2914
3307
|
const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
|
|
2915
3308
|
graphNodes.sort(compareNode);
|
|
2916
3309
|
edges.sort(compareEdge);
|
|
@@ -2927,6 +3320,7 @@ function buildGraph(nodes, edges, diagnostics = []) {
|
|
|
2927
3320
|
nodes: graphNodes,
|
|
2928
3321
|
edges: dedupedEdges,
|
|
2929
3322
|
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3323
|
+
...root ? { root } : {},
|
|
2930
3324
|
diagnostics: diagnostics.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line)
|
|
2931
3325
|
};
|
|
2932
3326
|
}
|
|
@@ -2958,7 +3352,8 @@ async function scanArtifacts(root, schema) {
|
|
|
2958
3352
|
scanDiagnostics.push(...parsed.diagnostics);
|
|
2959
3353
|
}
|
|
2960
3354
|
}
|
|
2961
|
-
const
|
|
3355
|
+
const absoluteRoot = isAbsolute3(root) ? root : resolve3(root);
|
|
3356
|
+
const graph = buildGraph(nodes, edges, scanDiagnostics, absoluteRoot);
|
|
2962
3357
|
return resolveMatrixEdges(graph);
|
|
2963
3358
|
}
|
|
2964
3359
|
function artifactTypeEntriesBySpecificity(schema) {
|
|
@@ -3298,23 +3693,34 @@ async function validateScenarioPrdLinkIndex(root, graph) {
|
|
|
3298
3693
|
function validateCodeCommentTraceabilityFormat(graph) {
|
|
3299
3694
|
const issues = [];
|
|
3300
3695
|
for (const node of graph.nodes) {
|
|
3301
|
-
if (node.type !== "test") {
|
|
3696
|
+
if (node.type !== "test" && node.type !== "implementation") {
|
|
3302
3697
|
continue;
|
|
3303
3698
|
}
|
|
3304
3699
|
const invalidComments = node.attrs?.invalidTraceabilityComments;
|
|
3305
|
-
if (
|
|
3306
|
-
|
|
3700
|
+
if (Array.isArray(invalidComments)) {
|
|
3701
|
+
for (const invalid of invalidComments) {
|
|
3702
|
+
const line = typeof invalid?.line === "number" ? invalid.line : node.line;
|
|
3703
|
+
const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
|
|
3704
|
+
issues.push(issue(
|
|
3705
|
+
"CODE_COMMENT_TRACEABILITY_FORMAT",
|
|
3706
|
+
`${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
|
|
3707
|
+
node.path,
|
|
3708
|
+
line,
|
|
3709
|
+
{ node: node.uid }
|
|
3710
|
+
));
|
|
3711
|
+
}
|
|
3307
3712
|
}
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3713
|
+
const deprecatedComments = node.attrs?.deprecatedTraceabilityComments;
|
|
3714
|
+
if (Array.isArray(deprecatedComments)) {
|
|
3715
|
+
for (const deprecated of deprecatedComments) {
|
|
3716
|
+
issues.push(issue(
|
|
3717
|
+
"E2E-TRACE-007",
|
|
3718
|
+
"@tc is deprecated; use @e2e_test instead",
|
|
3719
|
+
node.path,
|
|
3720
|
+
typeof deprecated?.line === "number" ? deprecated.line : node.line,
|
|
3721
|
+
{ node: node.uid, severity: "warning" }
|
|
3722
|
+
));
|
|
3723
|
+
}
|
|
3318
3724
|
}
|
|
3319
3725
|
}
|
|
3320
3726
|
return issues;
|
|
@@ -3796,13 +4202,16 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3796
4202
|
const isTest = isTestFile(path);
|
|
3797
4203
|
const nodeType = isTest ? "test" : "implementation";
|
|
3798
4204
|
const edgeKind = isTest ? "verifies" : "implements";
|
|
4205
|
+
const attrs = {};
|
|
4206
|
+
if (traceabilityComments.invalid.length > 0) attrs.invalidTraceabilityComments = traceabilityComments.invalid;
|
|
4207
|
+
if (traceabilityComments.deprecated.length > 0) attrs.deprecatedTraceabilityComments = traceabilityComments.deprecated;
|
|
3799
4208
|
const node = {
|
|
3800
4209
|
type: nodeType,
|
|
3801
4210
|
code: path,
|
|
3802
4211
|
title: path.split("/").at(-1) ?? path,
|
|
3803
4212
|
path,
|
|
3804
4213
|
line: 1,
|
|
3805
|
-
attrs
|
|
4214
|
+
attrs
|
|
3806
4215
|
};
|
|
3807
4216
|
let hasTags = false;
|
|
3808
4217
|
for (const { tags, lineNumber } of traceabilityComments.canonical) {
|
|
@@ -3813,7 +4222,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3813
4222
|
}
|
|
3814
4223
|
}
|
|
3815
4224
|
}
|
|
3816
|
-
if (hasTags || traceabilityComments.invalid.length > 0) {
|
|
4225
|
+
if (hasTags || traceabilityComments.invalid.length > 0 || traceabilityComments.deprecated.length > 0) {
|
|
3817
4226
|
nodes.push(node);
|
|
3818
4227
|
}
|
|
3819
4228
|
return { nodes, edges };
|
|
@@ -3821,6 +4230,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3821
4230
|
function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
3822
4231
|
const canonical = [];
|
|
3823
4232
|
const invalid = [];
|
|
4233
|
+
const deprecated = [];
|
|
3824
4234
|
for (const comment of scanCodeComments(raw)) {
|
|
3825
4235
|
if (!containsTraceabilityTag(comment.text, schema)) {
|
|
3826
4236
|
continue;
|
|
@@ -3836,6 +4246,9 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3836
4246
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3837
4247
|
if (parsed.valid) {
|
|
3838
4248
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4249
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4250
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4251
|
+
}
|
|
3839
4252
|
} else {
|
|
3840
4253
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3841
4254
|
}
|
|
@@ -3851,11 +4264,14 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3851
4264
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3852
4265
|
if (parsed.valid) {
|
|
3853
4266
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4267
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4268
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4269
|
+
}
|
|
3854
4270
|
} else {
|
|
3855
4271
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3856
4272
|
}
|
|
3857
4273
|
}
|
|
3858
|
-
return { canonical, invalid };
|
|
4274
|
+
return { canonical, invalid, deprecated };
|
|
3859
4275
|
}
|
|
3860
4276
|
function scanCodeComments(raw) {
|
|
3861
4277
|
const comments = [];
|
|
@@ -4022,7 +4438,14 @@ function expandCodeRange(value) {
|
|
|
4022
4438
|
return Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${String(start + index).padStart(width, "0")}`);
|
|
4023
4439
|
}
|
|
4024
4440
|
function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
4025
|
-
|
|
4441
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4442
|
+
for (const [type, definition] of Object.entries(schema.types)) {
|
|
4443
|
+
tokens.add(type);
|
|
4444
|
+
for (const alias of definition.aliases ?? []) tokens.add(alias);
|
|
4445
|
+
}
|
|
4446
|
+
if (tokens.size === 0) return false;
|
|
4447
|
+
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
4448
|
+
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
4026
4449
|
}
|
|
4027
4450
|
function parseDesign(path, raw) {
|
|
4028
4451
|
const parsed = matter(raw);
|
|
@@ -5027,6 +5450,29 @@ function validateE2eTests(graph) {
|
|
|
5027
5450
|
issues.push(issue("E2E_AC_UNKNOWN", `${node.uid} references unknown AC ${reference.feature}(${reference.ac})`, node.path, node.line, { node: node.uid, severity: "warning" }));
|
|
5028
5451
|
}
|
|
5029
5452
|
}
|
|
5453
|
+
const tcStatus = String(fields["status"] ?? "").trim().toLowerCase();
|
|
5454
|
+
if (tcStatus && !VALID_TC_STATUSES.has(tcStatus)) {
|
|
5455
|
+
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" }));
|
|
5456
|
+
}
|
|
5457
|
+
if (tcStatus === "waived") {
|
|
5458
|
+
const reason = String(fields["waived_reason"] ?? "").trim();
|
|
5459
|
+
if (!reason) {
|
|
5460
|
+
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" }));
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
const rawChainType = String(fields["chain_type"] ?? "").trim();
|
|
5464
|
+
const chainType = rawChainType.toLowerCase();
|
|
5465
|
+
if (rawChainType) {
|
|
5466
|
+
if (!VALID_CHAIN_TYPES.has(chainType) && !(chainType in DEPRECATED_CHAIN_TYPE_ALIASES)) {
|
|
5467
|
+
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" }));
|
|
5468
|
+
} else if (chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5469
|
+
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" }));
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
const rawAcCoverageRate = String(fields["ac_coverage_rate"] ?? "").trim();
|
|
5473
|
+
if (rawAcCoverageRate) {
|
|
5474
|
+
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" }));
|
|
5475
|
+
}
|
|
5030
5476
|
if (needsDesktopChainWarning(node)) {
|
|
5031
5477
|
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" }));
|
|
5032
5478
|
}
|
|
@@ -5085,10 +5531,10 @@ function validateE2eRegistry(graph) {
|
|
|
5085
5531
|
}
|
|
5086
5532
|
return issues;
|
|
5087
5533
|
}
|
|
5088
|
-
async function validateExecutableTraceability(root) {
|
|
5534
|
+
async function validateExecutableTraceability(root, config) {
|
|
5089
5535
|
const issues = [];
|
|
5536
|
+
const schema = config ?? await loadConfig(root);
|
|
5090
5537
|
const e2eDir = join5(root, "artifacts", "tests", "e2e");
|
|
5091
|
-
const specPatterns = ["heimdall/**/*.spec.ts", "heimdall/**/*.e2e.spec.ts"];
|
|
5092
5538
|
let e2eFiles;
|
|
5093
5539
|
try {
|
|
5094
5540
|
e2eFiles = (await readdir(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join5(e2eDir, name));
|
|
@@ -5131,16 +5577,23 @@ async function validateExecutableTraceability(root) {
|
|
|
5131
5577
|
}
|
|
5132
5578
|
const allFiles = await walk(root);
|
|
5133
5579
|
const specFiles = /* @__PURE__ */ new Set();
|
|
5134
|
-
|
|
5580
|
+
const configuredRunners = schema.e2e?.runners ?? [];
|
|
5581
|
+
if (configuredRunners.length > 0) {
|
|
5135
5582
|
for (const file of allFiles) {
|
|
5136
|
-
if (
|
|
5583
|
+
if (configuredRunners.some((runner) => isRunnerIncludeCandidate(file, runner))) {
|
|
5584
|
+
specFiles.add(file);
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
} else {
|
|
5588
|
+
for (const file of allFiles) {
|
|
5589
|
+
if (/\.(?:e2e\.)?spec\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(file)) {
|
|
5137
5590
|
specFiles.add(file);
|
|
5138
5591
|
}
|
|
5139
5592
|
}
|
|
5140
5593
|
}
|
|
5141
5594
|
const refToSource = /* @__PURE__ */ new Map();
|
|
5142
|
-
const tcAnnotationRegex = /\/\/!?\s*@tc\s+(\S+?)\s+\[(\w+)\]/;
|
|
5143
|
-
const tcAnnotationNoLevelRegex = /\/\/!?\s*@tc\s+(\S+)/;
|
|
5595
|
+
const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
|
|
5596
|
+
const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
|
|
5144
5597
|
for (const specFile of specFiles) {
|
|
5145
5598
|
const fullSpecPath = join5(root, specFile);
|
|
5146
5599
|
let content;
|
|
@@ -5204,12 +5657,21 @@ async function validateExecutableTraceability(root) {
|
|
|
5204
5657
|
const refEntries = parseExecutableRefLines(ref);
|
|
5205
5658
|
const validFiles = [];
|
|
5206
5659
|
for (const entry of refEntries) {
|
|
5207
|
-
const normalizedRefFile =
|
|
5208
|
-
const fileExists = specFiles.has(normalizedRefFile);
|
|
5660
|
+
const normalizedRefFile = resolveExecutableRefFile(entry.file, allFiles);
|
|
5661
|
+
const fileExists = normalizedRefFile !== void 0 && specFiles.has(normalizedRefFile);
|
|
5209
5662
|
if (!fileExists) {
|
|
5210
5663
|
issues.push(issue("E2E-TRACE-001", `executable_ref target file not found: ${entry.file}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5211
5664
|
continue;
|
|
5212
5665
|
}
|
|
5666
|
+
const runners = schema.e2e?.runners ?? [];
|
|
5667
|
+
if (runners.length > 0) {
|
|
5668
|
+
const acceptingRunners = await getAcceptingRunners(root, normalizedRefFile, runners);
|
|
5669
|
+
const hasE2eRunner = acceptingRunners.some((r) => r.kind === "e2e" || r.kind === "integration");
|
|
5670
|
+
const hasUnitRunner = acceptingRunners.some((r) => r.kind === "unit");
|
|
5671
|
+
if (hasUnitRunner && !hasE2eRunner && acceptingRunners.length > 0) {
|
|
5672
|
+
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" }));
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5213
5675
|
if (entry.testId) {
|
|
5214
5676
|
let content;
|
|
5215
5677
|
try {
|
|
@@ -5224,12 +5686,9 @@ async function validateExecutableTraceability(root) {
|
|
|
5224
5686
|
}
|
|
5225
5687
|
}
|
|
5226
5688
|
const annotationsForTc = refToSource.get(tcKey);
|
|
5227
|
-
const hasAnnotationInFile = annotationsForTc?.some((ann) =>
|
|
5228
|
-
const normalizedAnnFile = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5229
|
-
return normalizedAnnFile === normalizedRefFile;
|
|
5230
|
-
}) ?? false;
|
|
5689
|
+
const hasAnnotationInFile = annotationsForTc?.some((ann) => ann.file === normalizedRefFile) ?? false;
|
|
5231
5690
|
if (!hasAnnotationInFile) {
|
|
5232
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no
|
|
5691
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5233
5692
|
continue;
|
|
5234
5693
|
}
|
|
5235
5694
|
validFiles.push(normalizedRefFile);
|
|
@@ -5240,13 +5699,13 @@ async function validateExecutableTraceability(root) {
|
|
|
5240
5699
|
const batch = tcKey.split(":")[0];
|
|
5241
5700
|
if (!mdBatches.has(batch)) {
|
|
5242
5701
|
for (const ann of annotations) {
|
|
5243
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5702
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5244
5703
|
}
|
|
5245
5704
|
continue;
|
|
5246
5705
|
}
|
|
5247
5706
|
if (!mdToRef.has(tcKey) && !await hasMarkdownTc(tcKey, e2eDir)) {
|
|
5248
5707
|
for (const ann of annotations) {
|
|
5249
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5708
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5250
5709
|
}
|
|
5251
5710
|
}
|
|
5252
5711
|
}
|
|
@@ -5265,24 +5724,37 @@ async function validateExecutableTraceability(root) {
|
|
|
5265
5724
|
if (matchesAnyRef) {
|
|
5266
5725
|
continue;
|
|
5267
5726
|
}
|
|
5268
|
-
const primaryRef = refEntries[0];
|
|
5269
|
-
const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
|
|
5270
5727
|
const detail = `file: MD refs=[${refEntries.map((e) => e.file).join(", ")}] vs source=${ann.file}`;
|
|
5271
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194
|
|
5728
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5272
5729
|
}
|
|
5273
5730
|
}
|
|
5274
|
-
for (const [tcKey, { chainType, path, line }] of
|
|
5275
|
-
|
|
5731
|
+
for (const [tcKey, { chainType, path, line }] of allMdTcInfo) {
|
|
5732
|
+
const tcFields = tcKeyToFields.get(tcKey);
|
|
5733
|
+
const explicitChainType = String(tcFields?.["chain_type"] ?? "").trim().toLowerCase();
|
|
5734
|
+
if (explicitChainType !== "desktop_chain") {
|
|
5276
5735
|
continue;
|
|
5277
5736
|
}
|
|
5737
|
+
if (!mdToRef.has(tcKey)) {
|
|
5738
|
+
issues.push(issue("E2E-DESKTOP-CHAIN-MISSING", `desktop_chain TC ${tcKey} has no executable_ref`, path, line, { node: tcKey, severity: "warning" }));
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
for (const [tcKey, { chainType, path, line }] of mdToRef) {
|
|
5742
|
+
const normalizedDeclaredChainType = chainType.trim().toLowerCase();
|
|
5743
|
+
const hasLegalNonDesktopDeclaration = normalizedDeclaredChainType.length > 0 && VALID_CHAIN_TYPES.has(normalizedDeclaredChainType) && normalizedDeclaredChainType !== "desktop_chain";
|
|
5744
|
+
if (hasLegalNonDesktopDeclaration) continue;
|
|
5278
5745
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5279
5746
|
const sourceAnnotations = refToSource.get(tcKey);
|
|
5280
5747
|
if (!sourceAnnotations) {
|
|
5281
5748
|
continue;
|
|
5282
5749
|
}
|
|
5750
|
+
const tcFields = tcKeyToFields.get(tcKey);
|
|
5751
|
+
const explicitChainType = String(tcFields?.["chain_type"] ?? "").trim().toLowerCase();
|
|
5752
|
+
const hasExplicitDesktopChain = explicitChainType === "desktop_chain";
|
|
5753
|
+
if (hasExplicitDesktopChain) {
|
|
5754
|
+
continue;
|
|
5755
|
+
}
|
|
5283
5756
|
for (const ann of sourceAnnotations) {
|
|
5284
|
-
|
|
5285
|
-
if (!validFiles.has(normalizedAnnFile)) {
|
|
5757
|
+
if (!validFiles.has(ann.file)) {
|
|
5286
5758
|
continue;
|
|
5287
5759
|
}
|
|
5288
5760
|
if (ann.level === "mock_playwright") {
|
|
@@ -5308,10 +5780,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5308
5780
|
continue;
|
|
5309
5781
|
}
|
|
5310
5782
|
const validFiles = new Set(tcValidRefFiles.get(tcKey) ?? []);
|
|
5311
|
-
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) =>
|
|
5312
|
-
const normalized = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
|
|
5313
|
-
return validFiles.has(normalized);
|
|
5314
|
-
});
|
|
5783
|
+
const sourceAnnotations = refToSource.get(tcKey)?.filter((ann) => validFiles.has(ann.file));
|
|
5315
5784
|
const hasDesktopChain = sourceAnnotations?.some((ann) => ann.level === "desktop_chain") ?? false;
|
|
5316
5785
|
const hasBridge = sourceAnnotations?.some((ann) => ann.level === "ui_sidecar_bridge") ?? false;
|
|
5317
5786
|
if (isComplete) {
|
|
@@ -5320,7 +5789,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5320
5789
|
if (hasDesktopChain) {
|
|
5321
5790
|
hasValidEvidence = true;
|
|
5322
5791
|
} else if (hasBridge) {
|
|
5323
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5792
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5324
5793
|
if (partialResult.hasValidPartialRust) {
|
|
5325
5794
|
hasValidEvidence = true;
|
|
5326
5795
|
} else {
|
|
@@ -5360,7 +5829,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5360
5829
|
}
|
|
5361
5830
|
let partialDetail = "";
|
|
5362
5831
|
if (hasBridge) {
|
|
5363
|
-
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root);
|
|
5832
|
+
const partialResult = await validatePartialRustEvidence(tcFields, tcKey, root, allFiles);
|
|
5364
5833
|
if (partialResult.hasValidPartialRust) {
|
|
5365
5834
|
continue;
|
|
5366
5835
|
}
|
|
@@ -5377,6 +5846,287 @@ async function validateExecutableTraceability(root) {
|
|
|
5377
5846
|
}
|
|
5378
5847
|
return issues;
|
|
5379
5848
|
}
|
|
5849
|
+
async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
5850
|
+
const e2eNodes = graph.nodes.filter((n) => n.type === "e2e_test" && n.attrs?.fileLevelOnly !== true);
|
|
5851
|
+
const totalTestCases = e2eNodes.length;
|
|
5852
|
+
let withExecutableRef = 0;
|
|
5853
|
+
const statusBreakdown = {};
|
|
5854
|
+
const chainTypeBreakdown = {};
|
|
5855
|
+
const e2eDir = join5(root, "artifacts", "tests", "e2e");
|
|
5856
|
+
const tcFieldsMap = /* @__PURE__ */ new Map();
|
|
5857
|
+
let e2eFiles;
|
|
5858
|
+
try {
|
|
5859
|
+
e2eFiles = (await readdir(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join5(e2eDir, name));
|
|
5860
|
+
} catch {
|
|
5861
|
+
e2eFiles = [];
|
|
5862
|
+
}
|
|
5863
|
+
for (const filePath of e2eFiles) {
|
|
5864
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
5865
|
+
const lines = raw.split(/\r?\n/);
|
|
5866
|
+
const tcStarts = [];
|
|
5867
|
+
lines.forEach((line, index) => {
|
|
5868
|
+
const match = /^#{2,3}\s+(TC-\d+[a-z]?)\s*[::]?\s*.*$/.exec(line);
|
|
5869
|
+
if (match) {
|
|
5870
|
+
tcStarts.push({ id: match[1], index });
|
|
5871
|
+
}
|
|
5872
|
+
});
|
|
5873
|
+
const parsed = matter(raw);
|
|
5874
|
+
const batch = String(parsed.data.test_batch ?? basename2(filePath, extname(filePath))).trim();
|
|
5875
|
+
for (let i = 0; i < tcStarts.length; i++) {
|
|
5876
|
+
const start = tcStarts[i];
|
|
5877
|
+
const end = tcStarts[i + 1]?.index ?? lines.length;
|
|
5878
|
+
const block = lines.slice(start.index, end);
|
|
5879
|
+
const fields = extractE2eTcFields(block);
|
|
5880
|
+
tcFieldsMap.set(`${batch}:${start.id}`, fields);
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
for (const node of e2eNodes) {
|
|
5884
|
+
const tcKey = node.code;
|
|
5885
|
+
const fields = tcFieldsMap.get(tcKey) ?? asRecord(node.attrs?.tcFields);
|
|
5886
|
+
const execRef = String(fields["executable_ref"] ?? "").trim();
|
|
5887
|
+
if (execRef && !isPendingExecutableRef(execRef)) {
|
|
5888
|
+
withExecutableRef++;
|
|
5889
|
+
}
|
|
5890
|
+
const status = String(fields["status"] ?? "created").trim().toLowerCase();
|
|
5891
|
+
statusBreakdown[status] = (statusBreakdown[status] ?? 0) + 1;
|
|
5892
|
+
const chainType = String(fields["chain_type"] ?? "").trim().toLowerCase() || "unspecified";
|
|
5893
|
+
chainTypeBreakdown[chainType] = (chainTypeBreakdown[chainType] ?? 0) + 1;
|
|
5894
|
+
}
|
|
5895
|
+
const executableRefRate = totalTestCases > 0 ? `${withExecutableRef}/${totalTestCases} (${(withExecutableRef / totalTestCases * 100).toFixed(1)}%)` : "0/0";
|
|
5896
|
+
const scenarioNodes = graph.nodes.filter((n) => n.type === "scenario");
|
|
5897
|
+
const featureNodes = graph.nodes.filter((n) => n.type === "feature");
|
|
5898
|
+
const linkedScenarios = /* @__PURE__ */ new Set();
|
|
5899
|
+
const linkedFeatures = /* @__PURE__ */ new Set();
|
|
5900
|
+
const acCoveredScenarios = /* @__PURE__ */ new Set();
|
|
5901
|
+
const acCoveredFeatures = /* @__PURE__ */ new Set();
|
|
5902
|
+
const verifiedScenarios = /* @__PURE__ */ new Set();
|
|
5903
|
+
const verifiedFeatures = /* @__PURE__ */ new Set();
|
|
5904
|
+
for (const edge2 of graph.edges) {
|
|
5905
|
+
if (edge2.kind === "verifies" && edge2.from.startsWith("e2e_test:")) {
|
|
5906
|
+
if (edge2.to.startsWith("scenario:")) {
|
|
5907
|
+
linkedScenarios.add(edge2.to.replace("scenario:", ""));
|
|
5908
|
+
}
|
|
5909
|
+
if (edge2.to.startsWith("feature:")) {
|
|
5910
|
+
linkedFeatures.add(edge2.to.replace("feature:", ""));
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
for (const node of e2eNodes) {
|
|
5915
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
5916
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
5917
|
+
for (const feature of Object.keys(acCoverage)) {
|
|
5918
|
+
acCoveredFeatures.add(feature);
|
|
5919
|
+
}
|
|
5920
|
+
const relatedScenarios = toArray(fields["related_scenarios"] ?? node.attrs?.related_scenarios).map(String);
|
|
5921
|
+
for (const scenario of relatedScenarios) {
|
|
5922
|
+
acCoveredScenarios.add(scenario);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
const runners = (await loadConfig(root)).e2e?.runners ?? [];
|
|
5926
|
+
const allProjectFiles = await walk(root);
|
|
5927
|
+
for (const node of e2eNodes) {
|
|
5928
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
5929
|
+
const status = String(fields["status"] ?? "").trim().toLowerCase();
|
|
5930
|
+
const execRef = String(fields["executable_ref"] ?? "").trim();
|
|
5931
|
+
if (status !== "verified") continue;
|
|
5932
|
+
if (!execRef || isPendingExecutableRef(execRef)) continue;
|
|
5933
|
+
if (runners.length === 0) continue;
|
|
5934
|
+
let hasActiveE2eRef = false;
|
|
5935
|
+
for (const entry of parseExecutableRefLines(execRef)) {
|
|
5936
|
+
const normalized = resolveExecutableRefFile(entry.file, allProjectFiles);
|
|
5937
|
+
if (!normalized || !existsSync2(join5(root, normalized))) continue;
|
|
5938
|
+
const accepting = await getAcceptingRunners(root, normalized, runners);
|
|
5939
|
+
if (accepting.some((runner) => runner.kind === "e2e")) {
|
|
5940
|
+
hasActiveE2eRef = true;
|
|
5941
|
+
break;
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
if (!hasActiveE2eRef) continue;
|
|
5945
|
+
const relatedScenarios = toArray(fields["related_scenarios"] ?? node.attrs?.related_scenarios).map(String);
|
|
5946
|
+
for (const scenario of relatedScenarios) {
|
|
5947
|
+
verifiedScenarios.add(scenario);
|
|
5948
|
+
}
|
|
5949
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
5950
|
+
for (const feature of Object.keys(acCoverage)) {
|
|
5951
|
+
verifiedFeatures.add(feature);
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5954
|
+
const scenarioWaivers = new Set((thresholds.scenarioWaivers ?? []).map((w) => w.id));
|
|
5955
|
+
const featureWaivers = new Set((thresholds.featureWaivers ?? []).map((w) => w.id));
|
|
5956
|
+
const uncoveredScenarios = scenarioNodes.map((n) => n.code).filter((code) => !linkedScenarios.has(code) && !scenarioWaivers.has(code));
|
|
5957
|
+
const uncoveredFeatures = featureNodes.map((n) => n.code).filter((code) => !acCoveredFeatures.has(code) && !featureWaivers.has(code));
|
|
5958
|
+
const scenarioCoverage = {};
|
|
5959
|
+
for (const node of scenarioNodes) {
|
|
5960
|
+
scenarioCoverage[node.code] = {
|
|
5961
|
+
linked: linkedScenarios.has(node.code),
|
|
5962
|
+
acCovered: acCoveredScenarios.has(node.code),
|
|
5963
|
+
waived: scenarioWaivers.has(node.code),
|
|
5964
|
+
verified: !scenarioWaivers.has(node.code) && verifiedScenarios.has(node.code)
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
const featureCoverage = {};
|
|
5968
|
+
for (const node of featureNodes) {
|
|
5969
|
+
featureCoverage[node.code] = {
|
|
5970
|
+
linked: linkedFeatures.has(node.code),
|
|
5971
|
+
acCovered: acCoveredFeatures.has(node.code),
|
|
5972
|
+
waived: featureWaivers.has(node.code),
|
|
5973
|
+
verified: !featureWaivers.has(node.code) && verifiedFeatures.has(node.code)
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
const thresholdWarnings = [];
|
|
5977
|
+
const thresholdErrors = [];
|
|
5978
|
+
const warningRate = thresholds.executableRefWarning;
|
|
5979
|
+
const errorRate = thresholds.executableRefError;
|
|
5980
|
+
const actualRate = totalTestCases > 0 ? withExecutableRef / totalTestCases : 1;
|
|
5981
|
+
if (warningRate !== void 0 && actualRate < warningRate) {
|
|
5982
|
+
thresholdWarnings.push(`executable_ref coverage ${executableRefRate} < warning threshold ${(warningRate * 100).toFixed(0)}%`);
|
|
5983
|
+
}
|
|
5984
|
+
if (errorRate !== void 0 && actualRate < errorRate) {
|
|
5985
|
+
thresholdErrors.push(`executable_ref coverage ${executableRefRate} < error threshold ${(errorRate * 100).toFixed(0)}%`);
|
|
5986
|
+
}
|
|
5987
|
+
if (thresholds.reportUncoveredScenarios !== false && uncoveredScenarios.length > 0) {
|
|
5988
|
+
thresholdWarnings.push(`${uncoveredScenarios.length} scenario(s) have no E2E coverage: ${uncoveredScenarios.join(", ")}`);
|
|
5989
|
+
}
|
|
5990
|
+
if (thresholds.reportUncoveredFeatures !== false && uncoveredFeatures.length > 0) {
|
|
5991
|
+
thresholdWarnings.push(`${uncoveredFeatures.length} feature(s) have no E2E coverage: ${uncoveredFeatures.join(", ")}`);
|
|
5992
|
+
}
|
|
5993
|
+
const acCoverageRateByFeature = {};
|
|
5994
|
+
const featureAcMap = /* @__PURE__ */ new Map();
|
|
5995
|
+
for (const node of featureNodes) {
|
|
5996
|
+
const acs = parseAcceptanceCriteria(await readFile2(join5(root, node.path), "utf-8"));
|
|
5997
|
+
featureAcMap.set(node.code, new Set(acs));
|
|
5998
|
+
}
|
|
5999
|
+
const coveredAcByFeature = /* @__PURE__ */ new Map();
|
|
6000
|
+
for (const node of e2eNodes) {
|
|
6001
|
+
const fields = tcFieldsMap.get(node.code) ?? asRecord(node.attrs?.tcFields);
|
|
6002
|
+
const acCoverage = asRecord(fields["ac_coverage"] ?? node.attrs?.ac_coverage);
|
|
6003
|
+
for (const [feature, acs] of Object.entries(acCoverage)) {
|
|
6004
|
+
const existing = coveredAcByFeature.get(feature) ?? /* @__PURE__ */ new Set();
|
|
6005
|
+
for (const ac of toArray(acs)) {
|
|
6006
|
+
existing.add(String(ac));
|
|
6007
|
+
}
|
|
6008
|
+
coveredAcByFeature.set(feature, existing);
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
for (const [feature, allAcs] of featureAcMap) {
|
|
6012
|
+
const denominator = allAcs.size;
|
|
6013
|
+
if (denominator === 0) continue;
|
|
6014
|
+
const coveredAcs = coveredAcByFeature.get(feature) ?? /* @__PURE__ */ new Set();
|
|
6015
|
+
const numerator = [...coveredAcs].filter((ac) => allAcs.has(ac)).length;
|
|
6016
|
+
acCoverageRateByFeature[feature] = {
|
|
6017
|
+
numerator,
|
|
6018
|
+
denominator,
|
|
6019
|
+
rate: denominator > 0 ? numerator / denominator : 0
|
|
6020
|
+
};
|
|
6021
|
+
}
|
|
6022
|
+
return {
|
|
6023
|
+
totalTestCases,
|
|
6024
|
+
withExecutableRef,
|
|
6025
|
+
executableRefRate,
|
|
6026
|
+
statusBreakdown,
|
|
6027
|
+
chainTypeBreakdown,
|
|
6028
|
+
uncoveredScenarios,
|
|
6029
|
+
uncoveredFeatures,
|
|
6030
|
+
thresholdWarnings,
|
|
6031
|
+
thresholdErrors,
|
|
6032
|
+
acCoverageRateByFeature,
|
|
6033
|
+
scenarioCoverage,
|
|
6034
|
+
featureCoverage
|
|
6035
|
+
};
|
|
6036
|
+
}
|
|
6037
|
+
async function generateE2eRegistry(root, opts) {
|
|
6038
|
+
const e2eDir = join5(root, "artifacts", "tests", "e2e");
|
|
6039
|
+
let files;
|
|
6040
|
+
try {
|
|
6041
|
+
files = (await readdir(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).sort();
|
|
6042
|
+
} catch {
|
|
6043
|
+
return {
|
|
6044
|
+
registry_version: "1.0",
|
|
6045
|
+
generated_at: opts?.deterministic ? "1970-01-01T00:00:00.000Z" : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6046
|
+
total_batches: 0,
|
|
6047
|
+
total_test_cases: 0,
|
|
6048
|
+
batches: []
|
|
6049
|
+
};
|
|
6050
|
+
}
|
|
6051
|
+
const batches = [];
|
|
6052
|
+
let totalTestCases = 0;
|
|
6053
|
+
for (const file of files) {
|
|
6054
|
+
const filePath = join5(e2eDir, file);
|
|
6055
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
6056
|
+
const parsed = matter(raw);
|
|
6057
|
+
const data = parsed.data;
|
|
6058
|
+
const batch = String(data.test_batch ?? basename2(file, extname(file))).trim();
|
|
6059
|
+
const relPath = `artifacts/tests/e2e/${file}`;
|
|
6060
|
+
const scope = String(data.scope ?? "").trim();
|
|
6061
|
+
const acCoverage = normalizeAcCoverageForRegistry(data.ac_coverage);
|
|
6062
|
+
const relatedScenarios = toArray(data.related_scenarios).map(String).filter(Boolean);
|
|
6063
|
+
const lines = raw.split(/\r?\n/);
|
|
6064
|
+
const tcStarts = [];
|
|
6065
|
+
lines.forEach((line, index) => {
|
|
6066
|
+
const match = /^#{2,3}\s+(TC-\d+[a-z]?)\s*[::]?\s*.*$/.exec(line);
|
|
6067
|
+
if (match) {
|
|
6068
|
+
tcStarts.push({ id: match[1], index });
|
|
6069
|
+
}
|
|
6070
|
+
});
|
|
6071
|
+
const statusSummary = {};
|
|
6072
|
+
const blockingReasons = {};
|
|
6073
|
+
const frontmatterFixesBlock = String(data.fixes_block ?? "").trim();
|
|
6074
|
+
if (frontmatterFixesBlock && /no\s+(test\s+file|e2e)/i.test(frontmatterFixesBlock)) {
|
|
6075
|
+
for (const tc of tcStarts) {
|
|
6076
|
+
blockingReasons[tc.id] = frontmatterFixesBlock;
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
for (const start of tcStarts) {
|
|
6080
|
+
const end = tcStarts[tcStarts.indexOf(start) + 1]?.index ?? lines.length;
|
|
6081
|
+
const block = lines.slice(start.index, end);
|
|
6082
|
+
const fields = extractE2eTcFields(block);
|
|
6083
|
+
const status = String(fields["status"] ?? "created").trim().toLowerCase() || "created";
|
|
6084
|
+
statusSummary[status] = (statusSummary[status] ?? 0) + 1;
|
|
6085
|
+
if (status === "created" && !blockingReasons[start.id]) {
|
|
6086
|
+
const executableRef = String(fields["executable_ref"] ?? "").trim();
|
|
6087
|
+
const chainType = String(fields["chain_type"] ?? "").trim().toLowerCase();
|
|
6088
|
+
if (!executableRef && chainType === "desktop_chain") {
|
|
6089
|
+
blockingReasons[start.id] = "desktop_chain TC requires executable_ref";
|
|
6090
|
+
} else if (isPendingExecutableRef(executableRef)) {
|
|
6091
|
+
blockingReasons[start.id] = `pending: ${executableRef}`;
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
const testCaseCount = tcStarts.length;
|
|
6096
|
+
totalTestCases += testCaseCount;
|
|
6097
|
+
const batchStatus = Object.keys(blockingReasons).length > 0 ? "blocked" : void 0;
|
|
6098
|
+
batches.push({
|
|
6099
|
+
batch_id: batch,
|
|
6100
|
+
file: relPath,
|
|
6101
|
+
scope,
|
|
6102
|
+
ac_coverage: acCoverage,
|
|
6103
|
+
related_scenarios: relatedScenarios,
|
|
6104
|
+
test_case_count: testCaseCount,
|
|
6105
|
+
status_summary: statusSummary,
|
|
6106
|
+
status: batchStatus,
|
|
6107
|
+
blocking_reasons: Object.keys(blockingReasons).length > 0 ? blockingReasons : void 0
|
|
6108
|
+
});
|
|
6109
|
+
}
|
|
6110
|
+
return {
|
|
6111
|
+
registry_version: "1.0",
|
|
6112
|
+
generated_at: opts?.deterministic ? "1970-01-01T00:00:00.000Z" : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6113
|
+
total_batches: batches.length,
|
|
6114
|
+
total_test_cases: totalTestCases,
|
|
6115
|
+
batches
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
function normalizeAcCoverageForRegistry(value) {
|
|
6119
|
+
if (!value || typeof value !== "object") return {};
|
|
6120
|
+
const result = {};
|
|
6121
|
+
for (const [key, val] of Object.entries(value)) {
|
|
6122
|
+
if (Array.isArray(val)) {
|
|
6123
|
+
result[key] = val.map(String);
|
|
6124
|
+
} else if (typeof val === "string") {
|
|
6125
|
+
result[key] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
return result;
|
|
6129
|
+
}
|
|
5380
6130
|
function isPendingExecutableRef(ref) {
|
|
5381
6131
|
const stripped = ref.replace(/^[\s-*()]+/, "").trim();
|
|
5382
6132
|
return /^pending\b/i.test(stripped);
|
|
@@ -5394,6 +6144,20 @@ function parseExecutableRefLines(ref) {
|
|
|
5394
6144
|
}
|
|
5395
6145
|
return results;
|
|
5396
6146
|
}
|
|
6147
|
+
var VALID_TC_STATUSES = /* @__PURE__ */ new Set(["created", "automated", "verified", "waived"]);
|
|
6148
|
+
var VALID_CHAIN_TYPES = /* @__PURE__ */ new Set([
|
|
6149
|
+
"desktop_chain",
|
|
6150
|
+
"mock_playwright",
|
|
6151
|
+
"core_e2e",
|
|
6152
|
+
"cli_e2e",
|
|
6153
|
+
"ui_sidecar_bridge",
|
|
6154
|
+
"partial_sidecar",
|
|
6155
|
+
"partial_rust"
|
|
6156
|
+
]);
|
|
6157
|
+
var DEPRECATED_CHAIN_TYPE_ALIASES = {
|
|
6158
|
+
core_only: "core_e2e",
|
|
6159
|
+
frontend_only: "mock_playwright"
|
|
6160
|
+
};
|
|
5397
6161
|
function isDesktopChainType(chainType) {
|
|
5398
6162
|
const normalizedChainType = chainType.trim().toLowerCase();
|
|
5399
6163
|
return normalizedChainType === "desktop_chain" || normalizedChainType === "";
|
|
@@ -5401,7 +6165,7 @@ function isDesktopChainType(chainType) {
|
|
|
5401
6165
|
function parseChainCoverageStatus(chainCoverage) {
|
|
5402
6166
|
return chainCoverage.trim().toLowerCase().match(/^[a-z_]+/)?.[0] ?? "";
|
|
5403
6167
|
}
|
|
5404
|
-
async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
6168
|
+
async function validatePartialRustEvidence(tcFields, tcKey, root, allFiles) {
|
|
5405
6169
|
const partialEvidence = String(tcFields["partial_evidence"] ?? "");
|
|
5406
6170
|
if (!partialEvidence.trim()) {
|
|
5407
6171
|
return { hasValidPartialRust: false, detail: "no partial_evidence field" };
|
|
@@ -5425,7 +6189,10 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5425
6189
|
return { hasValidPartialRust: false, detail: "no .rs file in partial_evidence" };
|
|
5426
6190
|
}
|
|
5427
6191
|
for (const ref of rustRefs) {
|
|
5428
|
-
const normalizedPath =
|
|
6192
|
+
const normalizedPath = resolveExecutableRefFile(ref.file, allFiles);
|
|
6193
|
+
if (!normalizedPath) {
|
|
6194
|
+
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
6195
|
+
}
|
|
5429
6196
|
const fullPath = join5(root, normalizedPath);
|
|
5430
6197
|
let content;
|
|
5431
6198
|
try {
|
|
@@ -5434,14 +6201,14 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5434
6201
|
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
5435
6202
|
}
|
|
5436
6203
|
const tcAnnotationPattern = new RegExp(
|
|
5437
|
-
`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
6204
|
+
`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
5438
6205
|
);
|
|
5439
6206
|
if (!tcAnnotationPattern.test(content)) {
|
|
5440
|
-
const noLevelPattern = new RegExp(`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\b`);
|
|
6207
|
+
const noLevelPattern = new RegExp(`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\b`);
|
|
5441
6208
|
if (noLevelPattern.test(content)) {
|
|
5442
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has
|
|
6209
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has E2E trace annotation ${tcKey} but not tagged [partial_rust]` };
|
|
5443
6210
|
}
|
|
5444
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no
|
|
6211
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no E2E trace annotation ${tcKey}` };
|
|
5445
6212
|
}
|
|
5446
6213
|
}
|
|
5447
6214
|
return { hasValidPartialRust: true, detail: "ok" };
|
|
@@ -5461,6 +6228,46 @@ function detectTestLevel(specFile, content) {
|
|
|
5461
6228
|
}
|
|
5462
6229
|
return "desktop_chain";
|
|
5463
6230
|
}
|
|
6231
|
+
function resolveExecutableRefFile(refFile, allFiles) {
|
|
6232
|
+
const normalized = refFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6233
|
+
if (!normalized || isAbsolute3(refFile) || normalized.split("/").includes("..")) {
|
|
6234
|
+
return void 0;
|
|
6235
|
+
}
|
|
6236
|
+
if (allFiles.includes(normalized)) {
|
|
6237
|
+
return normalized;
|
|
6238
|
+
}
|
|
6239
|
+
const suffix = `/${normalized}`;
|
|
6240
|
+
const matches = allFiles.filter((file) => file.endsWith(suffix));
|
|
6241
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
6242
|
+
}
|
|
6243
|
+
function isRunnerIncludeCandidate(filePath, runner) {
|
|
6244
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
6245
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
6246
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) {
|
|
6247
|
+
return false;
|
|
6248
|
+
}
|
|
6249
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.slice(runnerRoot.length + 1);
|
|
6250
|
+
return runner.include.some((pattern) => matchesRunnerGlob(relativePath, pattern));
|
|
6251
|
+
}
|
|
6252
|
+
async function getAcceptingRunners(root, filePath, runners) {
|
|
6253
|
+
const accepting = [];
|
|
6254
|
+
for (const runner of runners) {
|
|
6255
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
6256
|
+
const runnerRoot = runner.root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
6257
|
+
const relativePath = runnerRoot === "." ? normalizedPath : normalizedPath.startsWith(runnerRoot + "/") ? normalizedPath.slice(runnerRoot.length + 1) : normalizedPath;
|
|
6258
|
+
if (runnerRoot !== "." && !normalizedPath.startsWith(`${runnerRoot}/`)) {
|
|
6259
|
+
continue;
|
|
6260
|
+
}
|
|
6261
|
+
const matchesInclude = runner.include.some((p) => matchesRunnerGlob(relativePath, p));
|
|
6262
|
+
if (!matchesInclude) continue;
|
|
6263
|
+
const matchesExclude = (runner.exclude ?? []).some((p) => matchesRunnerGlob(relativePath, p));
|
|
6264
|
+
if (matchesExclude) continue;
|
|
6265
|
+
const matchesTestIgnore = (runner.testIgnore ?? []).some((p) => matchesRunnerGlob(relativePath, p));
|
|
6266
|
+
if (matchesTestIgnore) continue;
|
|
6267
|
+
accepting.push(runner);
|
|
6268
|
+
}
|
|
6269
|
+
return accepting;
|
|
6270
|
+
}
|
|
5464
6271
|
function splitMarkdownCells(line) {
|
|
5465
6272
|
const cells = [];
|
|
5466
6273
|
let current = "";
|
|
@@ -5614,7 +6421,7 @@ function flattenAcCoverage(value) {
|
|
|
5614
6421
|
function needsDesktopChainWarning(node) {
|
|
5615
6422
|
const tcFields = asRecord(node.attrs?.tcFields);
|
|
5616
6423
|
const chainType = String(tcFields["chain_type"] ?? "").trim().toLowerCase();
|
|
5617
|
-
if (chainType
|
|
6424
|
+
if (chainType && VALID_CHAIN_TYPES.has(chainType) && chainType !== "desktop_chain" || chainType in DEPRECATED_CHAIN_TYPE_ALIASES) {
|
|
5618
6425
|
return false;
|
|
5619
6426
|
}
|
|
5620
6427
|
const chainCoverage = String(tcFields["chain_coverage"] ?? "").trim().toLowerCase();
|
|
@@ -5768,9 +6575,14 @@ function mergeRecord(base, override) {
|
|
|
5768
6575
|
function mergeArtifactTypes(base, override) {
|
|
5769
6576
|
const result = { ...base };
|
|
5770
6577
|
for (const [type, definition] of Object.entries(override ?? {})) {
|
|
6578
|
+
const aliases = [
|
|
6579
|
+
...base[type]?.aliases ?? [],
|
|
6580
|
+
...definition.aliases ?? []
|
|
6581
|
+
].filter((alias, index, all) => all.indexOf(alias) === index);
|
|
5771
6582
|
result[type] = {
|
|
5772
6583
|
...base[type] ?? {},
|
|
5773
|
-
...definition
|
|
6584
|
+
...definition,
|
|
6585
|
+
...aliases.length > 0 ? { aliases } : {}
|
|
5774
6586
|
};
|
|
5775
6587
|
}
|
|
5776
6588
|
return result;
|
|
@@ -5864,6 +6676,8 @@ var TIER_ORDER = ["baseline", "target", "direct", "matrix", "transitive"];
|
|
|
5864
6676
|
function resolveArtifactContext(graph, opts) {
|
|
5865
6677
|
const mode = opts.mode ?? "full";
|
|
5866
6678
|
const maxPerCategory = opts.maxPerCategory ?? 20;
|
|
6679
|
+
const universalBaseline = opts.universalBaseline ?? true;
|
|
6680
|
+
const root = opts.root ?? graph.root;
|
|
5867
6681
|
const legacyCount = [opts.feature, opts.scenario, opts.decision, opts.design, opts.e2e_test].filter(Boolean).length;
|
|
5868
6682
|
if (opts.target && legacyCount > 0) {
|
|
5869
6683
|
return {
|
|
@@ -6016,12 +6830,80 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6016
6830
|
return "direct";
|
|
6017
6831
|
}
|
|
6018
6832
|
const pathMap = /* @__PURE__ */ new Map();
|
|
6019
|
-
|
|
6020
|
-
const
|
|
6021
|
-
|
|
6022
|
-
if (
|
|
6833
|
+
if (universalBaseline) {
|
|
6834
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6835
|
+
const existing = pathMap.get(ap.path);
|
|
6836
|
+
if (existing) {
|
|
6837
|
+
if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
|
|
6838
|
+
} else {
|
|
6839
|
+
pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
if (root) {
|
|
6843
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6844
|
+
const fullPath = join5(root, ap.path);
|
|
6845
|
+
let stat;
|
|
6846
|
+
try {
|
|
6847
|
+
stat = statSync(fullPath);
|
|
6848
|
+
} catch {
|
|
6849
|
+
stat = null;
|
|
6850
|
+
}
|
|
6851
|
+
if (!stat) {
|
|
6852
|
+
const msg = `Required baseline artifact not found: ${ap.path}`;
|
|
6853
|
+
if (!missing.includes(msg)) {
|
|
6854
|
+
missing.push(msg);
|
|
6855
|
+
missingDetails.push({
|
|
6856
|
+
ref: ap.path,
|
|
6857
|
+
from: "baseline",
|
|
6858
|
+
kind: "missing-baseline",
|
|
6859
|
+
message: msg,
|
|
6860
|
+
suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6861
|
+
});
|
|
6862
|
+
}
|
|
6863
|
+
} else if (!stat.isFile()) {
|
|
6864
|
+
const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
|
|
6865
|
+
if (!missing.includes(msg)) {
|
|
6866
|
+
missing.push(msg);
|
|
6867
|
+
missingDetails.push({
|
|
6868
|
+
ref: ap.path,
|
|
6869
|
+
from: "baseline",
|
|
6870
|
+
kind: "missing-baseline",
|
|
6871
|
+
message: msg,
|
|
6872
|
+
suggestedAction: `\u5C06 ${ap.path} \u4ECE\u76EE\u5F55\u6539\u4E3A\u6587\u4EF6\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6873
|
+
});
|
|
6874
|
+
}
|
|
6875
|
+
} else {
|
|
6876
|
+
try {
|
|
6877
|
+
accessSync(fullPath, fsConstants.R_OK);
|
|
6878
|
+
} catch {
|
|
6879
|
+
const msg = `Required baseline artifact is not readable: ${ap.path}`;
|
|
6880
|
+
if (!missing.includes(msg)) {
|
|
6881
|
+
missing.push(msg);
|
|
6882
|
+
missingDetails.push({
|
|
6883
|
+
ref: ap.path,
|
|
6884
|
+
from: "baseline",
|
|
6885
|
+
kind: "missing-baseline",
|
|
6886
|
+
message: msg,
|
|
6887
|
+
suggestedAction: `\u4FEE\u590D ${ap.path} \u7684\u6587\u4EF6\u6743\u9650\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6888
|
+
});
|
|
6889
|
+
}
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6023
6893
|
} else {
|
|
6024
|
-
|
|
6894
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6895
|
+
const msg = `Cannot verify baseline without root: ${ap.path}`;
|
|
6896
|
+
if (!missing.includes(msg)) {
|
|
6897
|
+
missing.push(msg);
|
|
6898
|
+
missingDetails.push({
|
|
6899
|
+
ref: ap.path,
|
|
6900
|
+
from: "baseline",
|
|
6901
|
+
kind: "missing-baseline",
|
|
6902
|
+
message: msg,
|
|
6903
|
+
suggestedAction: `\u4F20\u9012 root \u53C2\u6570\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6904
|
+
});
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6025
6907
|
}
|
|
6026
6908
|
}
|
|
6027
6909
|
pathMap.set(targetNode.path, {
|
|
@@ -6122,7 +7004,8 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6122
7004
|
context,
|
|
6123
7005
|
missing,
|
|
6124
7006
|
missingDetails,
|
|
6125
|
-
omitted
|
|
7007
|
+
omitted,
|
|
7008
|
+
baselinePolicy: universalBaseline
|
|
6126
7009
|
};
|
|
6127
7010
|
}
|
|
6128
7011
|
function formatContextMarkdown(manifest) {
|
|
@@ -6206,7 +7089,9 @@ async function auditSinglePromptTarget(target, graph, options) {
|
|
|
6206
7089
|
try {
|
|
6207
7090
|
const manifest = resolveArtifactContext(graph, {
|
|
6208
7091
|
target: { type: target.type, id: target.id },
|
|
6209
|
-
mode: "implementation"
|
|
7092
|
+
mode: "implementation",
|
|
7093
|
+
root: options.root,
|
|
7094
|
+
universalBaseline: options.universalBaseline
|
|
6210
7095
|
});
|
|
6211
7096
|
if (manifest.missing.length > 0) {
|
|
6212
7097
|
entry.ok = false;
|
|
@@ -6362,13 +7247,15 @@ async function discoverAndAuditPromptBatch(root, options) {
|
|
|
6362
7247
|
type: d.type,
|
|
6363
7248
|
id: d.id
|
|
6364
7249
|
}));
|
|
7250
|
+
const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
|
|
6365
7251
|
return auditPromptBatch(root, targets, {
|
|
6366
7252
|
root,
|
|
6367
7253
|
outDir: options.outDir,
|
|
6368
7254
|
format: options.format,
|
|
6369
7255
|
maxChars: options.maxChars,
|
|
6370
7256
|
summaryOnly: options.summaryOnly,
|
|
6371
|
-
summaryDetail: options.summaryDetail
|
|
7257
|
+
summaryDetail: options.summaryDetail,
|
|
7258
|
+
universalBaseline: effectiveBaseline
|
|
6372
7259
|
}, graph);
|
|
6373
7260
|
}
|
|
6374
7261
|
|
|
@@ -6380,7 +7267,8 @@ async function runCli(argv, io = {}) {
|
|
|
6380
7267
|
const err = io.stderr ?? ((chunk) => process.stderr.write(chunk));
|
|
6381
7268
|
const root = String(parsed.flags.root ?? cwd);
|
|
6382
7269
|
try {
|
|
6383
|
-
|
|
7270
|
+
const hasHelpFlag = parsed.flags.help === true || parsed.positional.some((p) => p === "--help" || p === "-h");
|
|
7271
|
+
if (parsed.command === "--help" || parsed.command === "-h" || parsed.command === "help" || hasHelpFlag) {
|
|
6384
7272
|
out(helpText());
|
|
6385
7273
|
return 0;
|
|
6386
7274
|
}
|
|
@@ -6406,14 +7294,109 @@ async function runCli(argv, io = {}) {
|
|
|
6406
7294
|
const graph = await scanArtifacts(root, config);
|
|
6407
7295
|
const issues = validateGraph(graph, config);
|
|
6408
7296
|
issues.push(...await validateScenarioPrdLinkIndex(root, graph));
|
|
6409
|
-
issues.push(...await validateExecutableTraceability(root));
|
|
7297
|
+
issues.push(...await validateExecutableTraceability(root, config));
|
|
7298
|
+
const includeCoverage = includes.has("e2e-coverage") || config.e2e?.executable_ref_warning !== void 0 || config.e2e?.executable_ref_error !== void 0;
|
|
7299
|
+
let coverageStats = null;
|
|
7300
|
+
if (includeCoverage) {
|
|
7301
|
+
const e2eConfig = config.e2e ?? {};
|
|
7302
|
+
coverageStats = await computeE2eCoverageStats(graph, root, {
|
|
7303
|
+
executableRefWarning: e2eConfig.executable_ref_warning,
|
|
7304
|
+
executableRefError: e2eConfig.executable_ref_error,
|
|
7305
|
+
reportUncoveredScenarios: e2eConfig.report_uncovered_scenarios,
|
|
7306
|
+
reportUncoveredFeatures: e2eConfig.report_uncovered_features,
|
|
7307
|
+
scenarioWaivers: e2eConfig.scenario_waivers,
|
|
7308
|
+
featureWaivers: e2eConfig.feature_waivers
|
|
7309
|
+
});
|
|
7310
|
+
for (const msg of coverageStats.thresholdWarnings) {
|
|
7311
|
+
issues.push({
|
|
7312
|
+
code: "E2E_COVERAGE_WARNING",
|
|
7313
|
+
severity: "warning",
|
|
7314
|
+
message: msg,
|
|
7315
|
+
path: "e2e-coverage",
|
|
7316
|
+
line: 1
|
|
7317
|
+
});
|
|
7318
|
+
}
|
|
7319
|
+
for (const msg of coverageStats.thresholdErrors) {
|
|
7320
|
+
issues.push({
|
|
7321
|
+
code: "E2E_COVERAGE_ERROR",
|
|
7322
|
+
severity: "error",
|
|
7323
|
+
message: msg,
|
|
7324
|
+
path: "e2e-coverage",
|
|
7325
|
+
line: 1
|
|
7326
|
+
});
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
6410
7329
|
if (parsed.flags.format === "json") {
|
|
6411
|
-
|
|
7330
|
+
if (coverageStats) {
|
|
7331
|
+
const output = {
|
|
7332
|
+
issues,
|
|
7333
|
+
e2eCoverage: {
|
|
7334
|
+
totalTestCases: coverageStats.totalTestCases,
|
|
7335
|
+
withExecutableRef: coverageStats.withExecutableRef,
|
|
7336
|
+
executableRefRate: coverageStats.executableRefRate,
|
|
7337
|
+
statusBreakdown: coverageStats.statusBreakdown,
|
|
7338
|
+
chainTypeBreakdown: coverageStats.chainTypeBreakdown,
|
|
7339
|
+
uncoveredScenarios: coverageStats.uncoveredScenarios,
|
|
7340
|
+
uncoveredFeatures: coverageStats.uncoveredFeatures,
|
|
7341
|
+
acCoverageRateByFeature: coverageStats.acCoverageRateByFeature,
|
|
7342
|
+
scenarioCoverage: coverageStats.scenarioCoverage,
|
|
7343
|
+
featureCoverage: coverageStats.featureCoverage
|
|
7344
|
+
}
|
|
7345
|
+
};
|
|
7346
|
+
out(`${JSON.stringify(output, null, 2)}
|
|
6412
7347
|
`);
|
|
6413
|
-
|
|
6414
|
-
|
|
7348
|
+
} else {
|
|
7349
|
+
out(`${JSON.stringify(issues, null, 2)}
|
|
7350
|
+
`);
|
|
7351
|
+
}
|
|
6415
7352
|
} else {
|
|
6416
|
-
|
|
7353
|
+
if (coverageStats) {
|
|
7354
|
+
out(`E2E Coverage: ${coverageStats.executableRefRate} executable_ref
|
|
7355
|
+
`);
|
|
7356
|
+
out(` Status: ${JSON.stringify(coverageStats.statusBreakdown)}
|
|
7357
|
+
`);
|
|
7358
|
+
out(` Chain types: ${JSON.stringify(coverageStats.chainTypeBreakdown)}
|
|
7359
|
+
`);
|
|
7360
|
+
if (coverageStats.uncoveredScenarios.length > 0) {
|
|
7361
|
+
out(` Uncovered scenarios (${coverageStats.uncoveredScenarios.length}): ${coverageStats.uncoveredScenarios.join(", ")}
|
|
7362
|
+
`);
|
|
7363
|
+
}
|
|
7364
|
+
if (coverageStats.uncoveredFeatures.length > 0) {
|
|
7365
|
+
out(` Uncovered features (${coverageStats.uncoveredFeatures.length}): ${coverageStats.uncoveredFeatures.join(", ")}
|
|
7366
|
+
`);
|
|
7367
|
+
}
|
|
7368
|
+
if (Object.keys(coverageStats.acCoverageRateByFeature).length > 0) {
|
|
7369
|
+
out(` AC coverage by feature:
|
|
7370
|
+
`);
|
|
7371
|
+
for (const [feature, rate] of Object.entries(coverageStats.acCoverageRateByFeature)) {
|
|
7372
|
+
out(` ${feature}: ${rate.numerator}/${rate.denominator} (${(rate.rate * 100).toFixed(1)}%)
|
|
7373
|
+
`);
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
const scenarioStats = Object.values(coverageStats.scenarioCoverage);
|
|
7377
|
+
const featureStats = Object.values(coverageStats.featureCoverage);
|
|
7378
|
+
if (scenarioStats.length > 0) {
|
|
7379
|
+
const linked = scenarioStats.filter((s) => s.linked).length;
|
|
7380
|
+
const acCovered = scenarioStats.filter((s) => s.acCovered).length;
|
|
7381
|
+
const waived = scenarioStats.filter((s) => s.waived).length;
|
|
7382
|
+
const verified = scenarioStats.filter((s) => s.verified).length;
|
|
7383
|
+
out(` Scenario coverage: linked=${linked}, acCovered=${acCovered}, waived=${waived}, verified=${verified}
|
|
7384
|
+
`);
|
|
7385
|
+
}
|
|
7386
|
+
if (featureStats.length > 0) {
|
|
7387
|
+
const linked = featureStats.filter((s) => s.linked).length;
|
|
7388
|
+
const acCovered = featureStats.filter((s) => s.acCovered).length;
|
|
7389
|
+
const waived = featureStats.filter((s) => s.waived).length;
|
|
7390
|
+
const verified = featureStats.filter((s) => s.verified).length;
|
|
7391
|
+
out(` Feature coverage: linked=${linked}, acCovered=${acCovered}, waived=${waived}, verified=${verified}
|
|
7392
|
+
`);
|
|
7393
|
+
}
|
|
7394
|
+
}
|
|
7395
|
+
if (issues.length === 0) {
|
|
7396
|
+
out("No validation issues\n");
|
|
7397
|
+
} else {
|
|
7398
|
+
out(issues.map((issue2) => `${issue2.code} ${issue2.path}:${issue2.line} ${issue2.message}`).join("\n") + "\n");
|
|
7399
|
+
}
|
|
6417
7400
|
}
|
|
6418
7401
|
return issues.some((issue2) => issue2.severity === "error") && !parsed.flags["warning-only"] ? 1 : 0;
|
|
6419
7402
|
}
|
|
@@ -6487,7 +7470,9 @@ async function runCli(argv, io = {}) {
|
|
|
6487
7470
|
const manifest = resolveArtifactContext(graph, {
|
|
6488
7471
|
target: resolvedTarget,
|
|
6489
7472
|
mode: contextMode,
|
|
6490
|
-
maxPerCategory
|
|
7473
|
+
maxPerCategory,
|
|
7474
|
+
universalBaseline: config.context?.universal_baseline,
|
|
7475
|
+
root
|
|
6491
7476
|
});
|
|
6492
7477
|
if (parsed.flags.format === "json") {
|
|
6493
7478
|
out(`${JSON.stringify(manifest, null, 2)}
|
|
@@ -6528,7 +7513,9 @@ async function runCli(argv, io = {}) {
|
|
|
6528
7513
|
const manifest = resolveArtifactContext(graph, {
|
|
6529
7514
|
target: resolvedTarget,
|
|
6530
7515
|
mode: packetMode,
|
|
6531
|
-
maxPerCategory: packetMaxPerCategory
|
|
7516
|
+
maxPerCategory: packetMaxPerCategory,
|
|
7517
|
+
universalBaseline: config.context?.universal_baseline,
|
|
7518
|
+
root
|
|
6532
7519
|
});
|
|
6533
7520
|
if (manifest.missing.length > 0) {
|
|
6534
7521
|
err("Missing artifacts detected \u2014 cannot generate packet:\n");
|
|
@@ -6657,7 +7644,9 @@ async function runCli(argv, io = {}) {
|
|
|
6657
7644
|
const graph = await scanArtifacts(root);
|
|
6658
7645
|
const promptManifest = resolveArtifactContext(graph, {
|
|
6659
7646
|
target: resolvedTarget,
|
|
6660
|
-
mode: "implementation"
|
|
7647
|
+
mode: "implementation",
|
|
7648
|
+
universalBaseline: config.context?.universal_baseline,
|
|
7649
|
+
root
|
|
6661
7650
|
});
|
|
6662
7651
|
if (promptManifest.missing.length > 0) {
|
|
6663
7652
|
err("Missing artifacts detected \u2014 cannot generate prompt:\n");
|
|
@@ -6797,6 +7786,7 @@ async function runCli(argv, io = {}) {
|
|
|
6797
7786
|
}
|
|
6798
7787
|
let summary;
|
|
6799
7788
|
if (discover) {
|
|
7789
|
+
const discoverConfig = await loadConfig(root);
|
|
6800
7790
|
summary = await discoverAndAuditPackets(root, {
|
|
6801
7791
|
root,
|
|
6802
7792
|
outDir: auditOutDir,
|
|
@@ -6806,7 +7796,8 @@ async function runCli(argv, io = {}) {
|
|
|
6806
7796
|
limit: limit === 0 ? Infinity : limit,
|
|
6807
7797
|
summaryOnly,
|
|
6808
7798
|
sampleTargets,
|
|
6809
|
-
summaryDetail
|
|
7799
|
+
summaryDetail,
|
|
7800
|
+
universalBaseline: discoverConfig.context?.universal_baseline
|
|
6810
7801
|
});
|
|
6811
7802
|
} else {
|
|
6812
7803
|
const targetsContent = await readFile3(targetsFile, "utf-8");
|
|
@@ -6834,7 +7825,8 @@ async function runCli(argv, io = {}) {
|
|
|
6834
7825
|
summaryOnly,
|
|
6835
7826
|
sampleTargets,
|
|
6836
7827
|
summaryDetail,
|
|
6837
|
-
schema: auditConfig2
|
|
7828
|
+
schema: auditConfig2,
|
|
7829
|
+
universalBaseline: auditConfig2.context?.universal_baseline
|
|
6838
7830
|
});
|
|
6839
7831
|
}
|
|
6840
7832
|
if (parsed.flags.format === "json") {
|
|
@@ -6914,6 +7906,7 @@ async function runCli(argv, io = {}) {
|
|
|
6914
7906
|
}
|
|
6915
7907
|
let ppaSummary;
|
|
6916
7908
|
if (ppaDiscover) {
|
|
7909
|
+
const ppaDiscoverConfig = await loadConfig(root);
|
|
6917
7910
|
ppaSummary = await discoverAndAuditPromptBatch(root, {
|
|
6918
7911
|
root,
|
|
6919
7912
|
outDir: ppaOutDir,
|
|
@@ -6921,7 +7914,8 @@ async function runCli(argv, io = {}) {
|
|
|
6921
7914
|
maxChars: ppaMaxChars,
|
|
6922
7915
|
limit: ppaLimit === 0 ? Infinity : ppaLimit,
|
|
6923
7916
|
summaryOnly: ppaSummaryOnly,
|
|
6924
|
-
summaryDetail: ppaSummaryDetail
|
|
7917
|
+
summaryDetail: ppaSummaryDetail,
|
|
7918
|
+
universalBaseline: ppaDiscoverConfig.context?.universal_baseline
|
|
6925
7919
|
});
|
|
6926
7920
|
if (ppaSummary.total === 0) {
|
|
6927
7921
|
err(`\u9519\u8BEF\uFF1Adiscover \u6A21\u5F0F\u5728 ${root} \u4E2D\u672A\u627E\u5230\u4EFB\u4F55 target\u3002\u8BF7\u786E\u8BA4\u8FD9\u662F\u4E00\u4E2A\u6709\u6548\u7684 artifact root\u3002
|
|
@@ -6963,7 +7957,8 @@ async function runCli(argv, io = {}) {
|
|
|
6963
7957
|
maxChars: ppaMaxChars,
|
|
6964
7958
|
sourceTargetsPath: ppaTargetsFile,
|
|
6965
7959
|
summaryOnly: ppaSummaryOnly,
|
|
6966
|
-
summaryDetail: ppaSummaryDetail
|
|
7960
|
+
summaryDetail: ppaSummaryDetail,
|
|
7961
|
+
universalBaseline: ppaConfig.context?.universal_baseline
|
|
6967
7962
|
});
|
|
6968
7963
|
}
|
|
6969
7964
|
if (ppaFormat === "json") {
|
|
@@ -7027,7 +8022,8 @@ async function runCli(argv, io = {}) {
|
|
|
7027
8022
|
`);
|
|
7028
8023
|
return 1;
|
|
7029
8024
|
}
|
|
7030
|
-
const
|
|
8025
|
+
const config = await loadConfig(root);
|
|
8026
|
+
const result = await auditVersionLock(root, lockPath, void 0, config);
|
|
7031
8027
|
if (versionLockAuditFormat === "json") {
|
|
7032
8028
|
out(`${JSON.stringify(result, null, 2)}
|
|
7033
8029
|
`);
|
|
@@ -7076,6 +8072,10 @@ async function runCli(argv, io = {}) {
|
|
|
7076
8072
|
err("Usage: artifact-graph version-lock refresh (--all | --changed-only (--staged | --worktree | --base <ref>)) [--remove-orphans] [--format json|markdown] [--lock-path <path>]\n");
|
|
7077
8073
|
return 1;
|
|
7078
8074
|
}
|
|
8075
|
+
if (parsed.flags.help === true) {
|
|
8076
|
+
out("Usage: artifact-graph version-lock refresh (--all | --changed-only (--staged | --worktree | --base <ref>)) [--remove-orphans] [--format json|markdown] [--lock-path <path>]\n");
|
|
8077
|
+
return 0;
|
|
8078
|
+
}
|
|
7079
8079
|
if (refreshAll && refreshChangedOnly) {
|
|
7080
8080
|
err("Error: --all and --changed-only are mutually exclusive\n");
|
|
7081
8081
|
return 1;
|
|
@@ -7213,7 +8213,7 @@ async function runCli(argv, io = {}) {
|
|
|
7213
8213
|
err("Usage: artifact-graph validate-review-result --file <path> [--format json]\n");
|
|
7214
8214
|
return 1;
|
|
7215
8215
|
}
|
|
7216
|
-
const resolvedPath =
|
|
8216
|
+
const resolvedPath = isAbsolute4(filePath) ? filePath : join7(root, filePath);
|
|
7217
8217
|
let content;
|
|
7218
8218
|
try {
|
|
7219
8219
|
content = await readFile3(resolvedPath, "utf-8");
|
|
@@ -7246,6 +8246,39 @@ async function runCli(argv, io = {}) {
|
|
|
7246
8246
|
}
|
|
7247
8247
|
return validationErrors.length === 0 ? 0 : 1;
|
|
7248
8248
|
}
|
|
8249
|
+
case "generate-e2e-registry": {
|
|
8250
|
+
const checkMode = parsed.flags.check === true;
|
|
8251
|
+
const deterministic = checkMode || parsed.flags.deterministic === true;
|
|
8252
|
+
const registry = await generateE2eRegistry(root, { deterministic });
|
|
8253
|
+
const output = JSON.stringify(registry, null, 2) + "\n";
|
|
8254
|
+
const outPath = typeof parsed.flags.out === "string" ? parsed.flags.out : join7(root, "artifacts/tests/e2e/e2e-test-registry.json");
|
|
8255
|
+
if (checkMode) {
|
|
8256
|
+
let existing = "";
|
|
8257
|
+
try {
|
|
8258
|
+
existing = await readFile3(outPath, "utf-8");
|
|
8259
|
+
} catch {
|
|
8260
|
+
err(`Check failed: ${outPath} does not exist or is not readable
|
|
8261
|
+
`);
|
|
8262
|
+
return 1;
|
|
8263
|
+
}
|
|
8264
|
+
if (existing !== output) {
|
|
8265
|
+
err(`Registry drift detected: ${outPath} differs from deterministic generation
|
|
8266
|
+
`);
|
|
8267
|
+
return 1;
|
|
8268
|
+
}
|
|
8269
|
+
out(`Registry check passed: ${outPath} matches deterministic generation
|
|
8270
|
+
`);
|
|
8271
|
+
return 0;
|
|
8272
|
+
}
|
|
8273
|
+
if (typeof parsed.flags.out === "string") {
|
|
8274
|
+
await writeFile5(parsed.flags.out, output);
|
|
8275
|
+
out(`Registry written to ${parsed.flags.out} (${registry.total_batches} batches, ${registry.total_test_cases} TCs)
|
|
8276
|
+
`);
|
|
8277
|
+
} else {
|
|
8278
|
+
out(output);
|
|
8279
|
+
}
|
|
8280
|
+
return 0;
|
|
8281
|
+
}
|
|
7249
8282
|
default:
|
|
7250
8283
|
err(helpText());
|
|
7251
8284
|
return 1;
|
|
@@ -7284,12 +8317,18 @@ function parseArgs(argv) {
|
|
|
7284
8317
|
if (token.startsWith("--")) {
|
|
7285
8318
|
const key = token.slice(2);
|
|
7286
8319
|
const next = rest[index + 1];
|
|
7287
|
-
|
|
8320
|
+
const nextLooksLikeFlag = next && next.startsWith("-") && next.length > 1 && !/\d/.test(next[1]);
|
|
8321
|
+
if (next && !nextLooksLikeFlag) {
|
|
7288
8322
|
flags[key] = next;
|
|
7289
8323
|
index += 1;
|
|
7290
8324
|
} else {
|
|
7291
8325
|
flags[key] = true;
|
|
7292
8326
|
}
|
|
8327
|
+
} else if (token === "-h") {
|
|
8328
|
+
flags.help = true;
|
|
8329
|
+
} else if (token.startsWith("-") && token.length > 1) {
|
|
8330
|
+
const key = token.slice(1);
|
|
8331
|
+
flags[key] = true;
|
|
7293
8332
|
} else {
|
|
7294
8333
|
positional.push(token);
|
|
7295
8334
|
}
|
|
@@ -7302,7 +8341,7 @@ function helpText() {
|
|
|
7302
8341
|
Commands:
|
|
7303
8342
|
init
|
|
7304
8343
|
scan
|
|
7305
|
-
validate [--format json] [--warning-only] [--include scenario-prd-links]
|
|
8344
|
+
validate [--format json] [--warning-only] [--include scenario-prd-links,e2e-coverage]
|
|
7306
8345
|
query --from <code> [--format json]
|
|
7307
8346
|
context (--target <type>:<id> | --feature <id> | --scenario <id> | --decision <id> | --design <id> | --e2e-test <id>) [--mode full|implementation] [--max-per-category <n>] [--format json]
|
|
7308
8347
|
packet (--target <type>:<id> | --feature <id> | --scenario <id> | --decision <id> | --design <id> | --e2e-test <id>) [--mode full|implementation] [--max-per-category <n>] [--format json|markdown] [--out <path>] [--no-validate]
|
|
@@ -7320,6 +8359,7 @@ Commands:
|
|
|
7320
8359
|
render [--format mermaid]
|
|
7321
8360
|
doctor [--format json|markdown]
|
|
7322
8361
|
validate-review-result --file <path> [--format json]
|
|
8362
|
+
generate-e2e-registry [--deterministic] [--out <path>] [--check]
|
|
7323
8363
|
`;
|
|
7324
8364
|
}
|
|
7325
8365
|
function isCliEntrypoint(argvPath) {
|