qfai 0.3.0 → 0.3.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 +6 -8
- package/assets/init/.qfai/README.md +6 -5
- package/assets/init/.qfai/rules/conventions.md +21 -0
- package/assets/init/.qfai/specs/README.md +51 -0
- package/assets/init/.qfai/specs/spec-0001/delta.md +30 -0
- package/assets/init/.qfai/specs/spec-0001/scenario.md +10 -0
- package/assets/init/.qfai/{spec/spec-0001-sample.md → specs/spec-0001/spec.md} +3 -2
- package/assets/init/root/.github/workflows/qfai.yml +1 -1
- package/assets/init/root/qfai.config.yaml +5 -8
- package/dist/cli/index.cjs +619 -439
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +594 -410
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +590 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -12
- package/dist/index.d.ts +8 -12
- package/dist/index.mjs +569 -368
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/assets/init/.qfai/spec/README.md +0 -82
- package/assets/init/.qfai/spec/decisions/ADR-0001.md +0 -9
- package/assets/init/.qfai/spec/decisions/README.md +0 -37
- package/assets/init/.qfai/spec/scenarios/scenarios.feature +0 -6
package/dist/index.cjs
CHANGED
|
@@ -44,8 +44,8 @@ __export(src_exports, {
|
|
|
44
44
|
resolvePath: () => resolvePath,
|
|
45
45
|
resolveToolVersion: () => resolveToolVersion,
|
|
46
46
|
validateContracts: () => validateContracts,
|
|
47
|
-
validateDecisions: () => validateDecisions,
|
|
48
47
|
validateDefinedIds: () => validateDefinedIds,
|
|
48
|
+
validateDeltas: () => validateDeltas,
|
|
49
49
|
validateProject: () => validateProject,
|
|
50
50
|
validateScenarioContent: () => validateScenarioContent,
|
|
51
51
|
validateScenarios: () => validateScenarios,
|
|
@@ -61,13 +61,11 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
61
61
|
var import_yaml = require("yaml");
|
|
62
62
|
var defaultConfig = {
|
|
63
63
|
paths: {
|
|
64
|
-
specDir: ".qfai/spec",
|
|
65
|
-
decisionsDir: ".qfai/spec/decisions",
|
|
66
|
-
scenariosDir: ".qfai/spec/scenarios",
|
|
67
64
|
contractsDir: ".qfai/contracts",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
specsDir: ".qfai/specs",
|
|
66
|
+
rulesDir: ".qfai/rules",
|
|
67
|
+
outDir: ".qfai/out",
|
|
68
|
+
promptsDir: ".qfai/prompts",
|
|
71
69
|
srcDir: "src",
|
|
72
70
|
testsDir: "tests"
|
|
73
71
|
},
|
|
@@ -92,8 +90,7 @@ var defaultConfig = {
|
|
|
92
90
|
}
|
|
93
91
|
},
|
|
94
92
|
output: {
|
|
95
|
-
|
|
96
|
-
jsonPath: ".qfai/out/validate.json"
|
|
93
|
+
validateJsonPath: ".qfai/out/validate.json"
|
|
97
94
|
}
|
|
98
95
|
};
|
|
99
96
|
function getConfigPath(root) {
|
|
@@ -142,27 +139,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
142
139
|
return base;
|
|
143
140
|
}
|
|
144
141
|
return {
|
|
145
|
-
specDir: readString(
|
|
146
|
-
raw.specDir,
|
|
147
|
-
base.specDir,
|
|
148
|
-
"paths.specDir",
|
|
149
|
-
configPath,
|
|
150
|
-
issues
|
|
151
|
-
),
|
|
152
|
-
decisionsDir: readString(
|
|
153
|
-
raw.decisionsDir,
|
|
154
|
-
base.decisionsDir,
|
|
155
|
-
"paths.decisionsDir",
|
|
156
|
-
configPath,
|
|
157
|
-
issues
|
|
158
|
-
),
|
|
159
|
-
scenariosDir: readString(
|
|
160
|
-
raw.scenariosDir,
|
|
161
|
-
base.scenariosDir,
|
|
162
|
-
"paths.scenariosDir",
|
|
163
|
-
configPath,
|
|
164
|
-
issues
|
|
165
|
-
),
|
|
166
142
|
contractsDir: readString(
|
|
167
143
|
raw.contractsDir,
|
|
168
144
|
base.contractsDir,
|
|
@@ -170,24 +146,31 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
170
146
|
configPath,
|
|
171
147
|
issues
|
|
172
148
|
),
|
|
173
|
-
|
|
174
|
-
raw.
|
|
175
|
-
base.
|
|
176
|
-
"paths.
|
|
149
|
+
specsDir: readString(
|
|
150
|
+
raw.specsDir,
|
|
151
|
+
base.specsDir,
|
|
152
|
+
"paths.specsDir",
|
|
153
|
+
configPath,
|
|
154
|
+
issues
|
|
155
|
+
),
|
|
156
|
+
rulesDir: readString(
|
|
157
|
+
raw.rulesDir,
|
|
158
|
+
base.rulesDir,
|
|
159
|
+
"paths.rulesDir",
|
|
177
160
|
configPath,
|
|
178
161
|
issues
|
|
179
162
|
),
|
|
180
|
-
|
|
181
|
-
raw.
|
|
182
|
-
base.
|
|
183
|
-
"paths.
|
|
163
|
+
outDir: readString(
|
|
164
|
+
raw.outDir,
|
|
165
|
+
base.outDir,
|
|
166
|
+
"paths.outDir",
|
|
184
167
|
configPath,
|
|
185
168
|
issues
|
|
186
169
|
),
|
|
187
|
-
|
|
188
|
-
raw.
|
|
189
|
-
base.
|
|
190
|
-
"paths.
|
|
170
|
+
promptsDir: readString(
|
|
171
|
+
raw.promptsDir,
|
|
172
|
+
base.promptsDir,
|
|
173
|
+
"paths.promptsDir",
|
|
191
174
|
configPath,
|
|
192
175
|
issues
|
|
193
176
|
),
|
|
@@ -310,17 +293,10 @@ function normalizeOutput(raw, configPath, issues) {
|
|
|
310
293
|
return base;
|
|
311
294
|
}
|
|
312
295
|
return {
|
|
313
|
-
|
|
314
|
-
raw.
|
|
315
|
-
base.
|
|
316
|
-
"output.
|
|
317
|
-
configPath,
|
|
318
|
-
issues
|
|
319
|
-
),
|
|
320
|
-
jsonPath: readString(
|
|
321
|
-
raw.jsonPath,
|
|
322
|
-
base.jsonPath,
|
|
323
|
-
"output.jsonPath",
|
|
296
|
+
validateJsonPath: readString(
|
|
297
|
+
raw.validateJsonPath,
|
|
298
|
+
base.validateJsonPath,
|
|
299
|
+
"output.validateJsonPath",
|
|
324
300
|
configPath,
|
|
325
301
|
issues
|
|
326
302
|
)
|
|
@@ -387,20 +363,6 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
387
363
|
}
|
|
388
364
|
return fallback;
|
|
389
365
|
}
|
|
390
|
-
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
391
|
-
if (value === "text" || value === "json" || value === "github") {
|
|
392
|
-
return value;
|
|
393
|
-
}
|
|
394
|
-
if (value !== void 0) {
|
|
395
|
-
issues.push(
|
|
396
|
-
configIssue(
|
|
397
|
-
configPath,
|
|
398
|
-
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
399
|
-
)
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
return fallback;
|
|
403
|
-
}
|
|
404
366
|
function configIssue(file, message) {
|
|
405
367
|
return {
|
|
406
368
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -480,10 +442,11 @@ function isValidId(value, prefix) {
|
|
|
480
442
|
}
|
|
481
443
|
|
|
482
444
|
// src/core/report.ts
|
|
483
|
-
var
|
|
445
|
+
var import_promises13 = require("fs/promises");
|
|
446
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
484
447
|
|
|
485
448
|
// src/core/discovery.ts
|
|
486
|
-
var
|
|
449
|
+
var import_promises4 = require("fs/promises");
|
|
487
450
|
|
|
488
451
|
// src/core/fs.ts
|
|
489
452
|
var import_promises2 = require("fs/promises");
|
|
@@ -540,11 +503,50 @@ async function exists(target) {
|
|
|
540
503
|
}
|
|
541
504
|
}
|
|
542
505
|
|
|
506
|
+
// src/core/specLayout.ts
|
|
507
|
+
var import_promises3 = require("fs/promises");
|
|
508
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
509
|
+
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
510
|
+
async function collectSpecEntries(specsRoot) {
|
|
511
|
+
const dirs = await listSpecDirs(specsRoot);
|
|
512
|
+
const entries = dirs.map((dir) => ({
|
|
513
|
+
dir,
|
|
514
|
+
specPath: import_node_path3.default.join(dir, "spec.md"),
|
|
515
|
+
deltaPath: import_node_path3.default.join(dir, "delta.md"),
|
|
516
|
+
scenarioPath: import_node_path3.default.join(dir, "scenario.md")
|
|
517
|
+
}));
|
|
518
|
+
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
519
|
+
}
|
|
520
|
+
async function listSpecDirs(specsRoot) {
|
|
521
|
+
try {
|
|
522
|
+
const items = await (0, import_promises3.readdir)(specsRoot, { withFileTypes: true });
|
|
523
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path3.default.join(specsRoot, name));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (isMissingFileError(error)) {
|
|
526
|
+
return [];
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function isMissingFileError(error) {
|
|
532
|
+
if (!error || typeof error !== "object") {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
return error.code === "ENOENT";
|
|
536
|
+
}
|
|
537
|
+
|
|
543
538
|
// src/core/discovery.ts
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
539
|
+
async function collectSpecPackDirs(specsRoot) {
|
|
540
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
541
|
+
return entries.map((entry) => entry.dir);
|
|
542
|
+
}
|
|
543
|
+
async function collectSpecFiles(specsRoot) {
|
|
544
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
545
|
+
return filterExisting(entries.map((entry) => entry.specPath));
|
|
546
|
+
}
|
|
547
|
+
async function collectScenarioFiles(specsRoot) {
|
|
548
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
549
|
+
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
548
550
|
}
|
|
549
551
|
async function collectUiContractFiles(uiRoot) {
|
|
550
552
|
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
@@ -563,25 +565,38 @@ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
|
563
565
|
]);
|
|
564
566
|
return { ui, api, db };
|
|
565
567
|
}
|
|
566
|
-
function
|
|
567
|
-
const
|
|
568
|
-
|
|
568
|
+
async function filterExisting(files) {
|
|
569
|
+
const existing = [];
|
|
570
|
+
for (const file of files) {
|
|
571
|
+
if (await exists2(file)) {
|
|
572
|
+
existing.push(file);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return existing;
|
|
576
|
+
}
|
|
577
|
+
async function exists2(target) {
|
|
578
|
+
try {
|
|
579
|
+
await (0, import_promises4.access)(target);
|
|
580
|
+
return true;
|
|
581
|
+
} catch {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
569
584
|
}
|
|
570
585
|
|
|
571
586
|
// src/core/types.ts
|
|
572
587
|
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
573
588
|
|
|
574
589
|
// src/core/version.ts
|
|
575
|
-
var
|
|
590
|
+
var import_promises5 = require("fs/promises");
|
|
576
591
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
577
592
|
var import_node_url = require("url");
|
|
578
593
|
async function resolveToolVersion() {
|
|
579
|
-
if ("0.3.
|
|
580
|
-
return "0.3.
|
|
594
|
+
if ("0.3.2".length > 0) {
|
|
595
|
+
return "0.3.2";
|
|
581
596
|
}
|
|
582
597
|
try {
|
|
583
598
|
const packagePath = resolvePackageJsonPath();
|
|
584
|
-
const raw = await (0,
|
|
599
|
+
const raw = await (0, import_promises5.readFile)(packagePath, "utf-8");
|
|
585
600
|
const parsed = JSON.parse(raw);
|
|
586
601
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
587
602
|
return version.length > 0 ? version : "unknown";
|
|
@@ -596,7 +611,8 @@ function resolvePackageJsonPath() {
|
|
|
596
611
|
}
|
|
597
612
|
|
|
598
613
|
// src/core/validators/contracts.ts
|
|
599
|
-
var
|
|
614
|
+
var import_promises6 = require("fs/promises");
|
|
615
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
600
616
|
|
|
601
617
|
// src/core/contracts.ts
|
|
602
618
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
@@ -652,19 +668,10 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
652
668
|
];
|
|
653
669
|
async function validateContracts(root, config) {
|
|
654
670
|
const issues = [];
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
);
|
|
658
|
-
issues.push(
|
|
659
|
-
...await validateApiContracts(
|
|
660
|
-
resolvePath(root, config, "apiContractsDir")
|
|
661
|
-
)
|
|
662
|
-
);
|
|
663
|
-
issues.push(
|
|
664
|
-
...await validateDataContracts(
|
|
665
|
-
resolvePath(root, config, "dataContractsDir")
|
|
666
|
-
)
|
|
667
|
-
);
|
|
671
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
672
|
+
issues.push(...await validateUiContracts(import_node_path6.default.join(contractsRoot, "ui")));
|
|
673
|
+
issues.push(...await validateApiContracts(import_node_path6.default.join(contractsRoot, "api")));
|
|
674
|
+
issues.push(...await validateDataContracts(import_node_path6.default.join(contractsRoot, "db")));
|
|
668
675
|
return issues;
|
|
669
676
|
}
|
|
670
677
|
async function validateUiContracts(uiRoot) {
|
|
@@ -682,7 +689,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
682
689
|
}
|
|
683
690
|
const issues = [];
|
|
684
691
|
for (const file of files) {
|
|
685
|
-
const text = await (0,
|
|
692
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
686
693
|
const invalidIds = extractInvalidIds(text, [
|
|
687
694
|
"SPEC",
|
|
688
695
|
"BR",
|
|
@@ -749,7 +756,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
749
756
|
}
|
|
750
757
|
const issues = [];
|
|
751
758
|
for (const file of files) {
|
|
752
|
-
const text = await (0,
|
|
759
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
753
760
|
const invalidIds = extractInvalidIds(text, [
|
|
754
761
|
"SPEC",
|
|
755
762
|
"BR",
|
|
@@ -827,7 +834,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
827
834
|
}
|
|
828
835
|
const issues = [];
|
|
829
836
|
for (const file of files) {
|
|
830
|
-
const text = await (0,
|
|
837
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
831
838
|
const invalidIds = extractInvalidIds(text, [
|
|
832
839
|
"SPEC",
|
|
833
840
|
"BR",
|
|
@@ -897,72 +904,78 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
897
904
|
return issue7;
|
|
898
905
|
}
|
|
899
906
|
|
|
900
|
-
// src/core/validators/
|
|
901
|
-
var
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
const status = extractField(md, "Status");
|
|
913
|
-
const context = extractField(md, "Context");
|
|
914
|
-
const decision = extractField(md, "Decision");
|
|
915
|
-
const consequences = extractField(md, "Consequences");
|
|
916
|
-
const related = extractField(md, "Related");
|
|
917
|
-
if (status) fields.status = status;
|
|
918
|
-
if (context) fields.context = context;
|
|
919
|
-
if (decision) fields.decision = decision;
|
|
920
|
-
if (consequences) fields.consequences = consequences;
|
|
921
|
-
if (related) fields.related = related;
|
|
922
|
-
const parsed = {
|
|
923
|
-
file,
|
|
924
|
-
fields
|
|
925
|
-
};
|
|
926
|
-
if (adrId) {
|
|
927
|
-
parsed.adrId = adrId;
|
|
928
|
-
}
|
|
929
|
-
return parsed;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// src/core/validators/decisions.ts
|
|
933
|
-
var REQUIRED_FIELDS = [
|
|
934
|
-
{ key: "status", label: "Status" },
|
|
935
|
-
{ key: "context", label: "Context" },
|
|
936
|
-
{ key: "decision", label: "Decision" },
|
|
937
|
-
{ key: "consequences", label: "Consequences" }
|
|
938
|
-
];
|
|
939
|
-
async function validateDecisions(root, config) {
|
|
940
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
941
|
-
const files = await collectFiles(decisionsRoot, { extensions: [".md"] });
|
|
942
|
-
if (files.length === 0) {
|
|
907
|
+
// src/core/validators/delta.ts
|
|
908
|
+
var import_promises7 = require("fs/promises");
|
|
909
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
910
|
+
var SECTION_RE = /^##\s+変更区分/m;
|
|
911
|
+
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
912
|
+
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
913
|
+
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
914
|
+
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
915
|
+
async function validateDeltas(root, config) {
|
|
916
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
917
|
+
const packs = await collectSpecPackDirs(specsRoot);
|
|
918
|
+
if (packs.length === 0) {
|
|
943
919
|
return [];
|
|
944
920
|
}
|
|
945
921
|
const issues = [];
|
|
946
|
-
for (const
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
(
|
|
951
|
-
)
|
|
952
|
-
|
|
922
|
+
for (const pack of packs) {
|
|
923
|
+
const deltaPath = import_node_path7.default.join(pack, "delta.md");
|
|
924
|
+
let text;
|
|
925
|
+
try {
|
|
926
|
+
text = await (0, import_promises7.readFile)(deltaPath, "utf-8");
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (isMissingFileError2(error)) {
|
|
929
|
+
issues.push(
|
|
930
|
+
issue2(
|
|
931
|
+
"QFAI-DELTA-001",
|
|
932
|
+
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
933
|
+
"error",
|
|
934
|
+
deltaPath,
|
|
935
|
+
"delta.exists"
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
942
|
+
const hasSection = SECTION_RE.test(text);
|
|
943
|
+
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
944
|
+
const hasChange = CHANGE_LINE_RE.test(text);
|
|
945
|
+
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
953
946
|
issues.push(
|
|
954
947
|
issue2(
|
|
955
|
-
"QFAI-
|
|
956
|
-
|
|
948
|
+
"QFAI-DELTA-002",
|
|
949
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
957
950
|
"error",
|
|
958
|
-
|
|
959
|
-
"
|
|
951
|
+
deltaPath,
|
|
952
|
+
"delta.section"
|
|
953
|
+
)
|
|
954
|
+
);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
958
|
+
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
959
|
+
if (compatibilityChecked === changeChecked) {
|
|
960
|
+
issues.push(
|
|
961
|
+
issue2(
|
|
962
|
+
"QFAI-DELTA-003",
|
|
963
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
964
|
+
"error",
|
|
965
|
+
deltaPath,
|
|
966
|
+
"delta.classification"
|
|
960
967
|
)
|
|
961
968
|
);
|
|
962
969
|
}
|
|
963
970
|
}
|
|
964
971
|
return issues;
|
|
965
972
|
}
|
|
973
|
+
function isMissingFileError2(error) {
|
|
974
|
+
if (!error || typeof error !== "object") {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
return error.code === "ENOENT";
|
|
978
|
+
}
|
|
966
979
|
function issue2(code, message, severity, file, rule, refs) {
|
|
967
980
|
const issue7 = {
|
|
968
981
|
code,
|
|
@@ -982,15 +995,17 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
982
995
|
}
|
|
983
996
|
|
|
984
997
|
// src/core/validators/ids.ts
|
|
985
|
-
var
|
|
986
|
-
var
|
|
998
|
+
var import_promises9 = require("fs/promises");
|
|
999
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
987
1000
|
|
|
988
1001
|
// src/core/contractIndex.ts
|
|
989
|
-
var
|
|
1002
|
+
var import_promises8 = require("fs/promises");
|
|
1003
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
990
1004
|
async function buildContractIndex(root, config) {
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
const
|
|
1005
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1006
|
+
const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
|
|
1007
|
+
const apiRoot = import_node_path8.default.join(contractsRoot, "api");
|
|
1008
|
+
const dataRoot = import_node_path8.default.join(contractsRoot, "db");
|
|
994
1009
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
995
1010
|
collectUiContractFiles(uiRoot),
|
|
996
1011
|
collectApiContractFiles(apiRoot),
|
|
@@ -1009,7 +1024,7 @@ async function buildContractIndex(root, config) {
|
|
|
1009
1024
|
}
|
|
1010
1025
|
async function indexUiContracts(files, index) {
|
|
1011
1026
|
for (const file of files) {
|
|
1012
|
-
const text = await (0,
|
|
1027
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1013
1028
|
try {
|
|
1014
1029
|
const doc = parseStructuredContract(file, text);
|
|
1015
1030
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1021,7 +1036,7 @@ async function indexUiContracts(files, index) {
|
|
|
1021
1036
|
}
|
|
1022
1037
|
async function indexApiContracts(files, index) {
|
|
1023
1038
|
for (const file of files) {
|
|
1024
|
-
const text = await (0,
|
|
1039
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1025
1040
|
try {
|
|
1026
1041
|
const doc = parseStructuredContract(file, text);
|
|
1027
1042
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1033,7 +1048,7 @@ async function indexApiContracts(files, index) {
|
|
|
1033
1048
|
}
|
|
1034
1049
|
async function indexDataContracts(files, index) {
|
|
1035
1050
|
for (const file of files) {
|
|
1036
|
-
const text = await (0,
|
|
1051
|
+
const text = await (0, import_promises8.readFile)(file, "utf-8");
|
|
1037
1052
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1038
1053
|
}
|
|
1039
1054
|
}
|
|
@@ -1044,38 +1059,6 @@ function record(index, id, file) {
|
|
|
1044
1059
|
index.idToFiles.set(id, current);
|
|
1045
1060
|
}
|
|
1046
1061
|
|
|
1047
|
-
// src/core/parse/gherkin.ts
|
|
1048
|
-
var FEATURE_RE = /^\s*Feature:\s+/;
|
|
1049
|
-
var SCENARIO_RE = /^\s*Scenario:\s*(.+)\s*$/;
|
|
1050
|
-
var TAG_LINE_RE = /^\s*@/;
|
|
1051
|
-
function parseTags(line) {
|
|
1052
|
-
return line.trim().split(/\s+/).filter((tag) => tag.startsWith("@")).map((tag) => tag.replace(/^@/, ""));
|
|
1053
|
-
}
|
|
1054
|
-
function parseGherkinFeature(text, file) {
|
|
1055
|
-
const lines = text.split(/\r?\n/);
|
|
1056
|
-
const scenarios = [];
|
|
1057
|
-
let featurePresent = false;
|
|
1058
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1059
|
-
const line = lines[i] ?? "";
|
|
1060
|
-
if (FEATURE_RE.test(line)) {
|
|
1061
|
-
featurePresent = true;
|
|
1062
|
-
}
|
|
1063
|
-
const match = line.match(SCENARIO_RE);
|
|
1064
|
-
if (!match) continue;
|
|
1065
|
-
const scenarioName = match[1];
|
|
1066
|
-
if (!scenarioName) continue;
|
|
1067
|
-
const tags = [];
|
|
1068
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
1069
|
-
const previous = lines[j] ?? "";
|
|
1070
|
-
if (previous.trim() === "") continue;
|
|
1071
|
-
if (!TAG_LINE_RE.test(previous)) break;
|
|
1072
|
-
tags.unshift(...parseTags(previous));
|
|
1073
|
-
}
|
|
1074
|
-
scenarios.push({ name: scenarioName, line: i + 1, tags });
|
|
1075
|
-
}
|
|
1076
|
-
return { file, featurePresent, scenarios };
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
1062
|
// src/core/parse/markdown.ts
|
|
1080
1063
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1081
1064
|
function parseHeadings(md) {
|
|
@@ -1119,9 +1102,9 @@ function extractH2Sections(md) {
|
|
|
1119
1102
|
|
|
1120
1103
|
// src/core/parse/spec.ts
|
|
1121
1104
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1122
|
-
var BR_LINE_RE = /^\s
|
|
1123
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s
|
|
1124
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s
|
|
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.*)$/;
|
|
1125
1108
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1126
1109
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1127
1110
|
function parseSpec(md, file) {
|
|
@@ -1194,16 +1177,167 @@ function parseSpec(md, file) {
|
|
|
1194
1177
|
return parsed;
|
|
1195
1178
|
}
|
|
1196
1179
|
|
|
1197
|
-
// src/core/
|
|
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}$/;
|
|
1198
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
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
1281
|
+
const scenarios = [];
|
|
1282
|
+
for (const child of feature.children) {
|
|
1283
|
+
if (child.scenario) {
|
|
1284
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
1285
|
+
}
|
|
1286
|
+
if (child.rule) {
|
|
1287
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
1288
|
+
for (const ruleChild of child.rule.children) {
|
|
1289
|
+
if (ruleChild.scenario) {
|
|
1290
|
+
scenarios.push(
|
|
1291
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return scenarios;
|
|
1298
|
+
}
|
|
1299
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
1300
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
1301
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
1302
|
+
return {
|
|
1303
|
+
name: scenario.name,
|
|
1304
|
+
kind,
|
|
1305
|
+
line: scenario.location?.line,
|
|
1306
|
+
tags,
|
|
1307
|
+
steps: scenario.steps
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function collectTagNames(tags) {
|
|
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
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return texts;
|
|
1329
|
+
}
|
|
1330
|
+
function unique2(values) {
|
|
1331
|
+
return Array.from(new Set(values));
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/core/validators/ids.ts
|
|
1335
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
1199
1336
|
async function validateDefinedIds(root, config) {
|
|
1200
1337
|
const issues = [];
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
1204
|
-
const scenarioFiles = await collectFiles(scenarioRoot, {
|
|
1205
|
-
extensions: [".feature"]
|
|
1206
|
-
});
|
|
1338
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1339
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1340
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1207
1341
|
const defined = /* @__PURE__ */ new Map();
|
|
1208
1342
|
await collectSpecDefinitionIds(specFiles, defined);
|
|
1209
1343
|
await collectScenarioDefinitionIds(scenarioFiles, defined);
|
|
@@ -1232,7 +1366,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1232
1366
|
}
|
|
1233
1367
|
async function collectSpecDefinitionIds(files, out) {
|
|
1234
1368
|
for (const file of files) {
|
|
1235
|
-
const text = await (0,
|
|
1369
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1236
1370
|
const parsed = parseSpec(text, file);
|
|
1237
1371
|
if (parsed.specId) {
|
|
1238
1372
|
recordId(out, parsed.specId, file);
|
|
@@ -1242,11 +1376,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1242
1376
|
}
|
|
1243
1377
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1244
1378
|
for (const file of files) {
|
|
1245
|
-
const text = await (0,
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1379
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1380
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1381
|
+
if (!document || errors.length > 0) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
for (const scenario of document.scenarios) {
|
|
1248
1385
|
for (const tag of scenario.tags) {
|
|
1249
|
-
if (
|
|
1386
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
1250
1387
|
recordId(out, tag, file);
|
|
1251
1388
|
}
|
|
1252
1389
|
}
|
|
@@ -1260,7 +1397,7 @@ function recordId(out, id, file) {
|
|
|
1260
1397
|
}
|
|
1261
1398
|
function formatFileList(files, root) {
|
|
1262
1399
|
return files.map((file) => {
|
|
1263
|
-
const relative =
|
|
1400
|
+
const relative = import_node_path9.default.relative(root, file);
|
|
1264
1401
|
return relative.length > 0 ? relative : file;
|
|
1265
1402
|
}).join(", ");
|
|
1266
1403
|
}
|
|
@@ -1283,39 +1420,55 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1283
1420
|
}
|
|
1284
1421
|
|
|
1285
1422
|
// src/core/validators/scenario.ts
|
|
1286
|
-
var
|
|
1423
|
+
var import_promises10 = require("fs/promises");
|
|
1287
1424
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1288
1425
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1289
1426
|
var THEN_PATTERN = /\bThen\b/;
|
|
1290
|
-
var
|
|
1291
|
-
var
|
|
1292
|
-
var
|
|
1427
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1428
|
+
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1429
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1293
1430
|
async function validateScenarios(root, config) {
|
|
1294
|
-
const
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1431
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1432
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1433
|
+
if (entries.length === 0) {
|
|
1434
|
+
const expected = "spec-0001/scenario.md";
|
|
1435
|
+
const legacy = "spec-001/scenario.md";
|
|
1299
1436
|
return [
|
|
1300
1437
|
issue4(
|
|
1301
1438
|
"QFAI-SC-000",
|
|
1302
|
-
|
|
1439
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
|
|
1303
1440
|
"info",
|
|
1304
|
-
|
|
1441
|
+
specsRoot,
|
|
1305
1442
|
"scenario.files"
|
|
1306
1443
|
)
|
|
1307
1444
|
];
|
|
1308
1445
|
}
|
|
1309
1446
|
const issues = [];
|
|
1310
|
-
for (const
|
|
1311
|
-
|
|
1312
|
-
|
|
1447
|
+
for (const entry of entries) {
|
|
1448
|
+
let text;
|
|
1449
|
+
try {
|
|
1450
|
+
text = await (0, import_promises10.readFile)(entry.scenarioPath, "utf-8");
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (isMissingFileError3(error)) {
|
|
1453
|
+
issues.push(
|
|
1454
|
+
issue4(
|
|
1455
|
+
"QFAI-SC-001",
|
|
1456
|
+
"scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1457
|
+
"error",
|
|
1458
|
+
entry.scenarioPath,
|
|
1459
|
+
"scenario.exists"
|
|
1460
|
+
)
|
|
1461
|
+
);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
1466
|
+
issues.push(...validateScenarioContent(text, entry.scenarioPath));
|
|
1313
1467
|
}
|
|
1314
1468
|
return issues;
|
|
1315
1469
|
}
|
|
1316
1470
|
function validateScenarioContent(text, file) {
|
|
1317
1471
|
const issues = [];
|
|
1318
|
-
const parsed = parseGherkinFeature(text, file);
|
|
1319
1472
|
const invalidIds = extractInvalidIds(text, [
|
|
1320
1473
|
"SPEC",
|
|
1321
1474
|
"BR",
|
|
@@ -1337,9 +1490,47 @@ function validateScenarioContent(text, file) {
|
|
|
1337
1490
|
)
|
|
1338
1491
|
);
|
|
1339
1492
|
}
|
|
1493
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1494
|
+
if (!document || errors.length > 0) {
|
|
1495
|
+
issues.push(
|
|
1496
|
+
issue4(
|
|
1497
|
+
"QFAI-SC-010",
|
|
1498
|
+
`Gherkin \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errors.join(", ") || "unknown"}`,
|
|
1499
|
+
"error",
|
|
1500
|
+
file,
|
|
1501
|
+
"scenario.parse"
|
|
1502
|
+
)
|
|
1503
|
+
);
|
|
1504
|
+
return issues;
|
|
1505
|
+
}
|
|
1506
|
+
const featureSpecTags = document.featureTags.filter(
|
|
1507
|
+
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1508
|
+
);
|
|
1509
|
+
if (featureSpecTags.length === 0) {
|
|
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) {
|
|
1520
|
+
issues.push(
|
|
1521
|
+
issue4(
|
|
1522
|
+
"QFAI-SC-009",
|
|
1523
|
+
`Feature \u306E SPEC \u30BF\u30B0\u304C\u8907\u6570\u3042\u308A\u307E\u3059: ${featureSpecTags.join(", ")}`,
|
|
1524
|
+
"error",
|
|
1525
|
+
file,
|
|
1526
|
+
"scenario.featureSpec",
|
|
1527
|
+
featureSpecTags
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1340
1531
|
const missingStructure = [];
|
|
1341
|
-
if (!
|
|
1342
|
-
if (
|
|
1532
|
+
if (!document.featureName) missingStructure.push("Feature");
|
|
1533
|
+
if (document.scenarios.length === 0) missingStructure.push("Scenario");
|
|
1343
1534
|
if (missingStructure.length > 0) {
|
|
1344
1535
|
issues.push(
|
|
1345
1536
|
issue4(
|
|
@@ -1353,7 +1544,7 @@ function validateScenarioContent(text, file) {
|
|
|
1353
1544
|
)
|
|
1354
1545
|
);
|
|
1355
1546
|
}
|
|
1356
|
-
for (const scenario of
|
|
1547
|
+
for (const scenario of document.scenarios) {
|
|
1357
1548
|
if (scenario.tags.length === 0) {
|
|
1358
1549
|
issues.push(
|
|
1359
1550
|
issue4(
|
|
@@ -1367,13 +1558,16 @@ function validateScenarioContent(text, file) {
|
|
|
1367
1558
|
continue;
|
|
1368
1559
|
}
|
|
1369
1560
|
const missingTags = [];
|
|
1370
|
-
|
|
1371
|
-
|
|
1561
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE3.test(tag));
|
|
1562
|
+
if (scTags.length === 0) {
|
|
1563
|
+
missingTags.push("SC(0\u4EF6)");
|
|
1564
|
+
} else if (scTags.length > 1) {
|
|
1565
|
+
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1372
1566
|
}
|
|
1373
|
-
if (!scenario.tags.some((tag) =>
|
|
1567
|
+
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1374
1568
|
missingTags.push("SPEC");
|
|
1375
1569
|
}
|
|
1376
|
-
if (!scenario.tags.some((tag) =>
|
|
1570
|
+
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1377
1571
|
missingTags.push("BR");
|
|
1378
1572
|
}
|
|
1379
1573
|
if (missingTags.length > 0) {
|
|
@@ -1388,26 +1582,29 @@ function validateScenarioContent(text, file) {
|
|
|
1388
1582
|
);
|
|
1389
1583
|
}
|
|
1390
1584
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1585
|
+
for (const scenario of document.scenarios) {
|
|
1586
|
+
const missingSteps = [];
|
|
1587
|
+
const keywords = scenario.steps.map((step) => step.keyword.trim());
|
|
1588
|
+
if (!keywords.some((keyword) => GIVEN_PATTERN.test(keyword))) {
|
|
1589
|
+
missingSteps.push("Given");
|
|
1590
|
+
}
|
|
1591
|
+
if (!keywords.some((keyword) => WHEN_PATTERN.test(keyword))) {
|
|
1592
|
+
missingSteps.push("When");
|
|
1593
|
+
}
|
|
1594
|
+
if (!keywords.some((keyword) => THEN_PATTERN.test(keyword))) {
|
|
1595
|
+
missingSteps.push("Then");
|
|
1596
|
+
}
|
|
1597
|
+
if (missingSteps.length > 0) {
|
|
1598
|
+
issues.push(
|
|
1599
|
+
issue4(
|
|
1600
|
+
"QFAI-SC-005",
|
|
1601
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")} (${scenario.name})`,
|
|
1602
|
+
"warning",
|
|
1603
|
+
file,
|
|
1604
|
+
"scenario.steps"
|
|
1605
|
+
)
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1411
1608
|
}
|
|
1412
1609
|
return issues;
|
|
1413
1610
|
}
|
|
@@ -1428,18 +1625,25 @@ function issue4(code, message, severity, file, rule, refs) {
|
|
|
1428
1625
|
}
|
|
1429
1626
|
return issue7;
|
|
1430
1627
|
}
|
|
1628
|
+
function isMissingFileError3(error) {
|
|
1629
|
+
if (!error || typeof error !== "object") {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
return error.code === "ENOENT";
|
|
1633
|
+
}
|
|
1431
1634
|
|
|
1432
1635
|
// src/core/validators/spec.ts
|
|
1433
|
-
var
|
|
1636
|
+
var import_promises11 = require("fs/promises");
|
|
1434
1637
|
async function validateSpecs(root, config) {
|
|
1435
|
-
const specsRoot = resolvePath(root, config, "
|
|
1436
|
-
const
|
|
1437
|
-
if (
|
|
1438
|
-
const expected = "spec-0001
|
|
1638
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1639
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
1640
|
+
if (entries.length === 0) {
|
|
1641
|
+
const expected = "spec-0001/spec.md";
|
|
1642
|
+
const legacy = "spec-001/spec.md";
|
|
1439
1643
|
return [
|
|
1440
1644
|
issue5(
|
|
1441
1645
|
"QFAI-SPEC-000",
|
|
1442
|
-
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.
|
|
1646
|
+
`Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
|
|
1443
1647
|
"info",
|
|
1444
1648
|
specsRoot,
|
|
1445
1649
|
"spec.files"
|
|
@@ -1447,12 +1651,29 @@ async function validateSpecs(root, config) {
|
|
|
1447
1651
|
];
|
|
1448
1652
|
}
|
|
1449
1653
|
const issues = [];
|
|
1450
|
-
for (const
|
|
1451
|
-
|
|
1654
|
+
for (const entry of entries) {
|
|
1655
|
+
let text;
|
|
1656
|
+
try {
|
|
1657
|
+
text = await (0, import_promises11.readFile)(entry.specPath, "utf-8");
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
if (isMissingFileError4(error)) {
|
|
1660
|
+
issues.push(
|
|
1661
|
+
issue5(
|
|
1662
|
+
"QFAI-SPEC-005",
|
|
1663
|
+
"spec.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1664
|
+
"error",
|
|
1665
|
+
entry.specPath,
|
|
1666
|
+
"spec.exists"
|
|
1667
|
+
)
|
|
1668
|
+
);
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1452
1673
|
issues.push(
|
|
1453
1674
|
...validateSpecContent(
|
|
1454
1675
|
text,
|
|
1455
|
-
|
|
1676
|
+
entry.specPath,
|
|
1456
1677
|
config.validation.require.specSections
|
|
1457
1678
|
)
|
|
1458
1679
|
);
|
|
@@ -1574,29 +1795,25 @@ function issue5(code, message, severity, file, rule, refs) {
|
|
|
1574
1795
|
}
|
|
1575
1796
|
return issue7;
|
|
1576
1797
|
}
|
|
1798
|
+
function isMissingFileError4(error) {
|
|
1799
|
+
if (!error || typeof error !== "object") {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
return error.code === "ENOENT";
|
|
1803
|
+
}
|
|
1577
1804
|
|
|
1578
1805
|
// src/core/validators/traceability.ts
|
|
1579
|
-
var
|
|
1580
|
-
var
|
|
1581
|
-
var
|
|
1582
|
-
var
|
|
1583
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1584
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1585
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1806
|
+
var import_promises12 = require("fs/promises");
|
|
1807
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1808
|
+
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1809
|
+
var BR_TAG_RE3 = /^BR-\d{4}$/;
|
|
1586
1810
|
async function validateTraceability(root, config) {
|
|
1587
1811
|
const issues = [];
|
|
1588
|
-
const specsRoot = resolvePath(root, config, "
|
|
1589
|
-
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1590
|
-
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1812
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1591
1813
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1592
1814
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1593
1815
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
1594
|
-
const
|
|
1595
|
-
extensions: [".md"]
|
|
1596
|
-
});
|
|
1597
|
-
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1598
|
-
extensions: [".feature"]
|
|
1599
|
-
});
|
|
1816
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1600
1817
|
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1601
1818
|
const specIds = /* @__PURE__ */ new Set();
|
|
1602
1819
|
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
@@ -1608,7 +1825,7 @@ async function validateTraceability(root, config) {
|
|
|
1608
1825
|
const contractIndex = await buildContractIndex(root, config);
|
|
1609
1826
|
const contractIds = contractIndex.ids;
|
|
1610
1827
|
for (const file of specFiles) {
|
|
1611
|
-
const text = await (0,
|
|
1828
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1612
1829
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1613
1830
|
const parsed = parseSpec(text, file);
|
|
1614
1831
|
if (parsed.specId) {
|
|
@@ -1644,111 +1861,100 @@ async function validateTraceability(root, config) {
|
|
|
1644
1861
|
specToBrIds.set(parsed.specId, current);
|
|
1645
1862
|
}
|
|
1646
1863
|
}
|
|
1647
|
-
for (const file of decisionFiles) {
|
|
1648
|
-
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1649
|
-
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1650
|
-
}
|
|
1651
1864
|
for (const file of scenarioFiles) {
|
|
1652
|
-
const text = await (0,
|
|
1865
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1653
1866
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
const scIds = /* @__PURE__ */ new Set();
|
|
1658
|
-
const scenarioIds = /* @__PURE__ */ new Set();
|
|
1659
|
-
for (const scenario of parsed.scenarios) {
|
|
1660
|
-
for (const tag of scenario.tags) {
|
|
1661
|
-
if (SPEC_TAG_RE2.test(tag)) {
|
|
1662
|
-
specIdsInScenario.add(tag);
|
|
1663
|
-
}
|
|
1664
|
-
if (BR_TAG_RE2.test(tag)) {
|
|
1665
|
-
brIds.add(tag);
|
|
1666
|
-
}
|
|
1667
|
-
if (SC_TAG_RE3.test(tag)) {
|
|
1668
|
-
scIds.add(tag);
|
|
1669
|
-
}
|
|
1670
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1671
|
-
scenarioIds.add(tag);
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
const specIdsList = Array.from(specIdsInScenario);
|
|
1676
|
-
const brIdsList = Array.from(brIds);
|
|
1677
|
-
const scIdsList = Array.from(scIds);
|
|
1678
|
-
const scenarioIdsList = Array.from(scenarioIds);
|
|
1679
|
-
brIdsList.forEach((id) => brIdsInScenarios.add(id));
|
|
1680
|
-
scIdsList.forEach((id) => scIdsInScenarios.add(id));
|
|
1681
|
-
scenarioIdsList.forEach((id) => scenarioContractIds.add(id));
|
|
1682
|
-
if (scenarioIdsList.length > 0) {
|
|
1683
|
-
scIdsList.forEach((id) => scWithContracts.add(id));
|
|
1684
|
-
}
|
|
1685
|
-
const unknownSpecIds = specIdsList.filter((id) => !specIds.has(id));
|
|
1686
|
-
if (unknownSpecIds.length > 0) {
|
|
1687
|
-
issues.push(
|
|
1688
|
-
issue6(
|
|
1689
|
-
"QFAI-TRACE-005",
|
|
1690
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(", ")}`,
|
|
1691
|
-
"error",
|
|
1692
|
-
file,
|
|
1693
|
-
"traceability.scenarioSpecExists",
|
|
1694
|
-
unknownSpecIds
|
|
1695
|
-
)
|
|
1696
|
-
);
|
|
1697
|
-
}
|
|
1698
|
-
const unknownBrIds = brIdsList.filter((id) => !brIdsInSpecs.has(id));
|
|
1699
|
-
if (unknownBrIds.length > 0) {
|
|
1700
|
-
issues.push(
|
|
1701
|
-
issue6(
|
|
1702
|
-
"QFAI-TRACE-006",
|
|
1703
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(", ")}`,
|
|
1704
|
-
"error",
|
|
1705
|
-
file,
|
|
1706
|
-
"traceability.scenarioBrExists",
|
|
1707
|
-
unknownBrIds
|
|
1708
|
-
)
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
1711
|
-
const unknownContractIds = scenarioIdsList.filter(
|
|
1712
|
-
(id) => !contractIds.has(id)
|
|
1713
|
-
);
|
|
1714
|
-
if (unknownContractIds.length > 0) {
|
|
1715
|
-
issues.push(
|
|
1716
|
-
issue6(
|
|
1717
|
-
"QFAI-TRACE-008",
|
|
1718
|
-
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1719
|
-
", "
|
|
1720
|
-
)}`,
|
|
1721
|
-
config.validation.traceability.unknownContractIdSeverity,
|
|
1722
|
-
file,
|
|
1723
|
-
"traceability.scenarioContractExists",
|
|
1724
|
-
unknownContractIds
|
|
1725
|
-
)
|
|
1726
|
-
);
|
|
1867
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
1868
|
+
if (!document || errors.length > 0) {
|
|
1869
|
+
continue;
|
|
1727
1870
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1871
|
+
const atoms = buildScenarioAtoms(document);
|
|
1872
|
+
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1873
|
+
const atom = atoms[index];
|
|
1874
|
+
if (!atom) {
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1878
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE3.test(tag));
|
|
1879
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1880
|
+
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1881
|
+
scTags.forEach((id) => scIdsInScenarios.add(id));
|
|
1882
|
+
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1883
|
+
if (atom.contractIds.length > 0) {
|
|
1884
|
+
scTags.forEach((id) => scWithContracts.add(id));
|
|
1885
|
+
}
|
|
1886
|
+
const unknownSpecIds = specTags.filter((id) => !specIds.has(id));
|
|
1887
|
+
if (unknownSpecIds.length > 0) {
|
|
1888
|
+
issues.push(
|
|
1889
|
+
issue6(
|
|
1890
|
+
"QFAI-TRACE-005",
|
|
1891
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 SPEC \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownSpecIds.join(
|
|
1892
|
+
", "
|
|
1893
|
+
)} (${scenario.name})`,
|
|
1894
|
+
"error",
|
|
1895
|
+
file,
|
|
1896
|
+
"traceability.scenarioSpecExists",
|
|
1897
|
+
unknownSpecIds
|
|
1898
|
+
)
|
|
1899
|
+
);
|
|
1736
1900
|
}
|
|
1737
|
-
const
|
|
1738
|
-
if (
|
|
1901
|
+
const unknownBrIds = brTags.filter((id) => !brIdsInSpecs.has(id));
|
|
1902
|
+
if (unknownBrIds.length > 0) {
|
|
1739
1903
|
issues.push(
|
|
1740
1904
|
issue6(
|
|
1741
|
-
"QFAI-TRACE-
|
|
1742
|
-
`Scenario \
|
|
1905
|
+
"QFAI-TRACE-006",
|
|
1906
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044 BR \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownBrIds.join(
|
|
1743
1907
|
", "
|
|
1744
|
-
)} (
|
|
1908
|
+
)} (${scenario.name})`,
|
|
1745
1909
|
"error",
|
|
1746
1910
|
file,
|
|
1747
|
-
"traceability.
|
|
1748
|
-
|
|
1911
|
+
"traceability.scenarioBrExists",
|
|
1912
|
+
unknownBrIds
|
|
1749
1913
|
)
|
|
1750
1914
|
);
|
|
1751
1915
|
}
|
|
1916
|
+
const unknownContractIds = atom.contractIds.filter(
|
|
1917
|
+
(id) => !contractIds.has(id)
|
|
1918
|
+
);
|
|
1919
|
+
if (unknownContractIds.length > 0) {
|
|
1920
|
+
issues.push(
|
|
1921
|
+
issue6(
|
|
1922
|
+
"QFAI-TRACE-008",
|
|
1923
|
+
`Scenario \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1924
|
+
", "
|
|
1925
|
+
)} (${scenario.name})`,
|
|
1926
|
+
config.validation.traceability.unknownContractIdSeverity,
|
|
1927
|
+
file,
|
|
1928
|
+
"traceability.scenarioContractExists",
|
|
1929
|
+
unknownContractIds
|
|
1930
|
+
)
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (specTags.length > 0 && brTags.length > 0) {
|
|
1934
|
+
const allowedBrIds = /* @__PURE__ */ new Set();
|
|
1935
|
+
for (const specId of specTags) {
|
|
1936
|
+
const brIdsForSpec = specToBrIds.get(specId);
|
|
1937
|
+
if (!brIdsForSpec) {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
brIdsForSpec.forEach((id) => allowedBrIds.add(id));
|
|
1941
|
+
}
|
|
1942
|
+
const invalidBrIds = brTags.filter((id) => !allowedBrIds.has(id));
|
|
1943
|
+
if (invalidBrIds.length > 0) {
|
|
1944
|
+
issues.push(
|
|
1945
|
+
issue6(
|
|
1946
|
+
"QFAI-TRACE-007",
|
|
1947
|
+
`Scenario \u306E BR \u304C\u53C2\u7167 SPEC \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093: ${invalidBrIds.join(
|
|
1948
|
+
", "
|
|
1949
|
+
)} (SPEC: ${specTags.join(", ")}) (${scenario.name})`,
|
|
1950
|
+
"error",
|
|
1951
|
+
file,
|
|
1952
|
+
"traceability.scenarioBrUnderSpec",
|
|
1953
|
+
invalidBrIds
|
|
1954
|
+
)
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1752
1958
|
}
|
|
1753
1959
|
}
|
|
1754
1960
|
if (upstreamIds.size === 0) {
|
|
@@ -1791,7 +1997,7 @@ async function validateTraceability(root, config) {
|
|
|
1791
1997
|
", "
|
|
1792
1998
|
)}`,
|
|
1793
1999
|
"error",
|
|
1794
|
-
|
|
2000
|
+
specsRoot,
|
|
1795
2001
|
"traceability.scMustTouchContracts",
|
|
1796
2002
|
scWithoutContracts
|
|
1797
2003
|
)
|
|
@@ -1809,7 +2015,7 @@ async function validateTraceability(root, config) {
|
|
|
1809
2015
|
"QFAI_CONTRACT_ORPHAN",
|
|
1810
2016
|
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1811
2017
|
"error",
|
|
1812
|
-
|
|
2018
|
+
specsRoot,
|
|
1813
2019
|
"traceability.allowOrphanContracts",
|
|
1814
2020
|
orphanContracts
|
|
1815
2021
|
)
|
|
@@ -1846,7 +2052,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
1846
2052
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1847
2053
|
let found = false;
|
|
1848
2054
|
for (const file of targetFiles) {
|
|
1849
|
-
const text = await (0,
|
|
2055
|
+
const text = await (0, import_promises12.readFile)(file, "utf-8");
|
|
1850
2056
|
if (pattern.test(text)) {
|
|
1851
2057
|
found = true;
|
|
1852
2058
|
break;
|
|
@@ -1894,8 +2100,8 @@ async function validateProject(root, configResult) {
|
|
|
1894
2100
|
const issues = [
|
|
1895
2101
|
...configIssues,
|
|
1896
2102
|
...await validateSpecs(root, config),
|
|
2103
|
+
...await validateDeltas(root, config),
|
|
1897
2104
|
...await validateScenarios(root, config),
|
|
1898
|
-
...await validateDecisions(root, config),
|
|
1899
2105
|
...await validateContracts(root, config),
|
|
1900
2106
|
...await validateDefinedIds(root, config),
|
|
1901
2107
|
...await validateTraceability(root, config)
|
|
@@ -1924,21 +2130,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
1924
2130
|
const resolved = configResult ?? await loadConfig(root);
|
|
1925
2131
|
const config = resolved.config;
|
|
1926
2132
|
const configPath = resolved.configPath;
|
|
1927
|
-
const
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
1931
|
-
const
|
|
1932
|
-
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
2133
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2134
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2135
|
+
const apiRoot = import_node_path10.default.join(contractsRoot, "api");
|
|
2136
|
+
const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
|
|
2137
|
+
const dbRoot = import_node_path10.default.join(contractsRoot, "db");
|
|
1933
2138
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1934
2139
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1935
|
-
const specFiles = await collectSpecFiles(
|
|
1936
|
-
const scenarioFiles = await
|
|
1937
|
-
extensions: [".feature"]
|
|
1938
|
-
});
|
|
1939
|
-
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1940
|
-
extensions: [".md"]
|
|
1941
|
-
});
|
|
2140
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
2141
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1942
2142
|
const {
|
|
1943
2143
|
api: apiFiles,
|
|
1944
2144
|
ui: uiFiles,
|
|
@@ -1947,7 +2147,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1947
2147
|
const idsByPrefix = await collectIds([
|
|
1948
2148
|
...specFiles,
|
|
1949
2149
|
...scenarioFiles,
|
|
1950
|
-
...decisionFiles,
|
|
1951
2150
|
...apiFiles,
|
|
1952
2151
|
...uiFiles,
|
|
1953
2152
|
...dbFiles
|
|
@@ -1972,7 +2171,6 @@ async function createReportData(root, validation, configResult) {
|
|
|
1972
2171
|
summary: {
|
|
1973
2172
|
specs: specFiles.length,
|
|
1974
2173
|
scenarios: scenarioFiles.length,
|
|
1975
|
-
decisions: decisionFiles.length,
|
|
1976
2174
|
contracts: {
|
|
1977
2175
|
api: apiFiles.length,
|
|
1978
2176
|
ui: uiFiles.length,
|
|
@@ -2006,7 +2204,6 @@ function formatReportMarkdown(data) {
|
|
|
2006
2204
|
lines.push("## \u6982\u8981");
|
|
2007
2205
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2008
2206
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2009
|
-
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
2010
2207
|
lines.push(
|
|
2011
2208
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
2012
2209
|
);
|
|
@@ -2082,7 +2279,7 @@ async function collectIds(files) {
|
|
|
2082
2279
|
DATA: /* @__PURE__ */ new Set()
|
|
2083
2280
|
};
|
|
2084
2281
|
for (const file of files) {
|
|
2085
|
-
const text = await (0,
|
|
2282
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2086
2283
|
for (const prefix of ID_PREFIXES2) {
|
|
2087
2284
|
const ids = extractIds(text, prefix);
|
|
2088
2285
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -2100,7 +2297,7 @@ async function collectIds(files) {
|
|
|
2100
2297
|
async function collectUpstreamIds(files) {
|
|
2101
2298
|
const ids = /* @__PURE__ */ new Set();
|
|
2102
2299
|
for (const file of files) {
|
|
2103
|
-
const text = await (0,
|
|
2300
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2104
2301
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2105
2302
|
}
|
|
2106
2303
|
return ids;
|
|
@@ -2121,7 +2318,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2121
2318
|
}
|
|
2122
2319
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2123
2320
|
for (const file of targetFiles) {
|
|
2124
|
-
const text = await (0,
|
|
2321
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2125
2322
|
if (pattern.test(text)) {
|
|
2126
2323
|
return true;
|
|
2127
2324
|
}
|
|
@@ -2178,8 +2375,8 @@ function buildHotspots(issues) {
|
|
|
2178
2375
|
resolvePath,
|
|
2179
2376
|
resolveToolVersion,
|
|
2180
2377
|
validateContracts,
|
|
2181
|
-
validateDecisions,
|
|
2182
2378
|
validateDefinedIds,
|
|
2379
|
+
validateDeltas,
|
|
2183
2380
|
validateProject,
|
|
2184
2381
|
validateScenarioContent,
|
|
2185
2382
|
validateScenarios,
|