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/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import Database from "better-sqlite3";
3
3
  import matter from "gray-matter";
4
4
  import yaml from "js-yaml";
5
+ import { accessSync, constants as fsConstants, statSync } from "fs";
5
6
  import { mkdir as mkdir4, readFile as readFile2, readdir, writeFile as writeFile3 } from "fs/promises";
6
- import { basename as basename2, dirname as dirname3, extname, join as join5, relative as relative2 } from "path";
7
+ import { basename as basename2, dirname as dirname3, extname, isAbsolute as isAbsolute3, join as join5, relative as relative2, resolve as resolve3 } from "path";
7
8
 
8
9
  // src/packet-constants.ts
9
10
  var ALWAYS_PRESENT_ITEMS = [
@@ -82,13 +83,76 @@ function validatePacket(packet, schema) {
82
83
  path: "target.id"
83
84
  });
84
85
  }
85
- if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
86
- issues.push({
87
- severity: "error",
88
- code: "PKT-004",
89
- message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
90
- path: "requiredBaseline.total"
91
- });
86
+ const isBaselineExplicitlyDisabled = packet.baselinePolicy === false;
87
+ const expectedPaths = new Set(ALWAYS_PRESENT_ITEMS.map((item) => item.path));
88
+ if (isBaselineExplicitlyDisabled) {
89
+ if (packet.requiredBaseline.total !== 0) {
90
+ issues.push({
91
+ severity: "error",
92
+ code: "PKT-004",
93
+ message: `baselinePolicy=false requires requiredBaseline.total=0, got ${packet.requiredBaseline.total}`,
94
+ path: "requiredBaseline.total"
95
+ });
96
+ }
97
+ if (packet.requiredBaseline.items.length !== 0) {
98
+ issues.push({
99
+ severity: "error",
100
+ code: "PKT-004",
101
+ message: `baselinePolicy=false requires requiredBaseline.items=[], got ${packet.requiredBaseline.items.length} item(s)`,
102
+ path: "requiredBaseline.items"
103
+ });
104
+ }
105
+ } else {
106
+ if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
107
+ issues.push({
108
+ severity: "error",
109
+ code: "PKT-004",
110
+ message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
111
+ path: "requiredBaseline.total"
112
+ });
113
+ }
114
+ if (packet.requiredBaseline.items.length !== BASELINE_ITEMS_COUNT) {
115
+ issues.push({
116
+ severity: "error",
117
+ code: "PKT-004",
118
+ message: `requiredBaseline.items.length must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.items.length}`,
119
+ path: "requiredBaseline.items"
120
+ });
121
+ }
122
+ const actualPaths = packet.requiredBaseline.items.map((item) => item.path);
123
+ const actualPathSet = new Set(actualPaths);
124
+ if (actualPathSet.size !== actualPaths.length) {
125
+ issues.push({
126
+ severity: "error",
127
+ code: "PKT-004",
128
+ message: `requiredBaseline.items contains duplicate paths (${actualPaths.length} items, ${actualPathSet.size} unique)`,
129
+ path: "requiredBaseline.items"
130
+ });
131
+ }
132
+ const missingPaths = [];
133
+ for (const ep of expectedPaths) {
134
+ if (!actualPathSet.has(ep)) missingPaths.push(ep);
135
+ }
136
+ if (missingPaths.length > 0) {
137
+ issues.push({
138
+ severity: "error",
139
+ code: "PKT-004",
140
+ message: `requiredBaseline.items missing expected path(s): ${missingPaths.join(", ")}`,
141
+ path: "requiredBaseline.items"
142
+ });
143
+ }
144
+ const extraPaths = [];
145
+ for (const ap of actualPathSet) {
146
+ if (!expectedPaths.has(ap)) extraPaths.push(ap);
147
+ }
148
+ if (extraPaths.length > 0) {
149
+ issues.push({
150
+ severity: "error",
151
+ code: "PKT-004",
152
+ message: `requiredBaseline.items contains unexpected path(s): ${extraPaths.join(", ")}`,
153
+ path: "requiredBaseline.items"
154
+ });
155
+ }
92
156
  }
