qfai 0.3.8 → 0.4.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/README.md +7 -0
- package/assets/init/.qfai/README.md +4 -0
- package/assets/init/.qfai/specs/README.md +1 -1
- package/assets/init/root/qfai.config.yaml +2 -0
- package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -0
- package/dist/cli/index.cjs +351 -218
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +350 -217
- package/dist/cli/index.mjs.map +1 -1
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +4 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/report.d.ts +2 -0
- package/dist/core/report.d.ts.map +1 -1
- package/dist/core/report.js +34 -0
- package/dist/core/report.js.map +1 -1
- package/dist/core/traceability.d.ts +12 -0
- package/dist/core/traceability.d.ts.map +1 -0
- package/dist/core/traceability.js +70 -0
- package/dist/core/traceability.js.map +1 -0
- package/dist/core/validators/traceability.d.ts.map +1 -1
- package/dist/core/validators/traceability.js +11 -1
- package/dist/core/validators/traceability.js.map +1 -1
- package/dist/index.cjs +344 -205
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.mjs +348 -208
- package/dist/index.mjs.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
VALIDATION_SCHEMA_VERSION: () => VALIDATION_SCHEMA_VERSION,
|
|
34
33
|
createReportData: () => createReportData,
|
|
35
34
|
defaultConfig: () => defaultConfig,
|
|
36
35
|
extractAllIds: () => extractAllIds,
|
|
@@ -85,6 +84,8 @@ var defaultConfig = {
|
|
|
85
84
|
traceability: {
|
|
86
85
|
brMustHaveSc: true,
|
|
87
86
|
scMustTouchContracts: true,
|
|
87
|
+
scMustHaveTest: true,
|
|
88
|
+
scNoTestSeverity: "error",
|
|
88
89
|
allowOrphanContracts: false,
|
|
89
90
|
unknownContractIdSeverity: "error"
|
|
90
91
|
}
|
|
@@ -264,6 +265,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
264
265
|
configPath,
|
|
265
266
|
issues
|
|
266
267
|
),
|
|
268
|
+
scMustHaveTest: readBoolean(
|
|
269
|
+
traceabilityRaw?.scMustHaveTest,
|
|
270
|
+
base.traceability.scMustHaveTest,
|
|
271
|
+
"validation.traceability.scMustHaveTest",
|
|
272
|
+
configPath,
|
|
273
|
+
issues
|
|
274
|
+
),
|
|
275
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
276
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
277
|
+
base.traceability.scNoTestSeverity,
|
|
278
|
+
"validation.traceability.scNoTestSeverity",
|
|
279
|
+
configPath,
|
|
280
|
+
issues
|
|
281
|
+
),
|
|
267
282
|
allowOrphanContracts: readBoolean(
|
|
268
283
|
traceabilityRaw?.allowOrphanContracts,
|
|
269
284
|
base.traceability.allowOrphanContracts,
|
|
@@ -442,7 +457,7 @@ function isValidId(value, prefix) {
|
|
|
442
457
|
}
|
|
443
458
|
|
|
444
459
|
// src/core/report.ts
|
|
445
|
-
var
|
|
460
|
+
var import_promises14 = require("fs/promises");
|
|
446
461
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
447
462
|
|
|
448
463
|
// src/core/discovery.ts
|
|
@@ -583,20 +598,240 @@ async function exists2(target) {
|
|
|
583
598
|
}
|
|
584
599
|
}
|
|
585
600
|
|
|
586
|
-
// src/core/
|
|
587
|
-
var
|
|
601
|
+
// src/core/traceability.ts
|
|
602
|
+
var import_promises5 = require("fs/promises");
|
|
603
|
+
|
|
604
|
+
// src/core/gherkin/parse.ts
|
|
605
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
606
|
+
var import_node_crypto = require("crypto");
|
|
607
|
+
function parseGherkin(source, uri) {
|
|
608
|
+
const errors = [];
|
|
609
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
610
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
611
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
612
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
613
|
+
try {
|
|
614
|
+
const gherkinDocument = parser.parse(source);
|
|
615
|
+
gherkinDocument.uri = uri;
|
|
616
|
+
return { gherkinDocument, errors };
|
|
617
|
+
} catch (error) {
|
|
618
|
+
errors.push(formatError2(error));
|
|
619
|
+
return { gherkinDocument: null, errors };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function formatError2(error) {
|
|
623
|
+
if (error instanceof Error) {
|
|
624
|
+
return error.message;
|
|
625
|
+
}
|
|
626
|
+
return String(error);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/core/scenarioModel.ts
|
|
630
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
631
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
632
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
633
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
634
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
635
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
636
|
+
function parseScenarioDocument(text, uri) {
|
|
637
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
638
|
+
if (!gherkinDocument) {
|
|
639
|
+
return { document: null, errors };
|
|
640
|
+
}
|
|
641
|
+
const feature = gherkinDocument.feature;
|
|
642
|
+
if (!feature) {
|
|
643
|
+
return {
|
|
644
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
645
|
+
errors
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const featureTags = collectTagNames(feature.tags);
|
|
649
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
650
|
+
return {
|
|
651
|
+
document: {
|
|
652
|
+
uri,
|
|
653
|
+
featureName: feature.name,
|
|
654
|
+
featureTags,
|
|
655
|
+
scenarios
|
|
656
|
+
},
|
|
657
|
+
errors
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function buildScenarioAtoms(document) {
|
|
661
|
+
return document.scenarios.map((scenario) => {
|
|
662
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
663
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
664
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
665
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
666
|
+
scenario.tags.forEach((tag) => {
|
|
667
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
668
|
+
contractIds.add(tag);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
for (const step of scenario.steps) {
|
|
672
|
+
for (const text of collectStepTexts(step)) {
|
|
673
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
674
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
675
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const atom = {
|
|
679
|
+
uri: document.uri,
|
|
680
|
+
featureName: document.featureName ?? "",
|
|
681
|
+
scenarioName: scenario.name,
|
|
682
|
+
kind: scenario.kind,
|
|
683
|
+
brIds,
|
|
684
|
+
contractIds: Array.from(contractIds).sort()
|
|
685
|
+
};
|
|
686
|
+
if (scenario.line !== void 0) {
|
|
687
|
+
atom.line = scenario.line;
|
|
688
|
+
}
|
|
689
|
+
if (specIds.length === 1) {
|
|
690
|
+
const specId = specIds[0];
|
|
691
|
+
if (specId) {
|
|
692
|
+
atom.specId = specId;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (scIds.length === 1) {
|
|
696
|
+
const scId = scIds[0];
|
|
697
|
+
if (scId) {
|
|
698
|
+
atom.scId = scId;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return atom;
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
705
|
+
const scenarios = [];
|
|
706
|
+
for (const child of feature.children) {
|
|
707
|
+
if (child.scenario) {
|
|
708
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
709
|
+
}
|
|
710
|
+
if (child.rule) {
|
|
711
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
712
|
+
for (const ruleChild of child.rule.children) {
|
|
713
|
+
if (ruleChild.scenario) {
|
|
714
|
+
scenarios.push(
|
|
715
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return scenarios;
|
|
722
|
+
}
|
|
723
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
724
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
725
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
726
|
+
return {
|
|
727
|
+
name: scenario.name,
|
|
728
|
+
kind,
|
|
729
|
+
line: scenario.location?.line,
|
|
730
|
+
tags,
|
|
731
|
+
steps: scenario.steps
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function collectTagNames(tags) {
|
|
735
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
736
|
+
}
|
|
737
|
+
function collectStepTexts(step) {
|
|
738
|
+
const texts = [];
|
|
739
|
+
if (step.text) {
|
|
740
|
+
texts.push(step.text);
|
|
741
|
+
}
|
|
742
|
+
if (step.docString?.content) {
|
|
743
|
+
texts.push(step.docString.content);
|
|
744
|
+
}
|
|
745
|
+
if (step.dataTable?.rows) {
|
|
746
|
+
for (const row of step.dataTable.rows) {
|
|
747
|
+
for (const cell of row.cells) {
|
|
748
|
+
texts.push(cell.value);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return texts;
|
|
753
|
+
}
|
|
754
|
+
function unique2(values) {
|
|
755
|
+
return Array.from(new Set(values));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/core/traceability.ts
|
|
759
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
760
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
761
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const file of scenarioFiles) {
|
|
763
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
764
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
765
|
+
if (!document || errors.length > 0) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
for (const scenario of document.scenarios) {
|
|
769
|
+
for (const tag of scenario.tags) {
|
|
770
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
771
|
+
scIds.add(tag);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return scIds;
|
|
777
|
+
}
|
|
778
|
+
async function collectScTestReferences(testsRoot) {
|
|
779
|
+
const refs = /* @__PURE__ */ new Map();
|
|
780
|
+
const testFiles = await collectFiles(testsRoot, {
|
|
781
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
782
|
+
});
|
|
783
|
+
for (const file of testFiles) {
|
|
784
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
785
|
+
const scIds = extractIds(text, "SC");
|
|
786
|
+
if (scIds.length === 0) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
for (const scId of scIds) {
|
|
790
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
791
|
+
current.add(file);
|
|
792
|
+
refs.set(scId, current);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return refs;
|
|
796
|
+
}
|
|
797
|
+
function buildScCoverage(scIds, refs) {
|
|
798
|
+
const sortedScIds = toSortedArray(scIds);
|
|
799
|
+
const refsRecord = {};
|
|
800
|
+
const missingIds = [];
|
|
801
|
+
let covered = 0;
|
|
802
|
+
for (const scId of sortedScIds) {
|
|
803
|
+
const files = refs.get(scId);
|
|
804
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
805
|
+
refsRecord[scId] = sortedFiles;
|
|
806
|
+
if (sortedFiles.length === 0) {
|
|
807
|
+
missingIds.push(scId);
|
|
808
|
+
} else {
|
|
809
|
+
covered += 1;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
total: sortedScIds.length,
|
|
814
|
+
covered,
|
|
815
|
+
missing: missingIds.length,
|
|
816
|
+
missingIds,
|
|
817
|
+
refs: refsRecord
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function toSortedArray(values) {
|
|
821
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
822
|
+
}
|
|
588
823
|
|
|
589
824
|
// src/core/version.ts
|
|
590
|
-
var
|
|
825
|
+
var import_promises6 = require("fs/promises");
|
|
591
826
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
592
827
|
var import_node_url = require("url");
|
|
593
828
|
async function resolveToolVersion() {
|
|
594
|
-
if ("0.
|
|
595
|
-
return "0.
|
|
829
|
+
if ("0.4.0".length > 0) {
|
|
830
|
+
return "0.4.0";
|
|
596
831
|
}
|
|
597
832
|
try {
|
|
598
833
|
const packagePath = resolvePackageJsonPath();
|
|
599
|
-
const raw = await (0,
|
|
834
|
+
const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
|
|
600
835
|
const parsed = JSON.parse(raw);
|
|
601
836
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
602
837
|
return version.length > 0 ? version : "unknown";
|
|
@@ -611,7 +846,7 @@ function resolvePackageJsonPath() {
|
|
|
611
846
|
}
|
|
612
847
|
|
|
613
848
|
// src/core/validators/contracts.ts
|
|
614
|
-
var
|
|
849
|
+
var import_promises7 = require("fs/promises");
|
|
615
850
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
616
851
|
|
|
617
852
|
// src/core/contracts.ts
|
|
@@ -689,7 +924,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
689
924
|
}
|
|
690
925
|
const issues = [];
|
|
691
926
|
for (const file of files) {
|
|
692
|
-
const text = await (0,
|
|
927
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
693
928
|
const invalidIds = extractInvalidIds(text, [
|
|
694
929
|
"SPEC",
|
|
695
930
|
"BR",
|
|
@@ -718,7 +953,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
718
953
|
issues.push(
|
|
719
954
|
issue(
|
|
720
955
|
"QFAI-CONTRACT-001",
|
|
721
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
956
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
|
|
722
957
|
"error",
|
|
723
958
|
file,
|
|
724
959
|
"contracts.ui.parse"
|
|
@@ -756,7 +991,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
756
991
|
}
|
|
757
992
|
const issues = [];
|
|
758
993
|
for (const file of files) {
|
|
759
|
-
const text = await (0,
|
|
994
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
760
995
|
const invalidIds = extractInvalidIds(text, [
|
|
761
996
|
"SPEC",
|
|
762
997
|
"BR",
|
|
@@ -785,7 +1020,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
785
1020
|
issues.push(
|
|
786
1021
|
issue(
|
|
787
1022
|
"QFAI-CONTRACT-001",
|
|
788
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1023
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
|
|
789
1024
|
"error",
|
|
790
1025
|
file,
|
|
791
1026
|
"contracts.api.parse"
|
|
@@ -834,7 +1069,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
834
1069
|
}
|
|
835
1070
|
const issues = [];
|
|
836
1071
|
for (const file of files) {
|
|
837
|
-
const text = await (0,
|
|
1072
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
838
1073
|
const invalidIds = extractInvalidIds(text, [
|
|
839
1074
|
"SPEC",
|
|
840
1075
|
"BR",
|
|
@@ -880,7 +1115,7 @@ function lintSql(text, file) {
|
|
|
880
1115
|
function hasOpenApi(doc) {
|
|
881
1116
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
882
1117
|
}
|
|
883
|
-
function
|
|
1118
|
+
function formatError3(error) {
|
|
884
1119
|
if (error instanceof Error) {
|
|
885
1120
|
return error.message;
|
|
886
1121
|
}
|
|
@@ -905,7 +1140,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
905
1140
|
}
|
|
906
1141
|
|
|
907
1142
|
// src/core/validators/delta.ts
|
|
908
|
-
var
|
|
1143
|
+
var import_promises8 = require("fs/promises");
|
|
909
1144
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
910
1145
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
911
1146
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -923,7 +1158,7 @@ async function validateDeltas(root, config) {
|
|
|
923
1158
|
const deltaPath = import_node_path7.default.join(pack, "delta.md");
|
|
924
1159
|
let text;
|
|
925
1160
|
try {
|
|
926
|
-
text = await (0,
|
|
1161
|
+
text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
|
|
927
1162
|
} catch (error) {
|
|
928
1163
|
if (isMissingFileError2(error)) {
|
|
929
1164
|
issues.push(
|
|
@@ -995,11 +1230,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
995
1230
|
}
|
|
996
1231
|
|
|
997
1232
|
// src/core/validators/ids.ts
|
|
998
|
-
var
|
|
1233
|
+
var import_promises10 = require("fs/promises");
|
|
999
1234
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
1000
1235
|
|
|
1001
1236
|
// src/core/contractIndex.ts
|
|
1002
|
-
var
|
|
1237
|
+
var import_promises9 = require("fs/promises");
|
|
1003
1238
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
1004
1239
|
async function buildContractIndex(root, config) {
|
|
1005
1240
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -1024,7 +1259,7 @@ async function buildContractIndex(root, config) {
|
|
|
1024
1259
|
}
|
|
1025
1260
|
async function indexUiContracts(files, index) {
|
|
1026
1261
|
for (const file of files) {
|
|
1027
|
-
const text = await (0,
|
|
1262
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1028
1263
|
try {
|
|
1029
1264
|
const doc = parseStructuredContract(file, text);
|
|
1030
1265
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1036,7 +1271,7 @@ async function indexUiContracts(files, index) {
|
|
|
1036
1271
|
}
|
|
1037
1272
|
async function indexApiContracts(files, index) {
|
|
1038
1273
|
for (const file of files) {
|
|
1039
|
-
const text = await (0,
|
|
1274
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1040
1275
|
try {
|
|
1041
1276
|
const doc = parseStructuredContract(file, text);
|
|
1042
1277
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1048,7 +1283,7 @@ async function indexApiContracts(files, index) {
|
|
|
1048
1283
|
}
|
|
1049
1284
|
async function indexDataContracts(files, index) {
|
|
1050
1285
|
for (const file of files) {
|
|
1051
|
-
const text = await (0,
|
|
1286
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1052
1287
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1053
1288
|
}
|
|
1054
1289
|
}
|
|
@@ -1177,162 +1412,8 @@ function parseSpec(md, file) {
|
|
|
1177
1412
|
return parsed;
|
|
1178
1413
|
}
|
|
1179
1414
|
|
|
1180
|
-
// src/core/gherkin/parse.ts
|
|
1181
|
-
var import_gherkin = require("@cucumber/gherkin");
|
|
1182
|
-
var import_node_crypto = require("crypto");
|
|
1183
|
-
function parseGherkin(source, uri) {
|
|
1184
|
-
const errors = [];
|
|
1185
|
-
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1186
|
-
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1187
|
-
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1188
|
-
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1189
|
-
try {
|
|
1190
|
-
const gherkinDocument = parser.parse(source);
|
|
1191
|
-
gherkinDocument.uri = uri;
|
|
1192
|
-
return { gherkinDocument, errors };
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
errors.push(formatError3(error));
|
|
1195
|
-
return { gherkinDocument: null, errors };
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
function formatError3(error) {
|
|
1199
|
-
if (error instanceof Error) {
|
|
1200
|
-
return error.message;
|
|
1201
|
-
}
|
|
1202
|
-
return String(error);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/core/scenarioModel.ts
|
|
1206
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1207
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1208
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1209
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1210
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1211
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1212
|
-
function parseScenarioDocument(text, uri) {
|
|
1213
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1214
|
-
if (!gherkinDocument) {
|
|
1215
|
-
return { document: null, errors };
|
|
1216
|
-
}
|
|
1217
|
-
const feature = gherkinDocument.feature;
|
|
1218
|
-
if (!feature) {
|
|
1219
|
-
return {
|
|
1220
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1221
|
-
errors
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1225
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1226
|
-
return {
|
|
1227
|
-
document: {
|
|
1228
|
-
uri,
|
|
1229
|
-
featureName: feature.name,
|
|
1230
|
-
featureTags,
|
|
1231
|
-
scenarios
|
|
1232
|
-
},
|
|
1233
|
-
errors
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
function buildScenarioAtoms(document) {
|
|
1237
|
-
return document.scenarios.map((scenario) => {
|
|
1238
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1239
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1240
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1241
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1242
|
-
scenario.tags.forEach((tag) => {
|
|
1243
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1244
|
-
contractIds.add(tag);
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
for (const step of scenario.steps) {
|
|
1248
|
-
for (const text of collectStepTexts(step)) {
|
|
1249
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1250
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1251
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
const atom = {
|
|
1255
|
-
uri: document.uri,
|
|
1256
|
-
featureName: document.featureName ?? "",
|
|
1257
|
-
scenarioName: scenario.name,
|
|
1258
|
-
kind: scenario.kind,
|
|
1259
|
-
brIds,
|
|
1260
|
-
contractIds: Array.from(contractIds).sort()
|
|
1261
|
-
};
|
|
1262
|
-
if (scenario.line !== void 0) {
|
|
1263
|
-
atom.line = scenario.line;
|
|
1264
|
-
}
|
|
1265
|
-
if (specIds.length === 1) {
|
|
1266
|
-
const specId = specIds[0];
|
|
1267
|
-
if (specId) {
|
|
1268
|
-
atom.specId = specId;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
if (scIds.length === 1) {
|
|
1272
|
-
const scId = scIds[0];
|
|
1273
|
-
if (scId) {
|
|
1274
|
-
atom.scId = scId;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
return atom;
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
function collectScenarioNodes(feature, featureTags) {
|
|
1281
|
-
const scenarios = [];
|
|
1282
|
-
for (const child of feature.children) {
|
|
1283
|
-
if (child.scenario) {
|
|
1284
|
-
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1285
|
-
}
|
|
1286
|
-
if (child.rule) {
|
|
1287
|
-
const ruleTags = collectTagNames(child.rule.tags);
|
|
1288
|
-
for (const ruleChild of child.rule.children) {
|
|
1289
|
-
if (ruleChild.scenario) {
|
|
1290
|
-
scenarios.push(
|
|
1291
|
-
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
return scenarios;
|
|
1298
|
-
}
|
|
1299
|
-
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1300
|
-
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1301
|
-
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1302
|
-
return {
|
|
1303
|
-
name: scenario.name,
|
|
1304
|
-
kind,
|
|
1305
|
-
line: scenario.location?.line,
|
|
1306
|
-
tags,
|
|
1307
|
-
steps: scenario.steps
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
function collectTagNames(tags) {
|
|
1311
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1312
|
-
}
|
|
1313
|
-
function collectStepTexts(step) {
|
|
1314
|
-
const texts = [];
|
|
1315
|
-
if (step.text) {
|
|
1316
|
-
texts.push(step.text);
|
|
1317
|
-
}
|
|
1318
|
-
if (step.docString?.content) {
|
|
1319
|
-
texts.push(step.docString.content);
|
|
1320
|
-
}
|
|
1321
|
-
if (step.dataTable?.rows) {
|
|
1322
|
-
for (const row of step.dataTable.rows) {
|
|
1323
|
-
for (const cell of row.cells) {
|
|
1324
|
-
texts.push(cell.value);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
return texts;
|
|
1329
|
-
}
|
|
1330
|
-
function unique2(values) {
|
|
1331
|
-
return Array.from(new Set(values));
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
1415
|
// src/core/validators/ids.ts
|
|
1335
|
-
var
|
|
1416
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1336
1417
|
async function validateDefinedIds(root, config) {
|
|
1337
1418
|
const issues = [];
|
|
1338
1419
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1366,7 +1447,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1366
1447
|
}
|
|
1367
1448
|
async function collectSpecDefinitionIds(files, out) {
|
|
1368
1449
|
for (const file of files) {
|
|
1369
|
-
const text = await (0,
|
|
1450
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1370
1451
|
const parsed = parseSpec(text, file);
|
|
1371
1452
|
if (parsed.specId) {
|
|
1372
1453
|
recordId(out, parsed.specId, file);
|
|
@@ -1376,14 +1457,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1376
1457
|
}
|
|
1377
1458
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1378
1459
|
for (const file of files) {
|
|
1379
|
-
const text = await (0,
|
|
1460
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1380
1461
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1381
1462
|
if (!document || errors.length > 0) {
|
|
1382
1463
|
continue;
|
|
1383
1464
|
}
|
|
1384
1465
|
for (const scenario of document.scenarios) {
|
|
1385
1466
|
for (const tag of scenario.tags) {
|
|
1386
|
-
if (
|
|
1467
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1387
1468
|
recordId(out, tag, file);
|
|
1388
1469
|
}
|
|
1389
1470
|
}
|
|
@@ -1420,11 +1501,11 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1420
1501
|
}
|
|
1421
1502
|
|
|
1422
1503
|
// src/core/validators/scenario.ts
|
|
1423
|
-
var
|
|
1504
|
+
var import_promises11 = require("fs/promises");
|
|
1424
1505
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1425
1506
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1426
1507
|
var THEN_PATTERN = /\bThen\b/;
|
|
1427
|
-
var
|
|
1508
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1428
1509
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1429
1510
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1430
1511
|
async function validateScenarios(root, config) {
|
|
@@ -1447,7 +1528,7 @@ async function validateScenarios(root, config) {
|
|
|
1447
1528
|
for (const entry of entries) {
|
|
1448
1529
|
let text;
|
|
1449
1530
|
try {
|
|
1450
|
-
text = await (0,
|
|
1531
|
+
text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
|
|
1451
1532
|
} catch (error) {
|
|
1452
1533
|
if (isMissingFileError3(error)) {
|
|
1453
1534
|
issues.push(
|
|
@@ -1544,6 +1625,17 @@ function validateScenarioContent(text, file) {
|
|
|
1544
1625
|
)
|
|
1545
1626
|
);
|
|
1546
1627
|
}
|
|
1628
|
+
if (document.scenarios.length > 1) {
|
|
1629
|
+
issues.push(
|
|
1630
|
+
issue4(
|
|
1631
|
+
"QFAI-SC-011",
|
|
1632
|
+
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1633
|
+
"error",
|
|
1634
|
+
file,
|
|
1635
|
+
"scenario.single"
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1547
1639
|
for (const scenario of document.scenarios) {
|
|
1548
1640
|
if (scenario.tags.length === 0) {
|
|
1549
1641
|
issues.push(
|
|
@@ -1558,7 +1650,7 @@ function validateScenarioContent(text, file) {
|
|
|
1558
1650
|
continue;
|
|
1559
1651
|
}
|
|
1560
1652
|
const missingTags = [];
|
|
1561
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1653
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1562
1654
|
if (scTags.length === 0) {
|
|
1563
1655
|
missingTags.push("SC(0\u4EF6)");
|
|
1564
1656
|
} else if (scTags.length > 1) {
|
|
@@ -1633,7 +1725,7 @@ function isMissingFileError3(error) {
|
|
|
1633
1725
|
}
|
|
1634
1726
|
|
|
1635
1727
|
// src/core/validators/spec.ts
|
|
1636
|
-
var
|
|
1728
|
+
var import_promises12 = require("fs/promises");
|
|
1637
1729
|
async function validateSpecs(root, config) {
|
|
1638
1730
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1639
1731
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1654,7 +1746,7 @@ async function validateSpecs(root, config) {
|
|
|
1654
1746
|
for (const entry of entries) {
|
|
1655
1747
|
let text;
|
|
1656
1748
|
try {
|
|
1657
|
-
text = await (0,
|
|
1749
|
+
text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
|
|
1658
1750
|
} catch (error) {
|
|
1659
1751
|
if (isMissingFileError4(error)) {
|
|
1660
1752
|
issues.push(
|
|
@@ -1803,8 +1895,8 @@ function isMissingFileError4(error) {
|
|
|
1803
1895
|
}
|
|
1804
1896
|
|
|
1805
1897
|
// src/core/validators/traceability.ts
|
|
1806
|
-
var
|
|
1807
|
-
var
|
|
1898
|
+
var import_promises13 = require("fs/promises");
|
|
1899
|
+
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
1808
1900
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1809
1901
|
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1810
1902
|
async function validateTraceability(root, config) {
|
|
@@ -1825,7 +1917,7 @@ async function validateTraceability(root, config) {
|
|
|
1825
1917
|
const contractIndex = await buildContractIndex(root, config);
|
|
1826
1918
|
const contractIds = contractIndex.ids;
|
|
1827
1919
|
for (const file of specFiles) {
|
|
1828
|
-
const text = await (0,
|
|
1920
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1829
1921
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1830
1922
|
const parsed = parseSpec(text, file);
|
|
1831
1923
|
if (parsed.specId) {
|
|
@@ -1862,7 +1954,7 @@ async function validateTraceability(root, config) {
|
|
|
1862
1954
|
}
|
|
1863
1955
|
}
|
|
1864
1956
|
for (const file of scenarioFiles) {
|
|
1865
|
-
const text = await (0,
|
|
1957
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1866
1958
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1867
1959
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1868
1960
|
if (!document || errors.length > 0) {
|
|
@@ -1876,7 +1968,7 @@ async function validateTraceability(root, config) {
|
|
|
1876
1968
|
}
|
|
1877
1969
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1878
1970
|
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1879
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1971
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
|
|
1880
1972
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1881
1973
|
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1882
1974
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
@@ -2004,6 +2096,25 @@ async function validateTraceability(root, config) {
|
|
|
2004
2096
|
);
|
|
2005
2097
|
}
|
|
2006
2098
|
}
|
|
2099
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2100
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2101
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2102
|
+
const refs = scTestRefs.get(id);
|
|
2103
|
+
return !refs || refs.size === 0;
|
|
2104
|
+
});
|
|
2105
|
+
if (scWithoutTests.length > 0) {
|
|
2106
|
+
issues.push(
|
|
2107
|
+
issue6(
|
|
2108
|
+
"QFAI-TRACE-010",
|
|
2109
|
+
`SC \u304C tests \u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(", ")}\u3002tests/ \u914D\u4E0B\u306E\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\uFF08.ts/.tsx/.js/.jsx\uFF09\u306B SC ID \u3092\u30B3\u30E1\u30F3\u30C8\u307E\u305F\u306F\u30B3\u30FC\u30C9\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2110
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2111
|
+
testsRoot,
|
|
2112
|
+
"traceability.scMustHaveTest",
|
|
2113
|
+
scWithoutTests
|
|
2114
|
+
)
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2007
2118
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2008
2119
|
if (contractIds.size > 0) {
|
|
2009
2120
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -2052,7 +2163,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2052
2163
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2053
2164
|
let found = false;
|
|
2054
2165
|
for (const file of targetFiles) {
|
|
2055
|
-
const text = await (0,
|
|
2166
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2056
2167
|
if (pattern.test(text)) {
|
|
2057
2168
|
found = true;
|
|
2058
2169
|
break;
|
|
@@ -2108,7 +2219,6 @@ async function validateProject(root, configResult) {
|
|
|
2108
2219
|
];
|
|
2109
2220
|
const toolVersion = await resolveToolVersion();
|
|
2110
2221
|
return {
|
|
2111
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2112
2222
|
toolVersion,
|
|
2113
2223
|
issues,
|
|
2114
2224
|
counts: countIssues(issues)
|
|
@@ -2160,6 +2270,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2160
2270
|
srcRoot,
|
|
2161
2271
|
testsRoot
|
|
2162
2272
|
);
|
|
2273
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2274
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2275
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2163
2276
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2164
2277
|
const version = await resolveToolVersion();
|
|
2165
2278
|
return {
|
|
@@ -2188,7 +2301,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2188
2301
|
},
|
|
2189
2302
|
traceability: {
|
|
2190
2303
|
upstreamIdsFound: upstreamIds.size,
|
|
2191
|
-
referencedInCodeOrTests: traceability
|
|
2304
|
+
referencedInCodeOrTests: traceability,
|
|
2305
|
+
sc: scCoverage
|
|
2192
2306
|
},
|
|
2193
2307
|
issues: resolvedValidation.issues
|
|
2194
2308
|
};
|
|
@@ -2225,6 +2339,32 @@ function formatReportMarkdown(data) {
|
|
|
2225
2339
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2226
2340
|
);
|
|
2227
2341
|
lines.push("");
|
|
2342
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2343
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2344
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2345
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2346
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2347
|
+
lines.push("- missingIds: (none)");
|
|
2348
|
+
} else {
|
|
2349
|
+
lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
|
|
2350
|
+
}
|
|
2351
|
+
lines.push("");
|
|
2352
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2353
|
+
const scRefs = data.traceability.sc.refs;
|
|
2354
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2355
|
+
if (scIds.length === 0) {
|
|
2356
|
+
lines.push("- (none)");
|
|
2357
|
+
} else {
|
|
2358
|
+
for (const scId of scIds) {
|
|
2359
|
+
const refs = scRefs[scId] ?? [];
|
|
2360
|
+
if (refs.length === 0) {
|
|
2361
|
+
lines.push(`- ${scId}: (none)`);
|
|
2362
|
+
} else {
|
|
2363
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
lines.push("");
|
|
2228
2368
|
lines.push("## Hotspots");
|
|
2229
2369
|
const hotspots = buildHotspots(data.issues);
|
|
2230
2370
|
if (hotspots.length === 0) {
|
|
@@ -2279,25 +2419,25 @@ async function collectIds(files) {
|
|
|
2279
2419
|
DATA: /* @__PURE__ */ new Set()
|
|
2280
2420
|
};
|
|
2281
2421
|
for (const file of files) {
|
|
2282
|
-
const text = await (0,
|
|
2422
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2283
2423
|
for (const prefix of ID_PREFIXES2) {
|
|
2284
2424
|
const ids = extractIds(text, prefix);
|
|
2285
2425
|
ids.forEach((id) => result[prefix].add(id));
|
|
2286
2426
|
}
|
|
2287
2427
|
}
|
|
2288
2428
|
return {
|
|
2289
|
-
SPEC:
|
|
2290
|
-
BR:
|
|
2291
|
-
SC:
|
|
2292
|
-
UI:
|
|
2293
|
-
API:
|
|
2294
|
-
DATA:
|
|
2429
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2430
|
+
BR: toSortedArray2(result.BR),
|
|
2431
|
+
SC: toSortedArray2(result.SC),
|
|
2432
|
+
UI: toSortedArray2(result.UI),
|
|
2433
|
+
API: toSortedArray2(result.API),
|
|
2434
|
+
DATA: toSortedArray2(result.DATA)
|
|
2295
2435
|
};
|
|
2296
2436
|
}
|
|
2297
2437
|
async function collectUpstreamIds(files) {
|
|
2298
2438
|
const ids = /* @__PURE__ */ new Set();
|
|
2299
2439
|
for (const file of files) {
|
|
2300
|
-
const text = await (0,
|
|
2440
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2301
2441
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2302
2442
|
}
|
|
2303
2443
|
return ids;
|
|
@@ -2318,7 +2458,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2318
2458
|
}
|
|
2319
2459
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2320
2460
|
for (const file of targetFiles) {
|
|
2321
|
-
const text = await (0,
|
|
2461
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2322
2462
|
if (pattern.test(text)) {
|
|
2323
2463
|
return true;
|
|
2324
2464
|
}
|
|
@@ -2335,7 +2475,7 @@ function formatIdLine(label, values) {
|
|
|
2335
2475
|
}
|
|
2336
2476
|
return `- ${label}: ${values.join(", ")}`;
|
|
2337
2477
|
}
|
|
2338
|
-
function
|
|
2478
|
+
function toSortedArray2(values) {
|
|
2339
2479
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2340
2480
|
}
|
|
2341
2481
|
function buildHotspots(issues) {
|
|
@@ -2361,7 +2501,6 @@ function buildHotspots(issues) {
|
|
|
2361
2501
|
}
|
|
2362
2502
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2363
2503
|
0 && (module.exports = {
|
|
2364
|
-
VALIDATION_SCHEMA_VERSION,
|
|
2365
2504
|
createReportData,
|
|
2366
2505
|
defaultConfig,
|
|
2367
2506
|
extractAllIds,
|