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.mjs
CHANGED
|
@@ -28,6 +28,10 @@ var defaultConfig = {
|
|
|
28
28
|
traceability: {
|
|
29
29
|
brMustHaveSc: true,
|
|
30
30
|
scMustTouchContracts: true,
|
|
31
|
+
scMustHaveTest: true,
|
|
32
|
+
testFileGlobs: [],
|
|
33
|
+
testFileExcludeGlobs: [],
|
|
34
|
+
scNoTestSeverity: "error",
|
|
31
35
|
allowOrphanContracts: false,
|
|
32
36
|
unknownContractIdSeverity: "error"
|
|
33
37
|
}
|
|
@@ -207,6 +211,34 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
207
211
|
configPath,
|
|
208
212
|
issues
|
|
209
213
|
),
|
|
214
|
+
scMustHaveTest: readBoolean(
|
|
215
|
+
traceabilityRaw?.scMustHaveTest,
|
|
216
|
+
base.traceability.scMustHaveTest,
|
|
217
|
+
"validation.traceability.scMustHaveTest",
|
|
218
|
+
configPath,
|
|
219
|
+
issues
|
|
220
|
+
),
|
|
221
|
+
testFileGlobs: readStringArray(
|
|
222
|
+
traceabilityRaw?.testFileGlobs,
|
|
223
|
+
base.traceability.testFileGlobs,
|
|
224
|
+
"validation.traceability.testFileGlobs",
|
|
225
|
+
configPath,
|
|
226
|
+
issues
|
|
227
|
+
),
|
|
228
|
+
testFileExcludeGlobs: readStringArray(
|
|
229
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
230
|
+
base.traceability.testFileExcludeGlobs,
|
|
231
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
232
|
+
configPath,
|
|
233
|
+
issues
|
|
234
|
+
),
|
|
235
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
236
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
237
|
+
base.traceability.scNoTestSeverity,
|
|
238
|
+
"validation.traceability.scNoTestSeverity",
|
|
239
|
+
configPath,
|
|
240
|
+
issues
|
|
241
|
+
),
|
|
210
242
|
allowOrphanContracts: readBoolean(
|
|
211
243
|
traceabilityRaw?.allowOrphanContracts,
|
|
212
244
|
base.traceability.allowOrphanContracts,
|
|
@@ -385,8 +417,8 @@ function isValidId(value, prefix) {
|
|
|
385
417
|
}
|
|
386
418
|
|
|
387
419
|
// src/core/report.ts
|
|
388
|
-
import { readFile as
|
|
389
|
-
import
|
|
420
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
421
|
+
import path11 from "path";
|
|
390
422
|
|
|
391
423
|
// src/core/discovery.ts
|
|
392
424
|
import { access as access2 } from "fs/promises";
|
|
@@ -394,6 +426,7 @@ import { access as access2 } from "fs/promises";
|
|
|
394
426
|
// src/core/fs.ts
|
|
395
427
|
import { access, readdir } from "fs/promises";
|
|
396
428
|
import path2 from "path";
|
|
429
|
+
import fg from "fast-glob";
|
|
397
430
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
398
431
|
"node_modules",
|
|
399
432
|
".git",
|
|
@@ -415,6 +448,18 @@ async function collectFiles(root, options = {}) {
|
|
|
415
448
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
416
449
|
return entries;
|
|
417
450
|
}
|
|
451
|
+
async function collectFilesByGlobs(root, options) {
|
|
452
|
+
if (options.globs.length === 0) {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
return fg(options.globs, {
|
|
456
|
+
cwd: root,
|
|
457
|
+
ignore: options.ignore ?? [],
|
|
458
|
+
onlyFiles: true,
|
|
459
|
+
absolute: true,
|
|
460
|
+
unique: true
|
|
461
|
+
});
|
|
462
|
+
}
|
|
418
463
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
419
464
|
const items = await readdir(current, { withFileTypes: true });
|
|
420
465
|
for (const item of items) {
|
|
@@ -526,20 +571,335 @@ async function exists2(target) {
|
|
|
526
571
|
}
|
|
527
572
|
}
|
|
528
573
|
|
|
529
|
-
// src/core/
|
|
530
|
-
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
531
|
-
|
|
532
|
-
// src/core/version.ts
|
|
574
|
+
// src/core/traceability.ts
|
|
533
575
|
import { readFile as readFile2 } from "fs/promises";
|
|
534
576
|
import path4 from "path";
|
|
577
|
+
|
|
578
|
+
// src/core/gherkin/parse.ts
|
|
579
|
+
import {
|
|
580
|
+
AstBuilder,
|
|
581
|
+
GherkinClassicTokenMatcher,
|
|
582
|
+
Parser
|
|
583
|
+
} from "@cucumber/gherkin";
|
|
584
|
+
import { randomUUID } from "crypto";
|
|
585
|
+
function parseGherkin(source, uri) {
|
|
586
|
+
const errors = [];
|
|
587
|
+
const uuidFn = () => randomUUID();
|
|
588
|
+
const builder = new AstBuilder(uuidFn);
|
|
589
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
590
|
+
const parser = new Parser(builder, matcher);
|
|
591
|
+
try {
|
|
592
|
+
const gherkinDocument = parser.parse(source);
|
|
593
|
+
gherkinDocument.uri = uri;
|
|
594
|
+
return { gherkinDocument, errors };
|
|
595
|
+
} catch (error) {
|
|
596
|
+
errors.push(formatError2(error));
|
|
597
|
+
return { gherkinDocument: null, errors };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function formatError2(error) {
|
|
601
|
+
if (error instanceof Error) {
|
|
602
|
+
return error.message;
|
|
603
|
+
}
|
|
604
|
+
return String(error);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/core/scenarioModel.ts
|
|
608
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
609
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
610
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
611
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
612
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
613
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
614
|
+
function parseScenarioDocument(text, uri) {
|
|
615
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
616
|
+
if (!gherkinDocument) {
|
|
617
|
+
return { document: null, errors };
|
|
618
|
+
}
|
|
619
|
+
const feature = gherkinDocument.feature;
|
|
620
|
+
if (!feature) {
|
|
621
|
+
return {
|
|
622
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
623
|
+
errors
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const featureTags = collectTagNames(feature.tags);
|
|
627
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
628
|
+
return {
|
|
629
|
+
document: {
|
|
630
|
+
uri,
|
|
631
|
+
featureName: feature.name,
|
|
632
|
+
featureTags,
|
|
633
|
+
scenarios
|
|
634
|
+
},
|
|
635
|
+
errors
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function buildScenarioAtoms(document) {
|
|
639
|
+
return document.scenarios.map((scenario) => {
|
|
640
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
641
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
642
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
643
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
644
|
+
scenario.tags.forEach((tag) => {
|
|
645
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
646
|
+
contractIds.add(tag);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
for (const step of scenario.steps) {
|
|
650
|
+
for (const text of collectStepTexts(step)) {
|
|
651
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
652
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
653
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const atom = {
|
|
657
|
+
uri: document.uri,
|
|
658
|
+
featureName: document.featureName ?? "",
|
|
659
|
+
scenarioName: scenario.name,
|
|
660
|
+
kind: scenario.kind,
|
|
661
|
+
brIds,
|
|
662
|
+
contractIds: Array.from(contractIds).sort()
|
|
663
|
+
};
|
|
664
|
+
if (scenario.line !== void 0) {
|
|
665
|
+
atom.line = scenario.line;
|
|
666
|
+
}
|
|
667
|
+
if (specIds.length === 1) {
|
|
668
|
+
const specId = specIds[0];
|
|
669
|
+
if (specId) {
|
|
670
|
+
atom.specId = specId;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (scIds.length === 1) {
|
|
674
|
+
const scId = scIds[0];
|
|
675
|
+
if (scId) {
|
|
676
|
+
atom.scId = scId;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return atom;
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
683
|
+
const scenarios = [];
|
|
684
|
+
for (const child of feature.children) {
|
|
685
|
+
if (child.scenario) {
|
|
686
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
687
|
+
}
|
|
688
|
+
if (child.rule) {
|
|
689
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
690
|
+
for (const ruleChild of child.rule.children) {
|
|
691
|
+
if (ruleChild.scenario) {
|
|
692
|
+
scenarios.push(
|
|
693
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return scenarios;
|
|
700
|
+
}
|
|
701
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
702
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
703
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
704
|
+
return {
|
|
705
|
+
name: scenario.name,
|
|
706
|
+
kind,
|
|
707
|
+
line: scenario.location?.line,
|
|
708
|
+
tags,
|
|
709
|
+
steps: scenario.steps
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function collectTagNames(tags) {
|
|
713
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
714
|
+
}
|
|
715
|
+
function collectStepTexts(step) {
|
|
716
|
+
const texts = [];
|
|
717
|
+
if (step.text) {
|
|
718
|
+
texts.push(step.text);
|
|
719
|
+
}
|
|
720
|
+
if (step.docString?.content) {
|
|
721
|
+
texts.push(step.docString.content);
|
|
722
|
+
}
|
|
723
|
+
if (step.dataTable?.rows) {
|
|
724
|
+
for (const row of step.dataTable.rows) {
|
|
725
|
+
for (const cell of row.cells) {
|
|
726
|
+
texts.push(cell.value);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return texts;
|
|
731
|
+
}
|
|
732
|
+
function unique2(values) {
|
|
733
|
+
return Array.from(new Set(values));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/core/traceability.ts
|
|
737
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
738
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
739
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
740
|
+
"**/node_modules/**",
|
|
741
|
+
"**/.git/**",
|
|
742
|
+
"**/.qfai/**",
|
|
743
|
+
"**/dist/**",
|
|
744
|
+
"**/build/**",
|
|
745
|
+
"**/coverage/**",
|
|
746
|
+
"**/.next/**",
|
|
747
|
+
"**/out/**"
|
|
748
|
+
];
|
|
749
|
+
function extractAnnotatedScIds(text) {
|
|
750
|
+
const ids = /* @__PURE__ */ new Set();
|
|
751
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
752
|
+
const suffix = match[1];
|
|
753
|
+
if (suffix) {
|
|
754
|
+
ids.add(`SC-${suffix}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return Array.from(ids);
|
|
758
|
+
}
|
|
759
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
760
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const file of scenarioFiles) {
|
|
762
|
+
const text = await readFile2(file, "utf-8");
|
|
763
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
764
|
+
if (!document || errors.length > 0) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
for (const scenario of document.scenarios) {
|
|
768
|
+
for (const tag of scenario.tags) {
|
|
769
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
770
|
+
scIds.add(tag);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return scIds;
|
|
776
|
+
}
|
|
777
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
778
|
+
const sources = /* @__PURE__ */ new Map();
|
|
779
|
+
for (const file of scenarioFiles) {
|
|
780
|
+
const text = await readFile2(file, "utf-8");
|
|
781
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
782
|
+
if (!document || errors.length > 0) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
for (const scenario of document.scenarios) {
|
|
786
|
+
for (const tag of scenario.tags) {
|
|
787
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
791
|
+
current.add(file);
|
|
792
|
+
sources.set(tag, current);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return sources;
|
|
797
|
+
}
|
|
798
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
799
|
+
const refs = /* @__PURE__ */ new Map();
|
|
800
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
801
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
802
|
+
const mergedExcludeGlobs = Array.from(
|
|
803
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
804
|
+
);
|
|
805
|
+
if (normalizedGlobs.length === 0) {
|
|
806
|
+
return {
|
|
807
|
+
refs,
|
|
808
|
+
scan: {
|
|
809
|
+
globs: normalizedGlobs,
|
|
810
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
811
|
+
matchedFileCount: 0
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
let files = [];
|
|
816
|
+
try {
|
|
817
|
+
files = await collectFilesByGlobs(root, {
|
|
818
|
+
globs: normalizedGlobs,
|
|
819
|
+
ignore: mergedExcludeGlobs
|
|
820
|
+
});
|
|
821
|
+
} catch (error) {
|
|
822
|
+
return {
|
|
823
|
+
refs,
|
|
824
|
+
scan: {
|
|
825
|
+
globs: normalizedGlobs,
|
|
826
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
827
|
+
matchedFileCount: 0
|
|
828
|
+
},
|
|
829
|
+
error: formatError3(error)
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const normalizedFiles = Array.from(
|
|
833
|
+
new Set(files.map((file) => path4.normalize(file)))
|
|
834
|
+
);
|
|
835
|
+
for (const file of normalizedFiles) {
|
|
836
|
+
const text = await readFile2(file, "utf-8");
|
|
837
|
+
const scIds = extractAnnotatedScIds(text);
|
|
838
|
+
if (scIds.length === 0) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
for (const scId of scIds) {
|
|
842
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
843
|
+
current.add(file);
|
|
844
|
+
refs.set(scId, current);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
refs,
|
|
849
|
+
scan: {
|
|
850
|
+
globs: normalizedGlobs,
|
|
851
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
852
|
+
matchedFileCount: normalizedFiles.length
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function buildScCoverage(scIds, refs) {
|
|
857
|
+
const sortedScIds = toSortedArray(scIds);
|
|
858
|
+
const refsRecord = {};
|
|
859
|
+
const missingIds = [];
|
|
860
|
+
let covered = 0;
|
|
861
|
+
for (const scId of sortedScIds) {
|
|
862
|
+
const files = refs.get(scId);
|
|
863
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
864
|
+
refsRecord[scId] = sortedFiles;
|
|
865
|
+
if (sortedFiles.length === 0) {
|
|
866
|
+
missingIds.push(scId);
|
|
867
|
+
} else {
|
|
868
|
+
covered += 1;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
total: sortedScIds.length,
|
|
873
|
+
covered,
|
|
874
|
+
missing: missingIds.length,
|
|
875
|
+
missingIds,
|
|
876
|
+
refs: refsRecord
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function toSortedArray(values) {
|
|
880
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
881
|
+
}
|
|
882
|
+
function normalizeGlobs(globs) {
|
|
883
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
884
|
+
}
|
|
885
|
+
function formatError3(error) {
|
|
886
|
+
if (error instanceof Error) {
|
|
887
|
+
return error.message;
|
|
888
|
+
}
|
|
889
|
+
return String(error);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/core/version.ts
|
|
893
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
894
|
+
import path5 from "path";
|
|
535
895
|
import { fileURLToPath } from "url";
|
|
536
896
|
async function resolveToolVersion() {
|
|
537
|
-
if ("0.
|
|
538
|
-
return "0.
|
|
897
|
+
if ("0.4.2".length > 0) {
|
|
898
|
+
return "0.4.2";
|
|
539
899
|
}
|
|
540
900
|
try {
|
|
541
901
|
const packagePath = resolvePackageJsonPath();
|
|
542
|
-
const raw = await
|
|
902
|
+
const raw = await readFile3(packagePath, "utf-8");
|
|
543
903
|
const parsed = JSON.parse(raw);
|
|
544
904
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
545
905
|
return version.length > 0 ? version : "unknown";
|
|
@@ -550,18 +910,18 @@ async function resolveToolVersion() {
|
|
|
550
910
|
function resolvePackageJsonPath() {
|
|
551
911
|
const base = import.meta.url;
|
|
552
912
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
553
|
-
return
|
|
913
|
+
return path5.resolve(path5.dirname(basePath), "../../package.json");
|
|
554
914
|
}
|
|
555
915
|
|
|
556
916
|
// src/core/validators/contracts.ts
|
|
557
|
-
import { readFile as
|
|
558
|
-
import
|
|
917
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
918
|
+
import path7 from "path";
|
|
559
919
|
|
|
560
920
|
// src/core/contracts.ts
|
|
561
|
-
import
|
|
921
|
+
import path6 from "path";
|
|
562
922
|
import { parse as parseYaml2 } from "yaml";
|
|
563
923
|
function parseStructuredContract(file, text) {
|
|
564
|
-
const ext =
|
|
924
|
+
const ext = path6.extname(file).toLowerCase();
|
|
565
925
|
if (ext === ".json") {
|
|
566
926
|
return JSON.parse(text);
|
|
567
927
|
}
|
|
@@ -612,9 +972,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
612
972
|
async function validateContracts(root, config) {
|
|
613
973
|
const issues = [];
|
|
614
974
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
615
|
-
issues.push(...await validateUiContracts(
|
|
616
|
-
issues.push(...await validateApiContracts(
|
|
617
|
-
issues.push(...await validateDataContracts(
|
|
975
|
+
issues.push(...await validateUiContracts(path7.join(contractsRoot, "ui")));
|
|
976
|
+
issues.push(...await validateApiContracts(path7.join(contractsRoot, "api")));
|
|
977
|
+
issues.push(...await validateDataContracts(path7.join(contractsRoot, "db")));
|
|
618
978
|
return issues;
|
|
619
979
|
}
|
|
620
980
|
async function validateUiContracts(uiRoot) {
|
|
@@ -632,7 +992,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
632
992
|
}
|
|
633
993
|
const issues = [];
|
|
634
994
|
for (const file of files) {
|
|
635
|
-
const text = await
|
|
995
|
+
const text = await readFile4(file, "utf-8");
|
|
636
996
|
const invalidIds = extractInvalidIds(text, [
|
|
637
997
|
"SPEC",
|
|
638
998
|
"BR",
|
|
@@ -661,7 +1021,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
661
1021
|
issues.push(
|
|
662
1022
|
issue(
|
|
663
1023
|
"QFAI-CONTRACT-001",
|
|
664
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1024
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
665
1025
|
"error",
|
|
666
1026
|
file,
|
|
667
1027
|
"contracts.ui.parse"
|
|
@@ -699,7 +1059,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
699
1059
|
}
|
|
700
1060
|
const issues = [];
|
|
701
1061
|
for (const file of files) {
|
|
702
|
-
const text = await
|
|
1062
|
+
const text = await readFile4(file, "utf-8");
|
|
703
1063
|
const invalidIds = extractInvalidIds(text, [
|
|
704
1064
|
"SPEC",
|
|
705
1065
|
"BR",
|
|
@@ -728,7 +1088,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
728
1088
|
issues.push(
|
|
729
1089
|
issue(
|
|
730
1090
|
"QFAI-CONTRACT-001",
|
|
731
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1091
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
732
1092
|
"error",
|
|
733
1093
|
file,
|
|
734
1094
|
"contracts.api.parse"
|
|
@@ -777,7 +1137,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
777
1137
|
}
|
|
778
1138
|
const issues = [];
|
|
779
1139
|
for (const file of files) {
|
|
780
|
-
const text = await
|
|
1140
|
+
const text = await readFile4(file, "utf-8");
|
|
781
1141
|
const invalidIds = extractInvalidIds(text, [
|
|
782
1142
|
"SPEC",
|
|
783
1143
|
"BR",
|
|
@@ -823,7 +1183,7 @@ function lintSql(text, file) {
|
|
|
823
1183
|
function hasOpenApi(doc) {
|
|
824
1184
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
825
1185
|
}
|
|
826
|
-
function
|
|
1186
|
+
function formatError4(error) {
|
|
827
1187
|
if (error instanceof Error) {
|
|
828
1188
|
return error.message;
|
|
829
1189
|
}
|
|
@@ -848,8 +1208,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
848
1208
|
}
|
|
849
1209
|
|
|
850
1210
|
// src/core/validators/delta.ts
|
|
851
|
-
import { readFile as
|
|
852
|
-
import
|
|
1211
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1212
|
+
import path8 from "path";
|
|
853
1213
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
854
1214
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
855
1215
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -863,10 +1223,10 @@ async function validateDeltas(root, config) {
|
|
|
863
1223
|
}
|
|
864
1224
|
const issues = [];
|
|
865
1225
|
for (const pack of packs) {
|
|
866
|
-
const deltaPath =
|
|
1226
|
+
const deltaPath = path8.join(pack, "delta.md");
|
|
867
1227
|
let text;
|
|
868
1228
|
try {
|
|
869
|
-
text = await
|
|
1229
|
+
text = await readFile5(deltaPath, "utf-8");
|
|
870
1230
|
} catch (error) {
|
|
871
1231
|
if (isMissingFileError2(error)) {
|
|
872
1232
|
issues.push(
|
|
@@ -938,17 +1298,17 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
938
1298
|
}
|
|
939
1299
|
|
|
940
1300
|
// src/core/validators/ids.ts
|
|
941
|
-
import { readFile as
|
|
942
|
-
import
|
|
1301
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1302
|
+
import path10 from "path";
|
|
943
1303
|
|
|
944
1304
|
// src/core/contractIndex.ts
|
|
945
|
-
import { readFile as
|
|
946
|
-
import
|
|
1305
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1306
|
+
import path9 from "path";
|
|
947
1307
|
async function buildContractIndex(root, config) {
|
|
948
1308
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
949
|
-
const uiRoot =
|
|
950
|
-
const apiRoot =
|
|
951
|
-
const dataRoot =
|
|
1309
|
+
const uiRoot = path9.join(contractsRoot, "ui");
|
|
1310
|
+
const apiRoot = path9.join(contractsRoot, "api");
|
|
1311
|
+
const dataRoot = path9.join(contractsRoot, "db");
|
|
952
1312
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
953
1313
|
collectUiContractFiles(uiRoot),
|
|
954
1314
|
collectApiContractFiles(apiRoot),
|
|
@@ -967,7 +1327,7 @@ async function buildContractIndex(root, config) {
|
|
|
967
1327
|
}
|
|
968
1328
|
async function indexUiContracts(files, index) {
|
|
969
1329
|
for (const file of files) {
|
|
970
|
-
const text = await
|
|
1330
|
+
const text = await readFile6(file, "utf-8");
|
|
971
1331
|
try {
|
|
972
1332
|
const doc = parseStructuredContract(file, text);
|
|
973
1333
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -979,7 +1339,7 @@ async function indexUiContracts(files, index) {
|
|
|
979
1339
|
}
|
|
980
1340
|
async function indexApiContracts(files, index) {
|
|
981
1341
|
for (const file of files) {
|
|
982
|
-
const text = await
|
|
1342
|
+
const text = await readFile6(file, "utf-8");
|
|
983
1343
|
try {
|
|
984
1344
|
const doc = parseStructuredContract(file, text);
|
|
985
1345
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -991,7 +1351,7 @@ async function indexApiContracts(files, index) {
|
|
|
991
1351
|
}
|
|
992
1352
|
async function indexDataContracts(files, index) {
|
|
993
1353
|
for (const file of files) {
|
|
994
|
-
const text = await
|
|
1354
|
+
const text = await readFile6(file, "utf-8");
|
|
995
1355
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
996
1356
|
}
|
|
997
1357
|
}
|
|
@@ -1031,255 +1391,97 @@ function extractH2Sections(md) {
|
|
|
1031
1391
|
if (!current) continue;
|
|
1032
1392
|
const next = headings[i + 1];
|
|
1033
1393
|
const startLine = current.line + 1;
|
|
1034
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1035
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1036
|
-
sections.set(current.title.trim(), {
|
|
1037
|
-
title: current.title.trim(),
|
|
1038
|
-
startLine,
|
|
1039
|
-
endLine,
|
|
1040
|
-
body
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
return sections;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// src/core/parse/spec.ts
|
|
1047
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1048
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1049
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1050
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1051
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1052
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1053
|
-
function parseSpec(md, file) {
|
|
1054
|
-
const headings = parseHeadings(md);
|
|
1055
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1056
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1057
|
-
const sections = extractH2Sections(md);
|
|
1058
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1059
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1060
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1061
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1062
|
-
const brs = [];
|
|
1063
|
-
const brsWithoutPriority = [];
|
|
1064
|
-
const brsWithInvalidPriority = [];
|
|
1065
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1066
|
-
const lineText = brLines[i] ?? "";
|
|
1067
|
-
const lineNumber = startLine + i;
|
|
1068
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1069
|
-
if (validMatch) {
|
|
1070
|
-
const id = validMatch[1];
|
|
1071
|
-
const priority = validMatch[2];
|
|
1072
|
-
const text = validMatch[3];
|
|
1073
|
-
if (!id || !priority || !text) continue;
|
|
1074
|
-
brs.push({
|
|
1075
|
-
id,
|
|
1076
|
-
priority,
|
|
1077
|
-
text: text.trim(),
|
|
1078
|
-
line: lineNumber
|
|
1079
|
-
});
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1083
|
-
if (anyPriorityMatch) {
|
|
1084
|
-
const id = anyPriorityMatch[1];
|
|
1085
|
-
const priority = anyPriorityMatch[2];
|
|
1086
|
-
const text = anyPriorityMatch[3];
|
|
1087
|
-
if (!id || !priority || !text) continue;
|
|
1088
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1089
|
-
brsWithInvalidPriority.push({
|
|
1090
|
-
id,
|
|
1091
|
-
priority,
|
|
1092
|
-
text: text.trim(),
|
|
1093
|
-
line: lineNumber
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1099
|
-
if (noPriorityMatch) {
|
|
1100
|
-
const id = noPriorityMatch[1];
|
|
1101
|
-
const text = noPriorityMatch[2];
|
|
1102
|
-
if (!id || !text) continue;
|
|
1103
|
-
brsWithoutPriority.push({
|
|
1104
|
-
id,
|
|
1105
|
-
text: text.trim(),
|
|
1106
|
-
line: lineNumber
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
const parsed = {
|
|
1111
|
-
file,
|
|
1112
|
-
sections: sectionNames,
|
|
1113
|
-
brs,
|
|
1114
|
-
brsWithoutPriority,
|
|
1115
|
-
brsWithInvalidPriority
|
|
1116
|
-
};
|
|
1117
|
-
if (specId) {
|
|
1118
|
-
parsed.specId = specId;
|
|
1119
|
-
}
|
|
1120
|
-
return parsed;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// src/core/gherkin/parse.ts
|
|
1124
|
-
import {
|
|
1125
|
-
AstBuilder,
|
|
1126
|
-
GherkinClassicTokenMatcher,
|
|
1127
|
-
Parser
|
|
1128
|
-
} from "@cucumber/gherkin";
|
|
1129
|
-
import { randomUUID } from "crypto";
|
|
1130
|
-
function parseGherkin(source, uri) {
|
|
1131
|
-
const errors = [];
|
|
1132
|
-
const uuidFn = () => randomUUID();
|
|
1133
|
-
const builder = new AstBuilder(uuidFn);
|
|
1134
|
-
const matcher = new GherkinClassicTokenMatcher();
|
|
1135
|
-
const parser = new Parser(builder, matcher);
|
|
1136
|
-
try {
|
|
1137
|
-
const gherkinDocument = parser.parse(source);
|
|
1138
|
-
gherkinDocument.uri = uri;
|
|
1139
|
-
return { gherkinDocument, errors };
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
errors.push(formatError3(error));
|
|
1142
|
-
return { gherkinDocument: null, errors };
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
function formatError3(error) {
|
|
1146
|
-
if (error instanceof Error) {
|
|
1147
|
-
return error.message;
|
|
1148
|
-
}
|
|
1149
|
-
return String(error);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// src/core/scenarioModel.ts
|
|
1153
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1154
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1155
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1156
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1157
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1158
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1159
|
-
function parseScenarioDocument(text, uri) {
|
|
1160
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1161
|
-
if (!gherkinDocument) {
|
|
1162
|
-
return { document: null, errors };
|
|
1163
|
-
}
|
|
1164
|
-
const feature = gherkinDocument.feature;
|
|
1165
|
-
if (!feature) {
|
|
1166
|
-
return {
|
|
1167
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1168
|
-
errors
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1172
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1173
|
-
return {
|
|
1174
|
-
document: {
|
|
1175
|
-
uri,
|
|
1176
|
-
featureName: feature.name,
|
|
1177
|
-
featureTags,
|
|
1178
|
-
scenarios
|
|
1179
|
-
},
|
|
1180
|
-
errors
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
function buildScenarioAtoms(document) {
|
|
1184
|
-
return document.scenarios.map((scenario) => {
|
|
1185
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1186
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1187
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1188
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1189
|
-
scenario.tags.forEach((tag) => {
|
|
1190
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1191
|
-
contractIds.add(tag);
|
|
1192
|
-
}
|
|
1193
|
-
});
|
|
1194
|
-
for (const step of scenario.steps) {
|
|
1195
|
-
for (const text of collectStepTexts(step)) {
|
|
1196
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1197
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1198
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
const atom = {
|
|
1202
|
-
uri: document.uri,
|
|
1203
|
-
featureName: document.featureName ?? "",
|
|
1204
|
-
scenarioName: scenario.name,
|
|
1205
|
-
kind: scenario.kind,
|
|
1206
|
-
brIds,
|
|
1207
|
-
contractIds: Array.from(contractIds).sort()
|
|
1208
|
-
};
|
|
1209
|
-
if (scenario.line !== void 0) {
|
|
1210
|
-
atom.line = scenario.line;
|
|
1211
|
-
}
|
|
1212
|
-
if (specIds.length === 1) {
|
|
1213
|
-
const specId = specIds[0];
|
|
1214
|
-
if (specId) {
|
|
1215
|
-
atom.specId = specId;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
if (scIds.length === 1) {
|
|
1219
|
-
const scId = scIds[0];
|
|
1220
|
-
if (scId) {
|
|
1221
|
-
atom.scId = scId;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
return atom;
|
|
1225
|
-
});
|
|
1394
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1395
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1396
|
+
sections.set(current.title.trim(), {
|
|
1397
|
+
title: current.title.trim(),
|
|
1398
|
+
startLine,
|
|
1399
|
+
endLine,
|
|
1400
|
+
body
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return sections;
|
|
1226
1404
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1405
|
+
|
|
1406
|
+
// src/core/parse/spec.ts
|
|
1407
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1408
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1409
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1410
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1411
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1412
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1413
|
+
function parseSpec(md, file) {
|
|
1414
|
+
const headings = parseHeadings(md);
|
|
1415
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1416
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1417
|
+
const sections = extractH2Sections(md);
|
|
1418
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1419
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1420
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1421
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1422
|
+
const brs = [];
|
|
1423
|
+
const brsWithoutPriority = [];
|
|
1424
|
+
const brsWithInvalidPriority = [];
|
|
1425
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1426
|
+
const lineText = brLines[i] ?? "";
|
|
1427
|
+
const lineNumber = startLine + i;
|
|
1428
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1429
|
+
if (validMatch) {
|
|
1430
|
+
const id = validMatch[1];
|
|
1431
|
+
const priority = validMatch[2];
|
|
1432
|
+
const text = validMatch[3];
|
|
1433
|
+
if (!id || !priority || !text) continue;
|
|
1434
|
+
brs.push({
|
|
1435
|
+
id,
|
|
1436
|
+
priority,
|
|
1437
|
+
text: text.trim(),
|
|
1438
|
+
line: lineNumber
|
|
1439
|
+
});
|
|
1440
|
+
continue;
|
|
1232
1441
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1442
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1443
|
+
if (anyPriorityMatch) {
|
|
1444
|
+
const id = anyPriorityMatch[1];
|
|
1445
|
+
const priority = anyPriorityMatch[2];
|
|
1446
|
+
const text = anyPriorityMatch[3];
|
|
1447
|
+
if (!id || !priority || !text) continue;
|
|
1448
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1449
|
+
brsWithInvalidPriority.push({
|
|
1450
|
+
id,
|
|
1451
|
+
priority,
|
|
1452
|
+
text: text.trim(),
|
|
1453
|
+
line: lineNumber
|
|
1454
|
+
});
|
|
1241
1455
|
}
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1459
|
+
if (noPriorityMatch) {
|
|
1460
|
+
const id = noPriorityMatch[1];
|
|
1461
|
+
const text = noPriorityMatch[2];
|
|
1462
|
+
if (!id || !text) continue;
|
|
1463
|
+
brsWithoutPriority.push({
|
|
1464
|
+
id,
|
|
1465
|
+
text: text.trim(),
|
|
1466
|
+
line: lineNumber
|
|
1467
|
+
});
|
|
1242
1468
|
}
|
|
1243
1469
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
name: scenario.name,
|
|
1251
|
-
kind,
|
|
1252
|
-
line: scenario.location?.line,
|
|
1253
|
-
tags,
|
|
1254
|
-
steps: scenario.steps
|
|
1470
|
+
const parsed = {
|
|
1471
|
+
file,
|
|
1472
|
+
sections: sectionNames,
|
|
1473
|
+
brs,
|
|
1474
|
+
brsWithoutPriority,
|
|
1475
|
+
brsWithInvalidPriority
|
|
1255
1476
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1259
|
-
}
|
|
1260
|
-
function collectStepTexts(step) {
|
|
1261
|
-
const texts = [];
|
|
1262
|
-
if (step.text) {
|
|
1263
|
-
texts.push(step.text);
|
|
1264
|
-
}
|
|
1265
|
-
if (step.docString?.content) {
|
|
1266
|
-
texts.push(step.docString.content);
|
|
1267
|
-
}
|
|
1268
|
-
if (step.dataTable?.rows) {
|
|
1269
|
-
for (const row of step.dataTable.rows) {
|
|
1270
|
-
for (const cell of row.cells) {
|
|
1271
|
-
texts.push(cell.value);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1477
|
+
if (specId) {
|
|
1478
|
+
parsed.specId = specId;
|
|
1274
1479
|
}
|
|
1275
|
-
return
|
|
1276
|
-
}
|
|
1277
|
-
function unique2(values) {
|
|
1278
|
-
return Array.from(new Set(values));
|
|
1480
|
+
return parsed;
|
|
1279
1481
|
}
|
|
1280
1482
|
|
|
1281
1483
|
// src/core/validators/ids.ts
|
|
1282
|
-
var
|
|
1484
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1283
1485
|
async function validateDefinedIds(root, config) {
|
|
1284
1486
|
const issues = [];
|
|
1285
1487
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1313,7 +1515,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1313
1515
|
}
|
|
1314
1516
|
async function collectSpecDefinitionIds(files, out) {
|
|
1315
1517
|
for (const file of files) {
|
|
1316
|
-
const text = await
|
|
1518
|
+
const text = await readFile7(file, "utf-8");
|
|
1317
1519
|
const parsed = parseSpec(text, file);
|
|
1318
1520
|
if (parsed.specId) {
|
|
1319
1521
|
recordId(out, parsed.specId, file);
|
|
@@ -1323,14 +1525,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1323
1525
|
}
|
|
1324
1526
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1325
1527
|
for (const file of files) {
|
|
1326
|
-
const text = await
|
|
1528
|
+
const text = await readFile7(file, "utf-8");
|
|
1327
1529
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1328
1530
|
if (!document || errors.length > 0) {
|
|
1329
1531
|
continue;
|
|
1330
1532
|
}
|
|
1331
1533
|
for (const scenario of document.scenarios) {
|
|
1332
1534
|
for (const tag of scenario.tags) {
|
|
1333
|
-
if (
|
|
1535
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1334
1536
|
recordId(out, tag, file);
|
|
1335
1537
|
}
|
|
1336
1538
|
}
|
|
@@ -1344,7 +1546,7 @@ function recordId(out, id, file) {
|
|
|
1344
1546
|
}
|
|
1345
1547
|
function formatFileList(files, root) {
|
|
1346
1548
|
return files.map((file) => {
|
|
1347
|
-
const relative =
|
|
1549
|
+
const relative = path10.relative(root, file);
|
|
1348
1550
|
return relative.length > 0 ? relative : file;
|
|
1349
1551
|
}).join(", ");
|
|
1350
1552
|
}
|
|
@@ -1367,13 +1569,12 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1367
1569
|
}
|
|
1368
1570
|
|
|
1369
1571
|
// src/core/validators/scenario.ts
|
|
1370
|
-
import { readFile as
|
|
1572
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1371
1573
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1372
1574
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1373
1575
|
var THEN_PATTERN = /\bThen\b/;
|
|
1374
|
-
var
|
|
1576
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1375
1577
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1376
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1377
1578
|
async function validateScenarios(root, config) {
|
|
1378
1579
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1379
1580
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1394,7 +1595,7 @@ async function validateScenarios(root, config) {
|
|
|
1394
1595
|
for (const entry of entries) {
|
|
1395
1596
|
let text;
|
|
1396
1597
|
try {
|
|
1397
|
-
text = await
|
|
1598
|
+
text = await readFile8(entry.scenarioPath, "utf-8");
|
|
1398
1599
|
} catch (error) {
|
|
1399
1600
|
if (isMissingFileError3(error)) {
|
|
1400
1601
|
issues.push(
|
|
@@ -1453,17 +1654,7 @@ function validateScenarioContent(text, file) {
|
|
|
1453
1654
|
const featureSpecTags = document.featureTags.filter(
|
|
1454
1655
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1455
1656
|
);
|
|
1456
|
-
if (featureSpecTags.length
|
|
1457
|
-
issues.push(
|
|
1458
|
-
issue4(
|
|
1459
|
-
"QFAI-SC-009",
|
|
1460
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1461
|
-
"error",
|
|
1462
|
-
file,
|
|
1463
|
-
"scenario.featureSpec"
|
|
1464
|
-
)
|
|
1465
|
-
);
|
|
1466
|
-
} else if (featureSpecTags.length > 1) {
|
|
1657
|
+
if (featureSpecTags.length > 1) {
|
|
1467
1658
|
issues.push(
|
|
1468
1659
|
issue4(
|
|
1469
1660
|
"QFAI-SC-009",
|
|
@@ -1505,18 +1696,12 @@ function validateScenarioContent(text, file) {
|
|
|
1505
1696
|
continue;
|
|
1506
1697
|
}
|
|
1507
1698
|
const missingTags = [];
|
|
1508
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1699
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1509
1700
|
if (scTags.length === 0) {
|
|
1510
1701
|
missingTags.push("SC(0\u4EF6)");
|
|
1511
1702
|
} else if (scTags.length > 1) {
|
|
1512
1703
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1513
1704
|
}
|
|
1514
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1515
|
-
missingTags.push("SPEC");
|
|
1516
|
-
}
|
|
1517
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1518
|
-
missingTags.push("BR");
|
|
1519
|
-
}
|
|
1520
1705
|
if (missingTags.length > 0) {
|
|
1521
1706
|
issues.push(
|
|
1522
1707
|
issue4(
|
|
@@ -1580,7 +1765,7 @@ function isMissingFileError3(error) {
|
|
|
1580
1765
|
}
|
|
1581
1766
|
|
|
1582
1767
|
// src/core/validators/spec.ts
|
|
1583
|
-
import { readFile as
|
|
1768
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1584
1769
|
async function validateSpecs(root, config) {
|
|
1585
1770
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1586
1771
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1601,7 +1786,7 @@ async function validateSpecs(root, config) {
|
|
|
1601
1786
|
for (const entry of entries) {
|
|
1602
1787
|
let text;
|
|
1603
1788
|
try {
|
|
1604
|
-
text = await
|
|
1789
|
+
text = await readFile9(entry.specPath, "utf-8");
|
|
1605
1790
|
} catch (error) {
|
|
1606
1791
|
if (isMissingFileError4(error)) {
|
|
1607
1792
|
issues.push(
|
|
@@ -1750,10 +1935,9 @@ function isMissingFileError4(error) {
|
|
|
1750
1935
|
}
|
|
1751
1936
|
|
|
1752
1937
|
// src/core/validators/traceability.ts
|
|
1753
|
-
import { readFile as
|
|
1754
|
-
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1938
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1755
1939
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1756
|
-
var
|
|
1940
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1757
1941
|
async function validateTraceability(root, config) {
|
|
1758
1942
|
const issues = [];
|
|
1759
1943
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1772,7 +1956,7 @@ async function validateTraceability(root, config) {
|
|
|
1772
1956
|
const contractIndex = await buildContractIndex(root, config);
|
|
1773
1957
|
const contractIds = contractIndex.ids;
|
|
1774
1958
|
for (const file of specFiles) {
|
|
1775
|
-
const text = await
|
|
1959
|
+
const text = await readFile10(file, "utf-8");
|
|
1776
1960
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1777
1961
|
const parsed = parseSpec(text, file);
|
|
1778
1962
|
if (parsed.specId) {
|
|
@@ -1780,28 +1964,6 @@ async function validateTraceability(root, config) {
|
|
|
1780
1964
|
}
|
|
1781
1965
|
const brIds = parsed.brs.map((br) => br.id);
|
|
1782
1966
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1783
|
-
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1784
|
-
...extractIds(text, "UI"),
|
|
1785
|
-
...extractIds(text, "API"),
|
|
1786
|
-
...extractIds(text, "DATA")
|
|
1787
|
-
]);
|
|
1788
|
-
const unknownContractIds = Array.from(referencedContractIds).filter(
|
|
1789
|
-
(id) => !contractIds.has(id)
|
|
1790
|
-
);
|
|
1791
|
-
if (unknownContractIds.length > 0) {
|
|
1792
|
-
issues.push(
|
|
1793
|
-
issue6(
|
|
1794
|
-
"QFAI-TRACE-009",
|
|
1795
|
-
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1796
|
-
", "
|
|
1797
|
-
)}`,
|
|
1798
|
-
"error",
|
|
1799
|
-
file,
|
|
1800
|
-
"traceability.specContractExists",
|
|
1801
|
-
unknownContractIds
|
|
1802
|
-
)
|
|
1803
|
-
);
|
|
1804
|
-
}
|
|
1805
1967
|
if (parsed.specId) {
|
|
1806
1968
|
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1807
1969
|
brIds.forEach((id) => current.add(id));
|
|
@@ -1809,23 +1971,49 @@ async function validateTraceability(root, config) {
|
|
|
1809
1971
|
}
|
|
1810
1972
|
}
|
|
1811
1973
|
for (const file of scenarioFiles) {
|
|
1812
|
-
const text = await
|
|
1974
|
+
const text = await readFile10(file, "utf-8");
|
|
1813
1975
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1814
1976
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1815
1977
|
if (!document || errors.length > 0) {
|
|
1816
1978
|
continue;
|
|
1817
1979
|
}
|
|
1818
1980
|
const atoms = buildScenarioAtoms(document);
|
|
1981
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
1819
1982
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1820
1983
|
const atom = atoms[index];
|
|
1821
1984
|
if (!atom) {
|
|
1822
1985
|
continue;
|
|
1823
1986
|
}
|
|
1824
1987
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1825
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
1826
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1988
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
1989
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
1990
|
+
if (specTags.length === 0) {
|
|
1991
|
+
issues.push(
|
|
1992
|
+
issue6(
|
|
1993
|
+
"QFAI-TRACE-014",
|
|
1994
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
1995
|
+
"error",
|
|
1996
|
+
file,
|
|
1997
|
+
"traceability.scenarioSpecRequired"
|
|
1998
|
+
)
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
if (brTags.length === 0) {
|
|
2002
|
+
issues.push(
|
|
2003
|
+
issue6(
|
|
2004
|
+
"QFAI-TRACE-015",
|
|
2005
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2006
|
+
"error",
|
|
2007
|
+
file,
|
|
2008
|
+
"traceability.scenarioBrRequired"
|
|
2009
|
+
)
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
1827
2012
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1828
|
-
scTags.forEach((id) =>
|
|
2013
|
+
scTags.forEach((id) => {
|
|
2014
|
+
scIdsInScenarios.add(id);
|
|
2015
|
+
scIdsInFile.add(id);
|
|
2016
|
+
});
|
|
1829
2017
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1830
2018
|
if (atom.contractIds.length > 0) {
|
|
1831
2019
|
scTags.forEach((id) => scWithContracts.add(id));
|
|
@@ -1903,6 +2091,22 @@ async function validateTraceability(root, config) {
|
|
|
1903
2091
|
}
|
|
1904
2092
|
}
|
|
1905
2093
|
}
|
|
2094
|
+
if (scIdsInFile.size !== 1) {
|
|
2095
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2096
|
+
(a, b) => a.localeCompare(b)
|
|
2097
|
+
);
|
|
2098
|
+
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(", ")}`;
|
|
2099
|
+
issues.push(
|
|
2100
|
+
issue6(
|
|
2101
|
+
"QFAI-TRACE-012",
|
|
2102
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2103
|
+
"error",
|
|
2104
|
+
file,
|
|
2105
|
+
"traceability.specScOneToOne",
|
|
2106
|
+
invalidScIds
|
|
2107
|
+
)
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
1906
2110
|
}
|
|
1907
2111
|
if (upstreamIds.size === 0) {
|
|
1908
2112
|
return [
|
|
@@ -1951,6 +2155,66 @@ async function validateTraceability(root, config) {
|
|
|
1951
2155
|
);
|
|
1952
2156
|
}
|
|
1953
2157
|
}
|
|
2158
|
+
const scRefsResult = await collectScTestReferences(
|
|
2159
|
+
root,
|
|
2160
|
+
config.validation.traceability.testFileGlobs,
|
|
2161
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2162
|
+
);
|
|
2163
|
+
const scTestRefs = scRefsResult.refs;
|
|
2164
|
+
const testFileScan = scRefsResult.scan;
|
|
2165
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2166
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2167
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2168
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2169
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2170
|
+
issues.push(
|
|
2171
|
+
issue6(
|
|
2172
|
+
"QFAI-TRACE-013",
|
|
2173
|
+
`\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}`,
|
|
2174
|
+
"error",
|
|
2175
|
+
testsRoot,
|
|
2176
|
+
"traceability.testFileGlobs"
|
|
2177
|
+
)
|
|
2178
|
+
);
|
|
2179
|
+
} else {
|
|
2180
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2181
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2182
|
+
const refs = scTestRefs.get(id);
|
|
2183
|
+
return !refs || refs.size === 0;
|
|
2184
|
+
});
|
|
2185
|
+
if (scWithoutTests.length > 0) {
|
|
2186
|
+
issues.push(
|
|
2187
|
+
issue6(
|
|
2188
|
+
"QFAI-TRACE-010",
|
|
2189
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2190
|
+
", "
|
|
2191
|
+
)}\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`,
|
|
2192
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2193
|
+
testsRoot,
|
|
2194
|
+
"traceability.scMustHaveTest",
|
|
2195
|
+
scWithoutTests
|
|
2196
|
+
)
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2201
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2202
|
+
);
|
|
2203
|
+
if (unknownScIds.length > 0) {
|
|
2204
|
+
issues.push(
|
|
2205
|
+
issue6(
|
|
2206
|
+
"QFAI-TRACE-011",
|
|
2207
|
+
`\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(
|
|
2208
|
+
", "
|
|
2209
|
+
)}`,
|
|
2210
|
+
"error",
|
|
2211
|
+
testsRoot,
|
|
2212
|
+
"traceability.scUnknownInTests",
|
|
2213
|
+
unknownScIds
|
|
2214
|
+
)
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
1954
2218
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
1955
2219
|
if (contractIds.size > 0) {
|
|
1956
2220
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -1999,7 +2263,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1999
2263
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2000
2264
|
let found = false;
|
|
2001
2265
|
for (const file of targetFiles) {
|
|
2002
|
-
const text = await
|
|
2266
|
+
const text = await readFile10(file, "utf-8");
|
|
2003
2267
|
if (pattern.test(text)) {
|
|
2004
2268
|
found = true;
|
|
2005
2269
|
break;
|
|
@@ -2009,8 +2273,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2009
2273
|
issues.push(
|
|
2010
2274
|
issue6(
|
|
2011
2275
|
"QFAI-TRACE-002",
|
|
2012
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2013
|
-
"
|
|
2276
|
+
"\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",
|
|
2277
|
+
"info",
|
|
2014
2278
|
srcRoot,
|
|
2015
2279
|
"traceability.codeReferences"
|
|
2016
2280
|
)
|
|
@@ -2053,12 +2317,24 @@ async function validateProject(root, configResult) {
|
|
|
2053
2317
|
...await validateDefinedIds(root, config),
|
|
2054
2318
|
...await validateTraceability(root, config)
|
|
2055
2319
|
];
|
|
2320
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2321
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2322
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2323
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2324
|
+
root,
|
|
2325
|
+
config.validation.traceability.testFileGlobs,
|
|
2326
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2327
|
+
);
|
|
2328
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2056
2329
|
const toolVersion = await resolveToolVersion();
|
|
2057
2330
|
return {
|
|
2058
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2059
2331
|
toolVersion,
|
|
2060
2332
|
issues,
|
|
2061
|
-
counts: countIssues(issues)
|
|
2333
|
+
counts: countIssues(issues),
|
|
2334
|
+
traceability: {
|
|
2335
|
+
sc: scCoverage,
|
|
2336
|
+
testFiles
|
|
2337
|
+
}
|
|
2062
2338
|
};
|
|
2063
2339
|
}
|
|
2064
2340
|
function countIssues(issues) {
|
|
@@ -2079,9 +2355,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2079
2355
|
const configPath = resolved.configPath;
|
|
2080
2356
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2081
2357
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2082
|
-
const apiRoot =
|
|
2083
|
-
const uiRoot =
|
|
2084
|
-
const dbRoot =
|
|
2358
|
+
const apiRoot = path11.join(contractsRoot, "api");
|
|
2359
|
+
const uiRoot = path11.join(contractsRoot, "ui");
|
|
2360
|
+
const dbRoot = path11.join(contractsRoot, "db");
|
|
2085
2361
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2086
2362
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2087
2363
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2107,6 +2383,16 @@ async function createReportData(root, validation, configResult) {
|
|
|
2107
2383
|
srcRoot,
|
|
2108
2384
|
testsRoot
|
|
2109
2385
|
);
|
|
2386
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2387
|
+
const scRefsResult = await collectScTestReferences(
|
|
2388
|
+
root,
|
|
2389
|
+
config.validation.traceability.testFileGlobs,
|
|
2390
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2391
|
+
);
|
|
2392
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2393
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2394
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2395
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2110
2396
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2111
2397
|
const version = await resolveToolVersion();
|
|
2112
2398
|
return {
|
|
@@ -2135,7 +2421,10 @@ async function createReportData(root, validation, configResult) {
|
|
|
2135
2421
|
},
|
|
2136
2422
|
traceability: {
|
|
2137
2423
|
upstreamIdsFound: upstreamIds.size,
|
|
2138
|
-
referencedInCodeOrTests: traceability
|
|
2424
|
+
referencedInCodeOrTests: traceability,
|
|
2425
|
+
sc: scCoverage,
|
|
2426
|
+
scSources: scSourceRecord,
|
|
2427
|
+
testFiles
|
|
2139
2428
|
},
|
|
2140
2429
|
issues: resolvedValidation.issues
|
|
2141
2430
|
};
|
|
@@ -2172,6 +2461,65 @@ function formatReportMarkdown(data) {
|
|
|
2172
2461
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2173
2462
|
);
|
|
2174
2463
|
lines.push("");
|
|
2464
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2465
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2466
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2467
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2468
|
+
lines.push(
|
|
2469
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2470
|
+
);
|
|
2471
|
+
lines.push(
|
|
2472
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2473
|
+
data.traceability.testFiles.excludeGlobs
|
|
2474
|
+
)}`
|
|
2475
|
+
);
|
|
2476
|
+
lines.push(
|
|
2477
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2478
|
+
);
|
|
2479
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2480
|
+
lines.push("- missingIds: (none)");
|
|
2481
|
+
} else {
|
|
2482
|
+
const sources = data.traceability.scSources;
|
|
2483
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2484
|
+
const files = sources[id] ?? [];
|
|
2485
|
+
if (files.length === 0) {
|
|
2486
|
+
return id;
|
|
2487
|
+
}
|
|
2488
|
+
return `${id} (${files.join(", ")})`;
|
|
2489
|
+
});
|
|
2490
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2491
|
+
}
|
|
2492
|
+
lines.push("");
|
|
2493
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2494
|
+
const scRefs = data.traceability.sc.refs;
|
|
2495
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2496
|
+
if (scIds.length === 0) {
|
|
2497
|
+
lines.push("- (none)");
|
|
2498
|
+
} else {
|
|
2499
|
+
for (const scId of scIds) {
|
|
2500
|
+
const refs = scRefs[scId] ?? [];
|
|
2501
|
+
if (refs.length === 0) {
|
|
2502
|
+
lines.push(`- ${scId}: (none)`);
|
|
2503
|
+
} else {
|
|
2504
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
lines.push("");
|
|
2509
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2510
|
+
const specScIssues = data.issues.filter(
|
|
2511
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2512
|
+
);
|
|
2513
|
+
if (specScIssues.length === 0) {
|
|
2514
|
+
lines.push("- (none)");
|
|
2515
|
+
} else {
|
|
2516
|
+
for (const item of specScIssues) {
|
|
2517
|
+
const location = item.file ?? "(unknown)";
|
|
2518
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2519
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
lines.push("");
|
|
2175
2523
|
lines.push("## Hotspots");
|
|
2176
2524
|
const hotspots = buildHotspots(data.issues);
|
|
2177
2525
|
if (hotspots.length === 0) {
|
|
@@ -2226,25 +2574,25 @@ async function collectIds(files) {
|
|
|
2226
2574
|
DATA: /* @__PURE__ */ new Set()
|
|
2227
2575
|
};
|
|
2228
2576
|
for (const file of files) {
|
|
2229
|
-
const text = await
|
|
2577
|
+
const text = await readFile11(file, "utf-8");
|
|
2230
2578
|
for (const prefix of ID_PREFIXES2) {
|
|
2231
2579
|
const ids = extractIds(text, prefix);
|
|
2232
2580
|
ids.forEach((id) => result[prefix].add(id));
|
|
2233
2581
|
}
|
|
2234
2582
|
}
|
|
2235
2583
|
return {
|
|
2236
|
-
SPEC:
|
|
2237
|
-
BR:
|
|
2238
|
-
SC:
|
|
2239
|
-
UI:
|
|
2240
|
-
API:
|
|
2241
|
-
DATA:
|
|
2584
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2585
|
+
BR: toSortedArray2(result.BR),
|
|
2586
|
+
SC: toSortedArray2(result.SC),
|
|
2587
|
+
UI: toSortedArray2(result.UI),
|
|
2588
|
+
API: toSortedArray2(result.API),
|
|
2589
|
+
DATA: toSortedArray2(result.DATA)
|
|
2242
2590
|
};
|
|
2243
2591
|
}
|
|
2244
2592
|
async function collectUpstreamIds(files) {
|
|
2245
2593
|
const ids = /* @__PURE__ */ new Set();
|
|
2246
2594
|
for (const file of files) {
|
|
2247
|
-
const text = await
|
|
2595
|
+
const text = await readFile11(file, "utf-8");
|
|
2248
2596
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2249
2597
|
}
|
|
2250
2598
|
return ids;
|
|
@@ -2265,7 +2613,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2265
2613
|
}
|
|
2266
2614
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2267
2615
|
for (const file of targetFiles) {
|
|
2268
|
-
const text = await
|
|
2616
|
+
const text = await readFile11(file, "utf-8");
|
|
2269
2617
|
if (pattern.test(text)) {
|
|
2270
2618
|
return true;
|
|
2271
2619
|
}
|
|
@@ -2282,9 +2630,22 @@ function formatIdLine(label, values) {
|
|
|
2282
2630
|
}
|
|
2283
2631
|
return `- ${label}: ${values.join(", ")}`;
|
|
2284
2632
|
}
|
|
2285
|
-
function
|
|
2633
|
+
function formatList(values) {
|
|
2634
|
+
if (values.length === 0) {
|
|
2635
|
+
return "(none)";
|
|
2636
|
+
}
|
|
2637
|
+
return values.join(", ");
|
|
2638
|
+
}
|
|
2639
|
+
function toSortedArray2(values) {
|
|
2286
2640
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2287
2641
|
}
|
|
2642
|
+
function mapToSortedRecord(values) {
|
|
2643
|
+
const record2 = {};
|
|
2644
|
+
for (const [key, files] of values.entries()) {
|
|
2645
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
2646
|
+
}
|
|
2647
|
+
return record2;
|
|
2648
|
+
}
|
|
2288
2649
|
function buildHotspots(issues) {
|
|
2289
2650
|
const map = /* @__PURE__ */ new Map();
|
|
2290
2651
|
for (const issue7 of issues) {
|
|
@@ -2307,7 +2668,6 @@ function buildHotspots(issues) {
|
|
|
2307
2668
|
);
|
|
2308
2669
|
}
|
|
2309
2670
|
export {
|
|
2310
|
-
VALIDATION_SCHEMA_VERSION,
|
|
2311
2671
|
createReportData,
|
|
2312
2672
|
defaultConfig,
|
|
2313
2673
|
extractAllIds,
|