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/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 isAbsolute3, join as join7 } from "path";
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
- if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
95
- issues.push({
96
- severity: "error",
97
- code: "PKT-004",
98
- message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
99
- path: "requiredBaseline.total"
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 currentEdges) {
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 implementationEdges(index)) {
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 = implementationEdges(index);
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 audit = await auditVersionLock(root, safeLockPath);
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 a positive integer if present" });
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
- if (!isPlainObject(obj.producer)) {
2617
- errors.push({ path: "$.producer", message: "Must be an object" });
2618
- } else {
2619
- const p = obj.producer;
2620
- if (typeof p.executor !== "string") {
2621
- errors.push({ path: "$.producer.executor", message: "Must be a string" });
2622
- } else if (!VALID_EXECUTORS.has(p.executor)) {
2623
- errors.push({ path: "$.producer.executor", message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(p.executor)}` });
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
- return {
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 buildGraph(nodes, edges, diagnostics = []) {
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 graph = buildGraph(nodes, edges, scanDiagnostics);
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 (!Array.isArray(invalidComments)) {
3306
- continue;
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
- for (const invalid of invalidComments) {
3309
- const line = typeof invalid?.line === "number" ? invalid.line : node.line;
3310
- const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
3311
- issues.push(issue(
3312
- "CODE_COMMENT_TRACEABILITY_FORMAT",
3313
- `${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
3314
- node.path,
3315
- line,
3316
- { node: node.uid }
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: traceabilityComments.invalid.length > 0 ? { invalidTraceabilityComments: traceabilityComments.invalid } : {}
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
- return /@[\w][\w-]*\b/.test(value);
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
- for (const pattern of specPatterns) {
5580
+ const configuredRunners = schema.e2e?.runners ?? [];
5581
+ if (configuredRunners.length > 0) {
5135
5582
  for (const file of allFiles) {
5136
- if (matchesPattern(file, pattern)) {
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 = entry.file.startsWith("heimdall/") ? entry.file : `heimdall/${entry.file}`;
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 // @tc ${tcKey} line-comment annotation`, path, line, { node: tcKey, severity: "warning" }));
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", `@tc annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
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", `@tc annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
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 @tc mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
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 mdToRef) {
5275
- if (!isDesktopChainType(chainType)) {
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
- const normalizedAnnFile = ann.file.startsWith("heimdall/") ? ann.file : `heimdall/${ann.file}`;
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 = ref.file.startsWith("heimdall/") ? ref.file : `heimdall/${ref.file}`;
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 @tc ${tcKey} but not tagged [partial_rust]` };
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 @tc ${tcKey} annotation` };
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 === "frontend_only" || chainType === "core_only") {
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
- for (const ap of ALWAYS_PRESENT_ITEMS) {
6020
- const existing = pathMap.get(ap.path);
6021
- if (existing) {
6022
- if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
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
- pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
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
- if (parsed.command === "--help" || parsed.command === "-h" || parsed.command === "help") {
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
- out(`${JSON.stringify(issues, null, 2)}
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
- } else if (issues.length === 0) {
6414
- out("No validation issues\n");
7348
+ } else {
7349
+ out(`${JSON.stringify(issues, null, 2)}
7350
+ `);
7351
+ }
6415
7352
  } else {
6416
- out(issues.map((issue2) => `${issue2.code} ${issue2.path}:${issue2.line} ${issue2.message}`).join("\n") + "\n");
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 result = await auditVersionLock(root, lockPath);
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 = isAbsolute3(filePath) ? filePath : join7(root, filePath);
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
- if (next && !next.startsWith("--")) {
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) {