qfai 0.3.8 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/assets/init/.qfai/README.md +5 -0
- package/assets/init/.qfai/prompts/README.md +1 -0
- package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
- package/assets/init/.qfai/specs/README.md +1 -1
- package/assets/init/root/qfai.config.yaml +8 -0
- package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -0
- package/dist/cli/index.cjs +717 -364
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +719 -366
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +710 -351
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +156 -2
- package/dist/index.mjs +714 -354
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/dist/cli/commands/init.d.ts +0 -8
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js +0 -30
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/commands/report.d.ts +0 -7
- package/dist/cli/commands/report.d.ts.map +0 -1
- package/dist/cli/commands/report.js +0 -80
- package/dist/cli/commands/report.js.map +0 -1
- package/dist/cli/commands/validate.d.ts +0 -9
- package/dist/cli/commands/validate.d.ts.map +0 -1
- package/dist/cli/commands/validate.js +0 -57
- package/dist/cli/commands/validate.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -7
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/lib/args.d.ts +0 -18
- package/dist/cli/lib/args.d.ts.map +0 -1
- package/dist/cli/lib/args.js +0 -98
- package/dist/cli/lib/args.js.map +0 -1
- package/dist/cli/lib/assets.d.ts +0 -2
- package/dist/cli/lib/assets.d.ts.map +0 -1
- package/dist/cli/lib/assets.js +0 -24
- package/dist/cli/lib/assets.js.map +0 -1
- package/dist/cli/lib/failOn.d.ts +0 -5
- package/dist/cli/lib/failOn.d.ts.map +0 -1
- package/dist/cli/lib/failOn.js +0 -10
- package/dist/cli/lib/failOn.js.map +0 -1
- package/dist/cli/lib/fs.d.ts +0 -11
- package/dist/cli/lib/fs.d.ts.map +0 -1
- package/dist/cli/lib/fs.js +0 -91
- package/dist/cli/lib/fs.js.map +0 -1
- package/dist/cli/lib/logger.d.ts +0 -4
- package/dist/cli/lib/logger.d.ts.map +0 -1
- package/dist/cli/lib/logger.js +0 -10
- package/dist/cli/lib/logger.js.map +0 -1
- package/dist/cli/main.d.ts +0 -2
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -66
- package/dist/cli/main.js.map +0 -1
- package/dist/core/config.d.ts +0 -44
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -218
- package/dist/core/config.js.map +0 -1
- package/dist/core/contractIndex.d.ts +0 -13
- package/dist/core/contractIndex.d.ts.map +0 -1
- package/dist/core/contractIndex.js +0 -66
- package/dist/core/contractIndex.js.map +0 -1
- package/dist/core/contracts.d.ts +0 -5
- package/dist/core/contracts.d.ts.map +0 -1
- package/dist/core/contracts.js +0 -42
- package/dist/core/contracts.js.map +0 -1
- package/dist/core/discovery.d.ts +0 -14
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -55
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/fs.d.ts +0 -6
- package/dist/core/fs.d.ts.map +0 -1
- package/dist/core/fs.js +0 -55
- package/dist/core/fs.js.map +0 -1
- package/dist/core/gherkin/parse.d.ts +0 -7
- package/dist/core/gherkin/parse.d.ts.map +0 -1
- package/dist/core/gherkin/parse.js +0 -25
- package/dist/core/gherkin/parse.js.map +0 -1
- package/dist/core/ids.d.ts +0 -6
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -52
- package/dist/core/ids.js.map +0 -1
- package/dist/core/index.d.ts +0 -13
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -13
- package/dist/core/index.js.map +0 -1
- package/dist/core/parse/adr.d.ts +0 -13
- package/dist/core/parse/adr.d.ts.map +0 -1
- package/dist/core/parse/adr.js +0 -33
- package/dist/core/parse/adr.js.map +0 -1
- package/dist/core/parse/gherkin.d.ts +0 -12
- package/dist/core/parse/gherkin.d.ts.map +0 -1
- package/dist/core/parse/gherkin.js +0 -22
- package/dist/core/parse/gherkin.js.map +0 -1
- package/dist/core/parse/markdown.d.ts +0 -14
- package/dist/core/parse/markdown.d.ts.map +0 -1
- package/dist/core/parse/markdown.js +0 -45
- package/dist/core/parse/markdown.js.map +0 -1
- package/dist/core/parse/spec.d.ts +0 -28
- package/dist/core/parse/spec.d.ts.map +0 -1
- package/dist/core/parse/spec.js +0 -80
- package/dist/core/parse/spec.js.map +0 -1
- package/dist/core/report.d.ts +0 -39
- package/dist/core/report.d.ts.map +0 -1
- package/dist/core/report.js +0 -226
- package/dist/core/report.js.map +0 -1
- package/dist/core/scenarioModel.d.ts +0 -33
- package/dist/core/scenarioModel.d.ts.map +0 -1
- package/dist/core/scenarioModel.js +0 -130
- package/dist/core/scenarioModel.js.map +0 -1
- package/dist/core/specLayout.d.ts +0 -8
- package/dist/core/specLayout.d.ts.map +0 -1
- package/dist/core/specLayout.js +0 -36
- package/dist/core/specLayout.js.map +0 -1
- package/dist/core/types.d.ts +0 -25
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -2
- package/dist/core/types.js.map +0 -1
- package/dist/core/validate.d.ts +0 -4
- package/dist/core/validate.d.ts.map +0 -1
- package/dist/core/validate.js +0 -34
- package/dist/core/validate.js.map +0 -1
- package/dist/core/validators/contracts.d.ts +0 -5
- package/dist/core/validators/contracts.d.ts.map +0 -1
- package/dist/core/validators/contracts.js +0 -162
- package/dist/core/validators/contracts.js.map +0 -1
- package/dist/core/validators/delta.d.ts +0 -4
- package/dist/core/validators/delta.d.ts.map +0 -1
- package/dist/core/validators/delta.js +0 -68
- package/dist/core/validators/delta.js.map +0 -1
- package/dist/core/validators/ids.d.ts +0 -4
- package/dist/core/validators/ids.d.ts.map +0 -1
- package/dist/core/validators/ids.js +0 -88
- package/dist/core/validators/ids.js.map +0 -1
- package/dist/core/validators/scenario.d.ts +0 -5
- package/dist/core/validators/scenario.d.ts.map +0 -1
- package/dist/core/validators/scenario.js +0 -140
- package/dist/core/validators/scenario.js.map +0 -1
- package/dist/core/validators/spec.d.ts +0 -5
- package/dist/core/validators/spec.d.ts.map +0 -1
- package/dist/core/validators/spec.js +0 -94
- package/dist/core/validators/spec.js.map +0 -1
- package/dist/core/validators/traceability.d.ts +0 -4
- package/dist/core/validators/traceability.d.ts.map +0 -1
- package/dist/core/validators/traceability.js +0 -180
- package/dist/core/validators/traceability.js.map +0 -1
- package/dist/core/version.d.ts +0 -2
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js +0 -25
- package/dist/core/version.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
VALIDATION_SCHEMA_VERSION: () => VALIDATION_SCHEMA_VERSION,
|
|
34
33
|
createReportData: () => createReportData,
|
|
35
34
|
defaultConfig: () => defaultConfig,
|
|
36
35
|
extractAllIds: () => extractAllIds,
|
|
@@ -85,6 +84,10 @@ var defaultConfig = {
|
|
|
85
84
|
traceability: {
|
|
86
85
|
brMustHaveSc: true,
|
|
87
86
|
scMustTouchContracts: true,
|
|
87
|
+
scMustHaveTest: true,
|
|
88
|
+
testFileGlobs: [],
|
|
89
|
+
testFileExcludeGlobs: [],
|
|
90
|
+
scNoTestSeverity: "error",
|
|
88
91
|
allowOrphanContracts: false,
|
|
89
92
|
unknownContractIdSeverity: "error"
|
|
90
93
|
}
|
|
@@ -264,6 +267,34 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
264
267
|
configPath,
|
|
265
268
|
issues
|
|
266
269
|
),
|
|
270
|
+
scMustHaveTest: readBoolean(
|
|
271
|
+
traceabilityRaw?.scMustHaveTest,
|
|
272
|
+
base.traceability.scMustHaveTest,
|
|
273
|
+
"validation.traceability.scMustHaveTest",
|
|
274
|
+
configPath,
|
|
275
|
+
issues
|
|
276
|
+
),
|
|
277
|
+
testFileGlobs: readStringArray(
|
|
278
|
+
traceabilityRaw?.testFileGlobs,
|
|
279
|
+
base.traceability.testFileGlobs,
|
|
280
|
+
"validation.traceability.testFileGlobs",
|
|
281
|
+
configPath,
|
|
282
|
+
issues
|
|
283
|
+
),
|
|
284
|
+
testFileExcludeGlobs: readStringArray(
|
|
285
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
286
|
+
base.traceability.testFileExcludeGlobs,
|
|
287
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
288
|
+
configPath,
|
|
289
|
+
issues
|
|
290
|
+
),
|
|
291
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
292
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
293
|
+
base.traceability.scNoTestSeverity,
|
|
294
|
+
"validation.traceability.scNoTestSeverity",
|
|
295
|
+
configPath,
|
|
296
|
+
issues
|
|
297
|
+
),
|
|
267
298
|
allowOrphanContracts: readBoolean(
|
|
268
299
|
traceabilityRaw?.allowOrphanContracts,
|
|
269
300
|
base.traceability.allowOrphanContracts,
|
|
@@ -442,8 +473,8 @@ function isValidId(value, prefix) {
|
|
|
442
473
|
}
|
|
443
474
|
|
|
444
475
|
// src/core/report.ts
|
|
445
|
-
var
|
|
446
|
-
var
|
|
476
|
+
var import_promises14 = require("fs/promises");
|
|
477
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
447
478
|
|
|
448
479
|
// src/core/discovery.ts
|
|
449
480
|
var import_promises4 = require("fs/promises");
|
|
@@ -451,6 +482,7 @@ var import_promises4 = require("fs/promises");
|
|
|
451
482
|
// src/core/fs.ts
|
|
452
483
|
var import_promises2 = require("fs/promises");
|
|
453
484
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
485
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
454
486
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
455
487
|
"node_modules",
|
|
456
488
|
".git",
|
|
@@ -472,6 +504,18 @@ async function collectFiles(root, options = {}) {
|
|
|
472
504
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
473
505
|
return entries;
|
|
474
506
|
}
|
|
507
|
+
async function collectFilesByGlobs(root, options) {
|
|
508
|
+
if (options.globs.length === 0) {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
return (0, import_fast_glob.default)(options.globs, {
|
|
512
|
+
cwd: root,
|
|
513
|
+
ignore: options.ignore ?? [],
|
|
514
|
+
onlyFiles: true,
|
|
515
|
+
absolute: true,
|
|
516
|
+
unique: true
|
|
517
|
+
});
|
|
518
|
+
}
|
|
475
519
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
476
520
|
const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
|
|
477
521
|
for (const item of items) {
|
|
@@ -583,20 +627,331 @@ async function exists2(target) {
|
|
|
583
627
|
}
|
|
584
628
|
}
|
|
585
629
|
|
|
586
|
-
// src/core/
|
|
587
|
-
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
588
|
-
|
|
589
|
-
// src/core/version.ts
|
|
630
|
+
// src/core/traceability.ts
|
|
590
631
|
var import_promises5 = require("fs/promises");
|
|
591
632
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
633
|
+
|
|
634
|
+
// src/core/gherkin/parse.ts
|
|
635
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
636
|
+
var import_node_crypto = require("crypto");
|
|
637
|
+
function parseGherkin(source, uri) {
|
|
638
|
+
const errors = [];
|
|
639
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
640
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
641
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
642
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
643
|
+
try {
|
|
644
|
+
const gherkinDocument = parser.parse(source);
|
|
645
|
+
gherkinDocument.uri = uri;
|
|
646
|
+
return { gherkinDocument, errors };
|
|
647
|
+
} catch (error) {
|
|
648
|
+
errors.push(formatError2(error));
|
|
649
|
+
return { gherkinDocument: null, errors };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function formatError2(error) {
|
|
653
|
+
if (error instanceof Error) {
|
|
654
|
+
return error.message;
|
|
655
|
+
}
|
|
656
|
+
return String(error);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/core/scenarioModel.ts
|
|
660
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
661
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
662
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
663
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
664
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
665
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
666
|
+
function parseScenarioDocument(text, uri) {
|
|
667
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
668
|
+
if (!gherkinDocument) {
|
|
669
|
+
return { document: null, errors };
|
|
670
|
+
}
|
|
671
|
+
const feature = gherkinDocument.feature;
|
|
672
|
+
if (!feature) {
|
|
673
|
+
return {
|
|
674
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
675
|
+
errors
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const featureTags = collectTagNames(feature.tags);
|
|
679
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
680
|
+
return {
|
|
681
|
+
document: {
|
|
682
|
+
uri,
|
|
683
|
+
featureName: feature.name,
|
|
684
|
+
featureTags,
|
|
685
|
+
scenarios
|
|
686
|
+
},
|
|
687
|
+
errors
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function buildScenarioAtoms(document) {
|
|
691
|
+
return document.scenarios.map((scenario) => {
|
|
692
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
693
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
694
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
695
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
696
|
+
scenario.tags.forEach((tag) => {
|
|
697
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
698
|
+
contractIds.add(tag);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
for (const step of scenario.steps) {
|
|
702
|
+
for (const text of collectStepTexts(step)) {
|
|
703
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
704
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
705
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const atom = {
|
|
709
|
+
uri: document.uri,
|
|
710
|
+
featureName: document.featureName ?? "",
|
|
711
|
+
scenarioName: scenario.name,
|
|
712
|
+
kind: scenario.kind,
|
|
713
|
+
brIds,
|
|
714
|
+
contractIds: Array.from(contractIds).sort()
|
|
715
|
+
};
|
|
716
|
+
if (scenario.line !== void 0) {
|
|
717
|
+
atom.line = scenario.line;
|
|
718
|
+
}
|
|
719
|
+
if (specIds.length === 1) {
|
|
720
|
+
const specId = specIds[0];
|
|
721
|
+
if (specId) {
|
|
722
|
+
atom.specId = specId;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (scIds.length === 1) {
|
|
726
|
+
const scId = scIds[0];
|
|
727
|
+
if (scId) {
|
|
728
|
+
atom.scId = scId;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return atom;
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
735
|
+
const scenarios = [];
|
|
736
|
+
for (const child of feature.children) {
|
|
737
|
+
if (child.scenario) {
|
|
738
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
739
|
+
}
|
|
740
|
+
if (child.rule) {
|
|
741
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
742
|
+
for (const ruleChild of child.rule.children) {
|
|
743
|
+
if (ruleChild.scenario) {
|
|
744
|
+
scenarios.push(
|
|
745
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return scenarios;
|
|
752
|
+
}
|
|
753
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
754
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
755
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
756
|
+
return {
|
|
757
|
+
name: scenario.name,
|
|
758
|
+
kind,
|
|
759
|
+
line: scenario.location?.line,
|
|
760
|
+
tags,
|
|
761
|
+
steps: scenario.steps
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function collectTagNames(tags) {
|
|
765
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
766
|
+
}
|
|
767
|
+
function collectStepTexts(step) {
|
|
768
|
+
const texts = [];
|
|
769
|
+
if (step.text) {
|
|
770
|
+
texts.push(step.text);
|
|
771
|
+
}
|
|
772
|
+
if (step.docString?.content) {
|
|
773
|
+
texts.push(step.docString.content);
|
|
774
|
+
}
|
|
775
|
+
if (step.dataTable?.rows) {
|
|
776
|
+
for (const row of step.dataTable.rows) {
|
|
777
|
+
for (const cell of row.cells) {
|
|
778
|
+
texts.push(cell.value);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return texts;
|
|
783
|
+
}
|
|
784
|
+
function unique2(values) {
|
|
785
|
+
return Array.from(new Set(values));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/core/traceability.ts
|
|
789
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
790
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
791
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
792
|
+
"**/node_modules/**",
|
|
793
|
+
"**/.git/**",
|
|
794
|
+
"**/.qfai/**",
|
|
795
|
+
"**/dist/**",
|
|
796
|
+
"**/build/**",
|
|
797
|
+
"**/coverage/**",
|
|
798
|
+
"**/.next/**",
|
|
799
|
+
"**/out/**"
|
|
800
|
+
];
|
|
801
|
+
function extractAnnotatedScIds(text) {
|
|
802
|
+
const ids = /* @__PURE__ */ new Set();
|
|
803
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
804
|
+
const suffix = match[1];
|
|
805
|
+
if (suffix) {
|
|
806
|
+
ids.add(`SC-${suffix}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return Array.from(ids);
|
|
810
|
+
}
|
|
811
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
812
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
813
|
+
for (const file of scenarioFiles) {
|
|
814
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
815
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
816
|
+
if (!document || errors.length > 0) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
for (const scenario of document.scenarios) {
|
|
820
|
+
for (const tag of scenario.tags) {
|
|
821
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
822
|
+
scIds.add(tag);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return scIds;
|
|
828
|
+
}
|
|
829
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
830
|
+
const sources = /* @__PURE__ */ new Map();
|
|
831
|
+
for (const file of scenarioFiles) {
|
|
832
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
833
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
834
|
+
if (!document || errors.length > 0) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
for (const scenario of document.scenarios) {
|
|
838
|
+
for (const tag of scenario.tags) {
|
|
839
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
843
|
+
current.add(file);
|
|
844
|
+
sources.set(tag, current);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return sources;
|
|
849
|
+
}
|
|
850
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
851
|
+
const refs = /* @__PURE__ */ new Map();
|
|
852
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
853
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
854
|
+
const mergedExcludeGlobs = Array.from(
|
|
855
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
856
|
+
);
|
|
857
|
+
if (normalizedGlobs.length === 0) {
|
|
858
|
+
return {
|
|
859
|
+
refs,
|
|
860
|
+
scan: {
|
|
861
|
+
globs: normalizedGlobs,
|
|
862
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
863
|
+
matchedFileCount: 0
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
let files = [];
|
|
868
|
+
try {
|
|
869
|
+
files = await collectFilesByGlobs(root, {
|
|
870
|
+
globs: normalizedGlobs,
|
|
871
|
+
ignore: mergedExcludeGlobs
|
|
872
|
+
});
|
|
873
|
+
} catch (error) {
|
|
874
|
+
return {
|
|
875
|
+
refs,
|
|
876
|
+
scan: {
|
|
877
|
+
globs: normalizedGlobs,
|
|
878
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
879
|
+
matchedFileCount: 0
|
|
880
|
+
},
|
|
881
|
+
error: formatError3(error)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
const normalizedFiles = Array.from(
|
|
885
|
+
new Set(files.map((file) => import_node_path4.default.normalize(file)))
|
|
886
|
+
);
|
|
887
|
+
for (const file of normalizedFiles) {
|
|
888
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
889
|
+
const scIds = extractAnnotatedScIds(text);
|
|
890
|
+
if (scIds.length === 0) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
for (const scId of scIds) {
|
|
894
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
895
|
+
current.add(file);
|
|
896
|
+
refs.set(scId, current);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
refs,
|
|
901
|
+
scan: {
|
|
902
|
+
globs: normalizedGlobs,
|
|
903
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
904
|
+
matchedFileCount: normalizedFiles.length
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function buildScCoverage(scIds, refs) {
|
|
909
|
+
const sortedScIds = toSortedArray(scIds);
|
|
910
|
+
const refsRecord = {};
|
|
911
|
+
const missingIds = [];
|
|
912
|
+
let covered = 0;
|
|
913
|
+
for (const scId of sortedScIds) {
|
|
914
|
+
const files = refs.get(scId);
|
|
915
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
916
|
+
refsRecord[scId] = sortedFiles;
|
|
917
|
+
if (sortedFiles.length === 0) {
|
|
918
|
+
missingIds.push(scId);
|
|
919
|
+
} else {
|
|
920
|
+
covered += 1;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
total: sortedScIds.length,
|
|
925
|
+
covered,
|
|
926
|
+
missing: missingIds.length,
|
|
927
|
+
missingIds,
|
|
928
|
+
refs: refsRecord
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function toSortedArray(values) {
|
|
932
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
933
|
+
}
|
|
934
|
+
function normalizeGlobs(globs) {
|
|
935
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
936
|
+
}
|
|
937
|
+
function formatError3(error) {
|
|
938
|
+
if (error instanceof Error) {
|
|
939
|
+
return error.message;
|
|
940
|
+
}
|
|
941
|
+
return String(error);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/core/version.ts
|
|
945
|
+
var import_promises6 = require("fs/promises");
|
|
946
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
592
947
|
var import_node_url = require("url");
|
|
593
948
|
async function resolveToolVersion() {
|
|
594
|
-
if ("0.
|
|
595
|
-
return "0.
|
|
949
|
+
if ("0.4.2".length > 0) {
|
|
950
|
+
return "0.4.2";
|
|
596
951
|
}
|
|
597
952
|
try {
|
|
598
953
|
const packagePath = resolvePackageJsonPath();
|
|
599
|
-
const raw = await (0,
|
|
954
|
+
const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
|
|
600
955
|
const parsed = JSON.parse(raw);
|
|
601
956
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
602
957
|
return version.length > 0 ? version : "unknown";
|
|
@@ -607,18 +962,18 @@ async function resolveToolVersion() {
|
|
|
607
962
|
function resolvePackageJsonPath() {
|
|
608
963
|
const base = __filename;
|
|
609
964
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
610
|
-
return
|
|
965
|
+
return import_node_path5.default.resolve(import_node_path5.default.dirname(basePath), "../../package.json");
|
|
611
966
|
}
|
|
612
967
|
|
|
613
968
|
// src/core/validators/contracts.ts
|
|
614
|
-
var
|
|
615
|
-
var
|
|
969
|
+
var import_promises7 = require("fs/promises");
|
|
970
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
616
971
|
|
|
617
972
|
// src/core/contracts.ts
|
|
618
|
-
var
|
|
973
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
619
974
|
var import_yaml2 = require("yaml");
|
|
620
975
|
function parseStructuredContract(file, text) {
|
|
621
|
-
const ext =
|
|
976
|
+
const ext = import_node_path6.default.extname(file).toLowerCase();
|
|
622
977
|
if (ext === ".json") {
|
|
623
978
|
return JSON.parse(text);
|
|
624
979
|
}
|
|
@@ -669,9 +1024,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
669
1024
|
async function validateContracts(root, config) {
|
|
670
1025
|
const issues = [];
|
|
671
1026
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
672
|
-
issues.push(...await validateUiContracts(
|
|
673
|
-
issues.push(...await validateApiContracts(
|
|
674
|
-
issues.push(...await validateDataContracts(
|
|
1027
|
+
issues.push(...await validateUiContracts(import_node_path7.default.join(contractsRoot, "ui")));
|
|
1028
|
+
issues.push(...await validateApiContracts(import_node_path7.default.join(contractsRoot, "api")));
|
|
1029
|
+
issues.push(...await validateDataContracts(import_node_path7.default.join(contractsRoot, "db")));
|
|
675
1030
|
return issues;
|
|
676
1031
|
}
|
|
677
1032
|
async function validateUiContracts(uiRoot) {
|
|
@@ -689,7 +1044,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
689
1044
|
}
|
|
690
1045
|
const issues = [];
|
|
691
1046
|
for (const file of files) {
|
|
692
|
-
const text = await (0,
|
|
1047
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
693
1048
|
const invalidIds = extractInvalidIds(text, [
|
|
694
1049
|
"SPEC",
|
|
695
1050
|
"BR",
|
|
@@ -718,7 +1073,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
718
1073
|
issues.push(
|
|
719
1074
|
issue(
|
|
720
1075
|
"QFAI-CONTRACT-001",
|
|
721
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1076
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
722
1077
|
"error",
|
|
723
1078
|
file,
|
|
724
1079
|
"contracts.ui.parse"
|
|
@@ -756,7 +1111,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
756
1111
|
}
|
|
757
1112
|
const issues = [];
|
|
758
1113
|
for (const file of files) {
|
|
759
|
-
const text = await (0,
|
|
1114
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
760
1115
|
const invalidIds = extractInvalidIds(text, [
|
|
761
1116
|
"SPEC",
|
|
762
1117
|
"BR",
|
|
@@ -785,7 +1140,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
785
1140
|
issues.push(
|
|
786
1141
|
issue(
|
|
787
1142
|
"QFAI-CONTRACT-001",
|
|
788
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1143
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
789
1144
|
"error",
|
|
790
1145
|
file,
|
|
791
1146
|
"contracts.api.parse"
|
|
@@ -834,7 +1189,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
834
1189
|
}
|
|
835
1190
|
const issues = [];
|
|
836
1191
|
for (const file of files) {
|
|
837
|
-
const text = await (0,
|
|
1192
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
838
1193
|
const invalidIds = extractInvalidIds(text, [
|
|
839
1194
|
"SPEC",
|
|
840
1195
|
"BR",
|
|
@@ -880,7 +1235,7 @@ function lintSql(text, file) {
|
|
|
880
1235
|
function hasOpenApi(doc) {
|
|
881
1236
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
882
1237
|
}
|
|
883
|
-
function
|
|
1238
|
+
function formatError4(error) {
|
|
884
1239
|
if (error instanceof Error) {
|
|
885
1240
|
return error.message;
|
|
886
1241
|
}
|
|
@@ -905,8 +1260,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
905
1260
|
}
|
|
906
1261
|
|
|
907
1262
|
// src/core/validators/delta.ts
|
|
908
|
-
var
|
|
909
|
-
var
|
|
1263
|
+
var import_promises8 = require("fs/promises");
|
|
1264
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
910
1265
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
911
1266
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
912
1267
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -920,10 +1275,10 @@ async function validateDeltas(root, config) {
|
|
|
920
1275
|
}
|
|
921
1276
|
const issues = [];
|
|
922
1277
|
for (const pack of packs) {
|
|
923
|
-
const deltaPath =
|
|
1278
|
+
const deltaPath = import_node_path8.default.join(pack, "delta.md");
|
|
924
1279
|
let text;
|
|
925
1280
|
try {
|
|
926
|
-
text = await (0,
|
|
1281
|
+
text = await (0, import_promises8.readFile)(deltaPath, "utf-8");
|
|
927
1282
|
} catch (error) {
|
|
928
1283
|
if (isMissingFileError2(error)) {
|
|
929
1284
|
issues.push(
|
|
@@ -995,17 +1350,17 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
995
1350
|
}
|
|
996
1351
|
|
|
997
1352
|
// src/core/validators/ids.ts
|
|
998
|
-
var
|
|
999
|
-
var
|
|
1353
|
+
var import_promises10 = require("fs/promises");
|
|
1354
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1000
1355
|
|
|
1001
1356
|
// src/core/contractIndex.ts
|
|
1002
|
-
var
|
|
1003
|
-
var
|
|
1357
|
+
var import_promises9 = require("fs/promises");
|
|
1358
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1004
1359
|
async function buildContractIndex(root, config) {
|
|
1005
1360
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1006
|
-
const uiRoot =
|
|
1007
|
-
const apiRoot =
|
|
1008
|
-
const dataRoot =
|
|
1361
|
+
const uiRoot = import_node_path9.default.join(contractsRoot, "ui");
|
|
1362
|
+
const apiRoot = import_node_path9.default.join(contractsRoot, "api");
|
|
1363
|
+
const dataRoot = import_node_path9.default.join(contractsRoot, "db");
|
|
1009
1364
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1010
1365
|
collectUiContractFiles(uiRoot),
|
|
1011
1366
|
collectApiContractFiles(apiRoot),
|
|
@@ -1024,7 +1379,7 @@ async function buildContractIndex(root, config) {
|
|
|
1024
1379
|
}
|
|
1025
1380
|
async function indexUiContracts(files, index) {
|
|
1026
1381
|
for (const file of files) {
|
|
1027
|
-
const text = await (0,
|
|
1382
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1028
1383
|
try {
|
|
1029
1384
|
const doc = parseStructuredContract(file, text);
|
|
1030
1385
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1036,7 +1391,7 @@ async function indexUiContracts(files, index) {
|
|
|
1036
1391
|
}
|
|
1037
1392
|
async function indexApiContracts(files, index) {
|
|
1038
1393
|
for (const file of files) {
|
|
1039
|
-
const text = await (0,
|
|
1394
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1040
1395
|
try {
|
|
1041
1396
|
const doc = parseStructuredContract(file, text);
|
|
1042
1397
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1048,7 +1403,7 @@ async function indexApiContracts(files, index) {
|
|
|
1048
1403
|
}
|
|
1049
1404
|
async function indexDataContracts(files, index) {
|
|
1050
1405
|
for (const file of files) {
|
|
1051
|
-
const text = await (0,
|
|
1406
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1052
1407
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1053
1408
|
}
|
|
1054
1409
|
}
|
|
@@ -1088,251 +1443,97 @@ function extractH2Sections(md) {
|
|
|
1088
1443
|
if (!current) continue;
|
|
1089
1444
|
const next = headings[i + 1];
|
|
1090
1445
|
const startLine = current.line + 1;
|
|
1091
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1092
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1093
|
-
sections.set(current.title.trim(), {
|
|
1094
|
-
title: current.title.trim(),
|
|
1095
|
-
startLine,
|
|
1096
|
-
endLine,
|
|
1097
|
-
body
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
return sections;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// src/core/parse/spec.ts
|
|
1104
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1105
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1106
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1107
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1108
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1109
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1110
|
-
function parseSpec(md, file) {
|
|
1111
|
-
const headings = parseHeadings(md);
|
|
1112
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1113
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1114
|
-
const sections = extractH2Sections(md);
|
|
1115
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1116
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1117
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1118
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1119
|
-
const brs = [];
|
|
1120
|
-
const brsWithoutPriority = [];
|
|
1121
|
-
const brsWithInvalidPriority = [];
|
|
1122
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1123
|
-
const lineText = brLines[i] ?? "";
|
|
1124
|
-
const lineNumber = startLine + i;
|
|
1125
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1126
|
-
if (validMatch) {
|
|
1127
|
-
const id = validMatch[1];
|
|
1128
|
-
const priority = validMatch[2];
|
|
1129
|
-
const text = validMatch[3];
|
|
1130
|
-
if (!id || !priority || !text) continue;
|
|
1131
|
-
brs.push({
|
|
1132
|
-
id,
|
|
1133
|
-
priority,
|
|
1134
|
-
text: text.trim(),
|
|
1135
|
-
line: lineNumber
|
|
1136
|
-
});
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1140
|
-
if (anyPriorityMatch) {
|
|
1141
|
-
const id = anyPriorityMatch[1];
|
|
1142
|
-
const priority = anyPriorityMatch[2];
|
|
1143
|
-
const text = anyPriorityMatch[3];
|
|
1144
|
-
if (!id || !priority || !text) continue;
|
|
1145
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1146
|
-
brsWithInvalidPriority.push({
|
|
1147
|
-
id,
|
|
1148
|
-
priority,
|
|
1149
|
-
text: text.trim(),
|
|
1150
|
-
line: lineNumber
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1156
|
-
if (noPriorityMatch) {
|
|
1157
|
-
const id = noPriorityMatch[1];
|
|
1158
|
-
const text = noPriorityMatch[2];
|
|
1159
|
-
if (!id || !text) continue;
|
|
1160
|
-
brsWithoutPriority.push({
|
|
1161
|
-
id,
|
|
1162
|
-
text: text.trim(),
|
|
1163
|
-
line: lineNumber
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
const parsed = {
|
|
1168
|
-
file,
|
|
1169
|
-
sections: sectionNames,
|
|
1170
|
-
brs,
|
|
1171
|
-
brsWithoutPriority,
|
|
1172
|
-
brsWithInvalidPriority
|
|
1173
|
-
};
|
|
1174
|
-
if (specId) {
|
|
1175
|
-
parsed.specId = specId;
|
|
1176
|
-
}
|
|
1177
|
-
return parsed;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// src/core/gherkin/parse.ts
|
|
1181
|
-
var import_gherkin = require("@cucumber/gherkin");
|
|
1182
|
-
var import_node_crypto = require("crypto");
|
|
1183
|
-
function parseGherkin(source, uri) {
|
|
1184
|
-
const errors = [];
|
|
1185
|
-
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
1186
|
-
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
1187
|
-
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
1188
|
-
const parser = new import_gherkin.Parser(builder, matcher);
|
|
1189
|
-
try {
|
|
1190
|
-
const gherkinDocument = parser.parse(source);
|
|
1191
|
-
gherkinDocument.uri = uri;
|
|
1192
|
-
return { gherkinDocument, errors };
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
errors.push(formatError3(error));
|
|
1195
|
-
return { gherkinDocument: null, errors };
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
function formatError3(error) {
|
|
1199
|
-
if (error instanceof Error) {
|
|
1200
|
-
return error.message;
|
|
1201
|
-
}
|
|
1202
|
-
return String(error);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/core/scenarioModel.ts
|
|
1206
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1207
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1208
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1209
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1210
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1211
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1212
|
-
function parseScenarioDocument(text, uri) {
|
|
1213
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1214
|
-
if (!gherkinDocument) {
|
|
1215
|
-
return { document: null, errors };
|
|
1216
|
-
}
|
|
1217
|
-
const feature = gherkinDocument.feature;
|
|
1218
|
-
if (!feature) {
|
|
1219
|
-
return {
|
|
1220
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1221
|
-
errors
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1225
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1226
|
-
return {
|
|
1227
|
-
document: {
|
|
1228
|
-
uri,
|
|
1229
|
-
featureName: feature.name,
|
|
1230
|
-
featureTags,
|
|
1231
|
-
scenarios
|
|
1232
|
-
},
|
|
1233
|
-
errors
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
function buildScenarioAtoms(document) {
|
|
1237
|
-
return document.scenarios.map((scenario) => {
|
|
1238
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1239
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1240
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1241
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1242
|
-
scenario.tags.forEach((tag) => {
|
|
1243
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1244
|
-
contractIds.add(tag);
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
for (const step of scenario.steps) {
|
|
1248
|
-
for (const text of collectStepTexts(step)) {
|
|
1249
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1250
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1251
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
const atom = {
|
|
1255
|
-
uri: document.uri,
|
|
1256
|
-
featureName: document.featureName ?? "",
|
|
1257
|
-
scenarioName: scenario.name,
|
|
1258
|
-
kind: scenario.kind,
|
|
1259
|
-
brIds,
|
|
1260
|
-
contractIds: Array.from(contractIds).sort()
|
|
1261
|
-
};
|
|
1262
|
-
if (scenario.line !== void 0) {
|
|
1263
|
-
atom.line = scenario.line;
|
|
1264
|
-
}
|
|
1265
|
-
if (specIds.length === 1) {
|
|
1266
|
-
const specId = specIds[0];
|
|
1267
|
-
if (specId) {
|
|
1268
|
-
atom.specId = specId;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
if (scIds.length === 1) {
|
|
1272
|
-
const scId = scIds[0];
|
|
1273
|
-
if (scId) {
|
|
1274
|
-
atom.scId = scId;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
return atom;
|
|
1278
|
-
});
|
|
1446
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1447
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1448
|
+
sections.set(current.title.trim(), {
|
|
1449
|
+
title: current.title.trim(),
|
|
1450
|
+
startLine,
|
|
1451
|
+
endLine,
|
|
1452
|
+
body
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return sections;
|
|
1279
1456
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1457
|
+
|
|
1458
|
+
// src/core/parse/spec.ts
|
|
1459
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1460
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1461
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1462
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1463
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1464
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1465
|
+
function parseSpec(md, file) {
|
|
1466
|
+
const headings = parseHeadings(md);
|
|
1467
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1468
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1469
|
+
const sections = extractH2Sections(md);
|
|
1470
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1471
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1472
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1473
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1474
|
+
const brs = [];
|
|
1475
|
+
const brsWithoutPriority = [];
|
|
1476
|
+
const brsWithInvalidPriority = [];
|
|
1477
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1478
|
+
const lineText = brLines[i] ?? "";
|
|
1479
|
+
const lineNumber = startLine + i;
|
|
1480
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1481
|
+
if (validMatch) {
|
|
1482
|
+
const id = validMatch[1];
|
|
1483
|
+
const priority = validMatch[2];
|
|
1484
|
+
const text = validMatch[3];
|
|
1485
|
+
if (!id || !priority || !text) continue;
|
|
1486
|
+
brs.push({
|
|
1487
|
+
id,
|
|
1488
|
+
priority,
|
|
1489
|
+
text: text.trim(),
|
|
1490
|
+
line: lineNumber
|
|
1491
|
+
});
|
|
1492
|
+
continue;
|
|
1285
1493
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1494
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1495
|
+
if (anyPriorityMatch) {
|
|
1496
|
+
const id = anyPriorityMatch[1];
|
|
1497
|
+
const priority = anyPriorityMatch[2];
|
|
1498
|
+
const text = anyPriorityMatch[3];
|
|
1499
|
+
if (!id || !priority || !text) continue;
|
|
1500
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1501
|
+
brsWithInvalidPriority.push({
|
|
1502
|
+
id,
|
|
1503
|
+
priority,
|
|
1504
|
+
text: text.trim(),
|
|
1505
|
+
line: lineNumber
|
|
1506
|
+
});
|
|
1294
1507
|
}
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1511
|
+
if (noPriorityMatch) {
|
|
1512
|
+
const id = noPriorityMatch[1];
|
|
1513
|
+
const text = noPriorityMatch[2];
|
|
1514
|
+
if (!id || !text) continue;
|
|
1515
|
+
brsWithoutPriority.push({
|
|
1516
|
+
id,
|
|
1517
|
+
text: text.trim(),
|
|
1518
|
+
line: lineNumber
|
|
1519
|
+
});
|
|
1295
1520
|
}
|
|
1296
1521
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
name: scenario.name,
|
|
1304
|
-
kind,
|
|
1305
|
-
line: scenario.location?.line,
|
|
1306
|
-
tags,
|
|
1307
|
-
steps: scenario.steps
|
|
1522
|
+
const parsed = {
|
|
1523
|
+
file,
|
|
1524
|
+
sections: sectionNames,
|
|
1525
|
+
brs,
|
|
1526
|
+
brsWithoutPriority,
|
|
1527
|
+
brsWithInvalidPriority
|
|
1308
1528
|
};
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1312
|
-
}
|
|
1313
|
-
function collectStepTexts(step) {
|
|
1314
|
-
const texts = [];
|
|
1315
|
-
if (step.text) {
|
|
1316
|
-
texts.push(step.text);
|
|
1317
|
-
}
|
|
1318
|
-
if (step.docString?.content) {
|
|
1319
|
-
texts.push(step.docString.content);
|
|
1320
|
-
}
|
|
1321
|
-
if (step.dataTable?.rows) {
|
|
1322
|
-
for (const row of step.dataTable.rows) {
|
|
1323
|
-
for (const cell of row.cells) {
|
|
1324
|
-
texts.push(cell.value);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1529
|
+
if (specId) {
|
|
1530
|
+
parsed.specId = specId;
|
|
1327
1531
|
}
|
|
1328
|
-
return
|
|
1329
|
-
}
|
|
1330
|
-
function unique2(values) {
|
|
1331
|
-
return Array.from(new Set(values));
|
|
1532
|
+
return parsed;
|
|
1332
1533
|
}
|
|
1333
1534
|
|
|
1334
1535
|
// src/core/validators/ids.ts
|
|
1335
|
-
var
|
|
1536
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1336
1537
|
async function validateDefinedIds(root, config) {
|
|
1337
1538
|
const issues = [];
|
|
1338
1539
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1366,7 +1567,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1366
1567
|
}
|
|
1367
1568
|
async function collectSpecDefinitionIds(files, out) {
|
|
1368
1569
|
for (const file of files) {
|
|
1369
|
-
const text = await (0,
|
|
1570
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1370
1571
|
const parsed = parseSpec(text, file);
|
|
1371
1572
|
if (parsed.specId) {
|
|
1372
1573
|
recordId(out, parsed.specId, file);
|
|
@@ -1376,14 +1577,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1376
1577
|
}
|
|
1377
1578
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1378
1579
|
for (const file of files) {
|
|
1379
|
-
const text = await (0,
|
|
1580
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1380
1581
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1381
1582
|
if (!document || errors.length > 0) {
|
|
1382
1583
|
continue;
|
|
1383
1584
|
}
|
|
1384
1585
|
for (const scenario of document.scenarios) {
|
|
1385
1586
|
for (const tag of scenario.tags) {
|
|
1386
|
-
if (
|
|
1587
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1387
1588
|
recordId(out, tag, file);
|
|
1388
1589
|
}
|
|
1389
1590
|
}
|
|
@@ -1397,7 +1598,7 @@ function recordId(out, id, file) {
|
|
|
1397
1598
|
}
|
|
1398
1599
|
function formatFileList(files, root) {
|
|
1399
1600
|
return files.map((file) => {
|
|
1400
|
-
const relative =
|
|
1601
|
+
const relative = import_node_path10.default.relative(root, file);
|
|
1401
1602
|
return relative.length > 0 ? relative : file;
|
|
1402
1603
|
}).join(", ");
|
|
1403
1604
|
}
|
|
@@ -1420,13 +1621,12 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1420
1621
|
}
|
|
1421
1622
|
|
|
1422
1623
|
// src/core/validators/scenario.ts
|
|
1423
|
-
var
|
|
1624
|
+
var import_promises11 = require("fs/promises");
|
|
1424
1625
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1425
1626
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1426
1627
|
var THEN_PATTERN = /\bThen\b/;
|
|
1427
|
-
var
|
|
1628
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1428
1629
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1429
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1430
1630
|
async function validateScenarios(root, config) {
|
|
1431
1631
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1432
1632
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1447,7 +1647,7 @@ async function validateScenarios(root, config) {
|
|
|
1447
1647
|
for (const entry of entries) {
|
|
1448
1648
|
let text;
|
|
1449
1649
|
try {
|
|
1450
|
-
text = await (0,
|
|
1650
|
+
text = await (0, import_promises11.readFile)(entry.scenarioPath, "utf-8");
|
|
1451
1651
|
} catch (error) {
|
|
1452
1652
|
if (isMissingFileError3(error)) {
|
|
1453
1653
|
issues.push(
|
|
@@ -1506,17 +1706,7 @@ function validateScenarioContent(text, file) {
|
|
|
1506
1706
|
const featureSpecTags = document.featureTags.filter(
|
|
1507
1707
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1508
1708
|
);
|
|
1509
|
-
if (featureSpecTags.length
|
|
1510
|
-
issues.push(
|
|
1511
|
-
issue4(
|
|
1512
|
-
"QFAI-SC-009",
|
|
1513
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1514
|
-
"error",
|
|
1515
|
-
file,
|
|
1516
|
-
"scenario.featureSpec"
|
|
1517
|
-
)
|
|
1518
|
-
);
|
|
1519
|
-
} else if (featureSpecTags.length > 1) {
|
|
1709
|
+
if (featureSpecTags.length > 1) {
|
|
1520
1710
|
issues.push(
|
|
1521
1711
|
issue4(
|
|
1522
1712
|
"QFAI-SC-009",
|
|
@@ -1558,18 +1748,12 @@ function validateScenarioContent(text, file) {
|
|
|
1558
1748
|
continue;
|
|
1559
1749
|
}
|
|
1560
1750
|
const missingTags = [];
|
|
1561
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1751
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1562
1752
|
if (scTags.length === 0) {
|
|
1563
1753
|
missingTags.push("SC(0\u4EF6)");
|
|
1564
1754
|
} else if (scTags.length > 1) {
|
|
1565
1755
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1566
1756
|
}
|
|
1567
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1568
|
-
missingTags.push("SPEC");
|
|
1569
|
-
}
|
|
1570
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1571
|
-
missingTags.push("BR");
|
|
1572
|
-
}
|
|
1573
1757
|
if (missingTags.length > 0) {
|
|
1574
1758
|
issues.push(
|
|
1575
1759
|
issue4(
|
|
@@ -1633,7 +1817,7 @@ function isMissingFileError3(error) {
|
|
|
1633
1817
|
}
|
|
1634
1818
|
|
|
1635
1819
|
// src/core/validators/spec.ts
|
|
1636
|
-
var
|
|
1820
|
+
var import_promises12 = require("fs/promises");
|
|
1637
1821
|
async function validateSpecs(root, config) {
|
|
1638
1822
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1639
1823
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1654,7 +1838,7 @@ async function validateSpecs(root, config) {
|
|
|
1654
1838
|
for (const entry of entries) {
|
|
1655
1839
|
let text;
|
|
1656
1840
|
try {
|
|
1657
|
-
text = await (0,
|
|
1841
|
+
text = await (0, import_promises12.readFile)(entry.specPath, "utf-8");
|
|
1658
1842
|
} catch (error) {
|
|
1659
1843
|
if (isMissingFileError4(error)) {
|
|
1660
1844
|
issues.push(
|
|
@@ -1803,10 +1987,9 @@ function isMissingFileError4(error) {
|
|
|
1803
1987
|
}
|
|
1804
1988
|
|
|
1805
1989
|
// src/core/validators/traceability.ts
|
|
1806
|
-
var
|
|
1807
|
-
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1990
|
+
var import_promises13 = require("fs/promises");
|
|
1808
1991
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1809
|
-
var
|
|
1992
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1810
1993
|
async function validateTraceability(root, config) {
|
|
1811
1994
|
const issues = [];
|
|
1812
1995
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1825,7 +2008,7 @@ async function validateTraceability(root, config) {
|
|
|
1825
2008
|
const contractIndex = await buildContractIndex(root, config);
|
|
1826
2009
|
const contractIds = contractIndex.ids;
|
|
1827
2010
|
for (const file of specFiles) {
|
|
1828
|
-
const text = await (0,
|
|
2011
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1829
2012
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1830
2013
|
const parsed = parseSpec(text, file);
|
|
1831
2014
|
if (parsed.specId) {
|
|
@@ -1833,28 +2016,6 @@ async function validateTraceability(root, config) {
|
|
|
1833
2016
|
}
|
|
1834
2017
|
const brIds = parsed.brs.map((br) => br.id);
|
|
1835
2018
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1836
|
-
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1837
|
-
...extractIds(text, "UI"),
|
|
1838
|
-
...extractIds(text, "API"),
|
|
1839
|
-
...extractIds(text, "DATA")
|
|
1840
|
-
]);
|
|
1841
|
-
const unknownContractIds = Array.from(referencedContractIds).filter(
|
|
1842
|
-
(id) => !contractIds.has(id)
|
|
1843
|
-
);
|
|
1844
|
-
if (unknownContractIds.length > 0) {
|
|
1845
|
-
issues.push(
|
|
1846
|
-
issue6(
|
|
1847
|
-
"QFAI-TRACE-009",
|
|
1848
|
-
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1849
|
-
", "
|
|
1850
|
-
)}`,
|
|
1851
|
-
"error",
|
|
1852
|
-
file,
|
|
1853
|
-
"traceability.specContractExists",
|
|
1854
|
-
unknownContractIds
|
|
1855
|
-
)
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
2019
|
if (parsed.specId) {
|
|
1859
2020
|
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1860
2021
|
brIds.forEach((id) => current.add(id));
|
|
@@ -1862,23 +2023,49 @@ async function validateTraceability(root, config) {
|
|
|
1862
2023
|
}
|
|
1863
2024
|
}
|
|
1864
2025
|
for (const file of scenarioFiles) {
|
|
1865
|
-
const text = await (0,
|
|
2026
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1866
2027
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1867
2028
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1868
2029
|
if (!document || errors.length > 0) {
|
|
1869
2030
|
continue;
|
|
1870
2031
|
}
|
|
1871
2032
|
const atoms = buildScenarioAtoms(document);
|
|
2033
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
1872
2034
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1873
2035
|
const atom = atoms[index];
|
|
1874
2036
|
if (!atom) {
|
|
1875
2037
|
continue;
|
|
1876
2038
|
}
|
|
1877
2039
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1878
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
1879
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
2040
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
2041
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
2042
|
+
if (specTags.length === 0) {
|
|
2043
|
+
issues.push(
|
|
2044
|
+
issue6(
|
|
2045
|
+
"QFAI-TRACE-014",
|
|
2046
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2047
|
+
"error",
|
|
2048
|
+
file,
|
|
2049
|
+
"traceability.scenarioSpecRequired"
|
|
2050
|
+
)
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
if (brTags.length === 0) {
|
|
2054
|
+
issues.push(
|
|
2055
|
+
issue6(
|
|
2056
|
+
"QFAI-TRACE-015",
|
|
2057
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2058
|
+
"error",
|
|
2059
|
+
file,
|
|
2060
|
+
"traceability.scenarioBrRequired"
|
|
2061
|
+
)
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
1880
2064
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1881
|
-
scTags.forEach((id) =>
|
|
2065
|
+
scTags.forEach((id) => {
|
|
2066
|
+
scIdsInScenarios.add(id);
|
|
2067
|
+
scIdsInFile.add(id);
|
|
2068
|
+
});
|
|
1882
2069
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1883
2070
|
if (atom.contractIds.length > 0) {
|
|
1884
2071
|
scTags.forEach((id) => scWithContracts.add(id));
|
|
@@ -1956,6 +2143,22 @@ async function validateTraceability(root, config) {
|
|
|
1956
2143
|
}
|
|
1957
2144
|
}
|
|
1958
2145
|
}
|
|
2146
|
+
if (scIdsInFile.size !== 1) {
|
|
2147
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2148
|
+
(a, b) => a.localeCompare(b)
|
|
2149
|
+
);
|
|
2150
|
+
const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
|
|
2151
|
+
issues.push(
|
|
2152
|
+
issue6(
|
|
2153
|
+
"QFAI-TRACE-012",
|
|
2154
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2155
|
+
"error",
|
|
2156
|
+
file,
|
|
2157
|
+
"traceability.specScOneToOne",
|
|
2158
|
+
invalidScIds
|
|
2159
|
+
)
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
1959
2162
|
}
|
|
1960
2163
|
if (upstreamIds.size === 0) {
|
|
1961
2164
|
return [
|
|
@@ -2004,6 +2207,66 @@ async function validateTraceability(root, config) {
|
|
|
2004
2207
|
);
|
|
2005
2208
|
}
|
|
2006
2209
|
}
|
|
2210
|
+
const scRefsResult = await collectScTestReferences(
|
|
2211
|
+
root,
|
|
2212
|
+
config.validation.traceability.testFileGlobs,
|
|
2213
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2214
|
+
);
|
|
2215
|
+
const scTestRefs = scRefsResult.refs;
|
|
2216
|
+
const testFileScan = scRefsResult.scan;
|
|
2217
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2218
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2219
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2220
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2221
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2222
|
+
issues.push(
|
|
2223
|
+
issue6(
|
|
2224
|
+
"QFAI-TRACE-013",
|
|
2225
|
+
`\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
|
|
2226
|
+
"error",
|
|
2227
|
+
testsRoot,
|
|
2228
|
+
"traceability.testFileGlobs"
|
|
2229
|
+
)
|
|
2230
|
+
);
|
|
2231
|
+
} else {
|
|
2232
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2233
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2234
|
+
const refs = scTestRefs.get(id);
|
|
2235
|
+
return !refs || refs.size === 0;
|
|
2236
|
+
});
|
|
2237
|
+
if (scWithoutTests.length > 0) {
|
|
2238
|
+
issues.push(
|
|
2239
|
+
issue6(
|
|
2240
|
+
"QFAI-TRACE-010",
|
|
2241
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2242
|
+
", "
|
|
2243
|
+
)}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2244
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2245
|
+
testsRoot,
|
|
2246
|
+
"traceability.scMustHaveTest",
|
|
2247
|
+
scWithoutTests
|
|
2248
|
+
)
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2253
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2254
|
+
);
|
|
2255
|
+
if (unknownScIds.length > 0) {
|
|
2256
|
+
issues.push(
|
|
2257
|
+
issue6(
|
|
2258
|
+
"QFAI-TRACE-011",
|
|
2259
|
+
`\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
|
|
2260
|
+
", "
|
|
2261
|
+
)}`,
|
|
2262
|
+
"error",
|
|
2263
|
+
testsRoot,
|
|
2264
|
+
"traceability.scUnknownInTests",
|
|
2265
|
+
unknownScIds
|
|
2266
|
+
)
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2007
2270
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2008
2271
|
if (contractIds.size > 0) {
|
|
2009
2272
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -2052,7 +2315,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2052
2315
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2053
2316
|
let found = false;
|
|
2054
2317
|
for (const file of targetFiles) {
|
|
2055
|
-
const text = await (0,
|
|
2318
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2056
2319
|
if (pattern.test(text)) {
|
|
2057
2320
|
found = true;
|
|
2058
2321
|
break;
|
|
@@ -2062,8 +2325,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2062
2325
|
issues.push(
|
|
2063
2326
|
issue6(
|
|
2064
2327
|
"QFAI-TRACE-002",
|
|
2065
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2066
|
-
"
|
|
2328
|
+
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
|
|
2329
|
+
"info",
|
|
2067
2330
|
srcRoot,
|
|
2068
2331
|
"traceability.codeReferences"
|
|
2069
2332
|
)
|
|
@@ -2106,12 +2369,24 @@ async function validateProject(root, configResult) {
|
|
|
2106
2369
|
...await validateDefinedIds(root, config),
|
|
2107
2370
|
...await validateTraceability(root, config)
|
|
2108
2371
|
];
|
|
2372
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2373
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2374
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2375
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2376
|
+
root,
|
|
2377
|
+
config.validation.traceability.testFileGlobs,
|
|
2378
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2379
|
+
);
|
|
2380
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2109
2381
|
const toolVersion = await resolveToolVersion();
|
|
2110
2382
|
return {
|
|
2111
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2112
2383
|
toolVersion,
|
|
2113
2384
|
issues,
|
|
2114
|
-
counts: countIssues(issues)
|
|
2385
|
+
counts: countIssues(issues),
|
|
2386
|
+
traceability: {
|
|
2387
|
+
sc: scCoverage,
|
|
2388
|
+
testFiles
|
|
2389
|
+
}
|
|
2115
2390
|
};
|
|
2116
2391
|
}
|
|
2117
2392
|
function countIssues(issues) {
|
|
@@ -2132,9 +2407,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2132
2407
|
const configPath = resolved.configPath;
|
|
2133
2408
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2134
2409
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2135
|
-
const apiRoot =
|
|
2136
|
-
const uiRoot =
|
|
2137
|
-
const dbRoot =
|
|
2410
|
+
const apiRoot = import_node_path11.default.join(contractsRoot, "api");
|
|
2411
|
+
const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
|
|
2412
|
+
const dbRoot = import_node_path11.default.join(contractsRoot, "db");
|
|
2138
2413
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2139
2414
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2140
2415
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2160,6 +2435,16 @@ async function createReportData(root, validation, configResult) {
|
|
|
2160
2435
|
srcRoot,
|
|
2161
2436
|
testsRoot
|
|
2162
2437
|
);
|
|
2438
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2439
|
+
const scRefsResult = await collectScTestReferences(
|
|
2440
|
+
root,
|
|
2441
|
+
config.validation.traceability.testFileGlobs,
|
|
2442
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2443
|
+
);
|
|
2444
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2445
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2446
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2447
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2163
2448
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2164
2449
|
const version = await resolveToolVersion();
|
|
2165
2450
|
return {
|
|
@@ -2188,7 +2473,10 @@ async function createReportData(root, validation, configResult) {
|
|
|
2188
2473
|
},
|
|
2189
2474
|
traceability: {
|
|
2190
2475
|
upstreamIdsFound: upstreamIds.size,
|
|
2191
|
-
referencedInCodeOrTests: traceability
|
|
2476
|
+
referencedInCodeOrTests: traceability,
|
|
2477
|
+
sc: scCoverage,
|
|
2478
|
+
scSources: scSourceRecord,
|
|
2479
|
+
testFiles
|
|
2192
2480
|
},
|
|
2193
2481
|
issues: resolvedValidation.issues
|
|
2194
2482
|
};
|
|
@@ -2225,6 +2513,65 @@ function formatReportMarkdown(data) {
|
|
|
2225
2513
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2226
2514
|
);
|
|
2227
2515
|
lines.push("");
|
|
2516
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2517
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2518
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2519
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2520
|
+
lines.push(
|
|
2521
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2522
|
+
);
|
|
2523
|
+
lines.push(
|
|
2524
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2525
|
+
data.traceability.testFiles.excludeGlobs
|
|
2526
|
+
)}`
|
|
2527
|
+
);
|
|
2528
|
+
lines.push(
|
|
2529
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2530
|
+
);
|
|
2531
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2532
|
+
lines.push("- missingIds: (none)");
|
|
2533
|
+
} else {
|
|
2534
|
+
const sources = data.traceability.scSources;
|
|
2535
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2536
|
+
const files = sources[id] ?? [];
|
|
2537
|
+
if (files.length === 0) {
|
|
2538
|
+
return id;
|
|
2539
|
+
}
|
|
2540
|
+
return `${id} (${files.join(", ")})`;
|
|
2541
|
+
});
|
|
2542
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2543
|
+
}
|
|
2544
|
+
lines.push("");
|
|
2545
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2546
|
+
const scRefs = data.traceability.sc.refs;
|
|
2547
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2548
|
+
if (scIds.length === 0) {
|
|
2549
|
+
lines.push("- (none)");
|
|
2550
|
+
} else {
|
|
2551
|
+
for (const scId of scIds) {
|
|
2552
|
+
const refs = scRefs[scId] ?? [];
|
|
2553
|
+
if (refs.length === 0) {
|
|
2554
|
+
lines.push(`- ${scId}: (none)`);
|
|
2555
|
+
} else {
|
|
2556
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
lines.push("");
|
|
2561
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2562
|
+
const specScIssues = data.issues.filter(
|
|
2563
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2564
|
+
);
|
|
2565
|
+
if (specScIssues.length === 0) {
|
|
2566
|
+
lines.push("- (none)");
|
|
2567
|
+
} else {
|
|
2568
|
+
for (const item of specScIssues) {
|
|
2569
|
+
const location = item.file ?? "(unknown)";
|
|
2570
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2571
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
lines.push("");
|
|
2228
2575
|
lines.push("## Hotspots");
|
|
2229
2576
|
const hotspots = buildHotspots(data.issues);
|
|
2230
2577
|
if (hotspots.length === 0) {
|
|
@@ -2279,25 +2626,25 @@ async function collectIds(files) {
|
|
|
2279
2626
|
DATA: /* @__PURE__ */ new Set()
|
|
2280
2627
|
};
|
|
2281
2628
|
for (const file of files) {
|
|
2282
|
-
const text = await (0,
|
|
2629
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2283
2630
|
for (const prefix of ID_PREFIXES2) {
|
|
2284
2631
|
const ids = extractIds(text, prefix);
|
|
2285
2632
|
ids.forEach((id) => result[prefix].add(id));
|
|
2286
2633
|
}
|
|
2287
2634
|
}
|
|
2288
2635
|
return {
|
|
2289
|
-
SPEC:
|
|
2290
|
-
BR:
|
|
2291
|
-
SC:
|
|
2292
|
-
UI:
|
|
2293
|
-
API:
|
|
2294
|
-
DATA:
|
|
2636
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2637
|
+
BR: toSortedArray2(result.BR),
|
|
2638
|
+
SC: toSortedArray2(result.SC),
|
|
2639
|
+
UI: toSortedArray2(result.UI),
|
|
2640
|
+
API: toSortedArray2(result.API),
|
|
2641
|
+
DATA: toSortedArray2(result.DATA)
|
|
2295
2642
|
};
|
|
2296
2643
|
}
|
|
2297
2644
|
async function collectUpstreamIds(files) {
|
|
2298
2645
|
const ids = /* @__PURE__ */ new Set();
|
|
2299
2646
|
for (const file of files) {
|
|
2300
|
-
const text = await (0,
|
|
2647
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2301
2648
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2302
2649
|
}
|
|
2303
2650
|
return ids;
|
|
@@ -2318,7 +2665,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2318
2665
|
}
|
|
2319
2666
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2320
2667
|
for (const file of targetFiles) {
|
|
2321
|
-
const text = await (0,
|
|
2668
|
+
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2322
2669
|
if (pattern.test(text)) {
|
|
2323
2670
|
return true;
|
|
2324
2671
|
}
|
|
@@ -2335,9 +2682,22 @@ function formatIdLine(label, values) {
|
|
|
2335
2682
|
}
|
|
2336
2683
|
return `- ${label}: ${values.join(", ")}`;
|
|
2337
2684
|
}
|
|
2338
|
-
function
|
|
2685
|
+
function formatList(values) {
|
|
2686
|
+
if (values.length === 0) {
|
|
2687
|
+
return "(none)";
|
|
2688
|
+
}
|
|
2689
|
+
return values.join(", ");
|
|
2690
|
+
}
|
|
2691
|
+
function toSortedArray2(values) {
|
|
2339
2692
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2340
2693
|
}
|
|
2694
|
+
function mapToSortedRecord(values) {
|
|
2695
|
+
const record2 = {};
|
|
2696
|
+
for (const [key, files] of values.entries()) {
|
|
2697
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
2698
|
+
}
|
|
2699
|
+
return record2;
|
|
2700
|
+
}
|
|
2341
2701
|
function buildHotspots(issues) {
|
|
2342
2702
|
const map = /* @__PURE__ */ new Map();
|
|
2343
2703
|
for (const issue7 of issues) {
|
|
@@ -2361,7 +2721,6 @@ function buildHotspots(issues) {
|
|
|
2361
2721
|
}
|
|
2362
2722
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2363
2723
|
0 && (module.exports = {
|
|
2364
|
-
VALIDATION_SCHEMA_VERSION,
|
|
2365
2724
|
createReportData,
|
|
2366
2725
|
defaultConfig,
|
|
2367
2726
|
extractAllIds,
|