qfai 0.4.9 → 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 +10 -3
- package/assets/init/.qfai/README.md +9 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/commands/implement.md +2 -0
- package/assets/init/.qfai/promptpack/commands/plan.md +2 -0
- package/assets/init/.qfai/promptpack/commands/review.md +1 -0
- package/assets/init/.qfai/promptpack/steering/traceability.md +8 -2
- package/assets/init/.qfai/prompts/README.md +16 -0
- package/assets/init/.qfai/prompts/qfai-classify-change.md +33 -0
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +35 -0
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +36 -0
- 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 +551 -211
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +556 -216
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +364 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +368 -160
- 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.
|
|
1158
|
-
return "0.
|
|
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,12 +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(
|
|
2501
|
-
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2632
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2502
2633
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2634
|
+
const specContractRefs = await collectSpecContractRefs(
|
|
2635
|
+
specFiles,
|
|
2636
|
+
contractIdList
|
|
2637
|
+
);
|
|
2503
2638
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2504
|
-
for (const
|
|
2505
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2639
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2640
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2506
2641
|
}
|
|
2507
2642
|
const referencedContractCount = contractIdList.filter(
|
|
2508
2643
|
(id) => referencedContracts.has(id)
|
|
@@ -2511,8 +2646,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2511
2646
|
(id) => !referencedContracts.has(id)
|
|
2512
2647
|
).length;
|
|
2513
2648
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2514
|
-
const
|
|
2515
|
-
specContractRefs.
|
|
2649
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2650
|
+
specContractRefs.specToContracts
|
|
2516
2651
|
);
|
|
2517
2652
|
const idsByPrefix = await collectIds([
|
|
2518
2653
|
...specFiles,
|
|
@@ -2530,24 +2665,26 @@ async function createReportData(root, validation, configResult) {
|
|
|
2530
2665
|
srcRoot,
|
|
2531
2666
|
testsRoot
|
|
2532
2667
|
);
|
|
2533
|
-
const
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2668
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2669
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2670
|
+
resolvedRoot,
|
|
2671
|
+
resolvedValidationRaw
|
|
2538
2672
|
);
|
|
2539
|
-
const scCoverage =
|
|
2540
|
-
const testFiles =
|
|
2673
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2674
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2541
2675
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2542
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2543
|
-
|
|
2676
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2677
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2678
|
+
);
|
|
2544
2679
|
const version = await resolveToolVersion();
|
|
2680
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2681
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2545
2682
|
return {
|
|
2546
2683
|
tool: "qfai",
|
|
2547
2684
|
version,
|
|
2548
2685
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2549
|
-
root,
|
|
2550
|
-
configPath,
|
|
2686
|
+
root: displayRoot,
|
|
2687
|
+
configPath: displayConfigPath,
|
|
2551
2688
|
summary: {
|
|
2552
2689
|
specs: specFiles.length,
|
|
2553
2690
|
scenarios: scenarioFiles.length,
|
|
@@ -2556,7 +2693,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2556
2693
|
ui: uiFiles.length,
|
|
2557
2694
|
db: dbFiles.length
|
|
2558
2695
|
},
|
|
2559
|
-
counts:
|
|
2696
|
+
counts: normalizedValidation.counts
|
|
2560
2697
|
},
|
|
2561
2698
|
ids: {
|
|
2562
2699
|
spec: idsByPrefix.SPEC,
|
|
@@ -2580,21 +2717,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2580
2717
|
},
|
|
2581
2718
|
specs: {
|
|
2582
2719
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2583
|
-
|
|
2720
|
+
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2721
|
+
specToContracts: specToContractsRecord
|
|
2584
2722
|
}
|
|
2585
2723
|
},
|
|
2586
|
-
issues:
|
|
2724
|
+
issues: normalizedValidation.issues
|
|
2587
2725
|
};
|
|
2588
2726
|
}
|
|
2589
2727
|
function formatReportMarkdown(data) {
|
|
2590
2728
|
const lines = [];
|
|
2591
2729
|
lines.push("# QFAI Report");
|
|
2730
|
+
lines.push("");
|
|
2592
2731
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2593
2732
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2594
2733
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2595
2734
|
lines.push(`- \u7248: ${data.version}`);
|
|
2596
2735
|
lines.push("");
|
|
2597
2736
|
lines.push("## \u6982\u8981");
|
|
2737
|
+
lines.push("");
|
|
2598
2738
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2599
2739
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2600
2740
|
lines.push(
|
|
@@ -2605,6 +2745,7 @@ function formatReportMarkdown(data) {
|
|
|
2605
2745
|
);
|
|
2606
2746
|
lines.push("");
|
|
2607
2747
|
lines.push("## ID\u96C6\u8A08");
|
|
2748
|
+
lines.push("");
|
|
2608
2749
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2609
2750
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2610
2751
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2613,12 +2754,14 @@ function formatReportMarkdown(data) {
|
|
|
2613
2754
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2614
2755
|
lines.push("");
|
|
2615
2756
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2757
|
+
lines.push("");
|
|
2616
2758
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2617
2759
|
lines.push(
|
|
2618
2760
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2619
2761
|
);
|
|
2620
2762
|
lines.push("");
|
|
2621
2763
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2764
|
+
lines.push("");
|
|
2622
2765
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2623
2766
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2624
2767
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2627,6 +2770,7 @@ function formatReportMarkdown(data) {
|
|
|
2627
2770
|
);
|
|
2628
2771
|
lines.push("");
|
|
2629
2772
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2773
|
+
lines.push("");
|
|
2630
2774
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2631
2775
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2632
2776
|
(a, b) => a.localeCompare(b)
|
|
@@ -2645,24 +2789,36 @@ function formatReportMarkdown(data) {
|
|
|
2645
2789
|
}
|
|
2646
2790
|
lines.push("");
|
|
2647
2791
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2648
|
-
|
|
2792
|
+
lines.push("");
|
|
2793
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2649
2794
|
const specIds = Object.keys(specToContracts).sort(
|
|
2650
2795
|
(a, b) => a.localeCompare(b)
|
|
2651
2796
|
);
|
|
2652
2797
|
if (specIds.length === 0) {
|
|
2653
2798
|
lines.push("- (none)");
|
|
2654
2799
|
} else {
|
|
2655
|
-
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
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));
|
|
2807
|
+
}
|
|
2808
|
+
lines.push("");
|
|
2809
|
+
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2810
|
+
lines.push("");
|
|
2811
|
+
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2812
|
+
if (missingRefSpecs.length === 0) {
|
|
2813
|
+
lines.push("- (none)");
|
|
2814
|
+
} else {
|
|
2815
|
+
for (const specId of missingRefSpecs) {
|
|
2816
|
+
lines.push(`- ${specId}`);
|
|
2662
2817
|
}
|
|
2663
2818
|
}
|
|
2664
2819
|
lines.push("");
|
|
2665
2820
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2821
|
+
lines.push("");
|
|
2666
2822
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2667
2823
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2668
2824
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2692,6 +2848,7 @@ function formatReportMarkdown(data) {
|
|
|
2692
2848
|
}
|
|
2693
2849
|
lines.push("");
|
|
2694
2850
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2851
|
+
lines.push("");
|
|
2695
2852
|
const scRefs = data.traceability.sc.refs;
|
|
2696
2853
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2697
2854
|
if (scIds.length === 0) {
|
|
@@ -2708,6 +2865,7 @@ function formatReportMarkdown(data) {
|
|
|
2708
2865
|
}
|
|
2709
2866
|
lines.push("");
|
|
2710
2867
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2868
|
+
lines.push("");
|
|
2711
2869
|
const specScIssues = data.issues.filter(
|
|
2712
2870
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2713
2871
|
);
|
|
@@ -2722,6 +2880,7 @@ function formatReportMarkdown(data) {
|
|
|
2722
2880
|
}
|
|
2723
2881
|
lines.push("");
|
|
2724
2882
|
lines.push("## Hotspots");
|
|
2883
|
+
lines.push("");
|
|
2725
2884
|
const hotspots = buildHotspots(data.issues);
|
|
2726
2885
|
if (hotspots.length === 0) {
|
|
2727
2886
|
lines.push("- (none)");
|
|
@@ -2734,6 +2893,7 @@ function formatReportMarkdown(data) {
|
|
|
2734
2893
|
}
|
|
2735
2894
|
lines.push("");
|
|
2736
2895
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2896
|
+
lines.push("");
|
|
2737
2897
|
const traceIssues = data.issues.filter(
|
|
2738
2898
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2739
2899
|
);
|
|
@@ -2749,6 +2909,7 @@ function formatReportMarkdown(data) {
|
|
|
2749
2909
|
}
|
|
2750
2910
|
lines.push("");
|
|
2751
2911
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
2912
|
+
lines.push("");
|
|
2752
2913
|
if (data.issues.length === 0) {
|
|
2753
2914
|
lines.push("- (none)");
|
|
2754
2915
|
} else {
|
|
@@ -2765,29 +2926,41 @@ function formatReportMarkdown(data) {
|
|
|
2765
2926
|
function formatReportJson(data) {
|
|
2766
2927
|
return JSON.stringify(data, null, 2);
|
|
2767
2928
|
}
|
|
2768
|
-
async function collectSpecContractRefs(specFiles) {
|
|
2769
|
-
const
|
|
2929
|
+
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2930
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2770
2931
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2771
2932
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2933
|
+
for (const contractId of contractIdList) {
|
|
2934
|
+
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
2935
|
+
}
|
|
2772
2936
|
for (const file of specFiles) {
|
|
2773
2937
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2774
2938
|
const parsed = parseSpec(text, file);
|
|
2775
|
-
const specKey = parsed.specId
|
|
2939
|
+
const specKey = parsed.specId;
|
|
2940
|
+
if (!specKey) {
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2776
2943
|
const refs = parsed.contractRefs;
|
|
2777
2944
|
if (refs.lines.length === 0) {
|
|
2778
2945
|
missingRefSpecs.add(specKey);
|
|
2946
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2947
|
+
continue;
|
|
2779
2948
|
}
|
|
2780
|
-
const
|
|
2949
|
+
const current = specToContracts.get(specKey) ?? {
|
|
2950
|
+
status: "declared",
|
|
2951
|
+
ids: /* @__PURE__ */ new Set()
|
|
2952
|
+
};
|
|
2781
2953
|
for (const id of refs.ids) {
|
|
2782
|
-
|
|
2783
|
-
const specs = idToSpecs.get(id)
|
|
2784
|
-
specs
|
|
2785
|
-
|
|
2954
|
+
current.ids.add(id);
|
|
2955
|
+
const specs = idToSpecs.get(id);
|
|
2956
|
+
if (specs) {
|
|
2957
|
+
specs.add(specKey);
|
|
2958
|
+
}
|
|
2786
2959
|
}
|
|
2787
|
-
|
|
2960
|
+
specToContracts.set(specKey, current);
|
|
2788
2961
|
}
|
|
2789
2962
|
return {
|
|
2790
|
-
|
|
2963
|
+
specToContracts,
|
|
2791
2964
|
idToSpecs,
|
|
2792
2965
|
missingRefSpecs
|
|
2793
2966
|
};
|
|
@@ -2864,6 +3037,20 @@ function formatList(values) {
|
|
|
2864
3037
|
}
|
|
2865
3038
|
return values.join(", ");
|
|
2866
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
|
+
}
|
|
2867
3054
|
function toSortedArray2(values) {
|
|
2868
3055
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2869
3056
|
}
|
|
@@ -2874,6 +3061,27 @@ function mapToSortedRecord(values) {
|
|
|
2874
3061
|
}
|
|
2875
3062
|
return record2;
|
|
2876
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
|
+
}
|
|
2877
3085
|
function buildHotspots(issues) {
|
|
2878
3086
|
const map = /* @__PURE__ */ new Map();
|
|
2879
3087
|
for (const issue7 of issues) {
|
|
@@ -2902,6 +3110,7 @@ function buildHotspots(issues) {
|
|
|
2902
3110
|
extractAllIds,
|
|
2903
3111
|
extractIds,
|
|
2904
3112
|
extractInvalidIds,
|
|
3113
|
+
findConfigRoot,
|
|
2905
3114
|
formatReportJson,
|
|
2906
3115
|
formatReportMarkdown,
|
|
2907
3116
|
getConfigPath,
|