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/cli/index.mjs
CHANGED
|
@@ -154,7 +154,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// src/cli/commands/report.ts
|
|
157
|
-
import { mkdir as mkdir2, readFile as
|
|
157
|
+
import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
|
|
158
158
|
import path14 from "path";
|
|
159
159
|
|
|
160
160
|
// src/core/config.ts
|
|
@@ -187,6 +187,8 @@ var defaultConfig = {
|
|
|
187
187
|
traceability: {
|
|
188
188
|
brMustHaveSc: true,
|
|
189
189
|
scMustTouchContracts: true,
|
|
190
|
+
scMustHaveTest: true,
|
|
191
|
+
scNoTestSeverity: "error",
|
|
190
192
|
allowOrphanContracts: false,
|
|
191
193
|
unknownContractIdSeverity: "error"
|
|
192
194
|
}
|
|
@@ -366,6 +368,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
366
368
|
configPath,
|
|
367
369
|
issues
|
|
368
370
|
),
|
|
371
|
+
scMustHaveTest: readBoolean(
|
|
372
|
+
traceabilityRaw?.scMustHaveTest,
|
|
373
|
+
base.traceability.scMustHaveTest,
|
|
374
|
+
"validation.traceability.scMustHaveTest",
|
|
375
|
+
configPath,
|
|
376
|
+
issues
|
|
377
|
+
),
|
|
378
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
379
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
380
|
+
base.traceability.scNoTestSeverity,
|
|
381
|
+
"validation.traceability.scNoTestSeverity",
|
|
382
|
+
configPath,
|
|
383
|
+
issues
|
|
384
|
+
),
|
|
369
385
|
allowOrphanContracts: readBoolean(
|
|
370
386
|
traceabilityRaw?.allowOrphanContracts,
|
|
371
387
|
base.traceability.allowOrphanContracts,
|
|
@@ -491,7 +507,7 @@ function isRecord(value) {
|
|
|
491
507
|
}
|
|
492
508
|
|
|
493
509
|
// src/core/report.ts
|
|
494
|
-
import { readFile as
|
|
510
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
495
511
|
import path13 from "path";
|
|
496
512
|
|
|
497
513
|
// src/core/discovery.ts
|
|
@@ -685,20 +701,244 @@ function isValidId(value, prefix) {
|
|
|
685
701
|
return strict.test(value);
|
|
686
702
|
}
|
|
687
703
|
|
|
688
|
-
// src/core/
|
|
689
|
-
|
|
704
|
+
// src/core/traceability.ts
|
|
705
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
706
|
+
|
|
707
|
+
// src/core/gherkin/parse.ts
|
|
708
|
+
import {
|
|
709
|
+
AstBuilder,
|
|
710
|
+
GherkinClassicTokenMatcher,
|
|
711
|
+
Parser
|
|
712
|
+
} from "@cucumber/gherkin";
|
|
713
|
+
import { randomUUID } from "crypto";
|
|
714
|
+
function parseGherkin(source, uri) {
|
|
715
|
+
const errors = [];
|
|
716
|
+
const uuidFn = () => randomUUID();
|
|
717
|
+
const builder = new AstBuilder(uuidFn);
|
|
718
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
719
|
+
const parser = new Parser(builder, matcher);
|
|
720
|
+
try {
|
|
721
|
+
const gherkinDocument = parser.parse(source);
|
|
722
|
+
gherkinDocument.uri = uri;
|
|
723
|
+
return { gherkinDocument, errors };
|
|
724
|
+
} catch (error2) {
|
|
725
|
+
errors.push(formatError2(error2));
|
|
726
|
+
return { gherkinDocument: null, errors };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function formatError2(error2) {
|
|
730
|
+
if (error2 instanceof Error) {
|
|
731
|
+
return error2.message;
|
|
732
|
+
}
|
|
733
|
+
return String(error2);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/core/scenarioModel.ts
|
|
737
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
738
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
739
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
740
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
741
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
742
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
743
|
+
function parseScenarioDocument(text, uri) {
|
|
744
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
745
|
+
if (!gherkinDocument) {
|
|
746
|
+
return { document: null, errors };
|
|
747
|
+
}
|
|
748
|
+
const feature = gherkinDocument.feature;
|
|
749
|
+
if (!feature) {
|
|
750
|
+
return {
|
|
751
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
752
|
+
errors
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const featureTags = collectTagNames(feature.tags);
|
|
756
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
757
|
+
return {
|
|
758
|
+
document: {
|
|
759
|
+
uri,
|
|
760
|
+
featureName: feature.name,
|
|
761
|
+
featureTags,
|
|
762
|
+
scenarios
|
|
763
|
+
},
|
|
764
|
+
errors
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function buildScenarioAtoms(document) {
|
|
768
|
+
return document.scenarios.map((scenario) => {
|
|
769
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
770
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
771
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
772
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
773
|
+
scenario.tags.forEach((tag) => {
|
|
774
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
775
|
+
contractIds.add(tag);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
for (const step of scenario.steps) {
|
|
779
|
+
for (const text of collectStepTexts(step)) {
|
|
780
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
781
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
782
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const atom = {
|
|
786
|
+
uri: document.uri,
|
|
787
|
+
featureName: document.featureName ?? "",
|
|
788
|
+
scenarioName: scenario.name,
|
|
789
|
+
kind: scenario.kind,
|
|
790
|
+
brIds,
|
|
791
|
+
contractIds: Array.from(contractIds).sort()
|
|
792
|
+
};
|
|
793
|
+
if (scenario.line !== void 0) {
|
|
794
|
+
atom.line = scenario.line;
|
|
795
|
+
}
|
|
796
|
+
if (specIds.length === 1) {
|
|
797
|
+
const specId = specIds[0];
|
|
798
|
+
if (specId) {
|
|
799
|
+
atom.specId = specId;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (scIds.length === 1) {
|
|
803
|
+
const scId = scIds[0];
|
|
804
|
+
if (scId) {
|
|
805
|
+
atom.scId = scId;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return atom;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
812
|
+
const scenarios = [];
|
|
813
|
+
for (const child of feature.children) {
|
|
814
|
+
if (child.scenario) {
|
|
815
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
816
|
+
}
|
|
817
|
+
if (child.rule) {
|
|
818
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
819
|
+
for (const ruleChild of child.rule.children) {
|
|
820
|
+
if (ruleChild.scenario) {
|
|
821
|
+
scenarios.push(
|
|
822
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return scenarios;
|
|
829
|
+
}
|
|
830
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
831
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
832
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
833
|
+
return {
|
|
834
|
+
name: scenario.name,
|
|
835
|
+
kind,
|
|
836
|
+
line: scenario.location?.line,
|
|
837
|
+
tags,
|
|
838
|
+
steps: scenario.steps
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function collectTagNames(tags) {
|
|
842
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
843
|
+
}
|
|
844
|
+
function collectStepTexts(step) {
|
|
845
|
+
const texts = [];
|
|
846
|
+
if (step.text) {
|
|
847
|
+
texts.push(step.text);
|
|
848
|
+
}
|
|
849
|
+
if (step.docString?.content) {
|
|
850
|
+
texts.push(step.docString.content);
|
|
851
|
+
}
|
|
852
|
+
if (step.dataTable?.rows) {
|
|
853
|
+
for (const row of step.dataTable.rows) {
|
|
854
|
+
for (const cell of row.cells) {
|
|
855
|
+
texts.push(cell.value);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return texts;
|
|
860
|
+
}
|
|
861
|
+
function unique2(values) {
|
|
862
|
+
return Array.from(new Set(values));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/core/traceability.ts
|
|
866
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
867
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
868
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
869
|
+
for (const file of scenarioFiles) {
|
|
870
|
+
const text = await readFile2(file, "utf-8");
|
|
871
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
872
|
+
if (!document || errors.length > 0) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
for (const scenario of document.scenarios) {
|
|
876
|
+
for (const tag of scenario.tags) {
|
|
877
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
878
|
+
scIds.add(tag);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return scIds;
|
|
884
|
+
}
|
|
885
|
+
async function collectScTestReferences(testsRoot) {
|
|
886
|
+
const refs = /* @__PURE__ */ new Map();
|
|
887
|
+
const testFiles = await collectFiles(testsRoot, {
|
|
888
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
889
|
+
});
|
|
890
|
+
for (const file of testFiles) {
|
|
891
|
+
const text = await readFile2(file, "utf-8");
|
|
892
|
+
const scIds = extractIds(text, "SC");
|
|
893
|
+
if (scIds.length === 0) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
for (const scId of scIds) {
|
|
897
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
898
|
+
current.add(file);
|
|
899
|
+
refs.set(scId, current);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return refs;
|
|
903
|
+
}
|
|
904
|
+
function buildScCoverage(scIds, refs) {
|
|
905
|
+
const sortedScIds = toSortedArray(scIds);
|
|
906
|
+
const refsRecord = {};
|
|
907
|
+
const missingIds = [];
|
|
908
|
+
let covered = 0;
|
|
909
|
+
for (const scId of sortedScIds) {
|
|
910
|
+
const files = refs.get(scId);
|
|
911
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
912
|
+
refsRecord[scId] = sortedFiles;
|
|
913
|
+
if (sortedFiles.length === 0) {
|
|
914
|
+
missingIds.push(scId);
|
|
915
|
+
} else {
|
|
916
|
+
covered += 1;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
total: sortedScIds.length,
|
|
921
|
+
covered,
|
|
922
|
+
missing: missingIds.length,
|
|
923
|
+
missingIds,
|
|
924
|
+
refs: refsRecord
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function toSortedArray(values) {
|
|
928
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
929
|
+
}
|
|
690
930
|
|
|
691
931
|
// src/core/version.ts
|
|
692
|
-
import { readFile as
|
|
932
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
693
933
|
import path7 from "path";
|
|
694
934
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
695
935
|
async function resolveToolVersion() {
|
|
696
|
-
if ("0.
|
|
697
|
-
return "0.
|
|
936
|
+
if ("0.4.0".length > 0) {
|
|
937
|
+
return "0.4.0";
|
|
698
938
|
}
|
|
699
939
|
try {
|
|
700
940
|
const packagePath = resolvePackageJsonPath();
|
|
701
|
-
const raw = await
|
|
941
|
+
const raw = await readFile3(packagePath, "utf-8");
|
|
702
942
|
const parsed = JSON.parse(raw);
|
|
703
943
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
704
944
|
return version.length > 0 ? version : "unknown";
|
|
@@ -713,7 +953,7 @@ function resolvePackageJsonPath() {
|
|
|
713
953
|
}
|
|
714
954
|
|
|
715
955
|
// src/core/validators/contracts.ts
|
|
716
|
-
import { readFile as
|
|
956
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
717
957
|
import path9 from "path";
|
|
718
958
|
|
|
719
959
|
// src/core/contracts.ts
|
|
@@ -791,7 +1031,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
791
1031
|
}
|
|
792
1032
|
const issues = [];
|
|
793
1033
|
for (const file of files) {
|
|
794
|
-
const text = await
|
|
1034
|
+
const text = await readFile4(file, "utf-8");
|
|
795
1035
|
const invalidIds = extractInvalidIds(text, [
|
|
796
1036
|
"SPEC",
|
|
797
1037
|
"BR",
|
|
@@ -820,7 +1060,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
820
1060
|
issues.push(
|
|
821
1061
|
issue(
|
|
822
1062
|
"QFAI-CONTRACT-001",
|
|
823
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1063
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
|
|
824
1064
|
"error",
|
|
825
1065
|
file,
|
|
826
1066
|
"contracts.ui.parse"
|
|
@@ -858,7 +1098,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
858
1098
|
}
|
|
859
1099
|
const issues = [];
|
|
860
1100
|
for (const file of files) {
|
|
861
|
-
const text = await
|
|
1101
|
+
const text = await readFile4(file, "utf-8");
|
|
862
1102
|
const invalidIds = extractInvalidIds(text, [
|
|
863
1103
|
"SPEC",
|
|
864
1104
|
"BR",
|
|
@@ -887,7 +1127,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
887
1127
|
issues.push(
|
|
888
1128
|
issue(
|
|
889
1129
|
"QFAI-CONTRACT-001",
|
|
890
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1130
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
|
|
891
1131
|
"error",
|
|
892
1132
|
file,
|
|
893
1133
|
"contracts.api.parse"
|
|
@@ -936,7 +1176,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
936
1176
|
}
|
|
937
1177
|
const issues = [];
|
|
938
1178
|
for (const file of files) {
|
|
939
|
-
const text = await
|
|
1179
|
+
const text = await readFile4(file, "utf-8");
|
|
940
1180
|
const invalidIds = extractInvalidIds(text, [
|
|
941
1181
|
"SPEC",
|
|
942
1182
|
"BR",
|
|
@@ -982,7 +1222,7 @@ function lintSql(text, file) {
|
|
|
982
1222
|
function hasOpenApi(doc) {
|
|
983
1223
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
984
1224
|
}
|
|
985
|
-
function
|
|
1225
|
+
function formatError3(error2) {
|
|
986
1226
|
if (error2 instanceof Error) {
|
|
987
1227
|
return error2.message;
|
|
988
1228
|
}
|
|
@@ -1007,7 +1247,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1007
1247
|
}
|
|
1008
1248
|
|
|
1009
1249
|
// src/core/validators/delta.ts
|
|
1010
|
-
import { readFile as
|
|
1250
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1011
1251
|
import path10 from "path";
|
|
1012
1252
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1013
1253
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -1025,7 +1265,7 @@ async function validateDeltas(root, config) {
|
|
|
1025
1265
|
const deltaPath = path10.join(pack, "delta.md");
|
|
1026
1266
|
let text;
|
|
1027
1267
|
try {
|
|
1028
|
-
text = await
|
|
1268
|
+
text = await readFile5(deltaPath, "utf-8");
|
|
1029
1269
|
} catch (error2) {
|
|
1030
1270
|
if (isMissingFileError2(error2)) {
|
|
1031
1271
|
issues.push(
|
|
@@ -1097,11 +1337,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1097
1337
|
}
|
|
1098
1338
|
|
|
1099
1339
|
// src/core/validators/ids.ts
|
|
1100
|
-
import { readFile as
|
|
1340
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1101
1341
|
import path12 from "path";
|
|
1102
1342
|
|
|
1103
1343
|
// src/core/contractIndex.ts
|
|
1104
|
-
import { readFile as
|
|
1344
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1105
1345
|
import path11 from "path";
|
|
1106
1346
|
async function buildContractIndex(root, config) {
|
|
1107
1347
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -1126,7 +1366,7 @@ async function buildContractIndex(root, config) {
|
|
|
1126
1366
|
}
|
|
1127
1367
|
async function indexUiContracts(files, index) {
|
|
1128
1368
|
for (const file of files) {
|
|
1129
|
-
const text = await
|
|
1369
|
+
const text = await readFile6(file, "utf-8");
|
|
1130
1370
|
try {
|
|
1131
1371
|
const doc = parseStructuredContract(file, text);
|
|
1132
1372
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1138,7 +1378,7 @@ async function indexUiContracts(files, index) {
|
|
|
1138
1378
|
}
|
|
1139
1379
|
async function indexApiContracts(files, index) {
|
|
1140
1380
|
for (const file of files) {
|
|
1141
|
-
const text = await
|
|
1381
|
+
const text = await readFile6(file, "utf-8");
|
|
1142
1382
|
try {
|
|
1143
1383
|
const doc = parseStructuredContract(file, text);
|
|
1144
1384
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1150,7 +1390,7 @@ async function indexApiContracts(files, index) {
|
|
|
1150
1390
|
}
|
|
1151
1391
|
async function indexDataContracts(files, index) {
|
|
1152
1392
|
for (const file of files) {
|
|
1153
|
-
const text = await
|
|
1393
|
+
const text = await readFile6(file, "utf-8");
|
|
1154
1394
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1155
1395
|
}
|
|
1156
1396
|
}
|
|
@@ -1279,166 +1519,8 @@ function parseSpec(md, file) {
|
|
|
1279
1519
|
return parsed;
|
|
1280
1520
|
}
|
|
1281
1521
|
|
|
1282
|
-
// src/core/gherkin/parse.ts
|
|
1283
|
-
import {
|
|
1284
|
-
AstBuilder,
|
|
1285
|
-
GherkinClassicTokenMatcher,
|
|
1286
|
-
Parser
|
|
1287
|
-
} from "@cucumber/gherkin";
|
|
1288
|
-
import { randomUUID } from "crypto";
|
|
1289
|
-
function parseGherkin(source, uri) {
|
|
1290
|
-
const errors = [];
|
|
1291
|
-
const uuidFn = () => randomUUID();
|
|
1292
|
-
const builder = new AstBuilder(uuidFn);
|
|
1293
|
-
const matcher = new GherkinClassicTokenMatcher();
|
|
1294
|
-
const parser = new Parser(builder, matcher);
|
|
1295
|
-
try {
|
|
1296
|
-
const gherkinDocument = parser.parse(source);
|
|
1297
|
-
gherkinDocument.uri = uri;
|
|
1298
|
-
return { gherkinDocument, errors };
|
|
1299
|
-
} catch (error2) {
|
|
1300
|
-
errors.push(formatError3(error2));
|
|
1301
|
-
return { gherkinDocument: null, errors };
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
function formatError3(error2) {
|
|
1305
|
-
if (error2 instanceof Error) {
|
|
1306
|
-
return error2.message;
|
|
1307
|
-
}
|
|
1308
|
-
return String(error2);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// src/core/scenarioModel.ts
|
|
1312
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1313
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1314
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1315
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1316
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1317
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1318
|
-
function parseScenarioDocument(text, uri) {
|
|
1319
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1320
|
-
if (!gherkinDocument) {
|
|
1321
|
-
return { document: null, errors };
|
|
1322
|
-
}
|
|
1323
|
-
const feature = gherkinDocument.feature;
|
|
1324
|
-
if (!feature) {
|
|
1325
|
-
return {
|
|
1326
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1327
|
-
errors
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1331
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1332
|
-
return {
|
|
1333
|
-
document: {
|
|
1334
|
-
uri,
|
|
1335
|
-
featureName: feature.name,
|
|
1336
|
-
featureTags,
|
|
1337
|
-
scenarios
|
|
1338
|
-
},
|
|
1339
|
-
errors
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
function buildScenarioAtoms(document) {
|
|
1343
|
-
return document.scenarios.map((scenario) => {
|
|
1344
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1345
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1346
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1347
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1348
|
-
scenario.tags.forEach((tag) => {
|
|
1349
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1350
|
-
contractIds.add(tag);
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
for (const step of scenario.steps) {
|
|
1354
|
-
for (const text of collectStepTexts(step)) {
|
|
1355
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1356
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1357
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
const atom = {
|
|
1361
|
-
uri: document.uri,
|
|
1362
|
-
featureName: document.featureName ?? "",
|
|
1363
|
-
scenarioName: scenario.name,
|
|
1364
|
-
kind: scenario.kind,
|
|
1365
|
-
brIds,
|
|
1366
|
-
contractIds: Array.from(contractIds).sort()
|
|
1367
|
-
};
|
|
1368
|
-
if (scenario.line !== void 0) {
|
|
1369
|
-
atom.line = scenario.line;
|
|
1370
|
-
}
|
|
1371
|
-
if (specIds.length === 1) {
|
|
1372
|
-
const specId = specIds[0];
|
|
1373
|
-
if (specId) {
|
|
1374
|
-
atom.specId = specId;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
if (scIds.length === 1) {
|
|
1378
|
-
const scId = scIds[0];
|
|
1379
|
-
if (scId) {
|
|
1380
|
-
atom.scId = scId;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
return atom;
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
function collectScenarioNodes(feature, featureTags) {
|
|
1387
|
-
const scenarios = [];
|
|
1388
|
-
for (const child of feature.children) {
|
|
1389
|
-
if (child.scenario) {
|
|
1390
|
-
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1391
|
-
}
|
|
1392
|
-
if (child.rule) {
|
|
1393
|
-
const ruleTags = collectTagNames(child.rule.tags);
|
|
1394
|
-
for (const ruleChild of child.rule.children) {
|
|
1395
|
-
if (ruleChild.scenario) {
|
|
1396
|
-
scenarios.push(
|
|
1397
|
-
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
return scenarios;
|
|
1404
|
-
}
|
|
1405
|
-
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1406
|
-
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1407
|
-
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1408
|
-
return {
|
|
1409
|
-
name: scenario.name,
|
|
1410
|
-
kind,
|
|
1411
|
-
line: scenario.location?.line,
|
|
1412
|
-
tags,
|
|
1413
|
-
steps: scenario.steps
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
function collectTagNames(tags) {
|
|
1417
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1418
|
-
}
|
|
1419
|
-
function collectStepTexts(step) {
|
|
1420
|
-
const texts = [];
|
|
1421
|
-
if (step.text) {
|
|
1422
|
-
texts.push(step.text);
|
|
1423
|
-
}
|
|
1424
|
-
if (step.docString?.content) {
|
|
1425
|
-
texts.push(step.docString.content);
|
|
1426
|
-
}
|
|
1427
|
-
if (step.dataTable?.rows) {
|
|
1428
|
-
for (const row of step.dataTable.rows) {
|
|
1429
|
-
for (const cell of row.cells) {
|
|
1430
|
-
texts.push(cell.value);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
return texts;
|
|
1435
|
-
}
|
|
1436
|
-
function unique2(values) {
|
|
1437
|
-
return Array.from(new Set(values));
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
1522
|
// src/core/validators/ids.ts
|
|
1441
|
-
var
|
|
1523
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1442
1524
|
async function validateDefinedIds(root, config) {
|
|
1443
1525
|
const issues = [];
|
|
1444
1526
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1472,7 +1554,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1472
1554
|
}
|
|
1473
1555
|
async function collectSpecDefinitionIds(files, out) {
|
|
1474
1556
|
for (const file of files) {
|
|
1475
|
-
const text = await
|
|
1557
|
+
const text = await readFile7(file, "utf-8");
|
|
1476
1558
|
const parsed = parseSpec(text, file);
|
|
1477
1559
|
if (parsed.specId) {
|
|
1478
1560
|
recordId(out, parsed.specId, file);
|
|
@@ -1482,14 +1564,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1482
1564
|
}
|
|
1483
1565
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1484
1566
|
for (const file of files) {
|
|
1485
|
-
const text = await
|
|
1567
|
+
const text = await readFile7(file, "utf-8");
|
|
1486
1568
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1487
1569
|
if (!document || errors.length > 0) {
|
|
1488
1570
|
continue;
|
|
1489
1571
|
}
|
|
1490
1572
|
for (const scenario of document.scenarios) {
|
|
1491
1573
|
for (const tag of scenario.tags) {
|
|
1492
|
-
if (
|
|
1574
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1493
1575
|
recordId(out, tag, file);
|
|
1494
1576
|
}
|
|
1495
1577
|
}
|
|
@@ -1526,11 +1608,11 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1526
1608
|
}
|
|
1527
1609
|
|
|
1528
1610
|
// src/core/validators/scenario.ts
|
|
1529
|
-
import { readFile as
|
|
1611
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1530
1612
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1531
1613
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1532
1614
|
var THEN_PATTERN = /\bThen\b/;
|
|
1533
|
-
var
|
|
1615
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1534
1616
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1535
1617
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1536
1618
|
async function validateScenarios(root, config) {
|
|
@@ -1553,7 +1635,7 @@ async function validateScenarios(root, config) {
|
|
|
1553
1635
|
for (const entry of entries) {
|
|
1554
1636
|
let text;
|
|
1555
1637
|
try {
|
|
1556
|
-
text = await
|
|
1638
|
+
text = await readFile8(entry.scenarioPath, "utf-8");
|
|
1557
1639
|
} catch (error2) {
|
|
1558
1640
|
if (isMissingFileError3(error2)) {
|
|
1559
1641
|
issues.push(
|
|
@@ -1650,6 +1732,17 @@ function validateScenarioContent(text, file) {
|
|
|
1650
1732
|
)
|
|
1651
1733
|
);
|
|
1652
1734
|
}
|
|
1735
|
+
if (document.scenarios.length > 1) {
|
|
1736
|
+
issues.push(
|
|
1737
|
+
issue4(
|
|
1738
|
+
"QFAI-SC-011",
|
|
1739
|
+
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1740
|
+
"error",
|
|
1741
|
+
file,
|
|
1742
|
+
"scenario.single"
|
|
1743
|
+
)
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1653
1746
|
for (const scenario of document.scenarios) {
|
|
1654
1747
|
if (scenario.tags.length === 0) {
|
|
1655
1748
|
issues.push(
|
|
@@ -1664,7 +1757,7 @@ function validateScenarioContent(text, file) {
|
|
|
1664
1757
|
continue;
|
|
1665
1758
|
}
|
|
1666
1759
|
const missingTags = [];
|
|
1667
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1760
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1668
1761
|
if (scTags.length === 0) {
|
|
1669
1762
|
missingTags.push("SC(0\u4EF6)");
|
|
1670
1763
|
} else if (scTags.length > 1) {
|
|
@@ -1739,7 +1832,7 @@ function isMissingFileError3(error2) {
|
|
|
1739
1832
|
}
|
|
1740
1833
|
|
|
1741
1834
|
// src/core/validators/spec.ts
|
|
1742
|
-
import { readFile as
|
|
1835
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1743
1836
|
async function validateSpecs(root, config) {
|
|
1744
1837
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1745
1838
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1760,7 +1853,7 @@ async function validateSpecs(root, config) {
|
|
|
1760
1853
|
for (const entry of entries) {
|
|
1761
1854
|
let text;
|
|
1762
1855
|
try {
|
|
1763
|
-
text = await
|
|
1856
|
+
text = await readFile9(entry.specPath, "utf-8");
|
|
1764
1857
|
} catch (error2) {
|
|
1765
1858
|
if (isMissingFileError4(error2)) {
|
|
1766
1859
|
issues.push(
|
|
@@ -1909,8 +2002,8 @@ function isMissingFileError4(error2) {
|
|
|
1909
2002
|
}
|
|
1910
2003
|
|
|
1911
2004
|
// src/core/validators/traceability.ts
|
|
1912
|
-
import { readFile as
|
|
1913
|
-
var
|
|
2005
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2006
|
+
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
1914
2007
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1915
2008
|
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1916
2009
|
async function validateTraceability(root, config) {
|
|
@@ -1931,7 +2024,7 @@ async function validateTraceability(root, config) {
|
|
|
1931
2024
|
const contractIndex = await buildContractIndex(root, config);
|
|
1932
2025
|
const contractIds = contractIndex.ids;
|
|
1933
2026
|
for (const file of specFiles) {
|
|
1934
|
-
const text = await
|
|
2027
|
+
const text = await readFile10(file, "utf-8");
|
|
1935
2028
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1936
2029
|
const parsed = parseSpec(text, file);
|
|
1937
2030
|
if (parsed.specId) {
|
|
@@ -1968,7 +2061,7 @@ async function validateTraceability(root, config) {
|
|
|
1968
2061
|
}
|
|
1969
2062
|
}
|
|
1970
2063
|
for (const file of scenarioFiles) {
|
|
1971
|
-
const text = await
|
|
2064
|
+
const text = await readFile10(file, "utf-8");
|
|
1972
2065
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1973
2066
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1974
2067
|
if (!document || errors.length > 0) {
|
|
@@ -1982,7 +2075,7 @@ async function validateTraceability(root, config) {
|
|
|
1982
2075
|
}
|
|
1983
2076
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1984
2077
|
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1985
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
2078
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
|
|
1986
2079
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1987
2080
|
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1988
2081
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
@@ -2110,6 +2203,25 @@ async function validateTraceability(root, config) {
|
|
|
2110
2203
|
);
|
|
2111
2204
|
}
|
|
2112
2205
|
}
|
|
2206
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2207
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2208
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2209
|
+
const refs = scTestRefs.get(id);
|
|
2210
|
+
return !refs || refs.size === 0;
|
|
2211
|
+
});
|
|
2212
|
+
if (scWithoutTests.length > 0) {
|
|
2213
|
+
issues.push(
|
|
2214
|
+
issue6(
|
|
2215
|
+
"QFAI-TRACE-010",
|
|
2216
|
+
`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`,
|
|
2217
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2218
|
+
testsRoot,
|
|
2219
|
+
"traceability.scMustHaveTest",
|
|
2220
|
+
scWithoutTests
|
|
2221
|
+
)
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2113
2225
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2114
2226
|
if (contractIds.size > 0) {
|
|
2115
2227
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -2158,7 +2270,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2158
2270
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2159
2271
|
let found = false;
|
|
2160
2272
|
for (const file of targetFiles) {
|
|
2161
|
-
const text = await
|
|
2273
|
+
const text = await readFile10(file, "utf-8");
|
|
2162
2274
|
if (pattern.test(text)) {
|
|
2163
2275
|
found = true;
|
|
2164
2276
|
break;
|
|
@@ -2214,7 +2326,6 @@ async function validateProject(root, configResult) {
|
|
|
2214
2326
|
];
|
|
2215
2327
|
const toolVersion = await resolveToolVersion();
|
|
2216
2328
|
return {
|
|
2217
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2218
2329
|
toolVersion,
|
|
2219
2330
|
issues,
|
|
2220
2331
|
counts: countIssues(issues)
|
|
@@ -2266,6 +2377,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2266
2377
|
srcRoot,
|
|
2267
2378
|
testsRoot
|
|
2268
2379
|
);
|
|
2380
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2381
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2382
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2269
2383
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2270
2384
|
const version = await resolveToolVersion();
|
|
2271
2385
|
return {
|
|
@@ -2294,7 +2408,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2294
2408
|
},
|
|
2295
2409
|
traceability: {
|
|
2296
2410
|
upstreamIdsFound: upstreamIds.size,
|
|
2297
|
-
referencedInCodeOrTests: traceability
|
|
2411
|
+
referencedInCodeOrTests: traceability,
|
|
2412
|
+
sc: scCoverage
|
|
2298
2413
|
},
|
|
2299
2414
|
issues: resolvedValidation.issues
|
|
2300
2415
|
};
|
|
@@ -2331,6 +2446,32 @@ function formatReportMarkdown(data) {
|
|
|
2331
2446
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2332
2447
|
);
|
|
2333
2448
|
lines.push("");
|
|
2449
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2450
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2451
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2452
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2453
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2454
|
+
lines.push("- missingIds: (none)");
|
|
2455
|
+
} else {
|
|
2456
|
+
lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
|
|
2457
|
+
}
|
|
2458
|
+
lines.push("");
|
|
2459
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2460
|
+
const scRefs = data.traceability.sc.refs;
|
|
2461
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2462
|
+
if (scIds.length === 0) {
|
|
2463
|
+
lines.push("- (none)");
|
|
2464
|
+
} else {
|
|
2465
|
+
for (const scId of scIds) {
|
|
2466
|
+
const refs = scRefs[scId] ?? [];
|
|
2467
|
+
if (refs.length === 0) {
|
|
2468
|
+
lines.push(`- ${scId}: (none)`);
|
|
2469
|
+
} else {
|
|
2470
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
lines.push("");
|
|
2334
2475
|
lines.push("## Hotspots");
|
|
2335
2476
|
const hotspots = buildHotspots(data.issues);
|
|
2336
2477
|
if (hotspots.length === 0) {
|
|
@@ -2385,25 +2526,25 @@ async function collectIds(files) {
|
|
|
2385
2526
|
DATA: /* @__PURE__ */ new Set()
|
|
2386
2527
|
};
|
|
2387
2528
|
for (const file of files) {
|
|
2388
|
-
const text = await
|
|
2529
|
+
const text = await readFile11(file, "utf-8");
|
|
2389
2530
|
for (const prefix of ID_PREFIXES2) {
|
|
2390
2531
|
const ids = extractIds(text, prefix);
|
|
2391
2532
|
ids.forEach((id) => result[prefix].add(id));
|
|
2392
2533
|
}
|
|
2393
2534
|
}
|
|
2394
2535
|
return {
|
|
2395
|
-
SPEC:
|
|
2396
|
-
BR:
|
|
2397
|
-
SC:
|
|
2398
|
-
UI:
|
|
2399
|
-
API:
|
|
2400
|
-
DATA:
|
|
2536
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2537
|
+
BR: toSortedArray2(result.BR),
|
|
2538
|
+
SC: toSortedArray2(result.SC),
|
|
2539
|
+
UI: toSortedArray2(result.UI),
|
|
2540
|
+
API: toSortedArray2(result.API),
|
|
2541
|
+
DATA: toSortedArray2(result.DATA)
|
|
2401
2542
|
};
|
|
2402
2543
|
}
|
|
2403
2544
|
async function collectUpstreamIds(files) {
|
|
2404
2545
|
const ids = /* @__PURE__ */ new Set();
|
|
2405
2546
|
for (const file of files) {
|
|
2406
|
-
const text = await
|
|
2547
|
+
const text = await readFile11(file, "utf-8");
|
|
2407
2548
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2408
2549
|
}
|
|
2409
2550
|
return ids;
|
|
@@ -2424,7 +2565,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2424
2565
|
}
|
|
2425
2566
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2426
2567
|
for (const file of targetFiles) {
|
|
2427
|
-
const text = await
|
|
2568
|
+
const text = await readFile11(file, "utf-8");
|
|
2428
2569
|
if (pattern.test(text)) {
|
|
2429
2570
|
return true;
|
|
2430
2571
|
}
|
|
@@ -2441,7 +2582,7 @@ function formatIdLine(label, values) {
|
|
|
2441
2582
|
}
|
|
2442
2583
|
return `- ${label}: ${values.join(", ")}`;
|
|
2443
2584
|
}
|
|
2444
|
-
function
|
|
2585
|
+
function toSortedArray2(values) {
|
|
2445
2586
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2446
2587
|
}
|
|
2447
2588
|
function buildHotspots(issues) {
|
|
@@ -2508,16 +2649,11 @@ async function runReport(options) {
|
|
|
2508
2649
|
info(`wrote report: ${outPath}`);
|
|
2509
2650
|
}
|
|
2510
2651
|
async function readValidationResult(inputPath) {
|
|
2511
|
-
const raw = await
|
|
2652
|
+
const raw = await readFile12(inputPath, "utf-8");
|
|
2512
2653
|
const parsed = JSON.parse(raw);
|
|
2513
2654
|
if (!isValidationResult(parsed)) {
|
|
2514
2655
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
2515
2656
|
}
|
|
2516
|
-
if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
|
|
2517
|
-
throw new Error(
|
|
2518
|
-
`validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
|
|
2519
|
-
);
|
|
2520
|
-
}
|
|
2521
2657
|
return parsed;
|
|
2522
2658
|
}
|
|
2523
2659
|
function isValidationResult(value) {
|
|
@@ -2525,9 +2661,6 @@ function isValidationResult(value) {
|
|
|
2525
2661
|
return false;
|
|
2526
2662
|
}
|
|
2527
2663
|
const record2 = value;
|
|
2528
|
-
if (typeof record2.schemaVersion !== "string") {
|
|
2529
|
-
return false;
|
|
2530
|
-
}
|
|
2531
2664
|
if (typeof record2.toolVersion !== "string") {
|
|
2532
2665
|
return false;
|
|
2533
2666
|
}
|