93
157
  const constraints = packet.implementationBlueprintDraft.constraints;
94
158
  if (constraints.length !== BASELINE_CONSTRAINTS_COUNT) {
@@ -515,7 +579,10 @@ function assemblePacket(manifest, options) {
515
579
  missing: [...manifest.missing],
516
580
  missingDetails: manifest.missingDetails ? [...manifest.missingDetails] : void 0,
517
581
  implementationBlueprintDraft: blueprintDraft,
518
- validationCommands
582
+ validationCommands,
583
+ // @feature ACA17
584
+ // @decision D-ACA-17
585
+ baselinePolicy: manifest.baselinePolicy
519
586
  };
520
587
  return packet;
521
588
  }
@@ -736,7 +803,9 @@ async function auditSingleTarget(target, graph, options) {
736
803
  const manifest = resolveArtifactContext(graph, {
737
804
  target: { type: target.type, id: target.id },
738
805
  mode: options.mode,
739
- maxPerCategory: options.maxPerCategory
806
+ maxPerCategory: options.maxPerCategory,
807
+ universalBaseline: options.universalBaseline,
808
+ root: options.root
740
809
  });
741
810
  const packet = assemblePacket(manifest, {
742
811
  mode: options.mode,
@@ -873,6 +942,7 @@ async function discoverAndAuditPackets(root, options) {
873
942
  type: d.type,
874
943
  id: d.id
875
944
  }));
945
+ const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
876
946
  return auditPackets(root, targets, {
877
947
  root,
878
948
  outDir: options.outDir,
@@ -882,7 +952,8 @@ async function discoverAndAuditPackets(root, options) {
882
952
  summaryOnly: options.summaryOnly,
883
953
  sampleTargets: options.sampleTargets,
884
954
  summaryDetail: options.summaryDetail,
885
- schema: config
955
+ schema: config,
956
+ universalBaseline: effectiveBaseline
886
957
  }, graph);
887
958
  }
888
959
 
@@ -2597,12 +2668,35 @@ var VALID_DECISIONS = /* @__PURE__ */ new Set([
2597
2668
  var VALID_SEVERITIES = /* @__PURE__ */ new Set(["block", "warn", "info"]);
2598
2669
  var VALID_FINDING_STATUSES = /* @__PURE__ */ new Set(["open", "resolved", "accepted", "superseded"]);
2599
2670
  var VALID_EXECUTORS = /* @__PURE__ */ new Set(["script", "worker", "agent", "manual", "cli"]);
2671
+ var TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
2672
+ "schema_version",
2673
+ "run_id",
2674
+ "stage_id",
2675
+ "attempt",
2676
+ "status",
2677
+ "decision",
2678
+ "summary",
2679
+ "outputs",
2680
+ "warnings",
2681
+ "blocking_reason",
2682
+ "degradation",
2683
+ "producer",
2684
+ "acceptance",
2685
+ "evidence",
2686
+ "review",
2687
+ "repair"
2688
+ ]);
2600
2689
  function validateReviewResult(input) {
2601
2690
  const errors = [];
2602
2691
  if (!input || typeof input !== "object" || Array.isArray(input)) {
2603
2692
  return [{ path: "$", message: "Root must be a non-null object" }];
2604
2693
  }
2605
2694
  const obj = input;
2695
+ for (const key of Object.keys(obj)) {
2696
+ if (!TOP_LEVEL_FIELDS.has(key)) {
2697
+ errors.push({ path: `$.${key}`, message: "Unknown top-level property" });
2698
+ }
2699
+ }
2606
2700
  if (obj.schema_version !== "1.0") {
2607
2701
  errors.push({ path: "$.schema_version", message: `Must be "1.0", got ${JSON.stringify(obj.schema_version)}` });
2608
2702
  }
@@ -2621,8 +2715,8 @@ function validateReviewResult(input) {
2621
2715
  if (obj.stage_id !== void 0 && typeof obj.stage_id !== "string") {
2622
2716
  errors.push({ path: "$.stage_id", message: "Must be a string if present" });
2623
2717
  }
2624
- if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1)) {
2625
- errors.push({ path: "$.attempt", message: "Must be a positive integer if present" });
2718
+ if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1 || obj.attempt > 3)) {
2719
+ errors.push({ path: "$.attempt", message: "Must be an integer from 1 through 3 if present" });
2626
2720
  }
