qfai 0.5.0 → 0.5.2
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/README.md +3 -2
- package/assets/init/.qfai/README.md +6 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/steering/traceability.md +2 -1
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +1 -1
- package/assets/init/.qfai/specs/README.md +3 -1
- package/assets/init/.qfai/specs/spec-0001/scenario.md +2 -1
- package/assets/init/root/qfai.config.yaml +1 -1
- package/dist/cli/index.cjs +528 -207
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +533 -212
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +341 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.mjs +345 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(src_exports, {
|
|
|
35
35
|
extractAllIds: () => extractAllIds,
|
|
36
36
|
extractIds: () => extractIds,
|
|
37
37
|
extractInvalidIds: () => extractInvalidIds,
|
|
38
|
+
findConfigRoot: () => findConfigRoot,
|
|
38
39
|
formatReportJson: () => formatReportJson,
|
|
39
40
|
formatReportMarkdown: () => formatReportMarkdown,
|
|
40
41
|
getConfigPath: () => getConfigPath,
|
|
@@ -87,7 +88,7 @@ var defaultConfig = {
|
|
|
87
88
|
testFileGlobs: [],
|
|
88
89
|
testFileExcludeGlobs: [],
|
|
89
90
|
scNoTestSeverity: "error",
|
|
90
|
-
|
|
91
|
+
orphanContractsPolicy: "error",
|
|
91
92
|
unknownContractIdSeverity: "error"
|
|
92
93
|
}
|
|
93
94
|
},
|
|
@@ -98,6 +99,26 @@ var defaultConfig = {
|
|
|
98
99
|
function getConfigPath(root) {
|
|
99
100
|
return import_node_path.default.join(root, "qfai.config.yaml");
|
|
100
101
|
}
|
|
102
|
+
async function findConfigRoot(startDir) {
|
|
103
|
+
const resolvedStart = import_node_path.default.resolve(startDir);
|
|
104
|
+
let current = resolvedStart;
|
|
105
|
+
while (true) {
|
|
106
|
+
const configPath = getConfigPath(current);
|
|
107
|
+
if (await exists(configPath)) {
|
|
108
|
+
return { root: current, configPath, found: true };
|
|
109
|
+
}
|
|
110
|
+
const parent = import_node_path.default.dirname(current);
|
|
111
|
+
if (parent === current) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
current = parent;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
root: resolvedStart,
|
|
118
|
+
configPath: getConfigPath(resolvedStart),
|
|
119
|
+
found: false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
101
122
|
async function loadConfig(root) {
|
|
102
123
|
const configPath = getConfigPath(root);
|
|
103
124
|
const issues = [];
|
|
@@ -287,10 +308,10 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
287
308
|
configPath,
|
|
288
309
|
issues
|
|
289
310
|
),
|
|
290
|
-
|
|
291
|
-
traceabilityRaw?.
|
|
292
|
-
base.traceability.
|
|
293
|
-
"validation.traceability.
|
|
311
|
+
orphanContractsPolicy: readOrphanContractsPolicy(
|
|
312
|
+
traceabilityRaw?.orphanContractsPolicy,
|
|
313
|
+
base.traceability.orphanContractsPolicy,
|
|
314
|
+
"validation.traceability.orphanContractsPolicy",
|
|
294
315
|
configPath,
|
|
295
316
|
issues
|
|
296
317
|
),
|
|
@@ -386,6 +407,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
386
407
|
}
|
|
387
408
|
return fallback;
|
|
388
409
|
}
|
|
410
|
+
function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
|
|
411
|
+
if (value === "error" || value === "warning" || value === "allow") {
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
if (value !== void 0) {
|
|
415
|
+
issues.push(
|
|
416
|
+
configIssue(
|
|
417
|
+
configPath,
|
|
418
|
+
`${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
419
|
+
)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return fallback;
|
|
423
|
+
}
|
|
389
424
|
function configIssue(file, message) {
|
|
390
425
|
return {
|
|
391
426
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -401,6 +436,14 @@ function isMissingFile(error) {
|
|
|
401
436
|
}
|
|
402
437
|
return false;
|
|
403
438
|
}
|
|
439
|
+
async function exists(target) {
|
|
440
|
+
try {
|
|
441
|
+
await (0, import_promises.access)(target);
|
|
442
|
+
return true;
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
404
447
|
function formatError(error) {
|
|
405
448
|
if (error instanceof Error) {
|
|
406
449
|
return error.message;
|
|
@@ -466,7 +509,7 @@ function isValidId(value, prefix) {
|
|
|
466
509
|
|
|
467
510
|
// src/core/report.ts
|
|
468
511
|
var import_promises14 = require("fs/promises");
|
|
469
|
-
var
|
|
512
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
470
513
|
|
|
471
514
|
// src/core/contractIndex.ts
|
|
472
515
|
var import_promises5 = require("fs/promises");
|
|
@@ -489,7 +532,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
489
532
|
]);
|
|
490
533
|
async function collectFiles(root, options = {}) {
|
|
491
534
|
const entries = [];
|
|
492
|
-
if (!await
|
|
535
|
+
if (!await exists2(root)) {
|
|
493
536
|
return entries;
|
|
494
537
|
}
|
|
495
538
|
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
@@ -534,7 +577,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
534
577
|
}
|
|
535
578
|
}
|
|
536
579
|
}
|
|
537
|
-
async function
|
|
580
|
+
async function exists2(target) {
|
|
538
581
|
try {
|
|
539
582
|
await (0, import_promises2.access)(target);
|
|
540
583
|
return true;
|
|
@@ -608,13 +651,13 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
|
608
651
|
async function filterExisting(files) {
|
|
609
652
|
const existing = [];
|
|
610
653
|
for (const file of files) {
|
|
611
|
-
if (await
|
|
654
|
+
if (await exists3(file)) {
|
|
612
655
|
existing.push(file);
|
|
613
656
|
}
|
|
614
657
|
}
|
|
615
658
|
return existing;
|
|
616
659
|
}
|
|
617
|
-
async function
|
|
660
|
+
async function exists3(target) {
|
|
618
661
|
try {
|
|
619
662
|
await (0, import_promises4.access)(target);
|
|
620
663
|
return true;
|
|
@@ -674,6 +717,113 @@ function record(index, id, file) {
|
|
|
674
717
|
index.idToFiles.set(id, current);
|
|
675
718
|
}
|
|
676
719
|
|
|
720
|
+
// src/core/paths.ts
|
|
721
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
722
|
+
function toRelativePath(root, target) {
|
|
723
|
+
if (!target) {
|
|
724
|
+
return target;
|
|
725
|
+
}
|
|
726
|
+
if (!import_node_path5.default.isAbsolute(target)) {
|
|
727
|
+
return toPosixPath(target);
|
|
728
|
+
}
|
|
729
|
+
const relative = import_node_path5.default.relative(root, target);
|
|
730
|
+
if (!relative) {
|
|
731
|
+
return ".";
|
|
732
|
+
}
|
|
733
|
+
return toPosixPath(relative);
|
|
734
|
+
}
|
|
735
|
+
function toPosixPath(value) {
|
|
736
|
+
return value.replace(/\\/g, "/");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/core/normalize.ts
|
|
740
|
+
function normalizeIssuePaths(root, issues) {
|
|
741
|
+
return issues.map((issue7) => {
|
|
742
|
+
if (!issue7.file) {
|
|
743
|
+
return issue7;
|
|
744
|
+
}
|
|
745
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
746
|
+
if (normalized === issue7.file) {
|
|
747
|
+
return issue7;
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
...issue7,
|
|
751
|
+
file: normalized
|
|
752
|
+
};
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function normalizeScCoverage(root, sc) {
|
|
756
|
+
const refs = {};
|
|
757
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
758
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
...sc,
|
|
762
|
+
refs
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function normalizeValidationResult(root, result) {
|
|
766
|
+
return {
|
|
767
|
+
...result,
|
|
768
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
769
|
+
traceability: {
|
|
770
|
+
...result.traceability,
|
|
771
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/core/parse/contractRefs.ts
|
|
777
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
778
|
+
function parseContractRefs(text, options = {}) {
|
|
779
|
+
const linePattern = buildLinePattern(options);
|
|
780
|
+
const lines = [];
|
|
781
|
+
for (const match of text.matchAll(linePattern)) {
|
|
782
|
+
lines.push((match[1] ?? "").trim());
|
|
783
|
+
}
|
|
784
|
+
const ids = [];
|
|
785
|
+
const invalidTokens = [];
|
|
786
|
+
let hasNone = false;
|
|
787
|
+
for (const line of lines) {
|
|
788
|
+
if (line.length === 0) {
|
|
789
|
+
invalidTokens.push("(empty)");
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
793
|
+
for (const token of tokens) {
|
|
794
|
+
if (token.length === 0) {
|
|
795
|
+
invalidTokens.push("(empty)");
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (token === "none") {
|
|
799
|
+
hasNone = true;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
803
|
+
ids.push(token);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
invalidTokens.push(token);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
lines,
|
|
811
|
+
ids: unique2(ids),
|
|
812
|
+
invalidTokens: unique2(invalidTokens),
|
|
813
|
+
hasNone
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function buildLinePattern(options) {
|
|
817
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
818
|
+
return new RegExp(
|
|
819
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
820
|
+
"gm"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
function unique2(values) {
|
|
824
|
+
return Array.from(new Set(values));
|
|
825
|
+
}
|
|
826
|
+
|
|
677
827
|
// src/core/parse/markdown.ts
|
|
678
828
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
679
829
|
function parseHeadings(md) {
|
|
@@ -720,8 +870,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
|
720
870
|
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
721
871
|
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
722
872
|
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
723
|
-
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
724
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
725
873
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
726
874
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
727
875
|
function parseSpec(md, file) {
|
|
@@ -794,50 +942,10 @@ function parseSpec(md, file) {
|
|
|
794
942
|
}
|
|
795
943
|
return parsed;
|
|
796
944
|
}
|
|
797
|
-
function parseContractRefs(md) {
|
|
798
|
-
const lines = [];
|
|
799
|
-
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
800
|
-
lines.push((match[1] ?? "").trim());
|
|
801
|
-
}
|
|
802
|
-
const ids = [];
|
|
803
|
-
const invalidTokens = [];
|
|
804
|
-
let hasNone = false;
|
|
805
|
-
for (const line of lines) {
|
|
806
|
-
if (line.length === 0) {
|
|
807
|
-
invalidTokens.push("(empty)");
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
811
|
-
for (const token of tokens) {
|
|
812
|
-
if (token.length === 0) {
|
|
813
|
-
invalidTokens.push("(empty)");
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
if (token === "none") {
|
|
817
|
-
hasNone = true;
|
|
818
|
-
continue;
|
|
819
|
-
}
|
|
820
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
821
|
-
ids.push(token);
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
invalidTokens.push(token);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return {
|
|
828
|
-
lines,
|
|
829
|
-
ids: unique2(ids),
|
|
830
|
-
invalidTokens: unique2(invalidTokens),
|
|
831
|
-
hasNone
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
function unique2(values) {
|
|
835
|
-
return Array.from(new Set(values));
|
|
836
|
-
}
|
|
837
945
|
|
|
838
946
|
// src/core/traceability.ts
|
|
839
947
|
var import_promises6 = require("fs/promises");
|
|
840
|
-
var
|
|
948
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
841
949
|
|
|
842
950
|
// src/core/gherkin/parse.ts
|
|
843
951
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -868,9 +976,6 @@ function formatError2(error) {
|
|
|
868
976
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
869
977
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
870
978
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
871
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
872
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
873
|
-
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
874
979
|
function parseScenarioDocument(text, uri) {
|
|
875
980
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
876
981
|
if (!gherkinDocument) {
|
|
@@ -895,31 +1000,21 @@ function parseScenarioDocument(text, uri) {
|
|
|
895
1000
|
errors
|
|
896
1001
|
};
|
|
897
1002
|
}
|
|
898
|
-
function buildScenarioAtoms(document) {
|
|
1003
|
+
function buildScenarioAtoms(document, contractIds = []) {
|
|
1004
|
+
const uniqueContractIds = unique3(contractIds).sort(
|
|
1005
|
+
(a, b) => a.localeCompare(b)
|
|
1006
|
+
);
|
|
899
1007
|
return document.scenarios.map((scenario) => {
|
|
900
1008
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
901
1009
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
902
1010
|
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
903
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
904
|
-
scenario.tags.forEach((tag) => {
|
|
905
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
906
|
-
contractIds.add(tag);
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
for (const step of scenario.steps) {
|
|
910
|
-
for (const text of collectStepTexts(step)) {
|
|
911
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
912
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
913
|
-
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
1011
|
const atom = {
|
|
917
1012
|
uri: document.uri,
|
|
918
1013
|
featureName: document.featureName ?? "",
|
|
919
1014
|
scenarioName: scenario.name,
|
|
920
1015
|
kind: scenario.kind,
|
|
921
1016
|
brIds,
|
|
922
|
-
contractIds:
|
|
1017
|
+
contractIds: uniqueContractIds
|
|
923
1018
|
};
|
|
924
1019
|
if (scenario.line !== void 0) {
|
|
925
1020
|
atom.line = scenario.line;
|
|
@@ -972,23 +1067,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
972
1067
|
function collectTagNames(tags) {
|
|
973
1068
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
974
1069
|
}
|
|
975
|
-
function collectStepTexts(step) {
|
|
976
|
-
const texts = [];
|
|
977
|
-
if (step.text) {
|
|
978
|
-
texts.push(step.text);
|
|
979
|
-
}
|
|
980
|
-
if (step.docString?.content) {
|
|
981
|
-
texts.push(step.docString.content);
|
|
982
|
-
}
|
|
983
|
-
if (step.dataTable?.rows) {
|
|
984
|
-
for (const row of step.dataTable.rows) {
|
|
985
|
-
for (const cell of row.cells) {
|
|
986
|
-
texts.push(cell.value);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
return texts;
|
|
991
|
-
}
|
|
992
1070
|
function unique3(values) {
|
|
993
1071
|
return Array.from(new Set(values));
|
|
994
1072
|
}
|
|
@@ -1090,7 +1168,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1090
1168
|
};
|
|
1091
1169
|
}
|
|
1092
1170
|
const normalizedFiles = Array.from(
|
|
1093
|
-
new Set(files.map((file) =>
|
|
1171
|
+
new Set(files.map((file) => import_node_path6.default.normalize(file)))
|
|
1094
1172
|
);
|
|
1095
1173
|
for (const file of normalizedFiles) {
|
|
1096
1174
|
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
@@ -1151,11 +1229,11 @@ function formatError3(error) {
|
|
|
1151
1229
|
|
|
1152
1230
|
// src/core/version.ts
|
|
1153
1231
|
var import_promises7 = require("fs/promises");
|
|
1154
|
-
var
|
|
1232
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1155
1233
|
var import_node_url = require("url");
|
|
1156
1234
|
async function resolveToolVersion() {
|
|
1157
|
-
if ("0.5.
|
|
1158
|
-
return "0.5.
|
|
1235
|
+
if ("0.5.2".length > 0) {
|
|
1236
|
+
return "0.5.2";
|
|
1159
1237
|
}
|
|
1160
1238
|
try {
|
|
1161
1239
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1170,18 +1248,18 @@ async function resolveToolVersion() {
|
|
|
1170
1248
|
function resolvePackageJsonPath() {
|
|
1171
1249
|
const base = __filename;
|
|
1172
1250
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
1173
|
-
return
|
|
1251
|
+
return import_node_path7.default.resolve(import_node_path7.default.dirname(basePath), "../../package.json");
|
|
1174
1252
|
}
|
|
1175
1253
|
|
|
1176
1254
|
// src/core/validators/contracts.ts
|
|
1177
1255
|
var import_promises8 = require("fs/promises");
|
|
1178
|
-
var
|
|
1256
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1179
1257
|
|
|
1180
1258
|
// src/core/contracts.ts
|
|
1181
|
-
var
|
|
1259
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1182
1260
|
var import_yaml2 = require("yaml");
|
|
1183
1261
|
function parseStructuredContract(file, text) {
|
|
1184
|
-
const ext =
|
|
1262
|
+
const ext = import_node_path8.default.extname(file).toLowerCase();
|
|
1185
1263
|
if (ext === ".json") {
|
|
1186
1264
|
return JSON.parse(text);
|
|
1187
1265
|
}
|
|
@@ -1201,9 +1279,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1201
1279
|
async function validateContracts(root, config) {
|
|
1202
1280
|
const issues = [];
|
|
1203
1281
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1204
|
-
issues.push(...await validateUiContracts(
|
|
1205
|
-
issues.push(...await validateApiContracts(
|
|
1206
|
-
issues.push(...await validateDbContracts(
|
|
1282
|
+
issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
|
|
1283
|
+
issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
|
|
1284
|
+
issues.push(...await validateDbContracts(import_node_path9.default.join(contractsRoot, "db")));
|
|
1207
1285
|
const contractIndex = await buildContractIndex(root, config);
|
|
1208
1286
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1209
1287
|
return issues;
|
|
@@ -1486,7 +1564,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1486
1564
|
|
|
1487
1565
|
// src/core/validators/delta.ts
|
|
1488
1566
|
var import_promises9 = require("fs/promises");
|
|
1489
|
-
var
|
|
1567
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1490
1568
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1491
1569
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1492
1570
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1500,7 +1578,7 @@ async function validateDeltas(root, config) {
|
|
|
1500
1578
|
}
|
|
1501
1579
|
const issues = [];
|
|
1502
1580
|
for (const pack of packs) {
|
|
1503
|
-
const deltaPath =
|
|
1581
|
+
const deltaPath = import_node_path10.default.join(pack, "delta.md");
|
|
1504
1582
|
let text;
|
|
1505
1583
|
try {
|
|
1506
1584
|
text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
|
|
@@ -1576,7 +1654,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1576
1654
|
|
|
1577
1655
|
// src/core/validators/ids.ts
|
|
1578
1656
|
var import_promises10 = require("fs/promises");
|
|
1579
|
-
var
|
|
1657
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1580
1658
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1581
1659
|
async function validateDefinedIds(root, config) {
|
|
1582
1660
|
const issues = [];
|
|
@@ -1642,7 +1720,7 @@ function recordId(out, id, file) {
|
|
|
1642
1720
|
}
|
|
1643
1721
|
function formatFileList(files, root) {
|
|
1644
1722
|
return files.map((file) => {
|
|
1645
|
-
const relative =
|
|
1723
|
+
const relative = import_node_path11.default.relative(root, file);
|
|
1646
1724
|
return relative.length > 0 ? relative : file;
|
|
1647
1725
|
}).join(", ");
|
|
1648
1726
|
}
|
|
@@ -2079,7 +2157,7 @@ async function validateTraceability(root, config) {
|
|
|
2079
2157
|
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2080
2158
|
issues.push(
|
|
2081
2159
|
issue6(
|
|
2082
|
-
"QFAI-TRACE-
|
|
2160
|
+
"QFAI-TRACE-023",
|
|
2083
2161
|
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2084
2162
|
"error",
|
|
2085
2163
|
file,
|
|
@@ -2111,7 +2189,7 @@ async function validateTraceability(root, config) {
|
|
|
2111
2189
|
if (unknownContractIds.length > 0) {
|
|
2112
2190
|
issues.push(
|
|
2113
2191
|
issue6(
|
|
2114
|
-
"QFAI-TRACE-
|
|
2192
|
+
"QFAI-TRACE-024",
|
|
2115
2193
|
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2116
2194
|
", "
|
|
2117
2195
|
)}`,
|
|
@@ -2126,11 +2204,62 @@ async function validateTraceability(root, config) {
|
|
|
2126
2204
|
for (const file of scenarioFiles) {
|
|
2127
2205
|
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2128
2206
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2207
|
+
const scenarioContractRefs = parseContractRefs(text, {
|
|
2208
|
+
allowCommentPrefix: true
|
|
2209
|
+
});
|
|
2210
|
+
if (scenarioContractRefs.lines.length === 0) {
|
|
2211
|
+
issues.push(
|
|
2212
|
+
issue6(
|
|
2213
|
+
"QFAI-TRACE-031",
|
|
2214
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2215
|
+
"error",
|
|
2216
|
+
file,
|
|
2217
|
+
"traceability.scenarioContractRefRequired"
|
|
2218
|
+
)
|
|
2219
|
+
);
|
|
2220
|
+
} else {
|
|
2221
|
+
if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
2222
|
+
issues.push(
|
|
2223
|
+
issue6(
|
|
2224
|
+
"QFAI-TRACE-033",
|
|
2225
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2226
|
+
"error",
|
|
2227
|
+
file,
|
|
2228
|
+
"traceability.scenarioContractRefFormat"
|
|
2229
|
+
)
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
if (scenarioContractRefs.invalidTokens.length > 0) {
|
|
2233
|
+
issues.push(
|
|
2234
|
+
issue6(
|
|
2235
|
+
"QFAI-TRACE-032",
|
|
2236
|
+
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2237
|
+
", "
|
|
2238
|
+
)}`,
|
|
2239
|
+
"error",
|
|
2240
|
+
file,
|
|
2241
|
+
"traceability.scenarioContractRefFormat",
|
|
2242
|
+
scenarioContractRefs.invalidTokens
|
|
2243
|
+
)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2129
2247
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2130
2248
|
if (!document || errors.length > 0) {
|
|
2131
2249
|
continue;
|
|
2132
2250
|
}
|
|
2133
|
-
|
|
2251
|
+
if (document.scenarios.length !== 1) {
|
|
2252
|
+
issues.push(
|
|
2253
|
+
issue6(
|
|
2254
|
+
"QFAI-TRACE-030",
|
|
2255
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306F 1\u30D5\u30A1\u30A4\u30EB=1\u30B7\u30CA\u30EA\u30AA\u3067\u3059\u3002\u73FE\u5728: ${document.scenarios.length}\u4EF6 (file=${file})`,
|
|
2256
|
+
"error",
|
|
2257
|
+
file,
|
|
2258
|
+
"traceability.scenarioOnePerFile"
|
|
2259
|
+
)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
|
|
2134
2263
|
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2135
2264
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2136
2265
|
const atom = atoms[index];
|
|
@@ -2275,7 +2404,7 @@ async function validateTraceability(root, config) {
|
|
|
2275
2404
|
if (orphanBrIds.length > 0) {
|
|
2276
2405
|
issues.push(
|
|
2277
2406
|
issue6(
|
|
2278
|
-
"
|
|
2407
|
+
"QFAI-TRACE-009",
|
|
2279
2408
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
2280
2409
|
"error",
|
|
2281
2410
|
specsRoot,
|
|
@@ -2345,17 +2474,19 @@ async function validateTraceability(root, config) {
|
|
|
2345
2474
|
);
|
|
2346
2475
|
}
|
|
2347
2476
|
}
|
|
2348
|
-
|
|
2477
|
+
const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
|
|
2478
|
+
if (orphanPolicy !== "allow") {
|
|
2349
2479
|
if (contractIds.size > 0) {
|
|
2350
2480
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2351
2481
|
(id) => !specContractIds.has(id)
|
|
2352
2482
|
);
|
|
2353
2483
|
if (orphanContracts.length > 0) {
|
|
2484
|
+
const severity = orphanPolicy === "warning" ? "warning" : "error";
|
|
2354
2485
|
issues.push(
|
|
2355
2486
|
issue6(
|
|
2356
2487
|
"QFAI-TRACE-022",
|
|
2357
2488
|
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2358
|
-
|
|
2489
|
+
severity,
|
|
2359
2490
|
specsRoot,
|
|
2360
2491
|
"traceability.contractCoverage",
|
|
2361
2492
|
orphanContracts
|
|
@@ -2480,16 +2611,17 @@ function countIssues(issues) {
|
|
|
2480
2611
|
// src/core/report.ts
|
|
2481
2612
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2482
2613
|
async function createReportData(root, validation, configResult) {
|
|
2483
|
-
const
|
|
2614
|
+
const resolvedRoot = import_node_path12.default.resolve(root);
|
|
2615
|
+
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2484
2616
|
const config = resolved.config;
|
|
2485
2617
|
const configPath = resolved.configPath;
|
|
2486
|
-
const specsRoot = resolvePath(
|
|
2487
|
-
const contractsRoot = resolvePath(
|
|
2488
|
-
const apiRoot =
|
|
2489
|
-
const uiRoot =
|
|
2490
|
-
const dbRoot =
|
|
2491
|
-
const srcRoot = resolvePath(
|
|
2492
|
-
const testsRoot = resolvePath(
|
|
2618
|
+
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2619
|
+
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2620
|
+
const apiRoot = import_node_path12.default.join(contractsRoot, "api");
|
|
2621
|
+
const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
|
|
2622
|
+
const dbRoot = import_node_path12.default.join(contractsRoot, "db");
|
|
2623
|
+
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2624
|
+
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2493
2625
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
2494
2626
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2495
2627
|
const {
|
|
@@ -2497,15 +2629,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2497
2629
|
ui: uiFiles,
|
|
2498
2630
|
db: dbFiles
|
|
2499
2631
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2500
|
-
const contractIndex = await buildContractIndex(
|
|
2632
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2501
2633
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2502
2634
|
const specContractRefs = await collectSpecContractRefs(
|
|
2503
2635
|
specFiles,
|
|
2504
2636
|
contractIdList
|
|
2505
2637
|
);
|
|
2506
2638
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2507
|
-
for (const
|
|
2508
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2639
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2640
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2509
2641
|
}
|
|
2510
2642
|
const referencedContractCount = contractIdList.filter(
|
|
2511
2643
|
(id) => referencedContracts.has(id)
|
|
@@ -2514,8 +2646,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2514
2646
|
(id) => !referencedContracts.has(id)
|
|
2515
2647
|
).length;
|
|
2516
2648
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2517
|
-
const
|
|
2518
|
-
specContractRefs.
|
|
2649
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2650
|
+
specContractRefs.specToContracts
|
|
2519
2651
|
);
|
|
2520
2652
|
const idsByPrefix = await collectIds([
|
|
2521
2653
|
...specFiles,
|
|
@@ -2533,24 +2665,26 @@ async function createReportData(root, validation, configResult) {
|
|
|
2533
2665
|
srcRoot,
|
|
2534
2666
|
testsRoot
|
|
2535
2667
|
);
|
|
2536
|
-
const
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2668
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2669
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2670
|
+
resolvedRoot,
|
|
2671
|
+
resolvedValidationRaw
|
|
2541
2672
|
);
|
|
2542
|
-
const scCoverage =
|
|
2543
|
-
const testFiles =
|
|
2673
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2674
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2544
2675
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2545
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2546
|
-
|
|
2676
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2677
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2678
|
+
);
|
|
2547
2679
|
const version = await resolveToolVersion();
|
|
2680
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2681
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2548
2682
|
return {
|
|
2549
2683
|
tool: "qfai",
|
|
2550
2684
|
version,
|
|
2551
2685
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2552
|
-
root,
|
|
2553
|
-
configPath,
|
|
2686
|
+
root: displayRoot,
|
|
2687
|
+
configPath: displayConfigPath,
|
|
2554
2688
|
summary: {
|
|
2555
2689
|
specs: specFiles.length,
|
|
2556
2690
|
scenarios: scenarioFiles.length,
|
|
@@ -2559,7 +2693,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2559
2693
|
ui: uiFiles.length,
|
|
2560
2694
|
db: dbFiles.length
|
|
2561
2695
|
},
|
|
2562
|
-
counts:
|
|
2696
|
+
counts: normalizedValidation.counts
|
|
2563
2697
|
},
|
|
2564
2698
|
ids: {
|
|
2565
2699
|
spec: idsByPrefix.SPEC,
|
|
@@ -2584,21 +2718,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2584
2718
|
specs: {
|
|
2585
2719
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2586
2720
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2587
|
-
|
|
2721
|
+
specToContracts: specToContractsRecord
|
|
2588
2722
|
}
|
|
2589
2723
|
},
|
|
2590
|
-
issues:
|
|
2724
|
+
issues: normalizedValidation.issues
|
|
2591
2725
|
};
|
|
2592
2726
|
}
|
|
2593
2727
|
function formatReportMarkdown(data) {
|
|
2594
2728
|
const lines = [];
|
|
2595
2729
|
lines.push("# QFAI Report");
|
|
2730
|
+
lines.push("");
|
|
2596
2731
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2597
2732
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2598
2733
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2599
2734
|
lines.push(`- \u7248: ${data.version}`);
|
|
2600
2735
|
lines.push("");
|
|
2601
2736
|
lines.push("## \u6982\u8981");
|
|
2737
|
+
lines.push("");
|
|
2602
2738
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2603
2739
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2604
2740
|
lines.push(
|
|
@@ -2609,6 +2745,7 @@ function formatReportMarkdown(data) {
|
|
|
2609
2745
|
);
|
|
2610
2746
|
lines.push("");
|
|
2611
2747
|
lines.push("## ID\u96C6\u8A08");
|
|
2748
|
+
lines.push("");
|
|
2612
2749
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2613
2750
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2614
2751
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2617,12 +2754,14 @@ function formatReportMarkdown(data) {
|
|
|
2617
2754
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2618
2755
|
lines.push("");
|
|
2619
2756
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2757
|
+
lines.push("");
|
|
2620
2758
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2621
2759
|
lines.push(
|
|
2622
2760
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2623
2761
|
);
|
|
2624
2762
|
lines.push("");
|
|
2625
2763
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2764
|
+
lines.push("");
|
|
2626
2765
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2627
2766
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2628
2767
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2631,6 +2770,7 @@ function formatReportMarkdown(data) {
|
|
|
2631
2770
|
);
|
|
2632
2771
|
lines.push("");
|
|
2633
2772
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2773
|
+
lines.push("");
|
|
2634
2774
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2635
2775
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2636
2776
|
(a, b) => a.localeCompare(b)
|
|
@@ -2649,24 +2789,25 @@ function formatReportMarkdown(data) {
|
|
|
2649
2789
|
}
|
|
2650
2790
|
lines.push("");
|
|
2651
2791
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2652
|
-
|
|
2792
|
+
lines.push("");
|
|
2793
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2653
2794
|
const specIds = Object.keys(specToContracts).sort(
|
|
2654
2795
|
(a, b) => a.localeCompare(b)
|
|
2655
2796
|
);
|
|
2656
2797
|
if (specIds.length === 0) {
|
|
2657
2798
|
lines.push("- (none)");
|
|
2658
2799
|
} else {
|
|
2659
|
-
|
|
2660
|
-
const
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
}
|
|
2800
|
+
const rows = specIds.map((specId) => {
|
|
2801
|
+
const entry = specToContracts[specId];
|
|
2802
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
2803
|
+
const status = entry?.status ?? "missing";
|
|
2804
|
+
return [specId, status, contracts];
|
|
2805
|
+
});
|
|
2806
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2667
2807
|
}
|
|
2668
2808
|
lines.push("");
|
|
2669
2809
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2810
|
+
lines.push("");
|
|
2670
2811
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2671
2812
|
if (missingRefSpecs.length === 0) {
|
|
2672
2813
|
lines.push("- (none)");
|
|
@@ -2677,6 +2818,7 @@ function formatReportMarkdown(data) {
|
|
|
2677
2818
|
}
|
|
2678
2819
|
lines.push("");
|
|
2679
2820
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2821
|
+
lines.push("");
|
|
2680
2822
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2681
2823
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2682
2824
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2706,6 +2848,7 @@ function formatReportMarkdown(data) {
|
|
|
2706
2848
|
}
|
|
2707
2849
|
lines.push("");
|
|
2708
2850
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2851
|
+
lines.push("");
|
|
2709
2852
|
const scRefs = data.traceability.sc.refs;
|
|
2710
2853
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2711
2854
|
if (scIds.length === 0) {
|
|
@@ -2722,6 +2865,7 @@ function formatReportMarkdown(data) {
|
|
|
2722
2865
|
}
|
|
2723
2866
|
lines.push("");
|
|
2724
2867
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2868
|
+
lines.push("");
|
|
2725
2869
|
const specScIssues = data.issues.filter(
|
|
2726
2870
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2727
2871
|
);
|
|
@@ -2736,6 +2880,7 @@ function formatReportMarkdown(data) {
|
|
|
2736
2880
|
}
|
|
2737
2881
|
lines.push("");
|
|
2738
2882
|
lines.push("## Hotspots");
|
|
2883
|
+
lines.push("");
|
|
2739
2884
|
const hotspots = buildHotspots(data.issues);
|
|
2740
2885
|
if (hotspots.length === 0) {
|
|
2741
2886
|
lines.push("- (none)");
|
|
@@ -2748,6 +2893,7 @@ function formatReportMarkdown(data) {
|
|
|
2748
2893
|
}
|
|
2749
2894
|
lines.push("");
|
|
2750
2895
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2896
|
+
lines.push("");
|
|
2751
2897
|
const traceIssues = data.issues.filter(
|
|
2752
2898
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2753
2899
|
);
|
|
@@ -2763,6 +2909,7 @@ function formatReportMarkdown(data) {
|
|
|
2763
2909
|
}
|
|
2764
2910
|
lines.push("");
|
|
2765
2911
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
2912
|
+
lines.push("");
|
|
2766
2913
|
if (data.issues.length === 0) {
|
|
2767
2914
|
lines.push("- (none)");
|
|
2768
2915
|
} else {
|
|
@@ -2780,7 +2927,7 @@ function formatReportJson(data) {
|
|
|
2780
2927
|
return JSON.stringify(data, null, 2);
|
|
2781
2928
|
}
|
|
2782
2929
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2783
|
-
const
|
|
2930
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2784
2931
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2785
2932
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2786
2933
|
for (const contractId of contractIdList) {
|
|
@@ -2789,24 +2936,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2789
2936
|
for (const file of specFiles) {
|
|
2790
2937
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2791
2938
|
const parsed = parseSpec(text, file);
|
|
2792
|
-
const specKey = parsed.specId
|
|
2939
|
+
const specKey = parsed.specId;
|
|
2940
|
+
if (!specKey) {
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2793
2943
|
const refs = parsed.contractRefs;
|
|
2794
2944
|
if (refs.lines.length === 0) {
|
|
2795
2945
|
missingRefSpecs.add(specKey);
|
|
2946
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2796
2947
|
continue;
|
|
2797
2948
|
}
|
|
2798
|
-
const
|
|
2949
|
+
const current = specToContracts.get(specKey) ?? {
|
|
2950
|
+
status: "declared",
|
|
2951
|
+
ids: /* @__PURE__ */ new Set()
|
|
2952
|
+
};
|
|
2799
2953
|
for (const id of refs.ids) {
|
|
2800
|
-
|
|
2954
|
+
current.ids.add(id);
|
|
2801
2955
|
const specs = idToSpecs.get(id);
|
|
2802
2956
|
if (specs) {
|
|
2803
2957
|
specs.add(specKey);
|
|
2804
2958
|
}
|
|
2805
2959
|
}
|
|
2806
|
-
|
|
2960
|
+
specToContracts.set(specKey, current);
|
|
2807
2961
|
}
|
|
2808
2962
|
return {
|
|
2809
|
-
|
|
2963
|
+
specToContracts,
|
|
2810
2964
|
idToSpecs,
|
|
2811
2965
|
missingRefSpecs
|
|
2812
2966
|
};
|
|
@@ -2883,6 +3037,20 @@ function formatList(values) {
|
|
|
2883
3037
|
}
|
|
2884
3038
|
return values.join(", ");
|
|
2885
3039
|
}
|
|
3040
|
+
function formatMarkdownTable(headers, rows) {
|
|
3041
|
+
const widths = headers.map((header, index) => {
|
|
3042
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
3043
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
3044
|
+
});
|
|
3045
|
+
const formatRow = (cells) => {
|
|
3046
|
+
const padded = cells.map(
|
|
3047
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
3048
|
+
);
|
|
3049
|
+
return `| ${padded.join(" | ")} |`;
|
|
3050
|
+
};
|
|
3051
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
3052
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3053
|
+
}
|
|
2886
3054
|
function toSortedArray2(values) {
|
|
2887
3055
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2888
3056
|
}
|
|
@@ -2893,6 +3061,27 @@ function mapToSortedRecord(values) {
|
|
|
2893
3061
|
}
|
|
2894
3062
|
return record2;
|
|
2895
3063
|
}
|
|
3064
|
+
function mapToSpecContractRecord(values) {
|
|
3065
|
+
const record2 = {};
|
|
3066
|
+
for (const [key, entry] of values.entries()) {
|
|
3067
|
+
record2[key] = {
|
|
3068
|
+
status: entry.status,
|
|
3069
|
+
ids: toSortedArray2(entry.ids)
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
return record2;
|
|
3073
|
+
}
|
|
3074
|
+
function normalizeScSources(root, sources) {
|
|
3075
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3076
|
+
for (const [id, files] of sources.entries()) {
|
|
3077
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3078
|
+
for (const file of files) {
|
|
3079
|
+
mapped.add(toRelativePath(root, file));
|
|
3080
|
+
}
|
|
3081
|
+
normalized.set(id, mapped);
|
|
3082
|
+
}
|
|
3083
|
+
return normalized;
|
|
3084
|
+
}
|
|
2896
3085
|
function buildHotspots(issues) {
|
|
2897
3086
|
const map = /* @__PURE__ */ new Map();
|
|
2898
3087
|
for (const issue7 of issues) {
|
|
@@ -2921,6 +3110,7 @@ function buildHotspots(issues) {
|
|
|
2921
3110
|
extractAllIds,
|
|
2922
3111
|
extractIds,
|
|
2923
3112
|
extractInvalidIds,
|
|
3113
|
+
findConfigRoot,
|
|
2924
3114
|
formatReportJson,
|
|
2925
3115
|
formatReportMarkdown,
|
|
2926
3116
|
getConfigPath,
|