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/CHANGELOG.md +44 -0
- package/INSTALL.md +110 -16
- package/README.md +22 -6
- package/README.zh-CN.md +21 -6
- package/dist/cli.js +356 -77
- package/dist/index.cjs +315 -63
- package/dist/index.d.cts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +316 -64
- package/package.json +2 -1
- package/schemas/review-result.schema.json +74 -1
- package/templates/git-hooks/pre-commit.sh +18 -1
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
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
|
|
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 (
|
|
3326
|
-
|
|
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
|
-
|
|
3329
|
-
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
6040
|
-
const
|
|
6041
|
-
|
|
6042
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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"
|