2627
2721
  if (obj.outputs !== void 0) {
2628
2722
  checkStringArray(obj.outputs, "$.outputs", errors);
@@ -2633,20 +2727,14 @@ function validateReviewResult(input) {
2633
2727
  checkOptionalNullableString(obj.blocking_reason, "$.blocking_reason", errors);
2634
2728
  checkOptionalNullableString(obj.degradation, "$.degradation", errors);
2635
2729
  if (obj.producer !== void 0) {
2636
- if (!isPlainObject(obj.producer)) {
2637
- errors.push({ path: "$.producer", message: "Must be an object" });
2638
- } else {
2639
- const p = obj.producer;
2640
- if (typeof p.executor !== "string") {
2641
- errors.push({ path: "$.producer.executor", message: "Must be a string" });
2642
- } else if (!VALID_EXECUTORS.has(p.executor)) {
2643
- errors.push({ path: "$.producer.executor", message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(p.executor)}` });
2644
- }
2645
- if (typeof p.name !== "string") {
2646
- errors.push({ path: "$.producer.name", message: "Must be a string" });
2647
- }
2648
- checkOptionalString(p.skill, "$.producer.skill", errors);
2649
- }
2730
+ validateProducer(obj.producer, "$.producer", errors);
2731
+ }
2732
+ const successfulAcceptance = obj.status === "SUCCEEDED" && (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR");
2733
+ if (successfulAcceptance && obj.producer === void 0) {
2734
+ errors.push({ path: "$.producer", message: "Successful acceptance requires producer identity" });
2735
+ }
2736
+ if (obj.acceptance !== void 0) {
2737
+ validateAcceptance(obj.acceptance, obj.producer, "$.acceptance", errors);
2650
2738
  }
2651
2739
  if (obj.evidence !== void 0) {
2652
2740
  if (!Array.isArray(obj.evidence)) {
@@ -2672,6 +2760,17 @@ function validateReviewResult(input) {
2672
2760
  }
2673
2761
  if (obj.review !== void 0) {
2674
2762
  validateReviewData(obj.review, "$.review", errors);
2763
+ if (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR") {
2764
+ const findings = isPlainObject(obj.review) && Array.isArray(obj.review.findings) ? obj.review.findings : [];
2765
+ findings.forEach((finding, index) => {
2766
+ if (isPlainObject(finding) && finding.severity === "block" && (finding.status === void 0 || finding.status === "open")) {
2767
+ errors.push({
2768
+ path: `$.review.findings[${index}]`,
2769
+ message: `${obj.decision} cannot contain an open block finding`
2770
+ });
2771
+ }
2772
+ });
2773
+ }
2675
2774
  }
2676
2775
  if (obj.repair !== void 0) {
2677
2776
  if (!isPlainObject(obj.repair)) {
@@ -2707,6 +2806,47 @@ function checkStringArray(val, path, errors) {
2707
2806
  function isPlainObject(val) {
2708
2807
  return typeof val === "object" && val !== null && !Array.isArray(val);
2709
2808
  }
2809
+ function validateProducer(val, path, errors) {
2810
+ if (!isPlainObject(val)) {
2811
+ errors.push({ path, message: "Must be an object" });
2812
+ return;
2813
+ }
2814
+ if (typeof val.executor !== "string") {
2815
+ errors.push({ path: `${path}.executor`, message: "Must be a string" });
2816
+ } else if (!VALID_EXECUTORS.has(val.executor)) {
2817
+ errors.push({ path: `${path}.executor`, message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(val.executor)}` });
2818
+ }
2819
+ if (typeof val.name !== "string" || val.name.length === 0) {
2820
+ errors.push({ path: `${path}.name`, message: "Must be a non-empty string" });
2821
+ }
2822
+ checkOptionalString(val.skill, `${path}.skill`, errors);
2823
+ }
2824
+ function producerIdentity(val) {
2825
+ return JSON.stringify([val.executor, val.name]);
2826
+ }
2827
+ function validateAcceptance(val, resultProducer, path, errors) {
2828
+ if (!isPlainObject(val)) {
2829
+ errors.push({ path, message: "Must be an object" });
2830
+ return;
2831
+ }
2832
+ validateProducer(val.reviewer, `${path}.reviewer`, errors);
2833
+ if (!isPlainObject(val.source_result)) {
2834
+ errors.push({ path: `${path}.source_result`, message: "Must be an object" });
2835
+ return;
2836
+ }
2837
+ const source = val.source_result;
2838
+ if (typeof source.run_id !== "string" || source.run_id.length === 0) {
2839
+ errors.push({ path: `${path}.source_result.run_id`, message: "Must be a non-empty string" });
2840
+ }
2841
+ checkOptionalString(source.stage_id, `${path}.source_result.stage_id`, errors);
2842
+ validateProducer(source.producer, `${path}.source_result.producer`, errors);
2843
+ if (isPlainObject(val.reviewer) && isPlainObject(resultProducer) && producerIdentity(val.reviewer) !== producerIdentity(resultProducer)) {
2844
+ errors.push({ path: `${path}.reviewer`, message: "Acceptance reviewer must match the result producer" });
2845
+ }
2846
+ if (isPlainObject(val.reviewer) && isPlainObject(source.producer) && producerIdentity(val.reviewer) === producerIdentity(source.producer)) {
2847
+ errors.push({ path: `${path}.reviewer`, message: "Repair producer cannot accept its own result" });
2848
+ }
2849
+ }
2710
2850
  function checkOptionalString(val, path, errors) {
2711
2851
  if (val !== void 0 && typeof val !== "string") {
2712
2852
  errors.push({ path, message: "Must be a string if present" });
@@ -2875,7 +3015,7 @@ var DEFAULT_SCHEMA = {
2875
3015
  scenario: { paths: ["artifacts/scenarios/**/*.md"], displayName: "\u573A\u666F\u5267\u672C", role: "scenario", layer: "scenario", aliases: ["scenarios", "scenario-script"] },
2876
3016
  design: { paths: ["artifacts/design/**/*.md"], displayName: "\u8BBE\u8BA1\u89C4\u683C", role: "design", layer: "design", aliases: ["design-spec", "design_docs"] },
2877
3017
  test: { paths: ["heimdall/packages/**/*.test.ts"], displayName: "\u4EE3\u7801\u6CE8\u91CA\u8FFD\u6EAF", role: "context", layer: "implementation", aliases: ["code-test", "code-trace", "unit-test"] },
2878
- e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests"] },
3018
+ e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests", "tc"] },
2879
3019
  e2e_registry: { paths: ["artifacts/tests/e2e/e2e-test-registry.json"], displayName: "E2E \u6D4B\u8BD5\u6CE8\u518C\u8868", role: "context", layer: "verification", aliases: ["e2e-registry"] },
2880
3020
  "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"] },
2881
3021
  "test-strategy": { paths: ["artifacts/design/test-strategy.md"], displayName: "\u6D4B\u8BD5\u7B56\u7565", role: "context", layer: "verification", aliases: ["test_strategy"] },
@@ -2918,6 +3058,12 @@ async function loadConfig(root) {
2918
3058
  throw error;
2919
3059
  }
2920
3060
  }
3061
+ const ub = parsed.context?.universal_baseline;
3062
+ if (ub !== void 0 && typeof ub !== "boolean") {
3063
+ throw new Error(
3064
+ `Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
3065
+ );
3066
+ }
2921
3067
  return {
2922
3068
  ...DEFAULT_SCHEMA,
2923
3069
  ...parsed,
@@ -2930,7 +3076,7 @@ async function loadConfig(root) {
2930
3076
  idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
2931
3077
  };
2932
3078
  }
2933
- function buildGraph(nodes, edges, diagnostics = []) {
3079
+ function buildGraph(nodes, edges, diagnostics = [], root) {
2934
3080
  const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
2935
3081
  graphNodes.sort(compareNode);
2936
3082
  edges.sort(compareEdge);
@@ -2947,6 +3093,7 @@ function buildGraph(nodes, edges, diagnostics = []) {
2947
3093
  nodes: graphNodes,
2948
3094
  edges: dedupedEdges,
2949
3095
  generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
3096
+ ...root ? { root } : {},
2950
3097
  diagnostics: diagnostics.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line)
2951
3098
  };
2952
3099
  }
@@ -2978,7 +3125,8 @@ async function scanArtifacts(root, schema) {
2978
3125
  scanDiagnostics.push(...parsed.diagnostics);
2979
3126
  }
2980
3127
  }
2981
- const graph = buildGraph(nodes, edges, scanDiagnostics);
3128
+ const absoluteRoot = isAbsolute3(root) ? root : resolve3(root);
3129
+ const graph = buildGraph(nodes, edges, scanDiagnostics, absoluteRoot);
2982
3130
  return resolveMatrixEdges(graph);
2983
3131
  }
2984
3132
  function artifactTypeEntriesBySpecificity(schema) {
@@ -3318,23 +3466,34 @@ async function validateScenarioPrdLinkIndex(root, graph) {
3318
3466
  function validateCodeCommentTraceabilityFormat(graph) {
3319
3467
  const issues = [];
3320
3468
  for (const node of graph.nodes) {
3321
- if (node.type !== "test") {
3469
+ if (node.type !== "test" && node.type !== "implementation") {
3322
3470
  continue;
3323
3471
  }
3324
3472
  const invalidComments = node.attrs?.invalidTraceabilityComments;
3325
- if (!Array.isArray(invalidComments)) {
3326
- continue;
3473
+ if (Array.isArray(invalidComments)) {
3474
+ for (const invalid of invalidComments) {
3475
+ const line = typeof invalid?.line === "number" ? invalid.line : node.line;
3476
+ const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
3477
+ issues.push(issue(
3478
+ "CODE_COMMENT_TRACEABILITY_FORMAT",
3479
+ `${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
3480
+ node.path,
3481
+ line,
3482
+ { node: node.uid }
3483
+ ));
3484
+ }
3327
3485
  }
3328
- for (const invalid of invalidComments) {
3329
- const line = typeof invalid?.line === "number" ? invalid.line : node.line;
3330
- const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
3331
- issues.push(issue(
3332
- "CODE_COMMENT_TRACEABILITY_FORMAT",
3333
- `${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
3334
- node.path,
3335
- line,
3336
- { node: node.uid }
3337
- ));
3486
+ const deprecatedComments = node.attrs?.deprecatedTraceabilityComments;
3487
+ if (Array.isArray(deprecatedComments)) {
3488
+ for (const deprecated of deprecatedComments) {
3489
+ issues.push(issue(
3490
+ "E2E-TRACE-007",
3491
+ "@tc is deprecated; use @e2e_test instead",
3492
+ node.path,
3493
+ typeof deprecated?.line === "number" ? deprecated.line : node.line,
3494
+ { node: node.uid, severity: "warning" }
3495
+ ));
3496
+ }
3338
3497
  }
3339
3498
  }
3340
3499
  return issues;
@@ -3816,13 +3975,16 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
3816
3975
  const isTest = isTestFile(path);
3817
3976
  const nodeType = isTest ? "test" : "implementation";
3818
3977
  const edgeKind = isTest ? "verifies" : "implements";
3978
+ const attrs = {};
3979
+ if (traceabilityComments.invalid.length > 0) attrs.invalidTraceabilityComments = traceabilityComments.invalid;
3980
+ if (traceabilityComments.deprecated.length > 0) attrs.deprecatedTraceabilityComments = traceabilityComments.deprecated;
3819
3981
  const node = {
3820
3982
  type: nodeType,
3821
3983
  code: path,
3822
3984
  title: path.split("/").at(-1) ?? path,
3823
3985
  path,
3824
3986
  line: 1,
3825
- attrs: traceabilityComments.invalid.length > 0 ? { invalidTraceabilityComments: traceabilityComments.invalid } : {}
3987
+ attrs
3826
3988
  };
3827
3989
  let hasTags = false;
3828
3990
  for (const { tags, lineNumber } of traceabilityComments.canonical) {
@@ -3833,7 +3995,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
3833
3995
  }
3834
3996
  }
3835
3997
  }
3836
- if (hasTags || traceabilityComments.invalid.length > 0) {
3998
+ if (hasTags || traceabilityComments.invalid.length > 0 || traceabilityComments.deprecated.length > 0) {
3837
3999
  nodes.push(node);
3838
4000
  }
3839
4001
  return { nodes, edges };
@@ -3841,6 +4003,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
3841
4003
  function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
3842
4004
  const canonical = [];
3843
4005
  const invalid = [];
4006
+ const deprecated = [];
3844
4007
  for (const comment of scanCodeComments(raw)) {
3845
4008
  if (!containsTraceabilityTag(comment.text, schema)) {
3846
4009
  continue;
@@ -3856,6 +4019,9 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
3856
4019
  const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
3857
4020
  if (parsed.valid) {
3858
4021
  canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
4022
+ if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
4023
+ deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
4024
+ }
3859
4025
  } else {
3860
4026
  invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
3861
4027
  }
@@ -3871,11 +4037,14 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
3871
4037
  const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
3872
4038
  if (parsed.valid) {
3873
4039
  canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
4040
+ if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
4041
+ deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
4042
+ }
3874
4043
  } else {
3875
4044
  invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
3876
4045
  }
3877
4046
  }
3878
- return { canonical, invalid };
4047
+ return { canonical, invalid, deprecated };
3879
4048
  }
3880
4049
  function scanCodeComments(raw) {
3881
4050
  const comments = [];
@@ -4042,7 +4211,14 @@ function expandCodeRange(value) {
4042
4211
  return Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${String(start + index).padStart(width, "0")}`);
4043
4212
  }
4044
4213
  function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
4045
- return /@[\w][\w-]*\b/.test(value);
4214
+ const tokens = /* @__PURE__ */ new Set();
4215
+ for (const [type, definition] of Object.entries(schema.types)) {
4216
+ tokens.add(type);
4217
+ for (const alias of definition.aliases ?? []) tokens.add(alias);
4218
+ }
4219
+ if (tokens.size === 0) return false;
4220
+ const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
4221
+ return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
4046
4222
  }
4047
4223
  function parseDesign(path, raw) {
4048
4224
  const parsed = matter(raw);
@@ -5159,8 +5335,8 @@ async function validateExecutableTraceability(root) {
5159
5335
  }
5160
5336
  }
5161
5337
  const refToSource = /* @__PURE__ */ new Map();
5162
- const tcAnnotationRegex = /\/\/!?\s*@tc\s+(\S+?)\s+\[(\w+)\]/;
5163
- const tcAnnotationNoLevelRegex = /\/\/!?\s*@tc\s+(\S+)/;
5338
+ const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
5339
+ const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
5164
5340
  for (const specFile of specFiles) {
5165
5341
  const fullSpecPath = join5(root, specFile);
5166
5342
  let content;
@@ -5249,7 +5425,7 @@ async function validateExecutableTraceability(root) {
5249
5425
  return normalizedAnnFile === normalizedRefFile;
5250
5426
  }) ?? false;
5251
5427
  if (!hasAnnotationInFile) {
5252
- issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no // @tc ${tcKey} line-comment annotation`, path, line, { node: tcKey, severity: "warning" }));
5428
+ issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
5253
5429
  continue;
5254
5430
  }
5255
5431
  validFiles.push(normalizedRefFile);
@@ -5260,13 +5436,13 @@ async function validateExecutableTraceability(root) {
5260
5436
  const batch = tcKey.split(":")[0];
5261
5437
  if (!mdBatches.has(batch)) {
5262
5438
  for (const ann of annotations) {
5263
- issues.push(issue("E2E-TRACE-002", `@tc annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
5439
+ issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
5264
5440
  }
5265
5441
  continue;
5266
5442
  }
5267
5443
  if (!mdToRef.has(tcKey) && !await hasMarkdownTc(tcKey, e2eDir)) {
5268
5444
  for (const ann of annotations) {
5269
- issues.push(issue("E2E-TRACE-002", `@tc annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
5445
+ issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
5270
5446
  }
5271
5447
  }
5272
5448
  }
@@ -5288,7 +5464,7 @@ async function validateExecutableTraceability(root) {
5288
5464
  const primaryRef = refEntries[0];
5289
5465
  const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
5290
5466
  const detail = `file: MD refs=[${refEntries.map((e) => e.file).join(", ")}] vs source=${ann.file}`;
5291
- issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 @tc mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
5467
+ issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
5292
5468
  }
5293
5469
  }
5294
5470
  for (const [tcKey, { chainType, path, line }] of mdToRef) {
@@ -5454,14 +5630,14 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
5454
5630
  return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
5455
5631
  }
5456
5632
  const tcAnnotationPattern = new RegExp(
5457
- `//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
5633
+ `//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
5458
5634
  );
5459
5635
  if (!tcAnnotationPattern.test(content)) {
5460
- const noLevelPattern = new RegExp(`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\b`);
5636
+ const noLevelPattern = new RegExp(`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\b`);
5461
5637
  if (noLevelPattern.test(content)) {
5462
- return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has @tc ${tcKey} but not tagged [partial_rust]` };
5638
+ return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has E2E trace annotation ${tcKey} but not tagged [partial_rust]` };
5463
5639
  }
5464
- return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no @tc ${tcKey} annotation` };
5640
+ return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no E2E trace annotation ${tcKey}` };
5465
5641
  }
