artifact-graph 0.4.0 → 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 +50 -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.cjs
CHANGED
|
@@ -99,6 +99,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
99
99
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
100
100
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
101
101
|
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
102
|
+
var import_node_fs3 = require("fs");
|
|
102
103
|
var import_promises5 = require("fs/promises");
|
|
103
104
|
var import_node_path6 = require("path");
|
|
104
105
|
|
|
@@ -179,13 +180,76 @@ function validatePacket(packet, schema) {
|
|
|
179
180
|
path: "target.id"
|
|
180
181
|
});
|
|
181
182
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
const isBaselineExplicitlyDisabled = packet.baselinePolicy === false;
|
|
184
|
+
const expectedPaths = new Set(ALWAYS_PRESENT_ITEMS.map((item) => item.path));
|
|
185
|
+
if (isBaselineExplicitlyDisabled) {
|
|
186
|
+
if (packet.requiredBaseline.total !== 0) {
|
|
187
|
+
issues.push({
|
|
188
|
+
severity: "error",
|
|
189
|
+
code: "PKT-004",
|
|
190
|
+
message: `baselinePolicy=false requires requiredBaseline.total=0, got ${packet.requiredBaseline.total}`,
|
|
191
|
+
path: "requiredBaseline.total"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (packet.requiredBaseline.items.length !== 0) {
|
|
195
|
+
issues.push({
|
|
196
|
+
severity: "error",
|
|
197
|
+
code: "PKT-004",
|
|
198
|
+
message: `baselinePolicy=false requires requiredBaseline.items=[], got ${packet.requiredBaseline.items.length} item(s)`,
|
|
199
|
+
path: "requiredBaseline.items"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
if (packet.requiredBaseline.total !== BASELINE_ITEMS_COUNT) {
|
|
204
|
+
issues.push({
|
|
205
|
+
severity: "error",
|
|
206
|
+
code: "PKT-004",
|
|
207
|
+
message: `requiredBaseline.total must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.total}`,
|
|
208
|
+
path: "requiredBaseline.total"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (packet.requiredBaseline.items.length !== BASELINE_ITEMS_COUNT) {
|
|
212
|
+
issues.push({
|
|
213
|
+
severity: "error",
|
|
214
|
+
code: "PKT-004",
|
|
215
|
+
message: `requiredBaseline.items.length must be ${BASELINE_ITEMS_COUNT}, got ${packet.requiredBaseline.items.length}`,
|
|
216
|
+
path: "requiredBaseline.items"
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const actualPaths = packet.requiredBaseline.items.map((item) => item.path);
|
|
220
|
+
const actualPathSet = new Set(actualPaths);
|
|
221
|
+
if (actualPathSet.size !== actualPaths.length) {
|
|
222
|
+
issues.push({
|
|
223
|
+
severity: "error",
|
|
224
|
+
code: "PKT-004",
|
|
225
|
+
message: `requiredBaseline.items contains duplicate paths (${actualPaths.length} items, ${actualPathSet.size} unique)`,
|
|
226
|
+
path: "requiredBaseline.items"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const missingPaths = [];
|
|
230
|
+
for (const ep of expectedPaths) {
|
|
231
|
+
if (!actualPathSet.has(ep)) missingPaths.push(ep);
|
|
232
|
+
}
|
|
233
|
+
if (missingPaths.length > 0) {
|
|
234
|
+
issues.push({
|
|
235
|
+
severity: "error",
|
|
236
|
+
code: "PKT-004",
|
|
237
|
+
message: `requiredBaseline.items missing expected path(s): ${missingPaths.join(", ")}`,
|
|
238
|
+
path: "requiredBaseline.items"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const extraPaths = [];
|
|
242
|
+
for (const ap of actualPathSet) {
|
|
243
|
+
if (!expectedPaths.has(ap)) extraPaths.push(ap);
|
|
244
|
+
}
|
|
245
|
+
if (extraPaths.length > 0) {
|
|
246
|
+
issues.push({
|
|
247
|
+
severity: "error",
|
|
248
|
+
code: "PKT-004",
|
|
249
|
+
message: `requiredBaseline.items contains unexpected path(s): ${extraPaths.join(", ")}`,
|
|
250
|
+
path: "requiredBaseline.items"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
189
253
|
}
|
|
190
254
|
const constraints = packet.implementationBlueprintDraft.constraints;
|
|
191
255
|
if (constraints.length !== BASELINE_CONSTRAINTS_COUNT) {
|
|
@@ -612,7 +676,10 @@ function assemblePacket(manifest, options) {
|
|
|
612
676
|
missing: [...manifest.missing],
|
|
613
677
|
missingDetails: manifest.missingDetails ? [...manifest.missingDetails] : void 0,
|
|
614
678
|
implementationBlueprintDraft: blueprintDraft,
|
|
615
|
-
validationCommands
|
|
679
|
+
validationCommands,
|
|
680
|
+
// @feature ACA17
|
|
681
|
+
// @decision D-ACA-17
|
|
682
|
+
baselinePolicy: manifest.baselinePolicy
|
|
616
683
|
};
|
|
617
684
|
return packet;
|
|
618
685
|
}
|
|
@@ -833,7 +900,9 @@ async function auditSingleTarget(target, graph, options) {
|
|
|
833
900
|
const manifest = resolveArtifactContext(graph, {
|
|
834
901
|
target: { type: target.type, id: target.id },
|
|
835
902
|
mode: options.mode,
|
|
836
|
-
maxPerCategory: options.maxPerCategory
|
|
903
|
+
maxPerCategory: options.maxPerCategory,
|
|
904
|
+
universalBaseline: options.universalBaseline,
|
|
905
|
+
root: options.root
|
|
837
906
|
});
|
|
838
907
|
const packet = assemblePacket(manifest, {
|
|
839
908
|
mode: options.mode,
|
|
@@ -970,6 +1039,7 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
970
1039
|
type: d.type,
|
|
971
1040
|
id: d.id
|
|
972
1041
|
}));
|
|
1042
|
+
const effectiveBaseline = options.universalBaseline ?? config.context?.universal_baseline;
|
|
973
1043
|
return auditPackets(root, targets, {
|
|
974
1044
|
root,
|
|
975
1045
|
outDir: options.outDir,
|
|
@@ -979,7 +1049,8 @@ async function discoverAndAuditPackets(root, options) {
|
|
|
979
1049
|
summaryOnly: options.summaryOnly,
|
|
980
1050
|
sampleTargets: options.sampleTargets,
|
|
981
1051
|
summaryDetail: options.summaryDetail,
|
|
982
|
-
schema: config
|
|
1052
|
+
schema: config,
|
|
1053
|
+
universalBaseline: effectiveBaseline
|
|
983
1054
|
}, graph);
|
|
984
1055
|
}
|
|
985
1056
|
|
|
@@ -2694,12 +2765,35 @@ var VALID_DECISIONS = /* @__PURE__ */ new Set([
|
|
|
2694
2765
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["block", "warn", "info"]);
|
|
2695
2766
|
var VALID_FINDING_STATUSES = /* @__PURE__ */ new Set(["open", "resolved", "accepted", "superseded"]);
|
|
2696
2767
|
var VALID_EXECUTORS = /* @__PURE__ */ new Set(["script", "worker", "agent", "manual", "cli"]);
|
|
2768
|
+
var TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
|
|
2769
|
+
"schema_version",
|
|
2770
|
+
"run_id",
|
|
2771
|
+
"stage_id",
|
|
2772
|
+
"attempt",
|
|
2773
|
+
"status",
|
|
2774
|
+
"decision",
|
|
2775
|
+
"summary",
|
|
2776
|
+
"outputs",
|
|
2777
|
+
"warnings",
|
|
2778
|
+
"blocking_reason",
|
|
2779
|
+
"degradation",
|
|
2780
|
+
"producer",
|
|
2781
|
+
"acceptance",
|
|
2782
|
+
"evidence",
|
|
2783
|
+
"review",
|
|
2784
|
+
"repair"
|
|
2785
|
+
]);
|
|
2697
2786
|
function validateReviewResult(input) {
|
|
2698
2787
|
const errors = [];
|
|
2699
2788
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
2700
2789
|
return [{ path: "$", message: "Root must be a non-null object" }];
|
|
2701
2790
|
}
|
|
2702
2791
|
const obj = input;
|
|
2792
|
+
for (const key of Object.keys(obj)) {
|
|
2793
|
+
if (!TOP_LEVEL_FIELDS.has(key)) {
|
|
2794
|
+
errors.push({ path: `$.${key}`, message: "Unknown top-level property" });
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2703
2797
|
if (obj.schema_version !== "1.0") {
|
|
2704
2798
|
errors.push({ path: "$.schema_version", message: `Must be "1.0", got ${JSON.stringify(obj.schema_version)}` });
|
|
2705
2799
|
}
|
|
@@ -2718,8 +2812,8 @@ function validateReviewResult(input) {
|
|
|
2718
2812
|
if (obj.stage_id !== void 0 && typeof obj.stage_id !== "string") {
|
|
2719
2813
|
errors.push({ path: "$.stage_id", message: "Must be a string if present" });
|
|
2720
2814
|
}
|
|
2721
|
-
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1)) {
|
|
2722
|
-
errors.push({ path: "$.attempt", message: "Must be
|
|
2815
|
+
if (obj.attempt !== void 0 && (!Number.isInteger(obj.attempt) || obj.attempt < 1 || obj.attempt > 3)) {
|
|
2816
|
+
errors.push({ path: "$.attempt", message: "Must be an integer from 1 through 3 if present" });
|
|
2723
2817
|
}
|
|
2724
2818
|
if (obj.outputs !== void 0) {
|
|
2725
2819
|
checkStringArray(obj.outputs, "$.outputs", errors);
|
|
@@ -2730,20 +2824,14 @@ function validateReviewResult(input) {
|
|
|
2730
2824
|
checkOptionalNullableString(obj.blocking_reason, "$.blocking_reason", errors);
|
|
2731
2825
|
checkOptionalNullableString(obj.degradation, "$.degradation", errors);
|
|
2732
2826
|
if (obj.producer !== void 0) {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2742
|
-
if (typeof p.name !== "string") {
|
|
2743
|
-
errors.push({ path: "$.producer.name", message: "Must be a string" });
|
|
2744
|
-
}
|
|
2745
|
-
checkOptionalString(p.skill, "$.producer.skill", errors);
|
|
2746
|
-
}
|
|
2827
|
+
validateProducer(obj.producer, "$.producer", errors);
|
|
2828
|
+
}
|
|
2829
|
+
const successfulAcceptance = obj.status === "SUCCEEDED" && (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR");
|
|
2830
|
+
if (successfulAcceptance && obj.producer === void 0) {
|
|
2831
|
+
errors.push({ path: "$.producer", message: "Successful acceptance requires producer identity" });
|
|
2832
|
+
}
|
|
2833
|
+
if (obj.acceptance !== void 0) {
|
|
2834
|
+
validateAcceptance(obj.acceptance, obj.producer, "$.acceptance", errors);
|
|
2747
2835
|
}
|
|
2748
2836
|
if (obj.evidence !== void 0) {
|
|
2749
2837
|
if (!Array.isArray(obj.evidence)) {
|
|
@@ -2769,6 +2857,17 @@ function validateReviewResult(input) {
|
|
|
2769
2857
|
}
|
|
2770
2858
|
if (obj.review !== void 0) {
|
|
2771
2859
|
validateReviewData(obj.review, "$.review", errors);
|
|
2860
|
+
if (obj.decision === "PASS" || obj.decision === "PASS_WITH_RESIDUAL_MINOR") {
|
|
2861
|
+
const findings = isPlainObject(obj.review) && Array.isArray(obj.review.findings) ? obj.review.findings : [];
|
|
2862
|
+
findings.forEach((finding, index) => {
|
|
2863
|
+
if (isPlainObject(finding) && finding.severity === "block" && (finding.status === void 0 || finding.status === "open")) {
|
|
2864
|
+
errors.push({
|
|
2865
|
+
path: `$.review.findings[${index}]`,
|
|
2866
|
+
message: `${obj.decision} cannot contain an open block finding`
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2772
2871
|
}
|
|
2773
2872
|
if (obj.repair !== void 0) {
|
|
2774
2873
|
if (!isPlainObject(obj.repair)) {
|
|
@@ -2804,6 +2903,47 @@ function checkStringArray(val, path, errors) {
|
|
|
2804
2903
|
function isPlainObject(val) {
|
|
2805
2904
|
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
2806
2905
|
}
|
|
2906
|
+
function validateProducer(val, path, errors) {
|
|
2907
|
+
if (!isPlainObject(val)) {
|
|
2908
|
+
errors.push({ path, message: "Must be an object" });
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
if (typeof val.executor !== "string") {
|
|
2912
|
+
errors.push({ path: `${path}.executor`, message: "Must be a string" });
|
|
2913
|
+
} else if (!VALID_EXECUTORS.has(val.executor)) {
|
|
2914
|
+
errors.push({ path: `${path}.executor`, message: `Must be one of ${[...VALID_EXECUTORS].join(", ")}; got ${JSON.stringify(val.executor)}` });
|
|
2915
|
+
}
|
|
2916
|
+
if (typeof val.name !== "string" || val.name.length === 0) {
|
|
2917
|
+
errors.push({ path: `${path}.name`, message: "Must be a non-empty string" });
|
|
2918
|
+
}
|
|
2919
|
+
checkOptionalString(val.skill, `${path}.skill`, errors);
|
|
2920
|
+
}
|
|
2921
|
+
function producerIdentity(val) {
|
|
2922
|
+
return JSON.stringify([val.executor, val.name]);
|
|
2923
|
+
}
|
|
2924
|
+
function validateAcceptance(val, resultProducer, path, errors) {
|
|
2925
|
+
if (!isPlainObject(val)) {
|
|
2926
|
+
errors.push({ path, message: "Must be an object" });
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
validateProducer(val.reviewer, `${path}.reviewer`, errors);
|
|
2930
|
+
if (!isPlainObject(val.source_result)) {
|
|
2931
|
+
errors.push({ path: `${path}.source_result`, message: "Must be an object" });
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
const source = val.source_result;
|
|
2935
|
+
if (typeof source.run_id !== "string" || source.run_id.length === 0) {
|
|
2936
|
+
errors.push({ path: `${path}.source_result.run_id`, message: "Must be a non-empty string" });
|
|
2937
|
+
}
|
|
2938
|
+
checkOptionalString(source.stage_id, `${path}.source_result.stage_id`, errors);
|
|
2939
|
+
validateProducer(source.producer, `${path}.source_result.producer`, errors);
|
|
2940
|
+
if (isPlainObject(val.reviewer) && isPlainObject(resultProducer) && producerIdentity(val.reviewer) !== producerIdentity(resultProducer)) {
|
|
2941
|
+
errors.push({ path: `${path}.reviewer`, message: "Acceptance reviewer must match the result producer" });
|
|
2942
|
+
}
|
|
2943
|
+
if (isPlainObject(val.reviewer) && isPlainObject(source.producer) && producerIdentity(val.reviewer) === producerIdentity(source.producer)) {
|
|
2944
|
+
errors.push({ path: `${path}.reviewer`, message: "Repair producer cannot accept its own result" });
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2807
2947
|
function checkOptionalString(val, path, errors) {
|
|
2808
2948
|
if (val !== void 0 && typeof val !== "string") {
|
|
2809
2949
|
errors.push({ path, message: "Must be a string if present" });
|
|
@@ -2972,7 +3112,7 @@ var DEFAULT_SCHEMA = {
|
|
|
2972
3112
|
scenario: { paths: ["artifacts/scenarios/**/*.md"], displayName: "\u573A\u666F\u5267\u672C", role: "scenario", layer: "scenario", aliases: ["scenarios", "scenario-script"] },
|
|
2973
3113
|
design: { paths: ["artifacts/design/**/*.md"], displayName: "\u8BBE\u8BA1\u89C4\u683C", role: "design", layer: "design", aliases: ["design-spec", "design_docs"] },
|
|
2974
3114
|
test: { paths: ["heimdall/packages/**/*.test.ts"], displayName: "\u4EE3\u7801\u6CE8\u91CA\u8FFD\u6EAF", role: "context", layer: "implementation", aliases: ["code-test", "code-trace", "unit-test"] },
|
|
2975
|
-
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests"] },
|
|
3115
|
+
e2e_test: { paths: ["artifacts/tests/e2e/*.md"], displayName: "E2E \u6D4B\u8BD5\u89C4\u683C", role: "e2e_test", layer: "verification", aliases: ["e2e-test", "e2e_tests", "tc"] },
|
|
2976
3116
|
e2e_registry: { paths: ["artifacts/tests/e2e/e2e-test-registry.json"], displayName: "E2E \u6D4B\u8BD5\u6CE8\u518C\u8868", role: "context", layer: "verification", aliases: ["e2e-registry"] },
|
|
2977
3117
|
"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"] },
|
|
2978
3118
|
"test-strategy": { paths: ["artifacts/design/test-strategy.md"], displayName: "\u6D4B\u8BD5\u7B56\u7565", role: "context", layer: "verification", aliases: ["test_strategy"] },
|
|
@@ -3015,6 +3155,12 @@ async function loadConfig(root) {
|
|
|
3015
3155
|
throw error;
|
|
3016
3156
|
}
|
|
3017
3157
|
}
|
|
3158
|
+
const ub = parsed.context?.universal_baseline;
|
|
3159
|
+
if (ub !== void 0 && typeof ub !== "boolean") {
|
|
3160
|
+
throw new Error(
|
|
3161
|
+
`Invalid context.universal_baseline: ${JSON.stringify(ub)}. Must be boolean (true or false).`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3018
3164
|
return {
|
|
3019
3165
|
...DEFAULT_SCHEMA,
|
|
3020
3166
|
...parsed,
|
|
@@ -3027,7 +3173,7 @@ async function loadConfig(root) {
|
|
|
3027
3173
|
idRanges: mergeRecord(DEFAULT_SCHEMA.idRanges, parsed.idRanges)
|
|
3028
3174
|
};
|
|
3029
3175
|
}
|
|
3030
|
-
function buildGraph(nodes, edges, diagnostics = []) {
|
|
3176
|
+
function buildGraph(nodes, edges, diagnostics = [], root) {
|
|
3031
3177
|
const graphNodes = nodes.map((node) => ({ ...node, uid: toUid(node.type, node.code) }));
|
|
3032
3178
|
graphNodes.sort(compareNode);
|
|
3033
3179
|
edges.sort(compareEdge);
|
|
@@ -3044,6 +3190,7 @@ function buildGraph(nodes, edges, diagnostics = []) {
|
|
|
3044
3190
|
nodes: graphNodes,
|
|
3045
3191
|
edges: dedupedEdges,
|
|
3046
3192
|
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3193
|
+
...root ? { root } : {},
|
|
3047
3194
|
diagnostics: diagnostics.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line)
|
|
3048
3195
|
};
|
|
3049
3196
|
}
|
|
@@ -3075,7 +3222,8 @@ async function scanArtifacts(root, schema) {
|
|
|
3075
3222
|
scanDiagnostics.push(...parsed.diagnostics);
|
|
3076
3223
|
}
|
|
3077
3224
|
}
|
|
3078
|
-
const
|
|
3225
|
+
const absoluteRoot = (0, import_node_path6.isAbsolute)(root) ? root : (0, import_node_path6.resolve)(root);
|
|
3226
|
+
const graph = buildGraph(nodes, edges, scanDiagnostics, absoluteRoot);
|
|
3079
3227
|
return resolveMatrixEdges(graph);
|
|
3080
3228
|
}
|
|
3081
3229
|
function artifactTypeEntriesBySpecificity(schema) {
|
|
@@ -3415,23 +3563,34 @@ async function validateScenarioPrdLinkIndex(root, graph) {
|
|
|
3415
3563
|
function validateCodeCommentTraceabilityFormat(graph) {
|
|
3416
3564
|
const issues = [];
|
|
3417
3565
|
for (const node of graph.nodes) {
|
|
3418
|
-
if (node.type !== "test") {
|
|
3566
|
+
if (node.type !== "test" && node.type !== "implementation") {
|
|
3419
3567
|
continue;
|
|
3420
3568
|
}
|
|
3421
3569
|
const invalidComments = node.attrs?.invalidTraceabilityComments;
|
|
3422
|
-
if (
|
|
3423
|
-
|
|
3570
|
+
if (Array.isArray(invalidComments)) {
|
|
3571
|
+
for (const invalid of invalidComments) {
|
|
3572
|
+
const line = typeof invalid?.line === "number" ? invalid.line : node.line;
|
|
3573
|
+
const reason = typeof invalid?.reason === "string" ? invalid.reason : "traceability tags must use standalone // comments";
|
|
3574
|
+
issues.push(issue(
|
|
3575
|
+
"CODE_COMMENT_TRACEABILITY_FORMAT",
|
|
3576
|
+
`${reason}: ${typeof invalid?.text === "string" ? invalid.text : ""}`.trim(),
|
|
3577
|
+
node.path,
|
|
3578
|
+
line,
|
|
3579
|
+
{ node: node.uid }
|
|
3580
|
+
));
|
|
3581
|
+
}
|
|
3424
3582
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
const
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3583
|
+
const deprecatedComments = node.attrs?.deprecatedTraceabilityComments;
|
|
3584
|
+
if (Array.isArray(deprecatedComments)) {
|
|
3585
|
+
for (const deprecated of deprecatedComments) {
|
|
3586
|
+
issues.push(issue(
|
|
3587
|
+
"E2E-TRACE-007",
|
|
3588
|
+
"@tc is deprecated; use @e2e_test instead",
|
|
3589
|
+
node.path,
|
|
3590
|
+
typeof deprecated?.line === "number" ? deprecated.line : node.line,
|
|
3591
|
+
{ node: node.uid, severity: "warning" }
|
|
3592
|
+
));
|
|
3593
|
+
}
|
|
3435
3594
|
}
|
|
3436
3595
|
}
|
|
3437
3596
|
return issues;
|
|
@@ -3913,13 +4072,16 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3913
4072
|
const isTest = isTestFile(path);
|
|
3914
4073
|
const nodeType = isTest ? "test" : "implementation";
|
|
3915
4074
|
const edgeKind = isTest ? "verifies" : "implements";
|
|
4075
|
+
const attrs = {};
|
|
4076
|
+
if (traceabilityComments.invalid.length > 0) attrs.invalidTraceabilityComments = traceabilityComments.invalid;
|
|
4077
|
+
if (traceabilityComments.deprecated.length > 0) attrs.deprecatedTraceabilityComments = traceabilityComments.deprecated;
|
|
3916
4078
|
const node = {
|
|
3917
4079
|
type: nodeType,
|
|
3918
4080
|
code: path,
|
|
3919
4081
|
title: path.split("/").at(-1) ?? path,
|
|
3920
4082
|
path,
|
|
3921
4083
|
line: 1,
|
|
3922
|
-
attrs
|
|
4084
|
+
attrs
|
|
3923
4085
|
};
|
|
3924
4086
|
let hasTags = false;
|
|
3925
4087
|
for (const { tags, lineNumber } of traceabilityComments.canonical) {
|
|
@@ -3930,7 +4092,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3930
4092
|
}
|
|
3931
4093
|
}
|
|
3932
4094
|
}
|
|
3933
|
-
if (hasTags || traceabilityComments.invalid.length > 0) {
|
|
4095
|
+
if (hasTags || traceabilityComments.invalid.length > 0 || traceabilityComments.deprecated.length > 0) {
|
|
3934
4096
|
nodes.push(node);
|
|
3935
4097
|
}
|
|
3936
4098
|
return { nodes, edges };
|
|
@@ -3938,6 +4100,7 @@ function parseTest(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
3938
4100
|
function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
3939
4101
|
const canonical = [];
|
|
3940
4102
|
const invalid = [];
|
|
4103
|
+
const deprecated = [];
|
|
3941
4104
|
for (const comment of scanCodeComments(raw)) {
|
|
3942
4105
|
if (!containsTraceabilityTag(comment.text, schema)) {
|
|
3943
4106
|
continue;
|
|
@@ -3953,6 +4116,9 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3953
4116
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3954
4117
|
if (parsed.valid) {
|
|
3955
4118
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4119
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4120
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4121
|
+
}
|
|
3956
4122
|
} else {
|
|
3957
4123
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3958
4124
|
}
|
|
@@ -3968,11 +4134,14 @@ function scanTraceabilityComments(raw, schema = DEFAULT_SCHEMA) {
|
|
|
3968
4134
|
const parsed = parseTraceabilityTagLine(comment.text.trim(), schema);
|
|
3969
4135
|
if (parsed.valid) {
|
|
3970
4136
|
canonical.push({ tags: parsed.tags, lineNumber: comment.lineNumber });
|
|
4137
|
+
if (/(?:^|\s)@tc(?=\s|$)/.test(comment.text.trim())) {
|
|
4138
|
+
deprecated.push({ line: comment.lineNumber, text: comment.text.trim() });
|
|
4139
|
+
}
|
|
3971
4140
|
} else {
|
|
3972
4141
|
invalid.push({ line: comment.lineNumber, text: comment.text.trim(), reason: parsed.reason });
|
|
3973
4142
|
}
|
|
3974
4143
|
}
|
|
3975
|
-
return { canonical, invalid };
|
|
4144
|
+
return { canonical, invalid, deprecated };
|
|
3976
4145
|
}
|
|
3977
4146
|
function scanCodeComments(raw) {
|
|
3978
4147
|
const comments = [];
|
|
@@ -4139,7 +4308,14 @@ function expandCodeRange(value) {
|
|
|
4139
4308
|
return Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${String(start + index).padStart(width, "0")}`);
|
|
4140
4309
|
}
|
|
4141
4310
|
function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
4142
|
-
|
|
4311
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4312
|
+
for (const [type, definition] of Object.entries(schema.types)) {
|
|
4313
|
+
tokens.add(type);
|
|
4314
|
+
for (const alias of definition.aliases ?? []) tokens.add(alias);
|
|
4315
|
+
}
|
|
4316
|
+
if (tokens.size === 0) return false;
|
|
4317
|
+
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
4318
|
+
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
4143
4319
|
}
|
|
4144
4320
|
function parseDesign(path, raw) {
|
|
4145
4321
|
const parsed = (0, import_gray_matter.default)(raw);
|
|
@@ -5256,8 +5432,8 @@ async function validateExecutableTraceability(root) {
|
|
|
5256
5432
|
}
|
|
5257
5433
|
}
|
|
5258
5434
|
const refToSource = /* @__PURE__ */ new Map();
|
|
5259
|
-
const tcAnnotationRegex = /\/\/!?\s*@tc\s+(\S+?)\s+\[(\w+)\]/;
|
|
5260
|
-
const tcAnnotationNoLevelRegex = /\/\/!?\s*@tc\s+(\S+)/;
|
|
5435
|
+
const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
|
|
5436
|
+
const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
|
|
5261
5437
|
for (const specFile of specFiles) {
|
|
5262
5438
|
const fullSpecPath = (0, import_node_path6.join)(root, specFile);
|
|
5263
5439
|
let content;
|
|
@@ -5346,7 +5522,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5346
5522
|
return normalizedAnnFile === normalizedRefFile;
|
|
5347
5523
|
}) ?? false;
|
|
5348
5524
|
if (!hasAnnotationInFile) {
|
|
5349
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no
|
|
5525
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref target ${entry.file} has no E2E trace annotation ${tcKey}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5350
5526
|
continue;
|
|
5351
5527
|
}
|
|
5352
5528
|
validFiles.push(normalizedRefFile);
|
|
@@ -5357,13 +5533,13 @@ async function validateExecutableTraceability(root) {
|
|
|
5357
5533
|
const batch = tcKey.split(":")[0];
|
|
5358
5534
|
if (!mdBatches.has(batch)) {
|
|
5359
5535
|
for (const ann of annotations) {
|
|
5360
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5536
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent E2E batch "${batch}"`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5361
5537
|
}
|
|
5362
5538
|
continue;
|
|
5363
5539
|
}
|
|
5364
5540
|
if (!mdToRef.has(tcKey) && !await hasMarkdownTc(tcKey, e2eDir)) {
|
|
5365
5541
|
for (const ann of annotations) {
|
|
5366
|
-
issues.push(issue("E2E-TRACE-002",
|
|
5542
|
+
issues.push(issue("E2E-TRACE-002", `E2E trace annotation ${tcKey} references non-existent Markdown TC`, ann.file, ann.line, { node: tcKey, severity: "warning" }));
|
|
5367
5543
|
}
|
|
5368
5544
|
}
|
|
5369
5545
|
}
|
|
@@ -5385,7 +5561,7 @@ async function validateExecutableTraceability(root) {
|
|
|
5385
5561
|
const primaryRef = refEntries[0];
|
|
5386
5562
|
const normalizedPrimary = primaryRef?.file.startsWith("heimdall/") ? primaryRef?.file : `heimdall/${primaryRef?.file ?? ""}`;
|
|
5387
5563
|
const detail = `file: MD refs=[${refEntries.map((e) => e.file).join(", ")}] vs source=${ann.file}`;
|
|
5388
|
-
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194
|
|
5564
|
+
issues.push(issue("E2E-TRACE-003", `executable_ref \u2194 E2E trace annotation mismatch for ${tcKey}: ${detail}`, path, line, { node: tcKey, severity: "warning" }));
|
|
5389
5565
|
}
|
|
5390
5566
|
}
|
|
5391
5567
|
for (const [tcKey, { chainType, path, line }] of mdToRef) {
|
|
@@ -5551,14 +5727,14 @@ async function validatePartialRustEvidence(tcFields, tcKey, root) {
|
|
|
5551
5727
|
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
5552
5728
|
}
|
|
5553
5729
|
const tcAnnotationPattern = new RegExp(
|
|
5554
|
-
`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
5730
|
+
`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\s+\\[partial_rust\\]`
|
|
5555
5731
|
);
|
|
5556
5732
|
if (!tcAnnotationPattern.test(content)) {
|
|
5557
|
-
const noLevelPattern = new RegExp(`//[/!]?\\s*@tc\\s+${escapeRegExp(tcKey)}\\b`);
|
|
5733
|
+
const noLevelPattern = new RegExp(`//[/!]?\\s*@(?:e2e_test|tc)\\s+${escapeRegExp(tcKey)}\\b`);
|
|
5558
5734
|
if (noLevelPattern.test(content)) {
|
|
5559
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has
|
|
5735
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has E2E trace annotation ${tcKey} but not tagged [partial_rust]` };
|
|
5560
5736
|
}
|
|
5561
|
-
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no
|
|
5737
|
+
return { hasValidPartialRust: false, detail: `partial_rust file ${ref.file} has no E2E trace annotation ${tcKey}` };
|
|
5562
5738
|
}
|
|
5563
5739
|
}
|
|
5564
5740
|
return { hasValidPartialRust: true, detail: "ok" };
|
|
@@ -5885,9 +6061,14 @@ function mergeRecord(base, override) {
|
|
|
5885
6061
|
function mergeArtifactTypes(base, override) {
|
|
5886
6062
|
const result = { ...base };
|
|
5887
6063
|
for (const [type, definition] of Object.entries(override ?? {})) {
|
|
6064
|
+
const aliases = [
|
|
6065
|
+
...base[type]?.aliases ?? [],
|
|
6066
|
+
...definition.aliases ?? []
|
|
6067
|
+
].filter((alias, index, all) => all.indexOf(alias) === index);
|
|
5888
6068
|
result[type] = {
|
|
5889
6069
|
...base[type] ?? {},
|
|
5890
|
-
...definition
|
|
6070
|
+
...definition,
|
|
6071
|
+
...aliases.length > 0 ? { aliases } : {}
|
|
5891
6072
|
};
|
|
5892
6073
|
}
|
|
5893
6074
|
return result;
|
|
@@ -5981,6 +6162,8 @@ var TIER_ORDER = ["baseline", "target", "direct", "matrix", "transitive"];
|
|
|
5981
6162
|
function resolveArtifactContext(graph, opts) {
|
|
5982
6163
|
const mode = opts.mode ?? "full";
|
|
5983
6164
|
const maxPerCategory = opts.maxPerCategory ?? 20;
|
|
6165
|
+
const universalBaseline = opts.universalBaseline ?? true;
|
|
6166
|
+
const root = opts.root ?? graph.root;
|
|
5984
6167
|
const legacyCount = [opts.feature, opts.scenario, opts.decision, opts.design, opts.e2e_test].filter(Boolean).length;
|
|
5985
6168
|
if (opts.target && legacyCount > 0) {
|
|
5986
6169
|
return {
|
|
@@ -6133,12 +6316,80 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6133
6316
|
return "direct";
|
|
6134
6317
|
}
|
|
6135
6318
|
const pathMap = /* @__PURE__ */ new Map();
|
|
6136
|
-
|
|
6137
|
-
const
|
|
6138
|
-
|
|
6139
|
-
if (
|
|
6319
|
+
if (universalBaseline) {
|
|
6320
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6321
|
+
const existing = pathMap.get(ap.path);
|
|
6322
|
+
if (existing) {
|
|
6323
|
+
if (!existing.reasons.includes(ap.reason)) existing.reasons.push(ap.reason);
|
|
6324
|
+
} else {
|
|
6325
|
+
pathMap.set(ap.path, { path: ap.path, reasons: [ap.reason], category: "baseline", required: true, tier: "baseline" });
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
if (root) {
|
|
6329
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6330
|
+
const fullPath = (0, import_node_path6.join)(root, ap.path);
|
|
6331
|
+
let stat;
|
|
6332
|
+
try {
|
|
6333
|
+
stat = (0, import_node_fs3.statSync)(fullPath);
|
|
6334
|
+
} catch {
|
|
6335
|
+
stat = null;
|
|
6336
|
+
}
|
|
6337
|
+
if (!stat) {
|
|
6338
|
+
const msg = `Required baseline artifact not found: ${ap.path}`;
|
|
6339
|
+
if (!missing.includes(msg)) {
|
|
6340
|
+
missing.push(msg);
|
|
6341
|
+
missingDetails.push({
|
|
6342
|
+
ref: ap.path,
|
|
6343
|
+
from: "baseline",
|
|
6344
|
+
kind: "missing-baseline",
|
|
6345
|
+
message: msg,
|
|
6346
|
+
suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6347
|
+
});
|
|
6348
|
+
}
|
|
6349
|
+
} else if (!stat.isFile()) {
|
|
6350
|
+
const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
|
|
6351
|
+
if (!missing.includes(msg)) {
|
|
6352
|
+
missing.push(msg);
|
|
6353
|
+
missingDetails.push({
|
|
6354
|
+
ref: ap.path,
|
|
6355
|
+
from: "baseline",
|
|
6356
|
+
kind: "missing-baseline",
|
|
6357
|
+
message: msg,
|
|
6358
|
+
suggestedAction: `\u5C06 ${ap.path} \u4ECE\u76EE\u5F55\u6539\u4E3A\u6587\u4EF6\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6359
|
+
});
|
|
6360
|
+
}
|
|
6361
|
+
} else {
|
|
6362
|
+
try {
|
|
6363
|
+
(0, import_node_fs3.accessSync)(fullPath, import_node_fs3.constants.R_OK);
|
|
6364
|
+
} catch {
|
|
6365
|
+
const msg = `Required baseline artifact is not readable: ${ap.path}`;
|
|
6366
|
+
if (!missing.includes(msg)) {
|
|
6367
|
+
missing.push(msg);
|
|
6368
|
+
missingDetails.push({
|
|
6369
|
+
ref: ap.path,
|
|
6370
|
+
from: "baseline",
|
|
6371
|
+
kind: "missing-baseline",
|
|
6372
|
+
message: msg,
|
|
6373
|
+
suggestedAction: `\u4FEE\u590D ${ap.path} \u7684\u6587\u4EF6\u6743\u9650\uFF0C\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6374
|
+
});
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6140
6379
|
} else {
|
|
6141
|
-
|
|
6380
|
+
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
6381
|
+
const msg = `Cannot verify baseline without root: ${ap.path}`;
|
|
6382
|
+
if (!missing.includes(msg)) {
|
|
6383
|
+
missing.push(msg);
|
|
6384
|
+
missingDetails.push({
|
|
6385
|
+
ref: ap.path,
|
|
6386
|
+
from: "baseline",
|
|
6387
|
+
kind: "missing-baseline",
|
|
6388
|
+
message: msg,
|
|
6389
|
+
suggestedAction: `\u4F20\u9012 root \u53C2\u6570\u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
6390
|
+
});
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6142
6393
|
}
|
|
6143
6394
|
}
|
|
6144
6395
|
pathMap.set(targetNode.path, {
|
|
@@ -6239,7 +6490,8 @@ function resolveArtifactContext(graph, opts) {
|
|
|
6239
6490
|
context,
|
|
6240
6491
|
missing,
|
|
6241
6492
|
missingDetails,
|
|
6242
|
-
omitted
|
|
6493
|
+
omitted,
|
|
6494
|
+
baselinePolicy: universalBaseline
|
|
6243
6495
|
};
|
|
6244
6496
|
}
|
|
6245
6497
|
function formatContextMarkdown(manifest) {
|