qfai 0.3.6 → 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/prompts/makeBusinessFlow.md +1 -1
- package/assets/init/.qfai/prompts/makeOverview.md +1 -1
- package/assets/init/.qfai/rules/conventions.md +1 -1
- 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/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +0 -7
- package/dist/cli/commands/report.js.map +1 -1
- 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/types.d.ts +0 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/core/validate.js +0 -2
- package/dist/core/validate.js.map +1 -1
- package/dist/core/validators/scenario.d.ts.map +1 -1
- package/dist/core/validators/scenario.js +3 -0
- package/dist/core/validators/scenario.js.map +1 -1
- 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.cjs
CHANGED
|
@@ -177,7 +177,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// src/cli/commands/report.ts
|
|
180
|
-
var
|
|
180
|
+
var import_promises16 = require("fs/promises");
|
|
181
181
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
182
182
|
|
|
183
183
|
// src/core/config.ts
|
|
@@ -210,6 +210,8 @@ var defaultConfig = {
|
|
|
210
210
|
traceability: {
|
|
211
211
|
brMustHaveSc: true,
|
|
212
212
|
scMustTouchContracts: true,
|
|
213
|
+
scMustHaveTest: true,
|
|
214
|
+
scNoTestSeverity: "error",
|
|
213
215
|
allowOrphanContracts: false,
|
|
214
216
|
unknownContractIdSeverity: "error"
|
|
215
217
|
}
|
|
@@ -389,6 +391,20 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
389
391
|
configPath,
|
|
390
392
|
issues
|
|
391
393
|
),
|
|
394
|
+
scMustHaveTest: readBoolean(
|
|
395
|
+
traceabilityRaw?.scMustHaveTest,
|
|
396
|
+
base.traceability.scMustHaveTest,
|
|
397
|
+
"validation.traceability.scMustHaveTest",
|
|
398
|
+
configPath,
|
|
399
|
+
issues
|
|
400
|
+
),
|
|
401
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
402
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
403
|
+
base.traceability.scNoTestSeverity,
|
|
404
|
+
"validation.traceability.scNoTestSeverity",
|
|
405
|
+
configPath,
|
|
406
|
+
issues
|
|
407
|
+
),
|
|
392
408
|
allowOrphanContracts: readBoolean(
|
|
393
409
|
traceabilityRaw?.allowOrphanContracts,
|
|
394
410
|
base.traceability.allowOrphanContracts,
|
|
@@ -514,7 +530,7 @@ function isRecord(value) {
|
|
|
514
530
|
}
|
|
515
531
|
|
|
516
532
|
// src/core/report.ts
|
|
517
|
-
var
|
|
533
|
+
var import_promises15 = require("fs/promises");
|
|
518
534
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
519
535
|
|
|
520
536
|
// src/core/discovery.ts
|
|
@@ -708,20 +724,240 @@ function isValidId(value, prefix) {
|
|
|
708
724
|
return strict.test(value);
|
|
709
725
|
}
|
|
710
726
|
|
|
711
|
-
// src/core/
|
|
712
|
-
var
|
|
727
|
+
// src/core/traceability.ts
|
|
728
|
+
var import_promises6 = require("fs/promises");
|
|
729
|
+
|
|
730
|
+
// src/core/gherkin/parse.ts
|
|
731
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
732
|
+
var import_node_crypto = require("crypto");
|
|
733
|
+
function parseGherkin(source, uri) {
|
|
734
|
+
const errors = [];
|
|
735
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
736
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
737
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
738
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
739
|
+
try {
|
|
740
|
+
const gherkinDocument = parser.parse(source);
|
|
741
|
+
gherkinDocument.uri = uri;
|
|
742
|
+
return { gherkinDocument, errors };
|
|
743
|
+
} catch (error2) {
|
|
744
|
+
errors.push(formatError2(error2));
|
|
745
|
+
return { gherkinDocument: null, errors };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function formatError2(error2) {
|
|
749
|
+
if (error2 instanceof Error) {
|
|
750
|
+
return error2.message;
|
|
751
|
+
}
|
|
752
|
+
return String(error2);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/core/scenarioModel.ts
|
|
756
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
757
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
758
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
759
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
760
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
761
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
762
|
+
function parseScenarioDocument(text, uri) {
|
|
763
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
764
|
+
if (!gherkinDocument) {
|
|
765
|
+
return { document: null, errors };
|
|
766
|
+
}
|
|
767
|
+
const feature = gherkinDocument.feature;
|
|
768
|
+
if (!feature) {
|
|
769
|
+
return {
|
|
770
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
771
|
+
errors
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const featureTags = collectTagNames(feature.tags);
|
|
775
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
776
|
+
return {
|
|
777
|
+
document: {
|
|
778
|
+
uri,
|
|
779
|
+
featureName: feature.name,
|
|
780
|
+
featureTags,
|
|
781
|
+
scenarios
|
|
782
|
+
},
|
|
783
|
+
errors
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function buildScenarioAtoms(document) {
|
|
787
|
+
return document.scenarios.map((scenario) => {
|
|
788
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
789
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
790
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
791
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
792
|
+
scenario.tags.forEach((tag) => {
|
|
793
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
794
|
+
contractIds.add(tag);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
for (const step of scenario.steps) {
|
|
798
|
+
for (const text of collectStepTexts(step)) {
|
|
799
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
800
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
801
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const atom = {
|
|
805
|
+
uri: document.uri,
|
|
806
|
+
featureName: document.featureName ?? "",
|
|
807
|
+
scenarioName: scenario.name,
|
|
808
|
+
kind: scenario.kind,
|
|
809
|
+
brIds,
|
|
810
|
+
contractIds: Array.from(contractIds).sort()
|
|
811
|
+
};
|
|
812
|
+
if (scenario.line !== void 0) {
|
|
813
|
+
atom.line = scenario.line;
|
|
814
|
+
}
|
|
815
|
+
if (specIds.length === 1) {
|
|
816
|
+
const specId = specIds[0];
|
|
817
|
+
if (specId) {
|
|
818
|
+
atom.specId = specId;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (scIds.length === 1) {
|
|
822
|
+
const scId = scIds[0];
|
|
823
|
+
if (scId) {
|
|
824
|
+
atom.scId = scId;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return atom;
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
831
|
+
const scenarios = [];
|
|
832
|
+
for (const child of feature.children) {
|
|
833
|
+
if (child.scenario) {
|
|
834
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
835
|
+
}
|
|
836
|
+
if (child.rule) {
|
|
837
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
838
|
+
for (const ruleChild of child.rule.children) {
|
|
839
|
+
if (ruleChild.scenario) {
|
|
840
|
+
scenarios.push(
|
|
841
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return scenarios;
|
|
848
|
+
}
|
|
849
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
850
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
851
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
852
|
+
return {
|
|
853
|
+
name: scenario.name,
|
|
854
|
+
kind,
|
|
855
|
+
line: scenario.location?.line,
|
|
856
|
+
tags,
|
|
857
|
+
steps: scenario.steps
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function collectTagNames(tags) {
|
|
861
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
862
|
+
}
|
|
863
|
+
function collectStepTexts(step) {
|
|
864
|
+
const texts = [];
|
|
865
|
+
if (step.text) {
|
|
866
|
+
texts.push(step.text);
|
|
867
|
+
}
|
|
868
|
+
if (step.docString?.content) {
|
|
869
|
+
texts.push(step.docString.content);
|
|
870
|
+
}
|
|
871
|
+
if (step.dataTable?.rows) {
|
|
872
|
+
for (const row of step.dataTable.rows) {
|
|
873
|
+
for (const cell of row.cells) {
|
|
874
|
+
texts.push(cell.value);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return texts;
|
|
879
|
+
}
|
|
880
|
+
function unique2(values) {
|
|
881
|
+
return Array.from(new Set(values));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/core/traceability.ts
|
|
885
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
886
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
887
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
888
|
+
for (const file of scenarioFiles) {
|
|
889
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
890
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
891
|
+
if (!document || errors.length > 0) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
for (const scenario of document.scenarios) {
|
|
895
|
+
for (const tag of scenario.tags) {
|
|
896
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
897
|
+
scIds.add(tag);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return scIds;
|
|
903
|
+
}
|
|
904
|
+
async function collectScTestReferences(testsRoot) {
|
|
905
|
+
const refs = /* @__PURE__ */ new Map();
|
|
906
|
+
const testFiles = await collectFiles(testsRoot, {
|
|
907
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
908
|
+
});
|
|
909
|
+
for (const file of testFiles) {
|
|
910
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
911
|
+
const scIds = extractIds(text, "SC");
|
|
912
|
+
if (scIds.length === 0) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
for (const scId of scIds) {
|
|
916
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
917
|
+
current.add(file);
|
|
918
|
+
refs.set(scId, current);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return refs;
|
|
922
|
+
}
|
|
923
|
+
function buildScCoverage(scIds, refs) {
|
|
924
|
+
const sortedScIds = toSortedArray(scIds);
|
|
925
|
+
const refsRecord = {};
|
|
926
|
+
const missingIds = [];
|
|
927
|
+
let covered = 0;
|
|
928
|
+
for (const scId of sortedScIds) {
|
|
929
|
+
const files = refs.get(scId);
|
|
930
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
931
|
+
refsRecord[scId] = sortedFiles;
|
|
932
|
+
if (sortedFiles.length === 0) {
|
|
933
|
+
missingIds.push(scId);
|
|
934
|
+
} else {
|
|
935
|
+
covered += 1;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
total: sortedScIds.length,
|
|
940
|
+
covered,
|
|
941
|
+
missing: missingIds.length,
|
|
942
|
+
missingIds,
|
|
943
|
+
refs: refsRecord
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function toSortedArray(values) {
|
|
947
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
948
|
+
}
|
|
713
949
|
|
|
714
950
|
// src/core/version.ts
|
|
715
|
-
var
|
|
951
|
+
var import_promises7 = require("fs/promises");
|
|
716
952
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
717
953
|
var import_node_url2 = require("url");
|
|
718
954
|
async function resolveToolVersion() {
|
|
719
|
-
if ("0.
|
|
720
|
-
return "0.
|
|
955
|
+
if ("0.4.0".length > 0) {
|
|
956
|
+
return "0.4.0";
|
|
721
957
|
}
|
|
722
958
|
try {
|
|
723
959
|
const packagePath = resolvePackageJsonPath();
|
|
724
|
-
const raw = await (0,
|
|
960
|
+
const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
|
|
725
961
|
const parsed = JSON.parse(raw);
|
|
726
962
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
727
963
|
return version.length > 0 ? version : "unknown";
|
|
@@ -736,7 +972,7 @@ function resolvePackageJsonPath() {
|
|
|
736
972
|
}
|
|
737
973
|
|
|
738
974
|
// src/core/validators/contracts.ts
|
|
739
|
-
var
|
|
975
|
+
var import_promises8 = require("fs/promises");
|
|
740
976
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
741
977
|
|
|
742
978
|
// src/core/contracts.ts
|
|
@@ -814,7 +1050,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
814
1050
|
}
|
|
815
1051
|
const issues = [];
|
|
816
1052
|
for (const file of files) {
|
|
817
|
-
const text = await (0,
|
|
1053
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
818
1054
|
const invalidIds = extractInvalidIds(text, [
|
|
819
1055
|
"SPEC",
|
|
820
1056
|
"BR",
|
|
@@ -843,7 +1079,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
843
1079
|
issues.push(
|
|
844
1080
|
issue(
|
|
845
1081
|
"QFAI-CONTRACT-001",
|
|
846
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1082
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
|
|
847
1083
|
"error",
|
|
848
1084
|
file,
|
|
849
1085
|
"contracts.ui.parse"
|
|
@@ -881,7 +1117,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
881
1117
|
}
|
|
882
1118
|
const issues = [];
|
|
883
1119
|
for (const file of files) {
|
|
884
|
-
const text = await (0,
|
|
1120
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
885
1121
|
const invalidIds = extractInvalidIds(text, [
|
|
886
1122
|
"SPEC",
|
|
887
1123
|
"BR",
|
|
@@ -910,7 +1146,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
910
1146
|
issues.push(
|
|
911
1147
|
issue(
|
|
912
1148
|
"QFAI-CONTRACT-001",
|
|
913
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1149
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError3(error2)})`,
|
|
914
1150
|
"error",
|
|
915
1151
|
file,
|
|
916
1152
|
"contracts.api.parse"
|
|
@@ -959,7 +1195,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
959
1195
|
}
|
|
960
1196
|
const issues = [];
|
|
961
1197
|
for (const file of files) {
|
|
962
|
-
const text = await (0,
|
|
1198
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
963
1199
|
const invalidIds = extractInvalidIds(text, [
|
|
964
1200
|
"SPEC",
|
|
965
1201
|
"BR",
|
|
@@ -1005,7 +1241,7 @@ function lintSql(text, file) {
|
|
|
1005
1241
|
function hasOpenApi(doc) {
|
|
1006
1242
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1007
1243
|
}
|
|
1008
|
-
function
|
|
1244
|
+
function formatError3(error2) {
|
|
1009
1245
|
if (error2 instanceof Error) {
|
|
1010
1246
|
return error2.message;
|
|
1011
1247
|
}
|
|
@@ -1030,7 +1266,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1030
1266
|
}
|
|
1031
1267
|
|
|
1032
1268
|
// src/core/validators/delta.ts
|
|
1033
|
-
var
|
|
1269
|
+
var import_promises9 = require("fs/promises");
|
|
1034
1270
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
1035
1271
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1036
1272
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
@@ -1048,7 +1284,7 @@ async function validateDeltas(root, config) {
|
|
|
1048
1284
|
const deltaPath = import_node_path10.default.join(pack, "delta.md");
|
|
1049
1285
|
let text;
|
|
1050
1286
|
try {
|
|
1051
|
-
text = await (0,
|
|
1287
|
+
text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
|
|
1052
1288
|
} catch (error2) {
|
|
1053
1289
|
if (isMissingFileError2(error2)) {
|
|
1054
1290
|
issues.push(
|
|
@@ -1120,11 +1356,11 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1120
1356
|
}
|
|
1121
1357
|
|
|
1122
1358
|
// src/core/validators/ids.ts
|
|
1123
|
-
var
|
|
1359
|
+
var import_promises11 = require("fs/promises");
|
|
1124
1360
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
1125
1361
|
|
|
1126
1362
|
// src/core/contractIndex.ts
|
|
1127
|
-
var
|
|
1363
|
+
var import_promises10 = require("fs/promises");
|
|
1128
1364
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
1129
1365
|
async function buildContractIndex(root, config) {
|
|
1130
1366
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
@@ -1149,7 +1385,7 @@ async function buildContractIndex(root, config) {
|
|
|
1149
1385
|
}
|
|
1150
1386
|
async function indexUiContracts(files, index) {
|
|
1151
1387
|
for (const file of files) {
|
|
1152
|
-
const text = await (0,
|
|
1388
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1153
1389
|
try {
|
|
1154
1390
|
const doc = parseStructuredContract(file, text);
|
|
1155
1391
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1161,7 +1397,7 @@ async function indexUiContracts(files, index) {
|
|
|
1161
1397
|
}
|
|
1162
1398
|
async function indexApiContracts(files, index) {
|
|
1163
1399
|
for (const file of files) {
|
|
1164
|
-
const text = await (0,
|
|
1400
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1165
1401
|
try {
|
|
1166
1402
|
const doc = parseStructuredContract(file, text);
|
|
1167
1403
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1173,7 +1409,7 @@ async function indexApiContracts(files, index) {
|
|
|
1173
1409
|
}
|
|
1174
1410
|
async function indexDataContracts(files, index) {
|
|
1175
1411
|
for (const file of files) {
|
|
1176
|
-
const text = await (0,
|
|
1412
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1177
1413
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1178
1414
|
}
|
|
1179
1415
|
}
|
|
@@ -1302,162 +1538,8 @@ function parseSpec(md, file) {
|
|
|
1302
1538
|
return parsed;
|
|
1303
1539
|
}
|
|
1304
1540
|
|
|
1305
|
-
// src/core/gherkin/parse.ts
|
|
1306
|
-
var import_gherkin = require("@cucumber/gherkin");
|
|
1307
|
-
var import_node_crypto = require("crypto");
|
|
1308
|
-
function parseGherkin(source, uri) {
|
|
1309
|
-
const errors = [];
|
|
1310
|
-
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1311
|
-
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1312
|
-
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1313
|
-
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1314
|
-
try {
|
|
1315
|
-
const gherkinDocument = parser.parse(source);
|
|
1316
|
-
gherkinDocument.uri = uri;
|
|
1317
|
-
return { gherkinDocument, errors };
|
|
1318
|
-
} catch (error2) {
|
|
1319
|
-
errors.push(formatError3(error2));
|
|
1320
|
-
return { gherkinDocument: null, errors };
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
function formatError3(error2) {
|
|
1324
|
-
if (error2 instanceof Error) {
|
|
1325
|
-
return error2.message;
|
|
1326
|
-
}
|
|
1327
|
-
return String(error2);
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// src/core/scenarioModel.ts
|
|
1331
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1332
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1333
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1334
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1335
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1336
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1337
|
-
function parseScenarioDocument(text, uri) {
|
|
1338
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1339
|
-
if (!gherkinDocument) {
|
|
1340
|
-
return { document: null, errors };
|
|
1341
|
-
}
|
|
1342
|
-
const feature = gherkinDocument.feature;
|
|
1343
|
-
if (!feature) {
|
|
1344
|
-
return {
|
|
1345
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1346
|
-
errors
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1350
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1351
|
-
return {
|
|
1352
|
-
document: {
|
|
1353
|
-
uri,
|
|
1354
|
-
featureName: feature.name,
|
|
1355
|
-
featureTags,
|
|
1356
|
-
scenarios
|
|
1357
|
-
},
|
|
1358
|
-
errors
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
function buildScenarioAtoms(document) {
|
|
1362
|
-
return document.scenarios.map((scenario) => {
|
|
1363
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1364
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1365
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1366
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1367
|
-
scenario.tags.forEach((tag) => {
|
|
1368
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1369
|
-
contractIds.add(tag);
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
for (const step of scenario.steps) {
|
|
1373
|
-
for (const text of collectStepTexts(step)) {
|
|
1374
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1375
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1376
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
const atom = {
|
|
1380
|
-
uri: document.uri,
|
|
1381
|
-
featureName: document.featureName ?? "",
|
|
1382
|
-
scenarioName: scenario.name,
|
|
1383
|
-
kind: scenario.kind,
|
|
1384
|
-
brIds,
|
|
1385
|
-
contractIds: Array.from(contractIds).sort()
|
|
1386
|
-
};
|
|
1387
|
-
if (scenario.line !== void 0) {
|
|
1388
|
-
atom.line = scenario.line;
|
|
1389
|
-
}
|
|
1390
|
-
if (specIds.length === 1) {
|
|
1391
|
-
const specId = specIds[0];
|
|
1392
|
-
if (specId) {
|
|
1393
|
-
atom.specId = specId;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
if (scIds.length === 1) {
|
|
1397
|
-
const scId = scIds[0];
|
|
1398
|
-
if (scId) {
|
|
1399
|
-
atom.scId = scId;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
return atom;
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
function collectScenarioNodes(feature, featureTags) {
|
|
1406
|
-
const scenarios = [];
|
|
1407
|
-
for (const child of feature.children) {
|
|
1408
|
-
if (child.scenario) {
|
|
1409
|
-
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1410
|
-
}
|
|
1411
|
-
if (child.rule) {
|
|
1412
|
-
const ruleTags = collectTagNames(child.rule.tags);
|
|
1413
|
-
for (const ruleChild of child.rule.children) {
|
|
1414
|
-
if (ruleChild.scenario) {
|
|
1415
|
-
scenarios.push(
|
|
1416
|
-
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
return scenarios;
|
|
1423
|
-
}
|
|
1424
|
-
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1425
|
-
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1426
|
-
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1427
|
-
return {
|
|
1428
|
-
name: scenario.name,
|
|
1429
|
-
kind,
|
|
1430
|
-
line: scenario.location?.line,
|
|
1431
|
-
tags,
|
|
1432
|
-
steps: scenario.steps
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
function collectTagNames(tags) {
|
|
1436
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1437
|
-
}
|
|
1438
|
-
function collectStepTexts(step) {
|
|
1439
|
-
const texts = [];
|
|
1440
|
-
if (step.text) {
|
|
1441
|
-
texts.push(step.text);
|
|
1442
|
-
}
|
|
1443
|
-
if (step.docString?.content) {
|
|
1444
|
-
texts.push(step.docString.content);
|
|
1445
|
-
}
|
|
1446
|
-
if (step.dataTable?.rows) {
|
|
1447
|
-
for (const row of step.dataTable.rows) {
|
|
1448
|
-
for (const cell of row.cells) {
|
|
1449
|
-
texts.push(cell.value);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
return texts;
|
|
1454
|
-
}
|
|
1455
|
-
function unique2(values) {
|
|
1456
|
-
return Array.from(new Set(values));
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
1541
|
// src/core/validators/ids.ts
|
|
1460
|
-
var
|
|
1542
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1461
1543
|
async function validateDefinedIds(root, config) {
|
|
1462
1544
|
const issues = [];
|
|
1463
1545
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1491,7 +1573,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1491
1573
|
}
|
|
1492
1574
|
async function collectSpecDefinitionIds(files, out) {
|
|
1493
1575
|
for (const file of files) {
|
|
1494
|
-
const text = await (0,
|
|
1576
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1495
1577
|
const parsed = parseSpec(text, file);
|
|
1496
1578
|
if (parsed.specId) {
|
|
1497
1579
|
recordId(out, parsed.specId, file);
|
|
@@ -1501,14 +1583,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1501
1583
|
}
|
|
1502
1584
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1503
1585
|
for (const file of files) {
|
|
1504
|
-
const text = await (0,
|
|
1586
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1505
1587
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1506
1588
|
if (!document || errors.length > 0) {
|
|
1507
1589
|
continue;
|
|
1508
1590
|
}
|
|
1509
1591
|
for (const scenario of document.scenarios) {
|
|
1510
1592
|
for (const tag of scenario.tags) {
|
|
1511
|
-
if (
|
|
1593
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1512
1594
|
recordId(out, tag, file);
|
|
1513
1595
|
}
|
|
1514
1596
|
}
|
|
@@ -1545,11 +1627,11 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1545
1627
|
}
|
|
1546
1628
|
|
|
1547
1629
|
// src/core/validators/scenario.ts
|
|
1548
|
-
var
|
|
1630
|
+
var import_promises12 = require("fs/promises");
|
|
1549
1631
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1550
1632
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1551
1633
|
var THEN_PATTERN = /\bThen\b/;
|
|
1552
|
-
var
|
|
1634
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1553
1635
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1554
1636
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1555
1637
|
async function validateScenarios(root, config) {
|
|
@@ -1572,7 +1654,7 @@ async function validateScenarios(root, config) {
|
|
|
1572
1654
|
for (const entry of entries) {
|
|
1573
1655
|
let text;
|
|
1574
1656
|
try {
|
|
1575
|
-
text = await (0,
|
|
1657
|
+
text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
|
|
1576
1658
|
} catch (error2) {
|
|
1577
1659
|
if (isMissingFileError3(error2)) {
|
|
1578
1660
|
issues.push(
|
|
@@ -1669,6 +1751,17 @@ function validateScenarioContent(text, file) {
|
|
|
1669
1751
|
)
|
|
1670
1752
|
);
|
|
1671
1753
|
}
|
|
1754
|
+
if (document.scenarios.length > 1) {
|
|
1755
|
+
issues.push(
|
|
1756
|
+
issue4(
|
|
1757
|
+
"QFAI-SC-011",
|
|
1758
|
+
`Scenario \u306F1\u3064\u306E\u307F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u691C\u51FA: ${document.scenarios.length}\u4EF6\uFF09`,
|
|
1759
|
+
"error",
|
|
1760
|
+
file,
|
|
1761
|
+
"scenario.single"
|
|
1762
|
+
)
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1672
1765
|
for (const scenario of document.scenarios) {
|
|
1673
1766
|
if (scenario.tags.length === 0) {
|
|
1674
1767
|
issues.push(
|
|
@@ -1683,7 +1776,7 @@ function validateScenarioContent(text, file) {
|
|
|
1683
1776
|
continue;
|
|
1684
1777
|
}
|
|
1685
1778
|
const missingTags = [];
|
|
1686
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1779
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1687
1780
|
if (scTags.length === 0) {
|
|
1688
1781
|
missingTags.push("SC(0\u4EF6)");
|
|
1689
1782
|
} else if (scTags.length > 1) {
|
|
@@ -1758,7 +1851,7 @@ function isMissingFileError3(error2) {
|
|
|
1758
1851
|
}
|
|
1759
1852
|
|
|
1760
1853
|
// src/core/validators/spec.ts
|
|
1761
|
-
var
|
|
1854
|
+
var import_promises13 = require("fs/promises");
|
|
1762
1855
|
async function validateSpecs(root, config) {
|
|
1763
1856
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1764
1857
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1779,7 +1872,7 @@ async function validateSpecs(root, config) {
|
|
|
1779
1872
|
for (const entry of entries) {
|
|
1780
1873
|
let text;
|
|
1781
1874
|
try {
|
|
1782
|
-
text = await (0,
|
|
1875
|
+
text = await (0, import_promises13.readFile)(entry.specPath, "utf-8");
|
|
1783
1876
|
} catch (error2) {
|
|
1784
1877
|
if (isMissingFileError4(error2)) {
|
|
1785
1878
|
issues.push(
|
|
@@ -1928,8 +2021,8 @@ function isMissingFileError4(error2) {
|
|
|
1928
2021
|
}
|
|
1929
2022
|
|
|
1930
2023
|
// src/core/validators/traceability.ts
|
|
1931
|
-
var
|
|
1932
|
-
var
|
|
2024
|
+
var import_promises14 = require("fs/promises");
|
|
2025
|
+
var SC_TAG_RE5 = /^SC-\d{4}$/;
|
|
1933
2026
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1934
2027
|
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1935
2028
|
async function validateTraceability(root, config) {
|
|
@@ -1950,7 +2043,7 @@ async function validateTraceability(root, config) {
|
|
|
1950
2043
|
const contractIndex = await buildContractIndex(root, config);
|
|
1951
2044
|
const contractIds = contractIndex.ids;
|
|
1952
2045
|
for (const file of specFiles) {
|
|
1953
|
-
const text = await (0,
|
|
2046
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
1954
2047
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1955
2048
|
const parsed = parseSpec(text, file);
|
|
1956
2049
|
if (parsed.specId) {
|
|
@@ -1987,7 +2080,7 @@ async function validateTraceability(root, config) {
|
|
|
1987
2080
|
}
|
|
1988
2081
|
}
|
|
1989
2082
|
for (const file of scenarioFiles) {
|
|
1990
|
-
const text = await (0,
|
|
2083
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
1991
2084
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1992
2085
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1993
2086
|
if (!document || errors.length > 0) {
|
|
@@ -2001,7 +2094,7 @@ async function validateTraceability(root, config) {
|
|
|
2001
2094
|
}
|
|
2002
2095
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
2003
2096
|
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
2004
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
2097
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE5.test(tag));
|
|
2005
2098
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
2006
2099
|
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
2007
2100
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
@@ -2129,6 +2222,25 @@ async function validateTraceability(root, config) {
|
|
|
2129
2222
|
);
|
|
2130
2223
|
}
|
|
2131
2224
|
}
|
|
2225
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2226
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2227
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2228
|
+
const refs = scTestRefs.get(id);
|
|
2229
|
+
return !refs || refs.size === 0;
|
|
2230
|
+
});
|
|
2231
|
+
if (scWithoutTests.length > 0) {
|
|
2232
|
+
issues.push(
|
|
2233
|
+
issue6(
|
|
2234
|
+
"QFAI-TRACE-010",
|
|
2235
|
+
`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`,
|
|
2236
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2237
|
+
testsRoot,
|
|
2238
|
+
"traceability.scMustHaveTest",
|
|
2239
|
+
scWithoutTests
|
|
2240
|
+
)
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2132
2244
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2133
2245
|
if (contractIds.size > 0) {
|
|
2134
2246
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -2177,7 +2289,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2177
2289
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2178
2290
|
let found = false;
|
|
2179
2291
|
for (const file of targetFiles) {
|
|
2180
|
-
const text = await (0,
|
|
2292
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2181
2293
|
if (pattern.test(text)) {
|
|
2182
2294
|
found = true;
|
|
2183
2295
|
break;
|
|
@@ -2233,7 +2345,6 @@ async function validateProject(root, configResult) {
|
|
|
2233
2345
|
];
|
|
2234
2346
|
const toolVersion = await resolveToolVersion();
|
|
2235
2347
|
return {
|
|
2236
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2237
2348
|
toolVersion,
|
|
2238
2349
|
issues,
|
|
2239
2350
|
counts: countIssues(issues)
|
|
@@ -2285,6 +2396,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2285
2396
|
srcRoot,
|
|
2286
2397
|
testsRoot
|
|
2287
2398
|
);
|
|
2399
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2400
|
+
const scTestRefs = await collectScTestReferences(testsRoot);
|
|
2401
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2288
2402
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2289
2403
|
const version = await resolveToolVersion();
|
|
2290
2404
|
return {
|
|
@@ -2313,7 +2427,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2313
2427
|
},
|
|
2314
2428
|
traceability: {
|
|
2315
2429
|
upstreamIdsFound: upstreamIds.size,
|
|
2316
|
-
referencedInCodeOrTests: traceability
|
|
2430
|
+
referencedInCodeOrTests: traceability,
|
|
2431
|
+
sc: scCoverage
|
|
2317
2432
|
},
|
|
2318
2433
|
issues: resolvedValidation.issues
|
|
2319
2434
|
};
|
|
@@ -2350,6 +2465,32 @@ function formatReportMarkdown(data) {
|
|
|
2350
2465
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2351
2466
|
);
|
|
2352
2467
|
lines.push("");
|
|
2468
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2469
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2470
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2471
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2472
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2473
|
+
lines.push("- missingIds: (none)");
|
|
2474
|
+
} else {
|
|
2475
|
+
lines.push(`- missingIds: ${data.traceability.sc.missingIds.join(", ")}`);
|
|
2476
|
+
}
|
|
2477
|
+
lines.push("");
|
|
2478
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2479
|
+
const scRefs = data.traceability.sc.refs;
|
|
2480
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2481
|
+
if (scIds.length === 0) {
|
|
2482
|
+
lines.push("- (none)");
|
|
2483
|
+
} else {
|
|
2484
|
+
for (const scId of scIds) {
|
|
2485
|
+
const refs = scRefs[scId] ?? [];
|
|
2486
|
+
if (refs.length === 0) {
|
|
2487
|
+
lines.push(`- ${scId}: (none)`);
|
|
2488
|
+
} else {
|
|
2489
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
lines.push("");
|
|
2353
2494
|
lines.push("## Hotspots");
|
|
2354
2495
|
const hotspots = buildHotspots(data.issues);
|
|
2355
2496
|
if (hotspots.length === 0) {
|
|
@@ -2404,25 +2545,25 @@ async function collectIds(files) {
|
|
|
2404
2545
|
DATA: /* @__PURE__ */ new Set()
|
|
2405
2546
|
};
|
|
2406
2547
|
for (const file of files) {
|
|
2407
|
-
const text = await (0,
|
|
2548
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2408
2549
|
for (const prefix of ID_PREFIXES2) {
|
|
2409
2550
|
const ids = extractIds(text, prefix);
|
|
2410
2551
|
ids.forEach((id) => result[prefix].add(id));
|
|
2411
2552
|
}
|
|
2412
2553
|
}
|
|
2413
2554
|
return {
|
|
2414
|
-
SPEC:
|
|
2415
|
-
BR:
|
|
2416
|
-
SC:
|
|
2417
|
-
UI:
|
|
2418
|
-
API:
|
|
2419
|
-
DATA:
|
|
2555
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2556
|
+
BR: toSortedArray2(result.BR),
|
|
2557
|
+
SC: toSortedArray2(result.SC),
|
|
2558
|
+
UI: toSortedArray2(result.UI),
|
|
2559
|
+
API: toSortedArray2(result.API),
|
|
2560
|
+
DATA: toSortedArray2(result.DATA)
|
|
2420
2561
|
};
|
|
2421
2562
|
}
|
|
2422
2563
|
async function collectUpstreamIds(files) {
|
|
2423
2564
|
const ids = /* @__PURE__ */ new Set();
|
|
2424
2565
|
for (const file of files) {
|
|
2425
|
-
const text = await (0,
|
|
2566
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2426
2567
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2427
2568
|
}
|
|
2428
2569
|
return ids;
|
|
@@ -2443,7 +2584,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2443
2584
|
}
|
|
2444
2585
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2445
2586
|
for (const file of targetFiles) {
|
|
2446
|
-
const text = await (0,
|
|
2587
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2447
2588
|
if (pattern.test(text)) {
|
|
2448
2589
|
return true;
|
|
2449
2590
|
}
|
|
@@ -2460,7 +2601,7 @@ function formatIdLine(label, values) {
|
|
|
2460
2601
|
}
|
|
2461
2602
|
return `- ${label}: ${values.join(", ")}`;
|
|
2462
2603
|
}
|
|
2463
|
-
function
|
|
2604
|
+
function toSortedArray2(values) {
|
|
2464
2605
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2465
2606
|
}
|
|
2466
2607
|
function buildHotspots(issues) {
|
|
@@ -2518,8 +2659,8 @@ async function runReport(options) {
|
|
|
2518
2659
|
const defaultOut = options.format === "json" ? import_node_path14.default.join(outRoot, "report.json") : import_node_path14.default.join(outRoot, "report.md");
|
|
2519
2660
|
const out = options.outPath ?? defaultOut;
|
|
2520
2661
|
const outPath = import_node_path14.default.isAbsolute(out) ? out : import_node_path14.default.resolve(root, out);
|
|
2521
|
-
await (0,
|
|
2522
|
-
await (0,
|
|
2662
|
+
await (0, import_promises16.mkdir)(import_node_path14.default.dirname(outPath), { recursive: true });
|
|
2663
|
+
await (0, import_promises16.writeFile)(outPath, `${output}
|
|
2523
2664
|
`, "utf-8");
|
|
2524
2665
|
info(
|
|
2525
2666
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -2527,16 +2668,11 @@ async function runReport(options) {
|
|
|
2527
2668
|
info(`wrote report: ${outPath}`);
|
|
2528
2669
|
}
|
|
2529
2670
|
async function readValidationResult(inputPath) {
|
|
2530
|
-
const raw = await (0,
|
|
2671
|
+
const raw = await (0, import_promises16.readFile)(inputPath, "utf-8");
|
|
2531
2672
|
const parsed = JSON.parse(raw);
|
|
2532
2673
|
if (!isValidationResult(parsed)) {
|
|
2533
2674
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
2534
2675
|
}
|
|
2535
|
-
if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
|
|
2536
|
-
throw new Error(
|
|
2537
|
-
`validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
|
|
2538
|
-
);
|
|
2539
|
-
}
|
|
2540
2676
|
return parsed;
|
|
2541
2677
|
}
|
|
2542
2678
|
function isValidationResult(value) {
|
|
@@ -2544,9 +2680,6 @@ function isValidationResult(value) {
|
|
|
2544
2680
|
return false;
|
|
2545
2681
|
}
|
|
2546
2682
|
const record2 = value;
|
|
2547
|
-
if (typeof record2.schemaVersion !== "string") {
|
|
2548
|
-
return false;
|
|
2549
|
-
}
|
|
2550
2683
|
if (typeof record2.toolVersion !== "string") {
|
|
2551
2684
|
return false;
|
|
2552
2685
|
}
|
|
@@ -2568,7 +2701,7 @@ function isMissingFileError5(error2) {
|
|
|
2568
2701
|
}
|
|
2569
2702
|
|
|
2570
2703
|
// src/cli/commands/validate.ts
|
|
2571
|
-
var
|
|
2704
|
+
var import_promises17 = require("fs/promises");
|
|
2572
2705
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
2573
2706
|
|
|
2574
2707
|
// src/cli/lib/failOn.ts
|
|
@@ -2634,8 +2767,8 @@ function emitGitHub(issue7) {
|
|
|
2634
2767
|
}
|
|
2635
2768
|
async function emitJson(result, root, jsonPath) {
|
|
2636
2769
|
const abs = import_node_path15.default.isAbsolute(jsonPath) ? jsonPath : import_node_path15.default.resolve(root, jsonPath);
|
|
2637
|
-
await (0,
|
|
2638
|
-
await (0,
|
|
2770
|
+
await (0, import_promises17.mkdir)(import_node_path15.default.dirname(abs), { recursive: true });
|
|
2771
|
+
await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
2639
2772
|
`, "utf-8");
|
|
2640
2773
|
}
|
|
2641
2774
|
|