5466
5642
  }
5467
5643
  return { hasValidPartialRust: true, detail: "ok" };
@@ -5788,9 +5964,14 @@ function mergeRecord(base, override) {
5788
5964
  function mergeArtifactTypes(base, override) {
5789
5965
  const result = { ...base };
5790
5966
  for (const [type, definition] of Object.entries(override ?? {})) {
5967
+ const aliases = [
5968
+ ...base[type]?.aliases ?? [],
5969
+ ...definition.aliases ?? []
5970
+ ].filter((alias, index, all) => all.indexOf(alias) === index);
5791
5971
  result[type] = {
5792
5972
  ...base[type] ?? {},
5793
- ...definition
5973
+ ...definition,
5974
+ ...aliases.length > 0 ? { aliases } : {}
5794
5975
  };
5795
5976
  }
5796
5977
  return result;
@@ -5884,6 +6065,8 @@ var TIER_ORDER = ["baseline", "target", "direct", "matrix", "transitive"];
5884
6065
  function resolveArtifactContext(graph, opts) {
5885
6066
  const mode = opts.mode ?? "full";
5886
6067
  const maxPerCategory = opts.maxPerCategory ?? 20;
6068
+ const universalBaseline = opts.universalBaseline ?? true;
6069
+ const root = opts.root ?? graph.root;
5887
6070
  const legacyCount = [opts.feature, opts.scenario, opts.decision, opts.design, opts.e2e_test].filter(Boolean).length;
5888
6071
  if (opts.target && legacyCount > 0) {
5889
6072
  return {
@@ -6036,12 +6219,80 @@ function resolveArtifactContext(graph, opts) {
6036
6219
  return "direct";
6037
6220
  }
6038
6221
  const pathMap = /* @__PURE__ */ new Map();
6039
- for (const ap of ALWAYS_PRESENT_ITEMS) {
6040
- const existing = pathMap.get(ap.path);
6041
- if (existing) {
6042
- if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
6222
+ if (universalBaseline) {
6223
+ for (const ap of ALWAYS_PRESENT_ITEMS) {
6224
+ const existing = pathMap.get(ap.path);
6225
+ if (existing) {
6226
+ if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
6227
+ } else {
6228
+ pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
6229
+ }
6230
+ }
6231
+ if (root) {
6232
+ for (const ap of ALWAYS_PRESENT_ITEMS) {
6233
+ const fullPath = join5(root, ap.path);
6234
+ let stat;
6235
+ try {
6236
+ stat = statSync(fullPath);
6237
+ } catch {
6238
+ stat = null;
6239
+ }
6240
+ if (!stat) {
6241
+ const msg = `Required baseline artifact not found: ${ap.path}`;
6242
+ if (!missing.includes(msg)) {
6243
+ missing.push(msg);
6244
+ missingDetails.push({
6245
+ ref: ap.path,
6246
+ from: "baseline",
6247
+ kind: "missing-baseline",
6248
+ message: msg,
6249
+ suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6250
+ });
6251
+ }
6252
+ } else if (!stat.isFile()) {
6253
+ const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
6254
+ if (!missing.includes(msg)) {
6255
+ missing.push(msg);
6256
+ missingDetails.push({
6257
+ ref: ap.path,
6258
+ from: "baseline",
6259
+ kind: "missing-baseline",
6260
+ message: msg,
6261
+ suggestedAction: `\u5C06 ${ap.path} \u4ECE\u76EE\u5F55\u6539\u4E3A\u6587\u4EF6\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6262
+ });
6263
+ }
6264
+ } else {
6265
+ try {
6266
+ accessSync(fullPath, fsConstants.R_OK);
6267
+ } catch {
6268
+ const msg = `Required baseline artifact is not readable: ${ap.path}`;
6269
+ if (!missing.includes(msg)) {
6270
+ missing.push(msg);
6271
+ missingDetails.push({
6272
+ ref: ap.path,
6273
+ from: "baseline",
6274
+ kind: "missing-baseline",
6275
+ message: msg,
6276
+ suggestedAction: `\u4FEE\u590D ${ap.path} \u7684\u6587\u4EF6\u6743\u9650\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6277
+ });
6278
+ }
6279
+ }
6280
+ }
6281
+ }
6043
6282
  } else {
6044
- pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
6283
+ for (const ap of ALWAYS_PRESENT_ITEMS) {
6284
+ const msg = `Cannot verify baseline without root: ${ap.path}`;
6285
+ if (!missing.includes(msg)) {
6286
+ missing.push(msg);
6287
+ missingDetails.push({
6288
+ ref: ap.path,
6289
+ from: "baseline",
6290
+ kind: "missing-baseline",
6291
+ message: msg,
6292
+ suggestedAction: `\u4F20\u9012 root \u53C2\u6570\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
6293
+ });
6294
+ }
6295
+ }
6045
6296
  }
6046
6297
  }
6047
6298
  pathMap.set(targetNode.path, {
@@ -6142,7 +6393,8 @@ function resolveArtifactContext(graph, opts) {
6142
6393
  context,
6143
6394
  missing,
6144
6395
  missingDetails,
6145
- omitted
6396
+ omitted,
6397
+ baselinePolicy: universalBaseline
6146
6398
  };
6147
6399
  }
6148
6400
  function formatContextMarkdown(manifest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-graph",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Git-native Markdown artifact graph scanner and validator",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "@types/better-sqlite3": "^7.6.12",
44
44
  "@types/js-yaml": "^4.0.9",
45
45
  "@types/node": "^22.19.20",
46
+ "ajv": "^8.20.0",
46
47
  "tsup": "^8.4.0",
47
48
  "typescript": "^5.7.0",
48
49
  "vitest": "^3.1.0"