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.mjs
CHANGED
|
@@ -28,6 +28,8 @@ var defaultConfig = {
|
|
|
28
28
|
traceability: {
|
|
29
29
|
brMustHaveSc: true,
|
|
30
30
|
scMustTouchContracts: true,
|
|
31
|
+
scMustHaveTest: true,
|
|
32
|
+
scNoTestSeverity: "error",
|
|
31
33
|
allowOrphanContracts: false,
|
|
32
34
|
unknownContractIdSeverity: "error"
|
|
33
35
|
}
|
|
@@ -207,6 +209,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
207
209
|
configPath,
|
|
208
210
|
issues
|
|
209
211
|
),
|
|
212
|
+
scMustHaveTest: readBoolean(
|
|
213
|
+
traceabilityRaw?.scMustHaveTest,
|
|
214
|
+
base.traceability.scMustHaveTest,
|
|
215
|
+
"validation.traceability.scMustHaveTest",
|
|
216
|
+
configPath,
|
|
217
|
+
issues
|
|
218
|
+
),
|
|
219
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
220
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
221
|
+
base.traceability.scNoTestSeverity,
|
|
222
|
+
"validation.traceability.scNoTestSeverity",
|
|
223
|
+
configPath,
|
|
224
|
+
issues
|
|
225
|
+
),
|
|
210
226
|
allowOrphanContracts: readBoolean(
|
|
211
227
|
traceabilityRaw?.allowOrphanContracts,
|
|
212
228
|
base.traceability.allowOrphanContracts,
|
|
@@ -385,7 +401,7 @@ function isValidId(value, prefix) {
|
|
|
385
401
|
}
|
|
386
402
|
|
|
387
403
|
// src/core/report.ts
|
|
388
|
-
import { readFile as
|
|
404
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
389
405
|
import path10 from "path";
|
|
390
406
|
|
|
391
407
|
// src/core/discovery.ts
|
|
@@ -526,20 +542,244 @@ async function exists2(target) {
|
|
|
526
542
|
}
|
|
527
543
|
}
|
|
528
544
|
|
|
529
|
-
// src/core/
|
|
530
|
-
|
|
545
|
+
// src/core/traceability.ts
|
|
546
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
547
|
+
|
|
548
|
+
// src/core/gherkin/parse.ts
|
|
549
|
+
import {
|
|
550
|
+
AstBuilder,
|
|
551
|
+
GherkinClassicTokenMatcher,
|
|
552
|
+
Parser
|
|
553
|
+
} from "@cucumber/gherkin";
|
|
554
|
+
import { randomUUID } from "crypto";
|
|
555
|
+
function parseGherkin(source, uri) {
|
|
556
|
+
const errors = [];
|
|
557
|
+
const uuidFn = () => randomUUID();
|
|
558
|
+
const builder = new AstBuilder(uuidFn);
|
|
559
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
560
|
+
const parser = new Parser(builder, matcher);
|
|
561
|
+
try {
|
|
562
|
+
const gherkinDocument = parser.parse(source);
|
|
563
|
+
gherkinDocument.uri = uri;
|
|
564
|
+
return { gherkinDocument, errors };
|
|
565
|
+
} catch (error) {
|
|
566
|
+
errors.push(formatError2(error));
|
|
567
|
+
return { gherkinDocument: null, errors };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function formatError2(error) {
|
|
571
|
+
if (error instanceof Error) {
|
|
572
|
+
return error.message;
|
|
573
|
+
}
|
|
574
|
+
return String(error);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/core/scenarioModel.ts
|
|
578
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
579
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
580
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
581
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
582
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
583
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
584
|
+
function parseScenarioDocument(text, uri) {
|
|
585
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
586
|
+
if (!gherkinDocument) {
|
|
587
|
+
return { document: null, errors };
|
|
588
|
+
}
|
|
589
|
+
const feature = gherkinDocument.feature;
|
|
590
|
+
if (!feature) {
|
|
591
|
+
return {
|
|
592
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
593
|
+
errors
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const featureTags = collectTagNames(feature.tags);
|
|
597
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
598
|
+
return {
|
|
599
|
+
document: {
|
|
600
|
+
uri,
|
|
601
|
+
featureName: feature.name,
|
|
602
|
+
featureTags,
|
|
603
|
+
scenarios
|
|
604
|
+
},
|
|
605
|
+
errors
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function buildScenarioAtoms(document) {
|
|
609
|
+
return document.scenarios.map((scenario) => {
|
|
610
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
611
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
612
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
613
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
614
|
+
scenario.tags.forEach((tag) => {
|
|
615
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
616
|
+
contractIds.add(tag);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
for (const step of scenario.steps) {
|
|
620
|
+
for (const text of collectStepTexts(step)) {
|
|
621
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
622
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
623
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const atom = {
|
|
627
|
+
uri: document.uri,
|
|
628
|
+
featureName: document.featureName ?? "",
|
|
629
|
+
scenarioName: scenario.name,
|
|
630
|
+
kind: scenario.kind,
|
|
631
|
+
brIds,
|
|
632
|
+
contractIds: Array.from(contractIds).sort()
|
|
633
|
+
};
|
|
634
|
+
if (scenario.line !== void 0) {
|
|
635
|
+
atom.line = scenario.line;
|
|
636
|
+
}
|
|
637
|
+
if (specIds.length === 1) {
|
|
638
|
+
const specId = specIds[0];
|
|
639
|
+
if (specId) {
|
|
640
|
+
atom.specId = specId;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (scIds.length === 1) {
|
|
644
|
+
const scId = scIds[0];
|
|
645
|
+
if (scId) {
|
|
646
|
+
atom.scId = scId;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return atom;
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
653
|
+
const scenarios = [];
|
|
654
|
+
for (const child of feature.children) {
|
|
655
|
+
if (child.scenario) {
|
|
656
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
657
|
+
}
|
|
658
|
+
if (child.rule) {
|
|
659
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
660
|
+
for (const ruleChild of child.rule.children) {
|
|
661
|
+
if (ruleChild.scenario) {
|
|
662
|
+
scenarios.push(
|
|
663
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return scenarios;
|
|
670
|
+
}
|
|
671
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
672
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
673
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
674
|
+
return {
|
|
675
|
+
name: scenario.name,
|
|
676
|
+
kind,
|
|
677
|
+
line: scenario.location?.line,
|
|
678
|
+
tags,
|
|
679
|
+
steps: scenario.steps
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function collectTagNames(tags) {
|
|
683
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
684
|
+
}
|
|
685
|
+
function collectStepTexts(step) {
|
|
686
|
+
const texts = [];
|
|
687
|
+
if (step.text) {
|
|
688
|
+
texts.push(step.text);
|
|
689
|
+
}
|
|
690
|
+
if (step.docString?.content) {
|
|
691
|
+
texts.push(step.docString.content);
|
|
692
|
+
}
|
|
693
|
+
if (step.dataTable?.rows) {
|
|
694
|
+
for (const row of step.dataTable.rows) {
|
|
695
|
+
for (const cell of row.cells) {
|
|
696
|
+
texts.push(cell.value);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return texts;
|
|
701
|
+
}
|
|
702
|
+
function unique2(values) {
|
|
703
|
+
return Array.from(new Set(values));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/core/traceability.ts
|
|
707
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
708
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
709
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
710
|
+
for (const file of scenarioFiles) {
|
|
711
|
+
const text = await readFile2(file, "utf-8");
|
|
712
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
713
|
+
if (!document || errors.length > 0) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
for (const scenario of document.scenarios) {
|
|
717
|
+
for (const tag of scenario.tags) {
|
|
718
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
719
|
+
scIds.add(tag);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return scIds;
|
|
725
|
+
}
|
|
726
|
+
async function collectScTestReferences(testsRoot) {
|
|
727
|
+
const refs = /* @__PURE__ */ new Map();
|
|
728
|
+
const testFiles = await collectFiles(testsRoot, {
|
|
729
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
730
|
+
});
|
|
731
|
+
for (const file of testFiles) {
|
|
732
|
+
const text = await readFile2(file, "utf-8");
|
|
733
|
+
const scIds = extractIds(text, "SC");
|
|
734
|
+
if (scIds.length === 0) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
for (const scId of scIds) {
|
|
738
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
739
|
+
current.add(file);
|
|
740
|
+
refs.set(scId, current);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return refs;
|
|
744
|
+
}
|
|
745
|
+
function buildScCoverage(scIds, refs) {
|
|
746
|
+
const sortedScIds = toSortedArray(scIds);
|
|
747
|
+
const refsRecord = {};
|
|
748
|
+
const missingIds = [];
|
|
749
|
+
let covered = 0;
|
|
750
|
+
for (const scId of sortedScIds) {
|
|
751
|
+
const files = refs.get(scId);
|
|
752
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
753
|
+
refsRecord[scId] = sortedFiles;
|
|
754
|
+
if (sortedFiles.length === 0) {
|
|
755
|
+
missingIds.push(scId);
|
|
756
|
+
} else {
|
|
757
|
+
covered += 1;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
total: sortedScIds.length,
|
|
762
|
+
covered,
|
|
763
|
+
missing: missingIds.length,
|
|
764
|
+
missingIds,
|
|
765
|
+
refs: refsRecord
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function toSortedArray(values) {
|
|
769
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
770
|
+
}
|
|
531
771
|
|
|
532
772
|
// src/core/version.ts
|
|
533
|
-
import { readFile as
|
|
773
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
534
774
|
import path4 from "path";
|
|
535
775
|
import { fileURLToPath } from "url";
|
|
536
776
|
async function resolveToolVersion() {
|
|
537
|
-
if ("0.
|
|
538
|
-
return "0.
|
|
777
|
+
if ("0.4.0".length > 0) {
|
|
778
|
+
return "0.4.0";
|
|
539
779
|
}
|
|
540
780
|
try {
|
|
541
781
|
const packagePath = resolvePackageJsonPath();
|
|
542
|
-
const raw = await
|
|
782
|
+
const raw = await readFile3(packagePath, "utf-8");
|
|
543
783
|
const parsed = JSON.parse(raw);
|
|
544
784
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
545
785
|
return version.length > 0 ? version : "unknown";
|
|
@@ -554,7 +794,7 @@ function resolvePackageJsonPath() {
|
|
|
554
794
|
}
|
|
555
795
|
|
|
556
796
|
// src/core/validators/contracts.ts
|
|
557
|
-
import { readFile as
|
|
797
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
558
798
|
import path6 from "path";
|
|
559
799
|
|
|
560
800
|
// src/core/contracts.ts
|
|
@@ -632,7 +872,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
632
872
|
}
|
|
633
873
|
const issues = [];
|
|
634
874
|
for (const file of files) {
|
|
635
|
-
const text = await
|
|
875
|
+
const text = await readFile4(file, "utf-8");
|
|
636
876
|
const invalidIds = extractInvalidIds(text, [
|
|
637
877
|
"SPEC",
|
|
638
878
|
"BR",
|
|
@@ -661,7 +901,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
661
901
|
issues.push(
|
|
662
902
|
issue(
|
|
663
903
|
"QFAI-CONTRACT-001",
|
|
664
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
904
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
|
|
665
905
|
"error",
|
|
666
906
|
file,
|
|
667
907
|
"contracts.ui.parse"
|
|
@@ -699,7 +939,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
699
939
|
}
|
|
700
940
|
const issues = [];
|
|
701
941
|
for (const file of files) {
|
|
702
|
-
const text = await
|
|
942
|
+
const text = await readFile4(file, "utf-8");
|
|
703
943
|
const invalidIds = extractInvalidIds(text, [
|
|
704
944
|
"SPEC",
|
|
705
945
|
"BR",
|
|
@@ -728,7 +968,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
728
968
|
issues.push(
|
|
729
969
|
issue(
|
|
730
970
|
"QFAI-CONTRACT-001",
|
|
731
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
971
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error)})`,
|
|
732
972
|
"error",
|
|
733
973
|
file,
|
|
734
974
|
"contracts.api.parse"
|
|
@@ -777,7 +1017,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
777
1017
|
}
|
|
778
1018
|
const issues = [];
|
|
779
1019
|
for (const file of files) {
|
|
780
|
-
const text = await
|
|
1020
|
+
const text = await readFile4(file, "utf-8");
|
|
781
1021
|
const invalidIds = extractInvalidIds(text, [
|
|
782
1022
|
"SPEC",
|
|
783
1023
|
"BR",
|
|
@@ -823,7 +1063,7 @@ function lintSql(text, file) {
|
|
|
823
1063
|
function hasOpenApi(doc) {
|
|
824
1064
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
825
1065
|
}
|
|
826
|
-
function
|
|
1066
|
+
function formatError3(error) {
|
|
827
1067
|
if (error instanceof Error) {
|
|
828
1068
|
return error.message;
|
|
829
1069
|
}
|
|
@@ -848,7 +1088,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
848
1088
|
}
|
|
849
1089
|
|
|
850
1090
|
// src/core/validators/delta.ts
|
|
851
|
-
import { readFile as
|
|
1091
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
852
1092
|
import path7 from "path";
|
|
853
1093
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
854
1094
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -866,7 +1106,7 @@ async function validateDeltas(root, config) {
|
|
|
866
1106
|
const deltaPath = path7.join(pack, "delta.md");
|
|
867
1107
|
let text;
|
|
868
1108
|
try {
|
|
869
|
-
text = await
|
|
1109
|
+
text = await readFile5(deltaPath, "utf-8");
|
|
870
1110
|
} catch (error) {
|
|
871
1111
|
if (isMissingFileError2(error)) {
|
|
872
1112
|
issues.push(
|
|
@@ -938,11 +1178,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
938
1178
|
}
|
|
939
1179
|
|
|
940
1180
|
// src/core/validators/ids.ts
|
|
941
|
-
import { readFile as
|
|
1181
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
942
1182
|
import path9 from "path";
|
|
943
1183
|
|
|
944
1184
|
// src/core/contractIndex.ts
|
|
945
|
-
import { readFile as
|
|
1185
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
946
1186
|
import path8 from "path";
|
|
947
1187
|
async function buildContractIndex(root, config) {
|
|
948
1188
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -967,7 +1207,7 @@ async function buildContractIndex(root, config) {
|
|
|
967
1207
|
}
|
|
968
1208
|
async function indexUiContracts(files, index) {
|
|
969
1209
|
for (const file of files) {
|
|
970
|
-
const text = await
|
|
1210
|
+
const text = await readFile6(file, "utf-8");
|
|
971
1211
|
try {
|
|
972
1212
|
const doc = parseStructuredContract(file, text);
|
|
973
1213
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -979,7 +1219,7 @@ async function indexUiContracts(files, index) {
|
|
|
979
1219
|
}
|
|
980
1220
|
async function indexApiContracts(files, index) {
|
|
981
1221
|
for (const file of files) {
|
|
982
|
-
const text = await
|
|
1222
|
+
const text = await readFile6(file, "utf-8");
|
|
983
1223
|
try {
|
|
984
1224
|
const doc = parseStructuredContract(file, text);
|
|
985
1225
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -991,7 +1231,7 @@ async function indexApiContracts(files, index) {
|
|
|
991
1231
|
}
|
|
992
1232
|
async function indexDataContracts(files, index) {
|
|
993
1233
|
for (const file of files) {
|
|
994
|
-
const text = await
|
|
1234
|
+
const text = await readFile6(file, "utf-8");
|
|
995
1235
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
996
1236
|
}
|
|
997
1237
|
}
|
|
@@ -1120,166 +1360,8 @@ function parseSpec(md, file) {
|
|
|
1120
1360
|
return parsed;
|
|
1121
1361
|
}
|
|
1122
1362
|
|
|
1123
|
-
// src/core/gherkin/parse.ts
|
|
1124
|
-
import {
|
|
1125
|
-
AstBuilder,
|
|
1126
|
-
GherkinClassicTokenMatcher,
|
|
1127
|
-
Parser
|
|
1128
|
-
} from "@cucumber/gherkin";
|
|
1129
|
-
import { randomUUID } from "crypto";
|
|
1130
|
-
function parseGherkin(source, uri) {
|
|
1131
|
-
const errors = [];
|
|
1132
|
-
const uuidFn = () => randomUUID();
|
|
1133
|
-
const builder = new AstBuilder(uuidFn);
|
|
1134
|
-
const matcher = new GherkinClassicTokenMatcher();
|
|
1135
|
-
const parser = new Parser(builder, matcher);
|
|
1136
|
-
try {
|
|
1137
|
-
const gherkinDocument = parser.parse(source);
|
|
1138
|
-
gherkinDocument.uri = uri;
|
|
1139
|
-
return { gherkinDocument, errors };
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
errors.push(formatError3(error));
|
|
1142
|
-
return { gherkinDocument: null, errors };
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
function formatError3(error) {
|
|
1146
|
-
if (error instanceof Error) {
|
|
1147
|
-
return error.message;
|
|
1148
|
-
}
|
|
1149
|
-
return String(error);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// src/core/scenarioModel.ts
|
|
1153
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1154
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1155
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1156
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1157
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1158
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1159
|
-
function parseScenarioDocument(text, uri) {
|
|
1160
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1161
|
-
if (!gherkinDocument) {
|
|
1162
|
-
return { document: null, errors };
|
|
1163
|
-
}
|
|
1164
|
-
const feature = gherkinDocument.feature;
|
|
1165
|
-
if (!feature) {
|
|
1166
|
-
return {
|
|
1167
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1168
|
-
errors
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1172
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1173
|
-
return {
|
|
1174
|
-
document: {
|
|
1175
|
-
uri,
|
|
1176
|
-
featureName: feature.name,
|
|
1177
|
-
featureTags,
|
|
1178
|
-
scenarios
|
|
1179
|
-
},
|
|
1180
|
-
errors
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
function buildScenarioAtoms(document) {
|
|
1184
|
-
return document.scenarios.map((scenario) => {
|
|
1185
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1186
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1187
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1188
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1189
|
-
scenario.tags.forEach((tag) => {
|
|
1190
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1191
|
-
contractIds.add(tag);
|
|
1192
|
-
}
|
|
1193
|
-
});
|
|
1194
|
-
for (const step of scenario.steps) {
|
|
1195
|
-
for (const text of collectStepTexts(step)) {
|
|
1196
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1197
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1198
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
const atom = {
|
|
1202
|
-
uri: document.uri,
|
|
1203
|
-
featureName: document.featureName ?? "",
|
|
1204
|
-
scenarioName: scenario.name,
|
|
1205
|
-
kind: scenario.kind,
|
|
1206
|
-
brIds,
|
|
1207
|
-
contractIds: Array.from(contractIds).sort()
|
|
1208
|
-
};
|
|
1209
|
-
if (scenario.line !== void 0) {
|
|
1210
|
-
atom.line = scenario.line;
|
|
1211
|
-
}
|
|
1212
|
-
if (specIds.length === 1) {
|
|
1213
|
-
const specId = specIds[0];
|
|
1214
|
-
if (specId) {
|
|
1215
|
-
atom.specId = specId;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
if (scIds.length === 1) {
|
|
1219
|
-
const scId = scIds[0];
|
|
1220
|
-
if (scId) {
|
|
1221
|
-
atom.scId = scId;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
return atom;
|
|
1225
|
-
});
|
|
1226
|
-
}
|
|
1227
|
-
function collectScenarioNodes(feature, featureTags) {
|
|
1228
|
-
const scenarios = [];
|
|
1229
|
-
for (const child of feature.children) {
|
|
1230
|
-
if (child.scenario) {
|
|
1231
|
-
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1232
|
-
}
|
|
1233
|
-
if (child.rule) {
|
|
1234
|
-
const ruleTags = collectTagNames(child.rule.tags);
|
|
1235
|
-
for (const ruleChild of child.rule.children) {
|
|
1236
|
-
if (ruleChild.scenario) {
|
|
1237
|
-
scenarios.push(
|
|
1238
|
-
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1239
|
-
);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
return scenarios;
|
|
1245
|
-
}
|
|
1246
|
-
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1247
|
-
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1248
|
-
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1249
|
-
return {
|
|
1250
|
-
name: scenario.name,
|
|
1251
|
-
kind,
|
|
1252
|
-
line: scenario.location?.line,
|
|
1253
|
-
tags,
|
|
1254
|
-
steps: scenario.steps
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
function collectTagNames(tags) {
|
|
1258
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1259
|
-
}
|
|
1260
|
-
function collectStepTexts(step) {
|
|
1261
|
-
const texts = [];
|
|
1262
|
-
if (step.text) {
|
|
1263
|
-
texts.push(step.text);
|
|
1264
|
-
}
|
|
1265
|
-
if (step.docString?.content) {
|
|
1266
|
-
texts.push(step.docString.content);
|
|
1267
|
-
}
|
|
1268
|
-
if (step.dataTable?.rows) {
|
|
1269
|
-
for (const row of step.dataTable.rows) {
|
|
1270
|
-
for (const cell of row.cells) {
|
|
1271
|
-
texts.push(cell.value);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
return texts;
|
|
1276
|
-
}
|
|
1277
|
-
function unique2(values) {
|
|
1278
|
-
return Array.from(new Set(values));
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1363
|
// src/core/validators/ids.ts
|
|
1282
|
-
var
|
|
1364
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1283
1365
|
async function validateDefinedIds(root, config) {
|
|
1284
1366
|
const issues = [];
|
|
1285
1367
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1313,7 +1395,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1313
1395
|
}
|
|
1314
1396
|
async function collectSpecDefinitionIds(files, out) {
|
|
1315
1397
|
for (const file of files) {
|
|
1316
|
-
const text = await
|
|
1398
|
+
const text = await readFile7(file, "utf-8");
|
|
1317
1399
|
const parsed = parseSpec(text, file);
|
|
1318
1400
|
if (parsed.specId) {
|
|
1319
1401
|
recordId(out, parsed.specId, file);
|
|
@@ -1323,14 +1405,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1323
1405
|
}
|
|
1324
1406
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1325
1407
|
for (const file of files) {
|
|
1326
|
-
const text = await
|
|
1408
|
+
const text = await readFile7(file, "utf-8");
|
|
1327
1409
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1328
1410
|
if (!document || errors.length > 0) {
|
|
1329
1411
|
continue;
|
|
1330
1412
|
}
|
|
1331
1413
|
for (const scenario of document.scenarios) {
|
|
1332
1414
|
for (const tag of scenario.tags) {
|
|
1333
|
-
if (
|
|
1415
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1334
1416
|
recordId(out, tag, file);
|
|
1335
1417
|
}
|
|
1336
1418
|
}
|
|
@@ -1367,11 +1449,11 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1367
1449
|
}
|
|
1368
1450
|
|
|
1369
1451
|
// src/core/validators/scenario.ts
|
|
1370
|
-
import { readFile as
|
|
1452
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1371
1453
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1372
1454
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1373
1455
|
var THEN_PATTERN = /\bThen\b/;
|
|
1374
|
-
var
|
|
1456
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1375
1457
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1376
1458
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1377
1459
|
async function validateScenarios(root, config) {
|
|
@@ -1394,7 +1476,7 @@ async function validateScenarios(root, config) {
|
|
|
1394
1476
|
for (const entry of entries) {
|
|
1395
1477
|
let text;
|
|
1396
1478
|
try {
|
|
1397
|
-
text = await
|
|
1479
|
+
text = await readFile8(entry.scenarioPath, "utf-8");
|
|
1398
1480
|
} catch (error) {
|
|
1399
1481
|
if (isMissingFileError3(error)) {
|
|
1400
1482
|
issues.push(
|
|
@@ -1491,6 +1573,17 @@ function validateScenarioContent(text, file) {
|
|
|
1491
1573
|
)
|
|
1492
1574
|
);
|
|
1493
1575
|
}
|
|
1576
|
+
if (document.scenarios.length > 1) {
|
|
1577
|
+
issues.push(
|
|
1578
|
+
issue4(
|
|
1579
|
+
"QFAI-SC-011",
|
|
1580
|
+
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1581
|
+
"error",
|
|
1582
|
+
file,
|
|
1583
|
+
"scenario.single"
|
|
1584
|
+
)
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1494
1587
|
for (const scenario of document.scenarios) {
|
|
1495
1588
|
if (scenario.tags.length === 0) {
|
|
1496
1589
|
issues.push(
|
|
@@ -1505,7 +1598,7 @@ function validateScenarioContent(text, file) {
|
|
|
1505
1598
|
continue;
|
|
1506
1599
|
}
|
|
1507
1600
|
const missingTags = [];
|
|
1508
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1601
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1509
1602
|
if (scTags.length === 0) {
|
|
1510
1603
|
missingTags.push("SC(0\u4EF6)");
|
|
1511
1604
|
} else if (scTags.length > 1) {
|
|
@@ -1580,7 +1673,7 @@ function isMissingFileError3(error) {
|
|
|
1580
1673
|
}
|
|
1581
1674
|
|
|
1582
1675
|
// src/core/validators/spec.ts
|
|
1583
|
-
import { readFile as
|
|
1676
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1584
1677
|
async function validateSpecs(root, config) {
|
|
1585
1678
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1586
1679
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1601,7 +1694,7 @@ async function validateSpecs(root, config) {
|
|
|
1601
1694
|
for (const entry of entries) {
|
|
1602
1695
|
let text;
|
|
1603
1696
|
try {
|
|
1604
|
-
text = await
|
|
1697
|
+
text = await readFile9(entry.specPath, "utf-8");
|
|
1605
1698
|
} catch (error) {
|
|
1606
1699
|
if (isMissingFileError4(error)) {
|
|
1607
1700
|
issues.push(
|
|
@@ -1750,8 +1843,8 @@ function isMissingFileError4(error) {
|
|
|
1750
1843
|
}
|
|
1751
1844
|
|
|
1752
1845
|
// src/core/validators/traceability.ts
|
|
1753
|
-
import { readFile as
|
|
1754
|
-
var
|
|
1846
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1847
|
+
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
1755
1848
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1756
1849
|
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1757
1850
|
async function validateTraceability(root, config) {
|
|
@@ -1772,7 +1865,7 @@ async function validateTraceability(root, config) {
|
|
|
1772
1865
|
const contractIndex = await buildContractIndex(root, config);
|
|
1773
1866
|
const contractIds = contractIndex.ids;
|
|
1774
1867
|
for (const file of specFiles) {
|
|
1775
|
-
const text = await
|
|
1868
|
+
const text = await readFile10(file, "utf-8");
|
|
1776
1869
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1777
1870
|
const parsed = parseSpec(text, file);
|
|
1778
1871
|
if (parsed.specId) {
|
|
@@ -1809,7 +1902,7 @@ async function validateTraceability(root, config) {
|
|
|
1809
1902
|
}
|
|
1810
1903
|
}
|
|
1811
1904
|
for (const file of scenarioFiles) {
|
|
1812
|
-
const text = await
|
|
1905
|
+
const text = await readFile10(file, "utf-8");
|
|
1813
1906
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1814
1907
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1815
1908
|
if (!document || errors.length > 0) {
|
|
@@ -1823,7 +1916,7 @@ async function validateTraceability(root, config) {
|
|
|
1823
1916
|
}
|
|
1824
1917
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1825
1918
|
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1826
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1919
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
|
|
1827
1920
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1828
1921
|
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1829
1922
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
@@ -1951,6 +2044,25 @@ async function validateTraceability(root, config) {
|
|
|
1951
2044
|
);
|
|
1952
2045
|
}
|
|
1953
2046
|
}
|
|
2047
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2048
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2049
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2050
|
+
const refs = scTestRefs.get(id);
|
|
2051
|
+
return !refs || refs.size === 0;
|
|
2052
|
+
});
|
|
2053
|
+
if (scWithoutTests.length > 0) {
|
|
2054
|
+
issues.push(
|
|
2055
|
+
issue6(
|
|
2056
|
+
"QFAI-TRACE-010",
|
|
2057
|
+
`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`,
|
|
2058
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2059
|
+
testsRoot,
|
|
2060
|
+
"traceability.scMustHaveTest",
|
|
2061
|
+
scWithoutTests
|
|
2062
|
+
)
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1954
2066
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
1955
2067
|
if (contractIds.size > 0) {
|
|
1956
2068
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -1999,7 +2111,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1999
2111
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2000
2112
|
let found = false;
|
|
2001
2113
|
for (const file of targetFiles) {
|
|
2002
|
-
const text = await
|
|
2114
|
+
const text = await readFile10(file, "utf-8");
|
|
2003
2115
|
if (pattern.test(text)) {
|
|
2004
2116
|
found = true;
|
|
2005
2117
|
break;
|
|
@@ -2055,7 +2167,6 @@ async function validateProject(root, configResult) {
|
|
|
2055
2167
|
];
|
|
2056
2168
|
const toolVersion = await resolveToolVersion();
|
|
2057
2169
|
return {
|
|
2058
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2059
2170
|
toolVersion,
|
|
2060
2171
|
issues,
|
|
2061
2172
|
counts: countIssues(issues)
|
|
@@ -2107,6 +2218,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2107
2218
|
srcRoot,
|
|
2108
2219
|
testsRoot
|
|
2109
2220
|
);
|
|
2221
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2222
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2223
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2110
2224
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2111
2225
|
const version = await resolveToolVersion();
|
|
2112
2226
|
return {
|
|
@@ -2135,7 +2249,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2135
2249
|
},
|
|
2136
2250
|
traceability: {
|
|
2137
2251
|
upstreamIdsFound: upstreamIds.size,
|
|
2138
|
-
referencedInCodeOrTests: traceability
|
|
2252
|
+
referencedInCodeOrTests: traceability,
|
|
2253
|
+
sc: scCoverage
|
|
2139
2254
|
},
|
|
2140
2255
|
issues: resolvedValidation.issues
|
|
2141
2256
|
};
|
|
@@ -2172,6 +2287,32 @@ function formatReportMarkdown(data) {
|
|
|
2172
2287
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2173
2288
|
);
|
|
2174
2289
|
lines.push("");
|
|
2290
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2291
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2292
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2293
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2294
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2295
|
+
lines.push("- missingIds: (none)");
|
|
2296
|
+
} else {
|
|
2297
|
+
lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
|
|
2298
|
+
}
|
|
2299
|
+
lines.push("");
|
|
2300
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2301
|
+
const scRefs = data.traceability.sc.refs;
|
|
2302
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2303
|
+
if (scIds.length === 0) {
|
|
2304
|
+
lines.push("- (none)");
|
|
2305
|
+
} else {
|
|
2306
|
+
for (const scId of scIds) {
|
|
2307
|
+
const refs = scRefs[scId] ?? [];
|
|
2308
|
+
if (refs.length === 0) {
|
|
2309
|
+
lines.push(`- ${scId}: (none)`);
|
|
2310
|
+
} else {
|
|
2311
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
lines.push("");
|
|
2175
2316
|
lines.push("## Hotspots");
|
|
2176
2317
|
const hotspots = buildHotspots(data.issues);
|
|
2177
2318
|
if (hotspots.length === 0) {
|
|
@@ -2226,25 +2367,25 @@ async function collectIds(files) {
|
|
|
2226
2367
|
DATA: /* @__PURE__ */ new Set()
|
|
2227
2368
|
};
|
|
2228
2369
|
for (const file of files) {
|
|
2229
|
-
const text = await
|
|
2370
|
+
const text = await readFile11(file, "utf-8");
|
|
2230
2371
|
for (const prefix of ID_PREFIXES2) {
|
|
2231
2372
|
const ids = extractIds(text, prefix);
|
|
2232
2373
|
ids.forEach((id) => result[prefix].add(id));
|
|
2233
2374
|
}
|
|
2234
2375
|
}
|
|
2235
2376
|
return {
|
|
2236
|
-
SPEC:
|
|
2237
|
-
BR:
|
|
2238
|
-
SC:
|
|
2239
|
-
UI:
|
|
2240
|
-
API:
|
|
2241
|
-
DATA:
|
|
2377
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2378
|
+
BR: toSortedArray2(result.BR),
|
|
2379
|
+
SC: toSortedArray2(result.SC),
|
|
2380
|
+
UI: toSortedArray2(result.UI),
|
|
2381
|
+
API: toSortedArray2(result.API),
|
|
2382
|
+
DATA: toSortedArray2(result.DATA)
|
|
2242
2383
|
};
|
|
2243
2384
|
}
|
|
2244
2385
|
async function collectUpstreamIds(files) {
|
|
2245
2386
|
const ids = /* @__PURE__ */ new Set();
|
|
2246
2387
|
for (const file of files) {
|
|
2247
|
-
const text = await
|
|
2388
|
+
const text = await readFile11(file, "utf-8");
|
|
2248
2389
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2249
2390
|
}
|
|
2250
2391
|
return ids;
|
|
@@ -2265,7 +2406,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2265
2406
|
}
|
|
2266
2407
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2267
2408
|
for (const file of targetFiles) {
|
|
2268
|
-
const text = await
|
|
2409
|
+
const text = await readFile11(file, "utf-8");
|
|
2269
2410
|
if (pattern.test(text)) {
|
|
2270
2411
|
return true;
|
|
2271
2412
|
}
|
|
@@ -2282,7 +2423,7 @@ function formatIdLine(label, values) {
|
|
|
2282
2423
|
}
|
|
2283
2424
|
return `- ${label}: ${values.join(", ")}`;
|
|
2284
2425
|
}
|
|
2285
|
-
function
|
|
2426
|
+
function toSortedArray2(values) {
|
|
2286
2427
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2287
2428
|
}
|
|
2288
2429
|
function buildHotspots(issues) {
|
|
@@ -2307,7 +2448,6 @@ function buildHotspots(issues) {
|
|
|
2307
2448
|
);
|
|
2308
2449
|
}
|
|
2309
2450
|
export {
|
|
2310
|
-
VALIDATION_SCHEMA_VERSION,
|
|
2311
2451
|
createReportData,
|
|
2312
2452
|
defaultConfig,
|
|
2313
2453
|
extractAllIds,
|