artifact-graph 0.4.1 → 0.5.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, 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) {
@@ -499,7 +563,10 @@ function assemblePacket(manifest, options) {
499
563
  missing: [...manifest.missing],
500
564
  missingDetails: manifest.missingDetails ? [...manifest.missingDetails] : void 0,
501
565
  implementationBlueprintDraft: blueprintDraft,
502
- validationCommands
566
+ validationCommands,
567
+ // @feature ACA17
568
+ // @decision D-ACA-17
569
+ baselinePolicy: manifest.baselinePolicy
503
570
  };
504
571
  return packet;
505
572
  }
@@ -720,7 +787,9 @@ async function auditSingleTarget(target, graph, options) {
720
787
  const manifest = resolveArtifactContext(graph, {
721
788
  target: { type: target.type, id: target.id },
722
789
  mode: options.mode,
723
- maxPerCategory: options.maxPerCategory
790
+ maxPerCategory: options.maxPerCategory,
791
+ universalBaseline: options.universalBaseline,
792
+ root: options.root
724
793
  });
725
794
  const packet = assemblePacket(manifest, {
726
795
  mode: options.mode,
@@ -857,6 +926,7 @@ async function discoverAndAuditPackets(root, options) {
857
926
  type: d.type,
858
927
  id: d.id
859
928
  }));
929
+ const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
860
930
  return auditPackets(root, targets, {
861
931
  root,
862
932
  outDir: options.outDir,
@@ -866,7 +936,8 @@ async function discoverAndAuditPackets(root, options) {
866
936
  summaryOnly: options.summaryOnly,
867
937
  sampleTargets: options.sampleTargets,
868
938
  summaryDetail: options.summaryDetail,
869
- schema: config
939
+ schema: config,
940
+ universalBaseline: effectiveBaseline
870
941
  }, graph);
871
942
  }
872
943
 
@@ -2577,12 +2648,35 @@ var VALID_DECISIONS = /* @__PURE__ */ new Set([
2577
2648
  var VALID_SEVERITIES = /* @__PURE__ */ new Set(["block", "warn", "info"]);
2578
2649
  var VALID_FINDING_STATUSES = /* @__PURE__ */ new Set(["open", "resolved", "accepted", "superseded"]);
2579
2650
  var VALID_EXECUTORS = /* @__PURE__ */ new Set(["script", "worker", "agent", "manual", "cli"]);
2651
+ var TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
2652
+ "schema_version",
2653
+ "run_id",
2654
+ "stage_id",
2655
+ "attempt",
2656
+ "status",
2657
+ "decision",
2658
+ "summary",
2659
+ "outputs",
2660
+ "warnings",
2661
+ "blocking_reason",
2662
+ "degradation",
2663
+ "producer",
2664
+ "acceptance",
2665
+ "evidence",
2666
+ "review",
2667
+ "repair"
2668
+ ]);
2580
2669
  function validateReviewResult(input) {
2581
2670
  const errors = [];
2582
2671
  if (!input || typeof input !== "object" || Array.isArray(input)) {
2583
2672
  return [{ path: "$", message: "Root must be a non-null object" }];
2584
2673
  }
2585
2674
  const obj = input;
2675
+ for (const key of Object.keys(obj)) {
2676
+ if (!TOP_LEVEL_FIELDS.has(key)) {
2677
+ errors.push({ path: `$.${key}`, message: "Unknown top-level property" });
2678
+ }
2679
+ }
2586
2680
  if (obj.schema_version !== "1.0") {
2587
2681
  errors.push({ path: "$.schema_version", message: `Must be "1.0", got ${JSON.stringify(obj.schema_version)}` });
2588
2682
  }
@@ -2601,8 +2695,8 @@ function validateReviewResult(input) {
2601
2695
  if (obj.stage_id !== void 0 && typeof obj.stage_id !== "string") {
2602
2696
  errors.push({ path: "$.stage_id", message: "Must be a string if present" });
2603
2697
  }
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" });
2698
+ if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1 || obj.attempt > 3)) {
2699
+ errors.push({ path: "$.attempt", message: "Must be an integer from 1 through 3 if present" });
2606
2700
  }
2607
2701
  if (obj.outputs !== void 0) {
2608
2702
  checkStringArray(obj.outputs, "$.outputs", errors);
@@ -2613,20 +2707,14 @@ function validateReviewResult(input) {
2613
2707
  checkOptionalNullableString(obj.blocking_reason, "$.blocking_reason", errors);
2614
2708
  checkOptionalNullableString(obj.degradation, "$.degradation", errors);
2615
2709
  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
- }
2710
+ validateProducer(obj.producer, "$.producer", errors);
2711
+ }
2712
+ const successfulAcceptance = obj.status === "SUCCEEDED" && (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR");
2713
+ if (successfulAcceptance && obj.producer === void 0) {
2714
+ errors.push({ path: "$.producer", message: "Successful acceptance requires producer identity" });
2715
+ }
2716
+ if (obj.acceptance !== void 0) {
2717
+ validateAcceptance(obj.acceptance, obj.producer, "$.acceptance", errors);
2630
2718
  }
2631
2719
  if (obj.evidence !== void 0) {
2632
2720
  if (!Array.isArray(obj.evidence)) {
@@ -2652,6 +2740,17 @@ function validateReviewResult(input) {
2652
2740
  }
2653
2741
  if (obj.review !== void 0) {
2654
2742
  validateReviewData(obj.review, "$.review", errors);
2743
+ if (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR") {
2744
+ const findings = isPlainObject(obj.review) && Array.isArray(obj.review.findings) ? obj.review.findings : [];
2745
+ findings.forEach((finding, index) => {
2746
+ if (isPlainObject(finding) && finding.severity === "block" && (finding.status === void 0 || finding.status === "open")) {
2747
+ errors.push({
2748
+ path: `$.review.findings[${index}]`,
2749
+ message: `${obj.decision} cannot contain an open block finding`
2750
+ });
2751
+ }
2752
+ });
2753
+ }
2655
2754
  }
2656
2755
  if (obj.repair !== void 0) {
2657
2756
  if (!isPlainObject(obj.repair)) {
@@ -2687,6 +2786,47 @@ function checkStringArray(val, path, errors) {
2687
2786
  function isPlainObject(val) {
2688
2787
  return typeof val === "object" && val !== null && !Array.isArray(val);
2689
2788
  }
2789
+ function validateProducer(val, path, errors) {
2790
+ if (!isPlainObject(val)) {
2791
+ errors.push({ path, message: "Must be an object" });
2792
+ return;
2793
+ }
2794
+ if (typeof val.executor !== "string") {
2795
+ errors.push({ path: `${path}.executor`, message: "Must be a string" });
2796
+ } else if (!VALID_EXECUTORS.has(val.executor)) {
2797
+ errors.push({ path: `${path}.executor`, message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(val.executor)}` });
2798
+ }
2799
+ if (typeof val.name !== "string" || val.name.length === 0) {
2800
+ errors.push({ path: `${path}.name`, message: "Must be a non-empty string" });
2801
+ }
2802
+ checkOptionalString(val.skill, `${path}.skill`, errors);
2803
+ }
2804
+ function producerIdentity(val) {
2805
+ return JSON.stringify([val.executor, val.name]);
2806
+ }
2807
+ function validateAcceptance(val, resultProducer, path, errors) {
2808
+ if (!isPlainObject(val)) {
2809
+ errors.push({ path, message: "Must be an object" });
2810
+ return;
2811
+ }
2812
+ validateProducer(val.reviewer, `${path}.reviewer`, errors);
2813
+ if (!isPlainObject(val.source_result)) {
2814
+ errors.push({ path: `${path}.source_result`, message: "Must be an object" });
2815
+ return;
2816
+ }
2817
+ const source = val.source_result;
2818
+ if (typeof source.run_id !== "string" || source.run_id.length === 0) {
2819
+ errors.push({ path: `${path}.source_result.run_id`, message: "Must be a non-empty string" });
2820
+ }
2821
+ checkOptionalString(source.stage_id, `${path}.source_result.stage_id`, errors);
2822
+ validateProducer(source.producer, `${path}.source_result.producer`, errors);
2823
+ if (isPlainObject(val.reviewer) && isPlainObject(resultProducer) && producerIdentity(val.reviewer) !== producerIdentity(resultProducer)) {
2824
+ errors.push({ path: `${path}.reviewer`, message: "Acceptance reviewer must match the result producer" });
2825
+ }
2826
+ if (isPlainObject(val.reviewer) && isPlainObject(source.producer) && producerIdentity(val.reviewer) === producerIdentity(source.producer)) {
2827
+ errors.push({ path: `${path}.reviewer`, message: "Repair producer cannot accept its own result" });
2828
+ }
2829
+ }
2690
2830
  function checkOptionalString(val, path, errors) {
2691
2831
  if (val !== void 0 && typeof val !== "string") {
2692
2832
  errors.push({ path, message: "Must be a string if present" });
@@ -2855,7 +2995,7 @@ var DEFAULT_SCHEMA = {
2855
2995
  scenario: { paths: ["artifacts/scenarios/**/*.md"], displayName: "\u573A\u666F\u5267\u672C", role: "scenario", layer: "scenario", aliases: ["scenarios", "scenario-script"] },
2856
2996
  design: { paths: ["artifacts/design/**/*.md"], displayName: "\u8BBE\u8BA1\u89C4\u683C", role: "design", layer: "design", aliases: ["design-spec", "design_docs"] },
2857
2997
  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"] },
2998
+ 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
2999
  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
3000
  "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
3001
  "test-strategy": { paths: ["artifacts/design/test-strategy.md"], displayName: "\u6D4B\u8BD5\u7B56\u7565", role: "context", layer: "verification", aliases: ["test_strategy"] },
@@ -2898,6 +3038,12 @@ async function loadConfig(root) {
2898
3038
  throw error;
2899
3039
  }
2900
3040
  }
3041
+ const ub = parsed.context?.universal_baseline;
3042
+ if (ub !== void 0 && typeof ub !== "boolean") {
3043
+ throw new Error(
3044
+ `Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
3045
+ );
3046
+ }
2901
3047
  return {
2902
3048
  ...DEFAULT_SCHEMA,
2903
3049
  ...parsed,
@@ -2910,7 +3056,7 @@ async function loadConfig(root) {
2910
3056
  idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
2911
3057
  };
2912
3058
  }
2913
- function buildGraph(nodes, edges, diagnostics = []) {
3059
+ function buildGraph(nodes, edges, diagnostics = [], root) {
2914
3060
  const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
2915
3061
  graphNodes.sort(compareNode);
2916
3062
  edges.sort(compareEdge);
@@ -2927,6 +3073,7 @@ function buildGraph(nodes, edges, diagnostics = []) {
2927
3073
  nodes: graphNodes,
2928
3074
  edges: dedupedEdges,
2929
3075
  generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
3076
+ ...root ? { root } : {},
2930
3077
  diagnostics: diagnostics.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line)
2931
3078
  };
2932
3079
  }
@@ -2958,7 +3105,8 @@ async function scanArtifacts(root, schema) {
2958
3105
  scanDiagnostics.push(...parsed.diagnostics);
2959
3106
  }
2960
3107
  }
2961
- const graph = buildGraph(nodes, edges, scanDiagnostics);
3108
+ const absoluteRoot = isAbsolute3(root) ? root : resolve3(root);
3109
+ const graph = buildGraph(nodes, edges, scanDiagnostics, absoluteRoot);
2962
3110
  return resolveMatrixEdges(graph);
2963
3111
  }
2964
3112
  function artifactTypeEntriesBySpecificity(schema) {
@@ -3298,23 +3446,34 @@ async function validateScenarioPrdLinkIndex(root, graph) {
3298
3446
  function validateCodeCommentTraceabilityFormat(graph) {
3299
3447
  const issues = [];
3300
3448
  for (const node of graph.nodes) {
3301
- if (node.type !== "test") {
3449
+ if (node.type !== "test" && node.type !== "implementation") {
3302
3450
  continue;
3303
3451
  }
3304
3452
  const invalidComments = node.attrs?.invalidTraceabilityComments;
3305
- if (!Array.isArray(invalidComments)) {
3306
- continue;
3453
+ if (Array.isArray(invalidComments)) {
3454
+ for (const invalid of invalidComments) {
3455
+ const line = typeof invalid?.line === "number" ? invalid.line : node.line;
3456
+ const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
3457
+ issues.push(issue(
3458
+ "CODE_COMMENT_TRACEABILITY_FORMAT",
3459
+ `${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
3460
+ node.path,
3461
+ line,
3462
+ { node: node.uid }
3463
+ ));
3464
+ }
3307
3465
  }
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
- ));
3466
+ const deprecatedComments = node.attrs?.deprecatedTraceabilityComments;
3467
+ if (Array.isArray(deprecatedComments)) {
3468
+ for (const deprecated of deprecatedComments) {
3469
+ issues.push(issue(
3470
+ "E2E-TRACE-007",
3471
+ "@tc is deprecated; use @e2e_test instead",
3472
+ node.path,
3473
+ typeof deprecated?.line === "number" ? deprecated.line : node.line,
3474
+ { node: node.uid, severity: "warning" }
3475
+ ));
3476
+ }
3318
3477
  }
3319
3478
  }
3320
3479
  return issues;
@@ -3796,13 +3955,16 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
3796
3955
  const isTest = isTestFile(path);
3797
3956
  const nodeType = isTest ? "test" : "implementation";
3798
3957
  const edgeKind = isTest ? "verifies" : "implements";
3958
+ const attrs = {};
3959
+ if (traceabilityComments.invalid.length > 0) attrs.invalidTraceabilityComments = traceabilityComments.invalid;
3960
+ if (traceabilityComments.deprecated.length > 0) attrs.deprecatedTraceabilityComments = traceabilityComments.deprecated;
3799
3961
  const node = {
3800
3962
  type: nodeType,
3801
3963
  code: path,
3802
3964
  title: path.split("/").at(-1) ?? path,
3803
3965
  path,
3804
3966
  line: 1,
3805
- attrs: traceabilityComments.invalid.length > 0 ? { invalidTraceabilityComments: traceabilityComments.invalid } : {}
3967
+ attrs
3806
3968
  };
3807
3969
  let hasTags = false;
3808
3970
  for (const { tags, lineNumber } of traceabilityComments.canonical) {
@@ -3813,7 +3975,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
3813
3975
  }
3814
3976
  }
3815
3977
  }
3816
- if (hasTags || traceabilityComments.invalid.length > 0) {
3978
+ if (hasTags || traceabilityComments.invalid.length > 0 || traceabilityComments.deprecated.length > 0) {
3817
3979
  nodes.push(node);
3818
3980
  }
3819
3981
  return { nodes, edges };
@@ -3821,6 +3983,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
3821
3983
  function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
3822
3984
  const canonical = [];
3823
3985
  const invalid = [];
3986
+ const deprecated = [];
3824
3987
  for (const comment of scanCodeComments(raw)) {
3825
3988
  if (!containsTraceabilityTag(comment.text, schema)) {
3826
3989
  continue;
@@ -3836,6 +3999,9 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
3836
3999
  const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
3837
4000
  if (parsed.valid) {
3838
4001
  canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
4002
+ if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
4003
+ deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
4004
+ }
3839
4005
  } else {
3840
4006
  invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
3841
4007
  }
@@ -3851,11 +4017,14 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
3851
4017
  const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
3852
4018
  if (parsed.valid) {
3853
4019
  canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
4020
+ if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
4021
+ deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
4022
+ }
3854
4023
  } else {
3855
4024
  invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
3856
4025
  }
3857
4026
  }
3858
- return { canonical, invalid };
4027
+ return { canonical, invalid, deprecated };
3859
4028
  }
3860
4029
  function scanCodeComments(raw) {
3861
4030
  const comments = [];
@@ -4022,7 +4191,14 @@ function expandCodeRange(value) {
4022
4191
  return Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${String(start + index).padStart(width, "0")}`);
4023
4192
  }
4024
4193
  function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
4025
- return /@[\w][\w-]*\b/.test(value);
4194
+ const tokens = /* @__PURE__ */ new Set();
4195
+ for (const [type, definition] of Object.entries(schema.types)) {
4196
+ tokens.add(type);
4197
+ for (const alias of definition.aliases ?? []) tokens.add(alias);
4198
+ }
4199
+ if (tokens.size === 0) return false;
4200
+ const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
4201
+ return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
4026
4202
  }
4027
4203
  function parseDesign(path, raw) {
4028
4204
  const parsed = matter(raw);
@@ -5139,8 +5315,8 @@ async function validateExecutableTraceability(root) {
5139
5315
  }
5140
5316
  }
5141
5317
  const refToSource = /* @__PURE__ */ new Map();
5142
- const tcAnnotationRegex = /\/\/!?\s*@tc\s+(\S+?)\s+\[(\w+)\]/;
5143
- const tcAnnotationNoLevelRegex = /\/\/!?\s*@tc\s+(\S+)/;
5318
+ const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
5319
+ const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
5144
5320
  for (const specFile of specFiles) {
5145
5321
  const fullSpecPath = join5(root, specFile);
5146
5322
  let content;
@@ -5229,7 +5405,7 @@ async function validateExecutableTraceability(root) {
5229
5405
  return normalizedAnnFile === normalizedRefFile;
5230
5406
  }) ?? false;
5231
5407
  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" }));
5408
+ issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
5233
5409
  continue;
5234
5410
  }
5235
5411
  validFiles.push(normalizedRefFile);
@@ -5240,13 +5416,13 @@ async function validateExecutableTraceability(root) {
5240
5416
  const batch = tcKey.split(":")[0];
5241
5417
  if (!mdBatches.has(batch)) {
5242
5418
  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" }));
5419
+ 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
5420
  }
5245
5421
  continue;
5246
5422
  }
5247
5423
  if (!mdToRef.has(tcKey) && !await hasMarkdownTc(tcKey, e2eDir)) {
5248
5424
  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" }));
5425
+ issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
5250
5426
  }
5251
5427
  }
5252
5428
  }
@@ -5268,7 +5444,7 @@ async function validateExecutableTraceability(root) {
5268
5444
  const primaryRef = refEntries[0];
5269
5445
  const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
5270
5446
  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" }));
5447
+ issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
5272
5448
  }
5273
5449
  }
5274
5450
  for (const [tcKey, { chainType, path, line }] of mdToRef) {
@@ -5434,14 +5610,14 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
5434
5610
  return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
5435
5611
  }
5436
5612
  const tcAnnotationPattern = new RegExp(
5437
- `//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
5613
+ `//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
5438
5614
  );
5439
5615
  if (!tcAnnotationPattern.test(content)) {
5440
- const noLevelPattern = new RegExp(`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\b`);
5616
+ const noLevelPattern = new RegExp(`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\b`);
5441
5617
  if (noLevelPattern.test(content)) {
5442
- return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has @tc ${tcKey} but not tagged [partial_rust]` };
5618
+ return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has E2E trace annotation ${tcKey} but not tagged [partial_rust]` };
5443
5619
  }
5444
- return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no @tc ${tcKey} annotation` };
5620
+ return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no E2E trace annotation ${tcKey}` };
5445
5621
  }
5446
5622
  }
5447
5623
  return { hasValidPartialRust: true, detail: "ok" };
@@ -5768,9 +5944,14 @@ function mergeRecord(base, override) {
5768
5944
  function mergeArtifactTypes(base, override) {
5769
5945
  const result = { ...base };
5770
5946
  for (const [type, definition] of Object.entries(override ?? {})) {
5947
+ const aliases = [
5948
+ ...base[type]?.aliases ?? [],
5949
+ ...definition.aliases ?? []
5950
+ ].filter((alias, index, all) => all.indexOf(alias) === index);
5771
5951
  result[type] = {
5772
5952
  ...base[type] ?? {},
5773
- ...definition
5953
+ ...definition,
5954
+ ...aliases.length > 0 ? { aliases } : {}
5774
5955
  };
5775
5956
  }
5776
5957
  return result;
@@ -5864,6 +6045,8 @@ var TIER_ORDER = ["baseline", "target", "direct", "matrix", "transitive"];
5864
6045
  function resolveArtifactContext(graph, opts) {
5865
6046
  const mode = opts.mode ?? "full";
5866
6047
  const maxPerCategory = opts.maxPerCategory ?? 20;
6048
+ const universalBaseline = opts.universalBaseline ?? true;
6049
+ const root = opts.root ?? graph.root;
5867
6050
  const legacyCount = [opts.feature, opts.scenario, opts.decision, opts.design, opts.e2e_test].filter(Boolean).length;
5868
6051
  if (opts.target && legacyCount > 0) {
5869
6052
  return {
@@ -6016,12 +6199,80 @@ function resolveArtifactContext(graph, opts) {
6016
6199
  return "direct";
6017
6200
  }
6018
6201
  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);
6202
+ if (universalBaseline) {
6203
+ for (const ap of ALWAYS_PRESENT_ITEMS) {
6204
+ const existing = pathMap.get(ap.path);
6205
+ if (existing) {
6206
+ if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
6207
+ } else {
6208
+ pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
6209
+ }
6210
+ }
6211
+ if (root) {
6212
+ for (const ap of ALWAYS_PRESENT_ITEMS) {
6213
+ const fullPath = join5(root, ap.path);
6214
+ let stat;
6215
+ try {
6216
+ stat = statSync(fullPath);
6217
+ } catch {
6218
+ stat = null;
6219
+ }
6220
+ if (!stat) {
6221
+ const msg = `Required baseline artifact not found: ${ap.path}`;
6222
+ if (!missing.includes(msg)) {
6223
+ missing.push(msg);
6224
+ missingDetails.push({
6225
+ ref: ap.path,
6226
+ from: "baseline",
6227
+ kind: "missing-baseline",
6228
+ message: msg,
6229
+ suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6230
+ });
6231
+ }
6232
+ } else if (!stat.isFile()) {
6233
+ const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
6234
+ if (!missing.includes(msg)) {
6235
+ missing.push(msg);
6236
+ missingDetails.push({
6237
+ ref: ap.path,
6238
+ from: "baseline",
6239
+ kind: "missing-baseline",
6240
+ message: msg,
6241
+ suggestedAction: `\u5C06 ${ap.path} \u4ECE\u76EE\u5F55\u6539\u4E3A\u6587\u4EF6\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6242
+ });
6243
+ }
6244
+ } else {
6245
+ try {
6246
+ accessSync(fullPath, fsConstants.R_OK);
6247
+ } catch {
6248
+ const msg = `Required baseline artifact is not readable: ${ap.path}`;
6249
+ if (!missing.includes(msg)) {
6250
+ missing.push(msg);
6251
+ missingDetails.push({
6252
+ ref: ap.path,
6253
+ from: "baseline",
6254
+ kind: "missing-baseline",
6255
+ message: msg,
6256
+ suggestedAction: `\u4FEE\u590D ${ap.path} \u7684\u6587\u4EF6\u6743\u9650\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6257
+ });
6258
+ }
6259
+ }
6260
+ }
6261
+ }
6023
6262
  } else {
6024
- pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
6263
+ for (const ap of ALWAYS_PRESENT_ITEMS) {
6264
+ const msg = `Cannot verify baseline without root: ${ap.path}`;
6265
+ if (!missing.includes(msg)) {
6266
+ missing.push(msg);
6267
+ missingDetails.push({
6268
+ ref: ap.path,
6269
+ from: "baseline",
6270
+ kind: "missing-baseline",
6271
+ message: msg,
6272
+ suggestedAction: `\u4F20\u9012 root \u53C2\u6570\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6273
+ });
6274
+ }
6275
+ }
6025
6276
  }
6026
6277
  }
6027
6278
  pathMap.set(targetNode.path, {
@@ -6122,7 +6373,8 @@ function resolveArtifactContext(graph, opts) {
6122
6373
  context,
6123
6374
  missing,
6124
6375
  missingDetails,
6125
- omitted
6376
+ omitted,
6377
+ baselinePolicy: universalBaseline
6126
6378
  };
6127
6379
  }
6128
6380
  function formatContextMarkdown(manifest) {
@@ -6206,7 +6458,9 @@ async function auditSinglePromptTarget(target, graph, options) {
6206
6458
  try {
6207
6459
  const manifest = resolveArtifactContext(graph, {
6208
6460
  target: { type: target.type, id: target.id },
6209
- mode: "implementation"
6461
+ mode: "implementation",
6462
+ root: options.root,
6463
+ universalBaseline: options.universalBaseline
6210
6464
  });
6211
6465
  if (manifest.missing.length > 0) {
6212
6466
  entry.ok = false;
@@ -6362,13 +6616,15 @@ async function discoverAndAuditPromptBatch(root, options) {
6362
6616
  type: d.type,
6363
6617
  id: d.id
6364
6618
  }));
6619
+ const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
6365
6620
  return auditPromptBatch(root, targets, {
6366
6621
  root,
6367
6622
  outDir: options.outDir,
6368
6623
  format: options.format,
6369
6624
  maxChars: options.maxChars,
6370
6625
  summaryOnly: options.summaryOnly,
6371
- summaryDetail: options.summaryDetail
6626
+ summaryDetail: options.summaryDetail,
6627
+ universalBaseline: effectiveBaseline
6372
6628
  }, graph);
6373
6629
  }
6374
6630
 
@@ -6380,7 +6636,8 @@ async function runCli(argv, io = {}) {
6380
6636
  const err = io.stderr ?? ((chunk) => process.stderr.write(chunk));
6381
6637
  const root = String(parsed.flags.root ?? cwd);
6382
6638
  try {
6383
- if (parsed.command === "--help" || parsed.command === "-h" || parsed.command === "help") {
6639
+ const hasHelpFlag = parsed.flags.help === true || parsed.positional.some((p) => p === "--help" || p === "-h");
6640
+ if (parsed.command === "--help" || parsed.command === "-h" || parsed.command === "help" || hasHelpFlag) {
6384
6641
  out(helpText());
6385
6642
  return 0;
6386
6643
  }
@@ -6487,7 +6744,9 @@ async function runCli(argv, io = {}) {
6487
6744
  const manifest = resolveArtifactContext(graph, {
6488
6745
  target: resolvedTarget,
6489
6746
  mode: contextMode,
6490
- maxPerCategory
6747
+ maxPerCategory,
6748
+ universalBaseline: config.context?.universal_baseline,
6749
+ root
6491
6750
  });
6492
6751
  if (parsed.flags.format === "json") {
6493
6752
  out(`${JSON.stringify(manifest, null, 2)}
@@ -6528,7 +6787,9 @@ async function runCli(argv, io = {}) {
6528
6787
  const manifest = resolveArtifactContext(graph, {
6529
6788
  target: resolvedTarget,
6530
6789
  mode: packetMode,
6531
- maxPerCategory: packetMaxPerCategory
6790
+ maxPerCategory: packetMaxPerCategory,
6791
+ universalBaseline: config.context?.universal_baseline,
6792
+ root
6532
6793
  });
6533
6794
  if (manifest.missing.length > 0) {
6534
6795
  err("Missing artifacts detected \u2014 cannot generate packet:\n");
@@ -6657,7 +6918,9 @@ async function runCli(argv, io = {}) {
6657
6918
  const graph = await scanArtifacts(root);
6658
6919
  const promptManifest = resolveArtifactContext(graph, {
6659
6920
  target: resolvedTarget,
6660
- mode: "implementation"
6921
+ mode: "implementation",
6922
+ universalBaseline: config.context?.universal_baseline,
6923
+ root
6661
6924
  });
6662
6925
  if (promptManifest.missing.length > 0) {
6663
6926
  err("Missing artifacts detected \u2014 cannot generate prompt:\n");
@@ -6797,6 +7060,7 @@ async function runCli(argv, io = {}) {
6797
7060
  }
6798
7061
  let summary;
6799
7062
  if (discover) {
7063
+ const discoverConfig = await loadConfig(root);
6800
7064
  summary = await discoverAndAuditPackets(root, {
6801
7065
  root,
6802
7066
  outDir: auditOutDir,
@@ -6806,7 +7070,8 @@ async function runCli(argv, io = {}) {
6806
7070
  limit: limit === 0 ? Infinity : limit,
6807
7071
  summaryOnly,
6808
7072
  sampleTargets,
6809
- summaryDetail
7073
+ summaryDetail,
7074
+ universalBaseline: discoverConfig.context?.universal_baseline
6810
7075
  });
6811
7076
  } else {
6812
7077
  const targetsContent = await readFile3(targetsFile, "utf-8");
@@ -6834,7 +7099,8 @@ async function runCli(argv, io = {}) {
6834
7099
  summaryOnly,
6835
7100
  sampleTargets,
6836
7101
  summaryDetail,
6837
- schema: auditConfig2
7102
+ schema: auditConfig2,
7103
+ universalBaseline: auditConfig2.context?.universal_baseline
6838
7104
  });
6839
7105
  }
6840
7106
  if (parsed.flags.format === "json") {
@@ -6914,6 +7180,7 @@ async function runCli(argv, io = {}) {
6914
7180
  }
6915
7181
  let ppaSummary;
6916
7182
  if (ppaDiscover) {
7183
+ const ppaDiscoverConfig = await loadConfig(root);
6917
7184
  ppaSummary = await discoverAndAuditPromptBatch(root, {
6918
7185
  root,
6919
7186
  outDir: ppaOutDir,
@@ -6921,7 +7188,8 @@ async function runCli(argv, io = {}) {
6921
7188
  maxChars: ppaMaxChars,
6922
7189
  limit: ppaLimit === 0 ? Infinity : ppaLimit,
6923
7190
  summaryOnly: ppaSummaryOnly,
6924
- summaryDetail: ppaSummaryDetail
7191
+ summaryDetail: ppaSummaryDetail,
7192
+ universalBaseline: ppaDiscoverConfig.context?.universal_baseline
6925
7193
  });
6926
7194
  if (ppaSummary.total === 0) {
6927
7195
  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 +7231,8 @@ async function runCli(argv, io = {}) {
6963
7231
  maxChars: ppaMaxChars,
6964
7232
  sourceTargetsPath: ppaTargetsFile,
6965
7233
  summaryOnly: ppaSummaryOnly,
6966
- summaryDetail: ppaSummaryDetail
7234
+ summaryDetail: ppaSummaryDetail,
7235
+ universalBaseline: ppaConfig.context?.universal_baseline
6967
7236
  });
6968
7237
  }
6969
7238
  if (ppaFormat === "json") {
@@ -7076,6 +7345,10 @@ async function runCli(argv, io = {}) {
7076
7345
  err("Usage: artifact-graph version-lock refresh (--all | --changed-only (--staged | --worktree | --base <ref>)) [--remove-orphans] [--format json|markdown] [--lock-path <path>]\n");
7077
7346
  return 1;
7078
7347
  }
7348
+ if (parsed.flags.help === true) {
7349
+ out("Usage: artifact-graph version-lock refresh (--all | --changed-only (--staged | --worktree | --base <ref>)) [--remove-orphans] [--format json|markdown] [--lock-path <path>]\n");
7350
+ return 0;
7351
+ }
7079
7352
  if (refreshAll && refreshChangedOnly) {
7080
7353
  err("Error: --all and --changed-only are mutually exclusive\n");
7081
7354
  return 1;
@@ -7213,7 +7486,7 @@ async function runCli(argv, io = {}) {
7213
7486
  err("Usage: artifact-graph validate-review-result --file <path> [--format json]\n");
7214
7487
  return 1;
7215
7488
  }
7216
- const resolvedPath = isAbsolute3(filePath) ? filePath : join7(root, filePath);
7489
+ const resolvedPath = isAbsolute4(filePath) ? filePath : join7(root, filePath);
7217
7490
  let content;
7218
7491
  try {
7219
7492
  content = await readFile3(resolvedPath, "utf-8");
@@ -7284,12 +7557,18 @@ function parseArgs(argv) {
7284
7557
  if (token.startsWith("--")) {
7285
7558
  const key = token.slice(2);
7286
7559
  const next = rest[index + 1];
7287
- if (next && !next.startsWith("--")) {
7560
+ const nextLooksLikeFlag = next && next.startsWith("-") && next.length > 1 && !/\d/.test(next[1]);
7561
+ if (next && !nextLooksLikeFlag) {
7288
7562
  flags[key] = next;
7289
7563
  index += 1;
7290
7564
  } else {
7291
7565
  flags[key] = true;
7292
7566
  }
7567
+ } else if (token === "-h") {
7568
+ flags.help = true;
7569
+ } else if (token.startsWith("-") && token.length > 1) {
7570
+ const key = token.slice(1);
7571
+ flags[key] = true;
7293
7572
  } else {
7294
7573
  positional.push(token);
7295
7574
  }