qfai 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/assets/init/.qfai/README.md +6 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/steering/traceability.md +2 -1
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +1 -1
- package/assets/init/.qfai/specs/README.md +3 -1
- package/assets/init/.qfai/specs/spec-0001/scenario.md +2 -1
- package/assets/init/root/qfai.config.yaml +1 -1
- package/dist/cli/index.cjs +1263 -704
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +1246 -687
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +343 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +347 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/cli/index.mjs
CHANGED
|
@@ -1,165 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli/commands/
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
// src/cli/lib/fs.ts
|
|
7
|
-
import { access, copyFile, mkdir, readdir } from "fs/promises";
|
|
8
|
-
import path from "path";
|
|
9
|
-
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
10
|
-
const files = await collectTemplateFiles(sourceRoot);
|
|
11
|
-
return copyFiles(files, sourceRoot, destRoot, options);
|
|
12
|
-
}
|
|
13
|
-
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
14
|
-
const copied = [];
|
|
15
|
-
const skipped = [];
|
|
16
|
-
const conflicts = [];
|
|
17
|
-
if (!options.force) {
|
|
18
|
-
for (const file of files) {
|
|
19
|
-
const relative = path.relative(sourceRoot, file);
|
|
20
|
-
const dest = path.join(destRoot, relative);
|
|
21
|
-
if (!await shouldWrite(dest, options.force)) {
|
|
22
|
-
conflicts.push(dest);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (conflicts.length > 0) {
|
|
26
|
-
throw new Error(formatConflictMessage(conflicts));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
for (const file of files) {
|
|
30
|
-
const relative = path.relative(sourceRoot, file);
|
|
31
|
-
const dest = path.join(destRoot, relative);
|
|
32
|
-
if (!await shouldWrite(dest, options.force)) {
|
|
33
|
-
skipped.push(dest);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (!options.dryRun) {
|
|
37
|
-
await mkdir(path.dirname(dest), { recursive: true });
|
|
38
|
-
await copyFile(file, dest);
|
|
39
|
-
}
|
|
40
|
-
copied.push(dest);
|
|
41
|
-
}
|
|
42
|
-
return { copied, skipped };
|
|
43
|
-
}
|
|
44
|
-
function formatConflictMessage(conflicts) {
|
|
45
|
-
return [
|
|
46
|
-
"\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3068\u885D\u7A81\u3057\u307E\u3057\u305F\u3002\u5B89\u5168\u306E\u305F\u3081\u505C\u6B62\u3057\u307E\u3059\u3002",
|
|
47
|
-
"",
|
|
48
|
-
"\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
|
|
49
|
-
...conflicts.map((conflict) => `- ${conflict}`),
|
|
50
|
-
"",
|
|
51
|
-
"\u4E0A\u66F8\u304D\u3057\u3066\u7D9A\u884C\u3059\u308B\u5834\u5408\u306F --force \u3092\u4ED8\u3051\u3066\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
52
|
-
].join("\n");
|
|
53
|
-
}
|
|
54
|
-
async function collectTemplateFiles(root) {
|
|
55
|
-
const entries = [];
|
|
56
|
-
if (!await exists(root)) {
|
|
57
|
-
return entries;
|
|
58
|
-
}
|
|
59
|
-
const items = await readdir(root, { withFileTypes: true });
|
|
60
|
-
for (const item of items) {
|
|
61
|
-
const fullPath = path.join(root, item.name);
|
|
62
|
-
if (item.isDirectory()) {
|
|
63
|
-
const nested = await collectTemplateFiles(fullPath);
|
|
64
|
-
entries.push(...nested);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (item.isFile()) {
|
|
68
|
-
entries.push(fullPath);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return entries;
|
|
72
|
-
}
|
|
73
|
-
async function shouldWrite(target, force) {
|
|
74
|
-
if (force) {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
return !await exists(target);
|
|
78
|
-
}
|
|
79
|
-
async function exists(target) {
|
|
80
|
-
try {
|
|
81
|
-
await access(target);
|
|
82
|
-
return true;
|
|
83
|
-
} catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// src/cli/lib/assets.ts
|
|
89
|
-
import { existsSync } from "fs";
|
|
90
|
-
import path2 from "path";
|
|
91
|
-
import { fileURLToPath } from "url";
|
|
92
|
-
function getInitAssetsDir() {
|
|
93
|
-
const base = import.meta.url;
|
|
94
|
-
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
95
|
-
const baseDir = path2.dirname(basePath);
|
|
96
|
-
const candidates = [
|
|
97
|
-
path2.resolve(baseDir, "../../../assets/init"),
|
|
98
|
-
path2.resolve(baseDir, "../../assets/init")
|
|
99
|
-
];
|
|
100
|
-
for (const candidate of candidates) {
|
|
101
|
-
if (existsSync(candidate)) {
|
|
102
|
-
return candidate;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
throw new Error(
|
|
106
|
-
[
|
|
107
|
-
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
108
|
-
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
109
|
-
...candidates.map((candidate) => `- ${candidate}`)
|
|
110
|
-
].join("\n")
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// src/cli/lib/logger.ts
|
|
115
|
-
function info(message) {
|
|
116
|
-
process.stdout.write(`${message}
|
|
117
|
-
`);
|
|
118
|
-
}
|
|
119
|
-
function error(message) {
|
|
120
|
-
process.stderr.write(`${message}
|
|
121
|
-
`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/cli/commands/init.ts
|
|
125
|
-
async function runInit(options) {
|
|
126
|
-
const assetsRoot = getInitAssetsDir();
|
|
127
|
-
const rootAssets = path3.join(assetsRoot, "root");
|
|
128
|
-
const qfaiAssets = path3.join(assetsRoot, ".qfai");
|
|
129
|
-
const destRoot = path3.resolve(options.dir);
|
|
130
|
-
const destQfai = path3.join(destRoot, ".qfai");
|
|
131
|
-
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
132
|
-
force: options.force,
|
|
133
|
-
dryRun: options.dryRun
|
|
134
|
-
});
|
|
135
|
-
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
136
|
-
force: options.force,
|
|
137
|
-
dryRun: options.dryRun
|
|
138
|
-
});
|
|
139
|
-
report(
|
|
140
|
-
[...rootResult.copied, ...qfaiResult.copied],
|
|
141
|
-
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
142
|
-
options.dryRun,
|
|
143
|
-
"init"
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
function report(copied, skipped, dryRun, label) {
|
|
147
|
-
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
148
|
-
if (copied.length > 0) {
|
|
149
|
-
info(` created: ${copied.length}`);
|
|
150
|
-
}
|
|
151
|
-
if (skipped.length > 0) {
|
|
152
|
-
info(` skipped: ${skipped.length}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
3
|
+
// src/cli/commands/doctor.ts
|
|
4
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
+
import path8 from "path";
|
|
155
6
|
|
|
156
|
-
// src/
|
|
157
|
-
import {
|
|
158
|
-
import
|
|
7
|
+
// src/core/doctor.ts
|
|
8
|
+
import { access as access4 } from "fs/promises";
|
|
9
|
+
import path7 from "path";
|
|
159
10
|
|
|
160
11
|
// src/core/config.ts
|
|
161
|
-
import { readFile } from "fs/promises";
|
|
162
|
-
import
|
|
12
|
+
import { access, readFile } from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
163
14
|
import { parse as parseYaml } from "yaml";
|
|
164
15
|
var defaultConfig = {
|
|
165
16
|
paths: {
|
|
@@ -190,7 +41,7 @@ var defaultConfig = {
|
|
|
190
41
|
testFileGlobs: [],
|
|
191
42
|
testFileExcludeGlobs: [],
|
|
192
43
|
scNoTestSeverity: "error",
|
|
193
|
-
|
|
44
|
+
orphanContractsPolicy: "error",
|
|
194
45
|
unknownContractIdSeverity: "error"
|
|
195
46
|
}
|
|
196
47
|
},
|
|
@@ -199,7 +50,27 @@ var defaultConfig = {
|
|
|
199
50
|
}
|
|
200
51
|
};
|
|
201
52
|
function getConfigPath(root) {
|
|
202
|
-
return
|
|
53
|
+
return path.join(root, "qfai.config.yaml");
|
|
54
|
+
}
|
|
55
|
+
async function findConfigRoot(startDir) {
|
|
56
|
+
const resolvedStart = path.resolve(startDir);
|
|
57
|
+
let current = resolvedStart;
|
|
58
|
+
while (true) {
|
|
59
|
+
const configPath = getConfigPath(current);
|
|
60
|
+
if (await exists(configPath)) {
|
|
61
|
+
return { root: current, configPath, found: true };
|
|
62
|
+
}
|
|
63
|
+
const parent = path.dirname(current);
|
|
64
|
+
if (parent === current) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
current = parent;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
root: resolvedStart,
|
|
71
|
+
configPath: getConfigPath(resolvedStart),
|
|
72
|
+
found: false
|
|
73
|
+
};
|
|
203
74
|
}
|
|
204
75
|
async function loadConfig(root) {
|
|
205
76
|
const configPath = getConfigPath(root);
|
|
@@ -219,7 +90,7 @@ async function loadConfig(root) {
|
|
|
219
90
|
return { config: normalized, issues, configPath };
|
|
220
91
|
}
|
|
221
92
|
function resolvePath(root, config, key) {
|
|
222
|
-
return
|
|
93
|
+
return path.resolve(root, config.paths[key]);
|
|
223
94
|
}
|
|
224
95
|
function normalizeConfig(raw, configPath, issues) {
|
|
225
96
|
if (!isRecord(raw)) {
|
|
@@ -390,10 +261,10 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
390
261
|
configPath,
|
|
391
262
|
issues
|
|
392
263
|
),
|
|
393
|
-
|
|
394
|
-
traceabilityRaw?.
|
|
395
|
-
base.traceability.
|
|
396
|
-
"validation.traceability.
|
|
264
|
+
orphanContractsPolicy: readOrphanContractsPolicy(
|
|
265
|
+
traceabilityRaw?.orphanContractsPolicy,
|
|
266
|
+
base.traceability.orphanContractsPolicy,
|
|
267
|
+
"validation.traceability.orphanContractsPolicy",
|
|
397
268
|
configPath,
|
|
398
269
|
issues
|
|
399
270
|
),
|
|
@@ -489,6 +360,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
489
360
|
}
|
|
490
361
|
return fallback;
|
|
491
362
|
}
|
|
363
|
+
function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
|
|
364
|
+
if (value === "error" || value === "warning" || value === "allow") {
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
if (value !== void 0) {
|
|
368
|
+
issues.push(
|
|
369
|
+
configIssue(
|
|
370
|
+
configPath,
|
|
371
|
+
`${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return fallback;
|
|
376
|
+
}
|
|
492
377
|
function configIssue(file, message) {
|
|
493
378
|
return {
|
|
494
379
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -504,6 +389,14 @@ function isMissingFile(error2) {
|
|
|
504
389
|
}
|
|
505
390
|
return false;
|
|
506
391
|
}
|
|
392
|
+
async function exists(target) {
|
|
393
|
+
try {
|
|
394
|
+
await access(target);
|
|
395
|
+
return true;
|
|
396
|
+
} catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
507
400
|
function formatError(error2) {
|
|
508
401
|
if (error2 instanceof Error) {
|
|
509
402
|
return error2.message;
|
|
@@ -514,20 +407,12 @@ function isRecord(value) {
|
|
|
514
407
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
515
408
|
}
|
|
516
409
|
|
|
517
|
-
// src/core/report.ts
|
|
518
|
-
import { readFile as readFile11 } from "fs/promises";
|
|
519
|
-
import path14 from "path";
|
|
520
|
-
|
|
521
|
-
// src/core/contractIndex.ts
|
|
522
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
523
|
-
import path7 from "path";
|
|
524
|
-
|
|
525
410
|
// src/core/discovery.ts
|
|
526
411
|
import { access as access3 } from "fs/promises";
|
|
527
412
|
|
|
528
413
|
// src/core/fs.ts
|
|
529
|
-
import { access as access2, readdir
|
|
530
|
-
import
|
|
414
|
+
import { access as access2, readdir } from "fs/promises";
|
|
415
|
+
import path2 from "path";
|
|
531
416
|
import fg from "fast-glob";
|
|
532
417
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
533
418
|
"node_modules",
|
|
@@ -563,9 +448,9 @@ async function collectFilesByGlobs(root, options) {
|
|
|
563
448
|
});
|
|
564
449
|
}
|
|
565
450
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
566
|
-
const items = await
|
|
451
|
+
const items = await readdir(current, { withFileTypes: true });
|
|
567
452
|
for (const item of items) {
|
|
568
|
-
const fullPath =
|
|
453
|
+
const fullPath = path2.join(current, item.name);
|
|
569
454
|
if (item.isDirectory()) {
|
|
570
455
|
if (ignoreDirs.has(item.name)) {
|
|
571
456
|
continue;
|
|
@@ -575,7 +460,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
575
460
|
}
|
|
576
461
|
if (item.isFile()) {
|
|
577
462
|
if (extensions.length > 0) {
|
|
578
|
-
const ext =
|
|
463
|
+
const ext = path2.extname(item.name).toLowerCase();
|
|
579
464
|
if (!extensions.includes(ext)) {
|
|
580
465
|
continue;
|
|
581
466
|
}
|
|
@@ -594,23 +479,23 @@ async function exists2(target) {
|
|
|
594
479
|
}
|
|
595
480
|
|
|
596
481
|
// src/core/specLayout.ts
|
|
597
|
-
import { readdir as
|
|
598
|
-
import
|
|
482
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
483
|
+
import path3 from "path";
|
|
599
484
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
600
485
|
async function collectSpecEntries(specsRoot) {
|
|
601
486
|
const dirs = await listSpecDirs(specsRoot);
|
|
602
487
|
const entries = dirs.map((dir) => ({
|
|
603
488
|
dir,
|
|
604
|
-
specPath:
|
|
605
|
-
deltaPath:
|
|
606
|
-
scenarioPath:
|
|
489
|
+
specPath: path3.join(dir, "spec.md"),
|
|
490
|
+
deltaPath: path3.join(dir, "delta.md"),
|
|
491
|
+
scenarioPath: path3.join(dir, "scenario.md")
|
|
607
492
|
}));
|
|
608
493
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
609
494
|
}
|
|
610
495
|
async function listSpecDirs(specsRoot) {
|
|
611
496
|
try {
|
|
612
|
-
const items = await
|
|
613
|
-
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) =>
|
|
497
|
+
const items = await readdir2(specsRoot, { withFileTypes: true });
|
|
498
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
|
|
614
499
|
} catch (error2) {
|
|
615
500
|
if (isMissingFileError(error2)) {
|
|
616
501
|
return [];
|
|
@@ -673,311 +558,62 @@ async function exists3(target) {
|
|
|
673
558
|
}
|
|
674
559
|
}
|
|
675
560
|
|
|
676
|
-
// src/core/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
682
|
-
const id = match[1];
|
|
683
|
-
if (id) {
|
|
684
|
-
ids.push(id);
|
|
685
|
-
}
|
|
561
|
+
// src/core/paths.ts
|
|
562
|
+
import path4 from "path";
|
|
563
|
+
function toRelativePath(root, target) {
|
|
564
|
+
if (!target) {
|
|
565
|
+
return target;
|
|
686
566
|
}
|
|
687
|
-
|
|
567
|
+
if (!path4.isAbsolute(target)) {
|
|
568
|
+
return toPosixPath(target);
|
|
569
|
+
}
|
|
570
|
+
const relative = path4.relative(root, target);
|
|
571
|
+
if (!relative) {
|
|
572
|
+
return ".";
|
|
573
|
+
}
|
|
574
|
+
return toPosixPath(relative);
|
|
688
575
|
}
|
|
689
|
-
function
|
|
690
|
-
return
|
|
576
|
+
function toPosixPath(value) {
|
|
577
|
+
return value.replace(/\\/g, "/");
|
|
691
578
|
}
|
|
692
579
|
|
|
693
|
-
// src/core/
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
580
|
+
// src/core/traceability.ts
|
|
581
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
582
|
+
import path5 from "path";
|
|
583
|
+
|
|
584
|
+
// src/core/gherkin/parse.ts
|
|
585
|
+
import {
|
|
586
|
+
AstBuilder,
|
|
587
|
+
GherkinClassicTokenMatcher,
|
|
588
|
+
Parser
|
|
589
|
+
} from "@cucumber/gherkin";
|
|
590
|
+
import { randomUUID } from "crypto";
|
|
591
|
+
function parseGherkin(source, uri) {
|
|
592
|
+
const errors = [];
|
|
593
|
+
const uuidFn = () => randomUUID();
|
|
594
|
+
const builder = new AstBuilder(uuidFn);
|
|
595
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
596
|
+
const parser = new Parser(builder, matcher);
|
|
597
|
+
try {
|
|
598
|
+
const gherkinDocument = parser.parse(source);
|
|
599
|
+
gherkinDocument.uri = uri;
|
|
600
|
+
return { gherkinDocument, errors };
|
|
601
|
+
} catch (error2) {
|
|
602
|
+
errors.push(formatError2(error2));
|
|
603
|
+
return { gherkinDocument: null, errors };
|
|
718
604
|
}
|
|
719
605
|
}
|
|
720
|
-
function
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// src/core/ids.ts
|
|
728
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
729
|
-
var STRICT_ID_PATTERNS = {
|
|
730
|
-
SPEC: /\bSPEC-\d{4}\b/g,
|
|
731
|
-
BR: /\bBR-\d{4}\b/g,
|
|
732
|
-
SC: /\bSC-\d{4}\b/g,
|
|
733
|
-
UI: /\bUI-\d{4}\b/g,
|
|
734
|
-
API: /\bAPI-\d{4}\b/g,
|
|
735
|
-
DB: /\bDB-\d{4}\b/g,
|
|
736
|
-
ADR: /\bADR-\d{4}\b/g
|
|
737
|
-
};
|
|
738
|
-
var LOOSE_ID_PATTERNS = {
|
|
739
|
-
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
740
|
-
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
741
|
-
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
742
|
-
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
743
|
-
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
744
|
-
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
745
|
-
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
746
|
-
};
|
|
747
|
-
function extractIds(text, prefix) {
|
|
748
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
749
|
-
const matches = text.match(pattern);
|
|
750
|
-
return unique(matches ?? []);
|
|
751
|
-
}
|
|
752
|
-
function extractAllIds(text) {
|
|
753
|
-
const all = [];
|
|
754
|
-
ID_PREFIXES.forEach((prefix) => {
|
|
755
|
-
all.push(...extractIds(text, prefix));
|
|
756
|
-
});
|
|
757
|
-
return unique(all);
|
|
758
|
-
}
|
|
759
|
-
function extractInvalidIds(text, prefixes) {
|
|
760
|
-
const invalid = [];
|
|
761
|
-
for (const prefix of prefixes) {
|
|
762
|
-
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
763
|
-
for (const candidate of candidates) {
|
|
764
|
-
if (!isValidId(candidate, prefix)) {
|
|
765
|
-
invalid.push(candidate);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return unique(invalid);
|
|
770
|
-
}
|
|
771
|
-
function unique(values) {
|
|
772
|
-
return Array.from(new Set(values));
|
|
773
|
-
}
|
|
774
|
-
function isValidId(value, prefix) {
|
|
775
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
776
|
-
const strict = new RegExp(pattern.source);
|
|
777
|
-
return strict.test(value);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// src/core/parse/markdown.ts
|
|
781
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
782
|
-
function parseHeadings(md) {
|
|
783
|
-
const lines = md.split(/\r?\n/);
|
|
784
|
-
const headings = [];
|
|
785
|
-
for (let i = 0; i < lines.length; i++) {
|
|
786
|
-
const line = lines[i] ?? "";
|
|
787
|
-
const match = line.match(HEADING_RE);
|
|
788
|
-
if (!match) continue;
|
|
789
|
-
const levelToken = match[1];
|
|
790
|
-
const title = match[2];
|
|
791
|
-
if (!levelToken || !title) continue;
|
|
792
|
-
headings.push({
|
|
793
|
-
level: levelToken.length,
|
|
794
|
-
title: title.trim(),
|
|
795
|
-
line: i + 1
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
return headings;
|
|
799
|
-
}
|
|
800
|
-
function extractH2Sections(md) {
|
|
801
|
-
const lines = md.split(/\r?\n/);
|
|
802
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
803
|
-
const sections = /* @__PURE__ */ new Map();
|
|
804
|
-
for (let i = 0; i < headings.length; i++) {
|
|
805
|
-
const current = headings[i];
|
|
806
|
-
if (!current) continue;
|
|
807
|
-
const next = headings[i + 1];
|
|
808
|
-
const startLine = current.line + 1;
|
|
809
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
810
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
811
|
-
sections.set(current.title.trim(), {
|
|
812
|
-
title: current.title.trim(),
|
|
813
|
-
startLine,
|
|
814
|
-
endLine,
|
|
815
|
-
body
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
return sections;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// src/core/parse/spec.ts
|
|
822
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
823
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
824
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
825
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
826
|
-
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
827
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
828
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
829
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
830
|
-
function parseSpec(md, file) {
|
|
831
|
-
const headings = parseHeadings(md);
|
|
832
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
833
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
834
|
-
const sections = extractH2Sections(md);
|
|
835
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
836
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
837
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
838
|
-
const startLine = brSection?.startLine ?? 1;
|
|
839
|
-
const brs = [];
|
|
840
|
-
const brsWithoutPriority = [];
|
|
841
|
-
const brsWithInvalidPriority = [];
|
|
842
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
843
|
-
const lineText = brLines[i] ?? "";
|
|
844
|
-
const lineNumber = startLine + i;
|
|
845
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
846
|
-
if (validMatch) {
|
|
847
|
-
const id = validMatch[1];
|
|
848
|
-
const priority = validMatch[2];
|
|
849
|
-
const text = validMatch[3];
|
|
850
|
-
if (!id || !priority || !text) continue;
|
|
851
|
-
brs.push({
|
|
852
|
-
id,
|
|
853
|
-
priority,
|
|
854
|
-
text: text.trim(),
|
|
855
|
-
line: lineNumber
|
|
856
|
-
});
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
860
|
-
if (anyPriorityMatch) {
|
|
861
|
-
const id = anyPriorityMatch[1];
|
|
862
|
-
const priority = anyPriorityMatch[2];
|
|
863
|
-
const text = anyPriorityMatch[3];
|
|
864
|
-
if (!id || !priority || !text) continue;
|
|
865
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
866
|
-
brsWithInvalidPriority.push({
|
|
867
|
-
id,
|
|
868
|
-
priority,
|
|
869
|
-
text: text.trim(),
|
|
870
|
-
line: lineNumber
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
876
|
-
if (noPriorityMatch) {
|
|
877
|
-
const id = noPriorityMatch[1];
|
|
878
|
-
const text = noPriorityMatch[2];
|
|
879
|
-
if (!id || !text) continue;
|
|
880
|
-
brsWithoutPriority.push({
|
|
881
|
-
id,
|
|
882
|
-
text: text.trim(),
|
|
883
|
-
line: lineNumber
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
const parsed = {
|
|
888
|
-
file,
|
|
889
|
-
sections: sectionNames,
|
|
890
|
-
brs,
|
|
891
|
-
brsWithoutPriority,
|
|
892
|
-
brsWithInvalidPriority,
|
|
893
|
-
contractRefs: parseContractRefs(md)
|
|
894
|
-
};
|
|
895
|
-
if (specId) {
|
|
896
|
-
parsed.specId = specId;
|
|
897
|
-
}
|
|
898
|
-
return parsed;
|
|
899
|
-
}
|
|
900
|
-
function parseContractRefs(md) {
|
|
901
|
-
const lines = [];
|
|
902
|
-
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
903
|
-
lines.push((match[1] ?? "").trim());
|
|
904
|
-
}
|
|
905
|
-
const ids = [];
|
|
906
|
-
const invalidTokens = [];
|
|
907
|
-
let hasNone = false;
|
|
908
|
-
for (const line of lines) {
|
|
909
|
-
if (line.length === 0) {
|
|
910
|
-
invalidTokens.push("(empty)");
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
914
|
-
for (const token of tokens) {
|
|
915
|
-
if (token.length === 0) {
|
|
916
|
-
invalidTokens.push("(empty)");
|
|
917
|
-
continue;
|
|
918
|
-
}
|
|
919
|
-
if (token === "none") {
|
|
920
|
-
hasNone = true;
|
|
921
|
-
continue;
|
|
922
|
-
}
|
|
923
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
924
|
-
ids.push(token);
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
invalidTokens.push(token);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return {
|
|
931
|
-
lines,
|
|
932
|
-
ids: unique2(ids),
|
|
933
|
-
invalidTokens: unique2(invalidTokens),
|
|
934
|
-
hasNone
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
function unique2(values) {
|
|
938
|
-
return Array.from(new Set(values));
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// src/core/traceability.ts
|
|
942
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
943
|
-
import path8 from "path";
|
|
944
|
-
|
|
945
|
-
// src/core/gherkin/parse.ts
|
|
946
|
-
import {
|
|
947
|
-
AstBuilder,
|
|
948
|
-
GherkinClassicTokenMatcher,
|
|
949
|
-
Parser
|
|
950
|
-
} from "@cucumber/gherkin";
|
|
951
|
-
import { randomUUID } from "crypto";
|
|
952
|
-
function parseGherkin(source, uri) {
|
|
953
|
-
const errors = [];
|
|
954
|
-
const uuidFn = () => randomUUID();
|
|
955
|
-
const builder = new AstBuilder(uuidFn);
|
|
956
|
-
const matcher = new GherkinClassicTokenMatcher();
|
|
957
|
-
const parser = new Parser(builder, matcher);
|
|
958
|
-
try {
|
|
959
|
-
const gherkinDocument = parser.parse(source);
|
|
960
|
-
gherkinDocument.uri = uri;
|
|
961
|
-
return { gherkinDocument, errors };
|
|
962
|
-
} catch (error2) {
|
|
963
|
-
errors.push(formatError2(error2));
|
|
964
|
-
return { gherkinDocument: null, errors };
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
function formatError2(error2) {
|
|
968
|
-
if (error2 instanceof Error) {
|
|
969
|
-
return error2.message;
|
|
970
|
-
}
|
|
971
|
-
return String(error2);
|
|
606
|
+
function formatError2(error2) {
|
|
607
|
+
if (error2 instanceof Error) {
|
|
608
|
+
return error2.message;
|
|
609
|
+
}
|
|
610
|
+
return String(error2);
|
|
972
611
|
}
|
|
973
612
|
|
|
974
613
|
// src/core/scenarioModel.ts
|
|
975
614
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
976
615
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
977
616
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
978
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
979
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
980
|
-
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
981
617
|
function parseScenarioDocument(text, uri) {
|
|
982
618
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
983
619
|
if (!gherkinDocument) {
|
|
@@ -1002,31 +638,21 @@ function parseScenarioDocument(text, uri) {
|
|
|
1002
638
|
errors
|
|
1003
639
|
};
|
|
1004
640
|
}
|
|
1005
|
-
function buildScenarioAtoms(document) {
|
|
641
|
+
function buildScenarioAtoms(document, contractIds = []) {
|
|
642
|
+
const uniqueContractIds = unique(contractIds).sort(
|
|
643
|
+
(a, b) => a.localeCompare(b)
|
|
644
|
+
);
|
|
1006
645
|
return document.scenarios.map((scenario) => {
|
|
1007
646
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1008
647
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1009
|
-
const brIds =
|
|
1010
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1011
|
-
scenario.tags.forEach((tag) => {
|
|
1012
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
1013
|
-
contractIds.add(tag);
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
for (const step of scenario.steps) {
|
|
1017
|
-
for (const text of collectStepTexts(step)) {
|
|
1018
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1019
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1020
|
-
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
648
|
+
const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1023
649
|
const atom = {
|
|
1024
650
|
uri: document.uri,
|
|
1025
651
|
featureName: document.featureName ?? "",
|
|
1026
652
|
scenarioName: scenario.name,
|
|
1027
653
|
kind: scenario.kind,
|
|
1028
654
|
brIds,
|
|
1029
|
-
contractIds:
|
|
655
|
+
contractIds: uniqueContractIds
|
|
1030
656
|
};
|
|
1031
657
|
if (scenario.line !== void 0) {
|
|
1032
658
|
atom.line = scenario.line;
|
|
@@ -1079,24 +705,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
1079
705
|
function collectTagNames(tags) {
|
|
1080
706
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1081
707
|
}
|
|
1082
|
-
function
|
|
1083
|
-
const texts = [];
|
|
1084
|
-
if (step.text) {
|
|
1085
|
-
texts.push(step.text);
|
|
1086
|
-
}
|
|
1087
|
-
if (step.docString?.content) {
|
|
1088
|
-
texts.push(step.docString.content);
|
|
1089
|
-
}
|
|
1090
|
-
if (step.dataTable?.rows) {
|
|
1091
|
-
for (const row of step.dataTable.rows) {
|
|
1092
|
-
for (const cell of row.cells) {
|
|
1093
|
-
texts.push(cell.value);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return texts;
|
|
1098
|
-
}
|
|
1099
|
-
function unique3(values) {
|
|
708
|
+
function unique(values) {
|
|
1100
709
|
return Array.from(new Set(values));
|
|
1101
710
|
}
|
|
1102
711
|
|
|
@@ -1126,7 +735,7 @@ function extractAnnotatedScIds(text) {
|
|
|
1126
735
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
1127
736
|
const scIds = /* @__PURE__ */ new Set();
|
|
1128
737
|
for (const file of scenarioFiles) {
|
|
1129
|
-
const text = await
|
|
738
|
+
const text = await readFile2(file, "utf-8");
|
|
1130
739
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1131
740
|
if (!document || errors.length > 0) {
|
|
1132
741
|
continue;
|
|
@@ -1144,7 +753,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
1144
753
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1145
754
|
const sources = /* @__PURE__ */ new Map();
|
|
1146
755
|
for (const file of scenarioFiles) {
|
|
1147
|
-
const text = await
|
|
756
|
+
const text = await readFile2(file, "utf-8");
|
|
1148
757
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1149
758
|
if (!document || errors.length > 0) {
|
|
1150
759
|
continue;
|
|
@@ -1197,98 +806,785 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1197
806
|
};
|
|
1198
807
|
}
|
|
1199
808
|
const normalizedFiles = Array.from(
|
|
1200
|
-
new Set(files.map((file) =>
|
|
809
|
+
new Set(files.map((file) => path5.normalize(file)))
|
|
1201
810
|
);
|
|
1202
811
|
for (const file of normalizedFiles) {
|
|
1203
|
-
const text = await
|
|
812
|
+
const text = await readFile2(file, "utf-8");
|
|
1204
813
|
const scIds = extractAnnotatedScIds(text);
|
|
1205
814
|
if (scIds.length === 0) {
|
|
1206
815
|
continue;
|
|
1207
816
|
}
|
|
1208
|
-
for (const scId of scIds) {
|
|
1209
|
-
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
1210
|
-
current.add(file);
|
|
1211
|
-
refs.set(scId, current);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return {
|
|
1215
|
-
refs,
|
|
1216
|
-
scan: {
|
|
1217
|
-
globs: normalizedGlobs,
|
|
1218
|
-
excludeGlobs: mergedExcludeGlobs,
|
|
1219
|
-
matchedFileCount: normalizedFiles.length
|
|
817
|
+
for (const scId of scIds) {
|
|
818
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
819
|
+
current.add(file);
|
|
820
|
+
refs.set(scId, current);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
refs,
|
|
825
|
+
scan: {
|
|
826
|
+
globs: normalizedGlobs,
|
|
827
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
828
|
+
matchedFileCount: normalizedFiles.length
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function buildScCoverage(scIds, refs) {
|
|
833
|
+
const sortedScIds = toSortedArray(scIds);
|
|
834
|
+
const refsRecord = {};
|
|
835
|
+
const missingIds = [];
|
|
836
|
+
let covered = 0;
|
|
837
|
+
for (const scId of sortedScIds) {
|
|
838
|
+
const files = refs.get(scId);
|
|
839
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
840
|
+
refsRecord[scId] = sortedFiles;
|
|
841
|
+
if (sortedFiles.length === 0) {
|
|
842
|
+
missingIds.push(scId);
|
|
843
|
+
} else {
|
|
844
|
+
covered += 1;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
total: sortedScIds.length,
|
|
849
|
+
covered,
|
|
850
|
+
missing: missingIds.length,
|
|
851
|
+
missingIds,
|
|
852
|
+
refs: refsRecord
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function toSortedArray(values) {
|
|
856
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
857
|
+
}
|
|
858
|
+
function normalizeGlobs(globs) {
|
|
859
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
860
|
+
}
|
|
861
|
+
function formatError3(error2) {
|
|
862
|
+
if (error2 instanceof Error) {
|
|
863
|
+
return error2.message;
|
|
864
|
+
}
|
|
865
|
+
return String(error2);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/core/version.ts
|
|
869
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
870
|
+
import path6 from "path";
|
|
871
|
+
import { fileURLToPath } from "url";
|
|
872
|
+
async function resolveToolVersion() {
|
|
873
|
+
if ("0.6.0".length > 0) {
|
|
874
|
+
return "0.6.0";
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const packagePath = resolvePackageJsonPath();
|
|
878
|
+
const raw = await readFile3(packagePath, "utf-8");
|
|
879
|
+
const parsed = JSON.parse(raw);
|
|
880
|
+
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
881
|
+
return version.length > 0 ? version : "unknown";
|
|
882
|
+
} catch {
|
|
883
|
+
return "unknown";
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function resolvePackageJsonPath() {
|
|
887
|
+
const base = import.meta.url;
|
|
888
|
+
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
889
|
+
return path6.resolve(path6.dirname(basePath), "../../package.json");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/core/doctor.ts
|
|
893
|
+
async function exists4(target) {
|
|
894
|
+
try {
|
|
895
|
+
await access4(target);
|
|
896
|
+
return true;
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function addCheck(checks, check) {
|
|
902
|
+
checks.push(check);
|
|
903
|
+
}
|
|
904
|
+
function summarize(checks) {
|
|
905
|
+
const summary = { ok: 0, warning: 0, error: 0 };
|
|
906
|
+
for (const check of checks) {
|
|
907
|
+
summary[check.severity] += 1;
|
|
908
|
+
}
|
|
909
|
+
return summary;
|
|
910
|
+
}
|
|
911
|
+
function normalizeGlobs2(values) {
|
|
912
|
+
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
913
|
+
}
|
|
914
|
+
async function createDoctorData(options) {
|
|
915
|
+
const startDir = path7.resolve(options.startDir);
|
|
916
|
+
const checks = [];
|
|
917
|
+
const configPath = getConfigPath(startDir);
|
|
918
|
+
const search = options.rootExplicit ? {
|
|
919
|
+
root: startDir,
|
|
920
|
+
configPath,
|
|
921
|
+
found: await exists4(configPath)
|
|
922
|
+
} : await findConfigRoot(startDir);
|
|
923
|
+
const root = search.root;
|
|
924
|
+
const version = await resolveToolVersion();
|
|
925
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
926
|
+
addCheck(checks, {
|
|
927
|
+
id: "config.search",
|
|
928
|
+
severity: search.found ? "ok" : "warning",
|
|
929
|
+
title: "Config search",
|
|
930
|
+
message: search.found ? "qfai.config.yaml found" : "qfai.config.yaml not found (default config will be used)",
|
|
931
|
+
details: { configPath: toRelativePath(root, search.configPath) }
|
|
932
|
+
});
|
|
933
|
+
const {
|
|
934
|
+
config,
|
|
935
|
+
issues,
|
|
936
|
+
configPath: resolvedConfigPath
|
|
937
|
+
} = await loadConfig(root);
|
|
938
|
+
if (issues.length === 0) {
|
|
939
|
+
addCheck(checks, {
|
|
940
|
+
id: "config.load",
|
|
941
|
+
severity: "ok",
|
|
942
|
+
title: "Config load",
|
|
943
|
+
message: "Loaded and normalized with 0 issues",
|
|
944
|
+
details: { configPath: toRelativePath(root, resolvedConfigPath) }
|
|
945
|
+
});
|
|
946
|
+
} else {
|
|
947
|
+
addCheck(checks, {
|
|
948
|
+
id: "config.load",
|
|
949
|
+
severity: "warning",
|
|
950
|
+
title: "Config load",
|
|
951
|
+
message: `Loaded with ${issues.length} issue(s) (normalized with defaults when needed)`,
|
|
952
|
+
details: {
|
|
953
|
+
configPath: toRelativePath(root, resolvedConfigPath),
|
|
954
|
+
issues
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const pathKeys = [
|
|
959
|
+
"specsDir",
|
|
960
|
+
"contractsDir",
|
|
961
|
+
"outDir",
|
|
962
|
+
"srcDir",
|
|
963
|
+
"testsDir",
|
|
964
|
+
"rulesDir",
|
|
965
|
+
"promptsDir"
|
|
966
|
+
];
|
|
967
|
+
for (const key of pathKeys) {
|
|
968
|
+
const resolved = resolvePath(root, config, key);
|
|
969
|
+
const ok = await exists4(resolved);
|
|
970
|
+
addCheck(checks, {
|
|
971
|
+
id: `paths.${key}`,
|
|
972
|
+
severity: ok ? "ok" : "warning",
|
|
973
|
+
title: `Path exists: ${key}`,
|
|
974
|
+
message: ok ? `${key} exists` : `${key} is missing (did you run 'qfai init'?)`,
|
|
975
|
+
details: { path: toRelativePath(root, resolved) }
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
979
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
980
|
+
let missingFiles = 0;
|
|
981
|
+
for (const entry of entries) {
|
|
982
|
+
const requiredFiles = [entry.specPath, entry.deltaPath, entry.scenarioPath];
|
|
983
|
+
for (const filePath of requiredFiles) {
|
|
984
|
+
if (!await exists4(filePath)) {
|
|
985
|
+
missingFiles += 1;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
addCheck(checks, {
|
|
990
|
+
id: "spec.layout",
|
|
991
|
+
severity: missingFiles === 0 ? "ok" : "warning",
|
|
992
|
+
title: "Spec pack shape",
|
|
993
|
+
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
994
|
+
details: { specPacks: entries.length, missingFiles }
|
|
995
|
+
});
|
|
996
|
+
const validateJsonAbs = path7.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path7.resolve(root, config.output.validateJsonPath);
|
|
997
|
+
const validateJsonExists = await exists4(validateJsonAbs);
|
|
998
|
+
addCheck(checks, {
|
|
999
|
+
id: "output.validateJson",
|
|
1000
|
+
severity: validateJsonExists ? "ok" : "warning",
|
|
1001
|
+
title: "validate.json",
|
|
1002
|
+
message: validateJsonExists ? "validate.json exists (report can run)" : "validate.json is missing (run 'qfai validate' before 'qfai report')",
|
|
1003
|
+
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1004
|
+
});
|
|
1005
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1006
|
+
const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
|
|
1007
|
+
const exclude = normalizeGlobs2([
|
|
1008
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
1009
|
+
...config.validation.traceability.testFileExcludeGlobs
|
|
1010
|
+
]);
|
|
1011
|
+
try {
|
|
1012
|
+
const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
|
|
1013
|
+
const matchedCount = matched.length;
|
|
1014
|
+
const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
|
|
1015
|
+
addCheck(checks, {
|
|
1016
|
+
id: "traceability.testGlobs",
|
|
1017
|
+
severity,
|
|
1018
|
+
title: "Test file globs",
|
|
1019
|
+
message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
|
|
1020
|
+
details: {
|
|
1021
|
+
globs,
|
|
1022
|
+
excludeGlobs: exclude,
|
|
1023
|
+
scenarioFiles: scenarioFiles.length,
|
|
1024
|
+
scMustHaveTest: config.validation.traceability.scMustHaveTest
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
} catch (error2) {
|
|
1028
|
+
addCheck(checks, {
|
|
1029
|
+
id: "traceability.testGlobs",
|
|
1030
|
+
severity: "error",
|
|
1031
|
+
title: "Test file globs",
|
|
1032
|
+
message: "Glob scan failed (invalid pattern or filesystem error)",
|
|
1033
|
+
details: { globs, excludeGlobs: exclude, error: String(error2) }
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1037
|
+
const rel = path7.relative(outDirAbs, validateJsonAbs);
|
|
1038
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
1039
|
+
addCheck(checks, {
|
|
1040
|
+
id: "output.pathAlignment",
|
|
1041
|
+
severity: inside ? "ok" : "warning",
|
|
1042
|
+
title: "Output path alignment",
|
|
1043
|
+
message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
|
|
1044
|
+
details: {
|
|
1045
|
+
outDir: toRelativePath(root, outDirAbs),
|
|
1046
|
+
validateJsonPath: toRelativePath(root, validateJsonAbs)
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
return {
|
|
1050
|
+
tool: "qfai",
|
|
1051
|
+
version,
|
|
1052
|
+
doctorFormatVersion: 1,
|
|
1053
|
+
generatedAt,
|
|
1054
|
+
root: toRelativePath(process.cwd(), root),
|
|
1055
|
+
config: {
|
|
1056
|
+
startDir: toRelativePath(process.cwd(), startDir),
|
|
1057
|
+
found: search.found,
|
|
1058
|
+
configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
|
|
1059
|
+
},
|
|
1060
|
+
summary: summarize(checks),
|
|
1061
|
+
checks
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/cli/lib/logger.ts
|
|
1066
|
+
function info(message) {
|
|
1067
|
+
process.stdout.write(`${message}
|
|
1068
|
+
`);
|
|
1069
|
+
}
|
|
1070
|
+
function warn(message) {
|
|
1071
|
+
process.stdout.write(`${message}
|
|
1072
|
+
`);
|
|
1073
|
+
}
|
|
1074
|
+
function error(message) {
|
|
1075
|
+
process.stderr.write(`${message}
|
|
1076
|
+
`);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/cli/commands/doctor.ts
|
|
1080
|
+
function formatDoctorText(data) {
|
|
1081
|
+
const lines = [];
|
|
1082
|
+
lines.push(
|
|
1083
|
+
`qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
|
|
1084
|
+
);
|
|
1085
|
+
for (const check of data.checks) {
|
|
1086
|
+
lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
|
|
1087
|
+
}
|
|
1088
|
+
lines.push(
|
|
1089
|
+
`summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
|
|
1090
|
+
);
|
|
1091
|
+
return lines.join("\n");
|
|
1092
|
+
}
|
|
1093
|
+
function formatDoctorJson(data) {
|
|
1094
|
+
return JSON.stringify(data, null, 2);
|
|
1095
|
+
}
|
|
1096
|
+
async function runDoctor(options) {
|
|
1097
|
+
const data = await createDoctorData({
|
|
1098
|
+
startDir: options.root,
|
|
1099
|
+
rootExplicit: options.rootExplicit
|
|
1100
|
+
});
|
|
1101
|
+
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1102
|
+
if (options.outPath) {
|
|
1103
|
+
const outAbs = path8.isAbsolute(options.outPath) ? options.outPath : path8.resolve(process.cwd(), options.outPath);
|
|
1104
|
+
await mkdir(path8.dirname(outAbs), { recursive: true });
|
|
1105
|
+
await writeFile(outAbs, `${output}
|
|
1106
|
+
`, "utf-8");
|
|
1107
|
+
info(`doctor: wrote ${outAbs}`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
info(output);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/cli/commands/init.ts
|
|
1114
|
+
import path11 from "path";
|
|
1115
|
+
|
|
1116
|
+
// src/cli/lib/fs.ts
|
|
1117
|
+
import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
|
|
1118
|
+
import path9 from "path";
|
|
1119
|
+
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1120
|
+
const files = await collectTemplateFiles(sourceRoot);
|
|
1121
|
+
return copyFiles(files, sourceRoot, destRoot, options);
|
|
1122
|
+
}
|
|
1123
|
+
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
1124
|
+
const copied = [];
|
|
1125
|
+
const skipped = [];
|
|
1126
|
+
const conflicts = [];
|
|
1127
|
+
if (!options.force) {
|
|
1128
|
+
for (const file of files) {
|
|
1129
|
+
const relative = path9.relative(sourceRoot, file);
|
|
1130
|
+
const dest = path9.join(destRoot, relative);
|
|
1131
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1132
|
+
conflicts.push(dest);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (conflicts.length > 0) {
|
|
1136
|
+
throw new Error(formatConflictMessage(conflicts));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
for (const file of files) {
|
|
1140
|
+
const relative = path9.relative(sourceRoot, file);
|
|
1141
|
+
const dest = path9.join(destRoot, relative);
|
|
1142
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1143
|
+
skipped.push(dest);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (!options.dryRun) {
|
|
1147
|
+
await mkdir2(path9.dirname(dest), { recursive: true });
|
|
1148
|
+
await copyFile(file, dest);
|
|
1149
|
+
}
|
|
1150
|
+
copied.push(dest);
|
|
1151
|
+
}
|
|
1152
|
+
return { copied, skipped };
|
|
1153
|
+
}
|
|
1154
|
+
function formatConflictMessage(conflicts) {
|
|
1155
|
+
return [
|
|
1156
|
+
"\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3068\u885D\u7A81\u3057\u307E\u3057\u305F\u3002\u5B89\u5168\u306E\u305F\u3081\u505C\u6B62\u3057\u307E\u3059\u3002",
|
|
1157
|
+
"",
|
|
1158
|
+
"\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
|
|
1159
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
1160
|
+
"",
|
|
1161
|
+
"\u4E0A\u66F8\u304D\u3057\u3066\u7D9A\u884C\u3059\u308B\u5834\u5408\u306F --force \u3092\u4ED8\u3051\u3066\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
1162
|
+
].join("\n");
|
|
1163
|
+
}
|
|
1164
|
+
async function collectTemplateFiles(root) {
|
|
1165
|
+
const entries = [];
|
|
1166
|
+
if (!await exists5(root)) {
|
|
1167
|
+
return entries;
|
|
1168
|
+
}
|
|
1169
|
+
const items = await readdir3(root, { withFileTypes: true });
|
|
1170
|
+
for (const item of items) {
|
|
1171
|
+
const fullPath = path9.join(root, item.name);
|
|
1172
|
+
if (item.isDirectory()) {
|
|
1173
|
+
const nested = await collectTemplateFiles(fullPath);
|
|
1174
|
+
entries.push(...nested);
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (item.isFile()) {
|
|
1178
|
+
entries.push(fullPath);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return entries;
|
|
1182
|
+
}
|
|
1183
|
+
async function shouldWrite(target, force) {
|
|
1184
|
+
if (force) {
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
return !await exists5(target);
|
|
1188
|
+
}
|
|
1189
|
+
async function exists5(target) {
|
|
1190
|
+
try {
|
|
1191
|
+
await access5(target);
|
|
1192
|
+
return true;
|
|
1193
|
+
} catch {
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/cli/lib/assets.ts
|
|
1199
|
+
import { existsSync } from "fs";
|
|
1200
|
+
import path10 from "path";
|
|
1201
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1202
|
+
function getInitAssetsDir() {
|
|
1203
|
+
const base = import.meta.url;
|
|
1204
|
+
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1205
|
+
const baseDir = path10.dirname(basePath);
|
|
1206
|
+
const candidates = [
|
|
1207
|
+
path10.resolve(baseDir, "../../../assets/init"),
|
|
1208
|
+
path10.resolve(baseDir, "../../assets/init")
|
|
1209
|
+
];
|
|
1210
|
+
for (const candidate of candidates) {
|
|
1211
|
+
if (existsSync(candidate)) {
|
|
1212
|
+
return candidate;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
[
|
|
1217
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1218
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1219
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
1220
|
+
].join("\n")
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/cli/commands/init.ts
|
|
1225
|
+
async function runInit(options) {
|
|
1226
|
+
const assetsRoot = getInitAssetsDir();
|
|
1227
|
+
const rootAssets = path11.join(assetsRoot, "root");
|
|
1228
|
+
const qfaiAssets = path11.join(assetsRoot, ".qfai");
|
|
1229
|
+
const destRoot = path11.resolve(options.dir);
|
|
1230
|
+
const destQfai = path11.join(destRoot, ".qfai");
|
|
1231
|
+
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
1232
|
+
force: options.force,
|
|
1233
|
+
dryRun: options.dryRun
|
|
1234
|
+
});
|
|
1235
|
+
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
1236
|
+
force: options.force,
|
|
1237
|
+
dryRun: options.dryRun
|
|
1238
|
+
});
|
|
1239
|
+
report(
|
|
1240
|
+
[...rootResult.copied, ...qfaiResult.copied],
|
|
1241
|
+
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
1242
|
+
options.dryRun,
|
|
1243
|
+
"init"
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
function report(copied, skipped, dryRun, label) {
|
|
1247
|
+
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
1248
|
+
if (copied.length > 0) {
|
|
1249
|
+
info(` created: ${copied.length}`);
|
|
1250
|
+
}
|
|
1251
|
+
if (skipped.length > 0) {
|
|
1252
|
+
info(` skipped: ${skipped.length}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/cli/commands/report.ts
|
|
1257
|
+
import { mkdir as mkdir3, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
1258
|
+
import path18 from "path";
|
|
1259
|
+
|
|
1260
|
+
// src/core/normalize.ts
|
|
1261
|
+
function normalizeIssuePaths(root, issues) {
|
|
1262
|
+
return issues.map((issue7) => {
|
|
1263
|
+
if (!issue7.file) {
|
|
1264
|
+
return issue7;
|
|
1265
|
+
}
|
|
1266
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
1267
|
+
if (normalized === issue7.file) {
|
|
1268
|
+
return issue7;
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
...issue7,
|
|
1272
|
+
file: normalized
|
|
1273
|
+
};
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function normalizeScCoverage(root, sc) {
|
|
1277
|
+
const refs = {};
|
|
1278
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
1279
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
1280
|
+
}
|
|
1281
|
+
return {
|
|
1282
|
+
...sc,
|
|
1283
|
+
refs
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function normalizeValidationResult(root, result) {
|
|
1287
|
+
return {
|
|
1288
|
+
...result,
|
|
1289
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
1290
|
+
traceability: {
|
|
1291
|
+
...result.traceability,
|
|
1292
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/core/report.ts
|
|
1298
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1299
|
+
import path17 from "path";
|
|
1300
|
+
|
|
1301
|
+
// src/core/contractIndex.ts
|
|
1302
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1303
|
+
import path12 from "path";
|
|
1304
|
+
|
|
1305
|
+
// src/core/contractsDecl.ts
|
|
1306
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
1307
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
1308
|
+
function extractDeclaredContractIds(text) {
|
|
1309
|
+
const ids = [];
|
|
1310
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
1311
|
+
const id = match[1];
|
|
1312
|
+
if (id) {
|
|
1313
|
+
ids.push(id);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return ids;
|
|
1317
|
+
}
|
|
1318
|
+
function stripContractDeclarationLines(text) {
|
|
1319
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/core/contractIndex.ts
|
|
1323
|
+
async function buildContractIndex(root, config) {
|
|
1324
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1325
|
+
const uiRoot = path12.join(contractsRoot, "ui");
|
|
1326
|
+
const apiRoot = path12.join(contractsRoot, "api");
|
|
1327
|
+
const dbRoot = path12.join(contractsRoot, "db");
|
|
1328
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1329
|
+
collectUiContractFiles(uiRoot),
|
|
1330
|
+
collectApiContractFiles(apiRoot),
|
|
1331
|
+
collectDbContractFiles(dbRoot)
|
|
1332
|
+
]);
|
|
1333
|
+
const index = {
|
|
1334
|
+
ids: /* @__PURE__ */ new Set(),
|
|
1335
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
1336
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1337
|
+
};
|
|
1338
|
+
await indexContractFiles(uiFiles, index);
|
|
1339
|
+
await indexContractFiles(apiFiles, index);
|
|
1340
|
+
await indexContractFiles(dbFiles, index);
|
|
1341
|
+
return index;
|
|
1342
|
+
}
|
|
1343
|
+
async function indexContractFiles(files, index) {
|
|
1344
|
+
for (const file of files) {
|
|
1345
|
+
const text = await readFile4(file, "utf-8");
|
|
1346
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function record(index, id, file) {
|
|
1350
|
+
index.ids.add(id);
|
|
1351
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1352
|
+
current.add(file);
|
|
1353
|
+
index.idToFiles.set(id, current);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/core/ids.ts
|
|
1357
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
1358
|
+
var STRICT_ID_PATTERNS = {
|
|
1359
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1360
|
+
BR: /\bBR-\d{4}\b/g,
|
|
1361
|
+
SC: /\bSC-\d{4}\b/g,
|
|
1362
|
+
UI: /\bUI-\d{4}\b/g,
|
|
1363
|
+
API: /\bAPI-\d{4}\b/g,
|
|
1364
|
+
DB: /\bDB-\d{4}\b/g,
|
|
1365
|
+
ADR: /\bADR-\d{4}\b/g
|
|
1366
|
+
};
|
|
1367
|
+
var LOOSE_ID_PATTERNS = {
|
|
1368
|
+
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
1369
|
+
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
1370
|
+
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
1371
|
+
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1372
|
+
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1373
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1374
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1375
|
+
};
|
|
1376
|
+
function extractIds(text, prefix) {
|
|
1377
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1378
|
+
const matches = text.match(pattern);
|
|
1379
|
+
return unique2(matches ?? []);
|
|
1380
|
+
}
|
|
1381
|
+
function extractAllIds(text) {
|
|
1382
|
+
const all = [];
|
|
1383
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
1384
|
+
all.push(...extractIds(text, prefix));
|
|
1385
|
+
});
|
|
1386
|
+
return unique2(all);
|
|
1387
|
+
}
|
|
1388
|
+
function extractInvalidIds(text, prefixes) {
|
|
1389
|
+
const invalid = [];
|
|
1390
|
+
for (const prefix of prefixes) {
|
|
1391
|
+
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
1392
|
+
for (const candidate of candidates) {
|
|
1393
|
+
if (!isValidId(candidate, prefix)) {
|
|
1394
|
+
invalid.push(candidate);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return unique2(invalid);
|
|
1399
|
+
}
|
|
1400
|
+
function unique2(values) {
|
|
1401
|
+
return Array.from(new Set(values));
|
|
1402
|
+
}
|
|
1403
|
+
function isValidId(value, prefix) {
|
|
1404
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1405
|
+
const strict = new RegExp(pattern.source);
|
|
1406
|
+
return strict.test(value);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/core/parse/contractRefs.ts
|
|
1410
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1411
|
+
function parseContractRefs(text, options = {}) {
|
|
1412
|
+
const linePattern = buildLinePattern(options);
|
|
1413
|
+
const lines = [];
|
|
1414
|
+
for (const match of text.matchAll(linePattern)) {
|
|
1415
|
+
lines.push((match[1] ?? "").trim());
|
|
1416
|
+
}
|
|
1417
|
+
const ids = [];
|
|
1418
|
+
const invalidTokens = [];
|
|
1419
|
+
let hasNone = false;
|
|
1420
|
+
for (const line of lines) {
|
|
1421
|
+
if (line.length === 0) {
|
|
1422
|
+
invalidTokens.push("(empty)");
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
1426
|
+
for (const token of tokens) {
|
|
1427
|
+
if (token.length === 0) {
|
|
1428
|
+
invalidTokens.push("(empty)");
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (token === "none") {
|
|
1432
|
+
hasNone = true;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
1436
|
+
ids.push(token);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
invalidTokens.push(token);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return {
|
|
1443
|
+
lines,
|
|
1444
|
+
ids: unique3(ids),
|
|
1445
|
+
invalidTokens: unique3(invalidTokens),
|
|
1446
|
+
hasNone
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function buildLinePattern(options) {
|
|
1450
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
1451
|
+
return new RegExp(
|
|
1452
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
1453
|
+
"gm"
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
function unique3(values) {
|
|
1457
|
+
return Array.from(new Set(values));
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/core/parse/markdown.ts
|
|
1461
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1462
|
+
function parseHeadings(md) {
|
|
1463
|
+
const lines = md.split(/\r?\n/);
|
|
1464
|
+
const headings = [];
|
|
1465
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1466
|
+
const line = lines[i] ?? "";
|
|
1467
|
+
const match = line.match(HEADING_RE);
|
|
1468
|
+
if (!match) continue;
|
|
1469
|
+
const levelToken = match[1];
|
|
1470
|
+
const title = match[2];
|
|
1471
|
+
if (!levelToken || !title) continue;
|
|
1472
|
+
headings.push({
|
|
1473
|
+
level: levelToken.length,
|
|
1474
|
+
title: title.trim(),
|
|
1475
|
+
line: i + 1
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
return headings;
|
|
1479
|
+
}
|
|
1480
|
+
function extractH2Sections(md) {
|
|
1481
|
+
const lines = md.split(/\r?\n/);
|
|
1482
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1483
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1484
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1485
|
+
const current = headings[i];
|
|
1486
|
+
if (!current) continue;
|
|
1487
|
+
const next = headings[i + 1];
|
|
1488
|
+
const startLine = current.line + 1;
|
|
1489
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1490
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1491
|
+
sections.set(current.title.trim(), {
|
|
1492
|
+
title: current.title.trim(),
|
|
1493
|
+
startLine,
|
|
1494
|
+
endLine,
|
|
1495
|
+
body
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
return sections;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/core/parse/spec.ts
|
|
1502
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1503
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1504
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1505
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1506
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1507
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1508
|
+
function parseSpec(md, file) {
|
|
1509
|
+
const headings = parseHeadings(md);
|
|
1510
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1511
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1512
|
+
const sections = extractH2Sections(md);
|
|
1513
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1514
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1515
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1516
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1517
|
+
const brs = [];
|
|
1518
|
+
const brsWithoutPriority = [];
|
|
1519
|
+
const brsWithInvalidPriority = [];
|
|
1520
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1521
|
+
const lineText = brLines[i] ?? "";
|
|
1522
|
+
const lineNumber = startLine + i;
|
|
1523
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1524
|
+
if (validMatch) {
|
|
1525
|
+
const id = validMatch[1];
|
|
1526
|
+
const priority = validMatch[2];
|
|
1527
|
+
const text = validMatch[3];
|
|
1528
|
+
if (!id || !priority || !text) continue;
|
|
1529
|
+
brs.push({
|
|
1530
|
+
id,
|
|
1531
|
+
priority,
|
|
1532
|
+
text: text.trim(),
|
|
1533
|
+
line: lineNumber
|
|
1534
|
+
});
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1538
|
+
if (anyPriorityMatch) {
|
|
1539
|
+
const id = anyPriorityMatch[1];
|
|
1540
|
+
const priority = anyPriorityMatch[2];
|
|
1541
|
+
const text = anyPriorityMatch[3];
|
|
1542
|
+
if (!id || !priority || !text) continue;
|
|
1543
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1544
|
+
brsWithInvalidPriority.push({
|
|
1545
|
+
id,
|
|
1546
|
+
priority,
|
|
1547
|
+
text: text.trim(),
|
|
1548
|
+
line: lineNumber
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
continue;
|
|
1220
1552
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
refsRecord[scId] = sortedFiles;
|
|
1232
|
-
if (sortedFiles.length === 0) {
|
|
1233
|
-
missingIds.push(scId);
|
|
1234
|
-
} else {
|
|
1235
|
-
covered += 1;
|
|
1553
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1554
|
+
if (noPriorityMatch) {
|
|
1555
|
+
const id = noPriorityMatch[1];
|
|
1556
|
+
const text = noPriorityMatch[2];
|
|
1557
|
+
if (!id || !text) continue;
|
|
1558
|
+
brsWithoutPriority.push({
|
|
1559
|
+
id,
|
|
1560
|
+
text: text.trim(),
|
|
1561
|
+
line: lineNumber
|
|
1562
|
+
});
|
|
1236
1563
|
}
|
|
1237
1564
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1565
|
+
const parsed = {
|
|
1566
|
+
file,
|
|
1567
|
+
sections: sectionNames,
|
|
1568
|
+
brs,
|
|
1569
|
+
brsWithoutPriority,
|
|
1570
|
+
brsWithInvalidPriority,
|
|
1571
|
+
contractRefs: parseContractRefs(md)
|
|
1244
1572
|
};
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
1248
|
-
}
|
|
1249
|
-
function normalizeGlobs(globs) {
|
|
1250
|
-
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1251
|
-
}
|
|
1252
|
-
function formatError3(error2) {
|
|
1253
|
-
if (error2 instanceof Error) {
|
|
1254
|
-
return error2.message;
|
|
1255
|
-
}
|
|
1256
|
-
return String(error2);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// src/core/version.ts
|
|
1260
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1261
|
-
import path9 from "path";
|
|
1262
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1263
|
-
async function resolveToolVersion() {
|
|
1264
|
-
if ("0.5.0".length > 0) {
|
|
1265
|
-
return "0.5.0";
|
|
1266
|
-
}
|
|
1267
|
-
try {
|
|
1268
|
-
const packagePath = resolvePackageJsonPath();
|
|
1269
|
-
const raw = await readFile4(packagePath, "utf-8");
|
|
1270
|
-
const parsed = JSON.parse(raw);
|
|
1271
|
-
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
1272
|
-
return version.length > 0 ? version : "unknown";
|
|
1273
|
-
} catch {
|
|
1274
|
-
return "unknown";
|
|
1573
|
+
if (specId) {
|
|
1574
|
+
parsed.specId = specId;
|
|
1275
1575
|
}
|
|
1276
|
-
|
|
1277
|
-
function resolvePackageJsonPath() {
|
|
1278
|
-
const base = import.meta.url;
|
|
1279
|
-
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1280
|
-
return path9.resolve(path9.dirname(basePath), "../../package.json");
|
|
1576
|
+
return parsed;
|
|
1281
1577
|
}
|
|
1282
1578
|
|
|
1283
1579
|
// src/core/validators/contracts.ts
|
|
1284
1580
|
import { readFile as readFile5 } from "fs/promises";
|
|
1285
|
-
import
|
|
1581
|
+
import path14 from "path";
|
|
1286
1582
|
|
|
1287
1583
|
// src/core/contracts.ts
|
|
1288
|
-
import
|
|
1584
|
+
import path13 from "path";
|
|
1289
1585
|
import { parse as parseYaml2 } from "yaml";
|
|
1290
1586
|
function parseStructuredContract(file, text) {
|
|
1291
|
-
const ext =
|
|
1587
|
+
const ext = path13.extname(file).toLowerCase();
|
|
1292
1588
|
if (ext === ".json") {
|
|
1293
1589
|
return JSON.parse(text);
|
|
1294
1590
|
}
|
|
@@ -1308,9 +1604,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1308
1604
|
async function validateContracts(root, config) {
|
|
1309
1605
|
const issues = [];
|
|
1310
1606
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1311
|
-
issues.push(...await validateUiContracts(
|
|
1312
|
-
issues.push(...await validateApiContracts(
|
|
1313
|
-
issues.push(...await validateDbContracts(
|
|
1607
|
+
issues.push(...await validateUiContracts(path14.join(contractsRoot, "ui")));
|
|
1608
|
+
issues.push(...await validateApiContracts(path14.join(contractsRoot, "api")));
|
|
1609
|
+
issues.push(...await validateDbContracts(path14.join(contractsRoot, "db")));
|
|
1314
1610
|
const contractIndex = await buildContractIndex(root, config);
|
|
1315
1611
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1316
1612
|
return issues;
|
|
@@ -1593,7 +1889,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1593
1889
|
|
|
1594
1890
|
// src/core/validators/delta.ts
|
|
1595
1891
|
import { readFile as readFile6 } from "fs/promises";
|
|
1596
|
-
import
|
|
1892
|
+
import path15 from "path";
|
|
1597
1893
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1598
1894
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1599
1895
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1607,7 +1903,7 @@ async function validateDeltas(root, config) {
|
|
|
1607
1903
|
}
|
|
1608
1904
|
const issues = [];
|
|
1609
1905
|
for (const pack of packs) {
|
|
1610
|
-
const deltaPath =
|
|
1906
|
+
const deltaPath = path15.join(pack, "delta.md");
|
|
1611
1907
|
let text;
|
|
1612
1908
|
try {
|
|
1613
1909
|
text = await readFile6(deltaPath, "utf-8");
|
|
@@ -1683,7 +1979,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1683
1979
|
|
|
1684
1980
|
// src/core/validators/ids.ts
|
|
1685
1981
|
import { readFile as readFile7 } from "fs/promises";
|
|
1686
|
-
import
|
|
1982
|
+
import path16 from "path";
|
|
1687
1983
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1688
1984
|
async function validateDefinedIds(root, config) {
|
|
1689
1985
|
const issues = [];
|
|
@@ -1749,7 +2045,7 @@ function recordId(out, id, file) {
|
|
|
1749
2045
|
}
|
|
1750
2046
|
function formatFileList(files, root) {
|
|
1751
2047
|
return files.map((file) => {
|
|
1752
|
-
const relative =
|
|
2048
|
+
const relative = path16.relative(root, file);
|
|
1753
2049
|
return relative.length > 0 ? relative : file;
|
|
1754
2050
|
}).join(", ");
|
|
1755
2051
|
}
|
|
@@ -2186,7 +2482,7 @@ async function validateTraceability(root, config) {
|
|
|
2186
2482
|
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2187
2483
|
issues.push(
|
|
2188
2484
|
issue6(
|
|
2189
|
-
"QFAI-TRACE-
|
|
2485
|
+
"QFAI-TRACE-023",
|
|
2190
2486
|
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2191
2487
|
"error",
|
|
2192
2488
|
file,
|
|
@@ -2218,7 +2514,7 @@ async function validateTraceability(root, config) {
|
|
|
2218
2514
|
if (unknownContractIds.length > 0) {
|
|
2219
2515
|
issues.push(
|
|
2220
2516
|
issue6(
|
|
2221
|
-
"QFAI-TRACE-
|
|
2517
|
+
"QFAI-TRACE-024",
|
|
2222
2518
|
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2223
2519
|
", "
|
|
2224
2520
|
)}`,
|
|
@@ -2233,11 +2529,62 @@ async function validateTraceability(root, config) {
|
|
|
2233
2529
|
for (const file of scenarioFiles) {
|
|
2234
2530
|
const text = await readFile10(file, "utf-8");
|
|
2235
2531
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2532
|
+
const scenarioContractRefs = parseContractRefs(text, {
|
|
2533
|
+
allowCommentPrefix: true
|
|
2534
|
+
});
|
|
2535
|
+
if (scenarioContractRefs.lines.length === 0) {
|
|
2536
|
+
issues.push(
|
|
2537
|
+
issue6(
|
|
2538
|
+
"QFAI-TRACE-031",
|
|
2539
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2540
|
+
"error",
|
|
2541
|
+
file,
|
|
2542
|
+
"traceability.scenarioContractRefRequired"
|
|
2543
|
+
)
|
|
2544
|
+
);
|
|
2545
|
+
} else {
|
|
2546
|
+
if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
2547
|
+
issues.push(
|
|
2548
|
+
issue6(
|
|
2549
|
+
"QFAI-TRACE-033",
|
|
2550
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2551
|
+
"error",
|
|
2552
|
+
file,
|
|
2553
|
+
"traceability.scenarioContractRefFormat"
|
|
2554
|
+
)
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
if (scenarioContractRefs.invalidTokens.length > 0) {
|
|
2558
|
+
issues.push(
|
|
2559
|
+
issue6(
|
|
2560
|
+
"QFAI-TRACE-032",
|
|
2561
|
+
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2562
|
+
", "
|
|
2563
|
+
)}`,
|
|
2564
|
+
"error",
|
|
2565
|
+
file,
|
|
2566
|
+
"traceability.scenarioContractRefFormat",
|
|
2567
|
+
scenarioContractRefs.invalidTokens
|
|
2568
|
+
)
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2236
2572
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2237
2573
|
if (!document || errors.length > 0) {
|
|
2238
2574
|
continue;
|
|
2239
2575
|
}
|
|
2240
|
-
|
|
2576
|
+
if (document.scenarios.length !== 1) {
|
|
2577
|
+
issues.push(
|
|
2578
|
+
issue6(
|
|
2579
|
+
"QFAI-TRACE-030",
|
|
2580
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306F 1\u30D5\u30A1\u30A4\u30EB=1\u30B7\u30CA\u30EA\u30AA\u3067\u3059\u3002\u73FE\u5728: ${document.scenarios.length}\u4EF6 (file=${file})`,
|
|
2581
|
+
"error",
|
|
2582
|
+
file,
|
|
2583
|
+
"traceability.scenarioOnePerFile"
|
|
2584
|
+
)
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
|
|
2241
2588
|
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2242
2589
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2243
2590
|
const atom = atoms[index];
|
|
@@ -2382,7 +2729,7 @@ async function validateTraceability(root, config) {
|
|
|
2382
2729
|
if (orphanBrIds.length > 0) {
|
|
2383
2730
|
issues.push(
|
|
2384
2731
|
issue6(
|
|
2385
|
-
"
|
|
2732
|
+
"QFAI-TRACE-009",
|
|
2386
2733
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
2387
2734
|
"error",
|
|
2388
2735
|
specsRoot,
|
|
@@ -2452,17 +2799,19 @@ async function validateTraceability(root, config) {
|
|
|
2452
2799
|
);
|
|
2453
2800
|
}
|
|
2454
2801
|
}
|
|
2455
|
-
|
|
2802
|
+
const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
|
|
2803
|
+
if (orphanPolicy !== "allow") {
|
|
2456
2804
|
if (contractIds.size > 0) {
|
|
2457
2805
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2458
2806
|
(id) => !specContractIds.has(id)
|
|
2459
2807
|
);
|
|
2460
2808
|
if (orphanContracts.length > 0) {
|
|
2809
|
+
const severity = orphanPolicy === "warning" ? "warning" : "error";
|
|
2461
2810
|
issues.push(
|
|
2462
2811
|
issue6(
|
|
2463
2812
|
"QFAI-TRACE-022",
|
|
2464
2813
|
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2465
|
-
|
|
2814
|
+
severity,
|
|
2466
2815
|
specsRoot,
|
|
2467
2816
|
"traceability.contractCoverage",
|
|
2468
2817
|
orphanContracts
|
|
@@ -2587,16 +2936,17 @@ function countIssues(issues) {
|
|
|
2587
2936
|
// src/core/report.ts
|
|
2588
2937
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2589
2938
|
async function createReportData(root, validation, configResult) {
|
|
2590
|
-
const
|
|
2939
|
+
const resolvedRoot = path17.resolve(root);
|
|
2940
|
+
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2591
2941
|
const config = resolved.config;
|
|
2592
2942
|
const configPath = resolved.configPath;
|
|
2593
|
-
const specsRoot = resolvePath(
|
|
2594
|
-
const contractsRoot = resolvePath(
|
|
2595
|
-
const apiRoot =
|
|
2596
|
-
const uiRoot =
|
|
2597
|
-
const dbRoot =
|
|
2598
|
-
const srcRoot = resolvePath(
|
|
2599
|
-
const testsRoot = resolvePath(
|
|
2943
|
+
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2944
|
+
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2945
|
+
const apiRoot = path17.join(contractsRoot, "api");
|
|
2946
|
+
const uiRoot = path17.join(contractsRoot, "ui");
|
|
2947
|
+
const dbRoot = path17.join(contractsRoot, "db");
|
|
2948
|
+
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2949
|
+
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2600
2950
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
2601
2951
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2602
2952
|
const {
|
|
@@ -2604,15 +2954,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2604
2954
|
ui: uiFiles,
|
|
2605
2955
|
db: dbFiles
|
|
2606
2956
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2607
|
-
const contractIndex = await buildContractIndex(
|
|
2957
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2608
2958
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2609
2959
|
const specContractRefs = await collectSpecContractRefs(
|
|
2610
2960
|
specFiles,
|
|
2611
2961
|
contractIdList
|
|
2612
2962
|
);
|
|
2613
2963
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2614
|
-
for (const
|
|
2615
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2964
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2965
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2616
2966
|
}
|
|
2617
2967
|
const referencedContractCount = contractIdList.filter(
|
|
2618
2968
|
(id) => referencedContracts.has(id)
|
|
@@ -2621,8 +2971,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2621
2971
|
(id) => !referencedContracts.has(id)
|
|
2622
2972
|
).length;
|
|
2623
2973
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2624
|
-
const
|
|
2625
|
-
specContractRefs.
|
|
2974
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2975
|
+
specContractRefs.specToContracts
|
|
2626
2976
|
);
|
|
2627
2977
|
const idsByPrefix = await collectIds([
|
|
2628
2978
|
...specFiles,
|
|
@@ -2640,24 +2990,28 @@ async function createReportData(root, validation, configResult) {
|
|
|
2640
2990
|
srcRoot,
|
|
2641
2991
|
testsRoot
|
|
2642
2992
|
);
|
|
2643
|
-
const
|
|
2644
|
-
const
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2993
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2994
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2995
|
+
resolvedRoot,
|
|
2996
|
+
resolvedValidationRaw
|
|
2648
2997
|
);
|
|
2649
|
-
const scCoverage =
|
|
2650
|
-
const testFiles =
|
|
2998
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2999
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2651
3000
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2652
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2653
|
-
|
|
3001
|
+
const scSourceRecord = mapToSortedRecord(
|
|
3002
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
3003
|
+
);
|
|
2654
3004
|
const version = await resolveToolVersion();
|
|
3005
|
+
const reportFormatVersion = 1;
|
|
3006
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
3007
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2655
3008
|
return {
|
|
2656
3009
|
tool: "qfai",
|
|
2657
3010
|
version,
|
|
3011
|
+
reportFormatVersion,
|
|
2658
3012
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2659
|
-
root,
|
|
2660
|
-
configPath,
|
|
3013
|
+
root: displayRoot,
|
|
3014
|
+
configPath: displayConfigPath,
|
|
2661
3015
|
summary: {
|
|
2662
3016
|
specs: specFiles.length,
|
|
2663
3017
|
scenarios: scenarioFiles.length,
|
|
@@ -2666,7 +3020,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2666
3020
|
ui: uiFiles.length,
|
|
2667
3021
|
db: dbFiles.length
|
|
2668
3022
|
},
|
|
2669
|
-
counts:
|
|
3023
|
+
counts: normalizedValidation.counts
|
|
2670
3024
|
},
|
|
2671
3025
|
ids: {
|
|
2672
3026
|
spec: idsByPrefix.SPEC,
|
|
@@ -2691,21 +3045,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2691
3045
|
specs: {
|
|
2692
3046
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2693
3047
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2694
|
-
|
|
3048
|
+
specToContracts: specToContractsRecord
|
|
2695
3049
|
}
|
|
2696
3050
|
},
|
|
2697
|
-
issues:
|
|
3051
|
+
issues: normalizedValidation.issues
|
|
2698
3052
|
};
|
|
2699
3053
|
}
|
|
2700
3054
|
function formatReportMarkdown(data) {
|
|
2701
3055
|
const lines = [];
|
|
2702
3056
|
lines.push("# QFAI Report");
|
|
3057
|
+
lines.push("");
|
|
2703
3058
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2704
3059
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2705
3060
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2706
3061
|
lines.push(`- \u7248: ${data.version}`);
|
|
2707
3062
|
lines.push("");
|
|
2708
3063
|
lines.push("## \u6982\u8981");
|
|
3064
|
+
lines.push("");
|
|
2709
3065
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2710
3066
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2711
3067
|
lines.push(
|
|
@@ -2716,6 +3072,7 @@ function formatReportMarkdown(data) {
|
|
|
2716
3072
|
);
|
|
2717
3073
|
lines.push("");
|
|
2718
3074
|
lines.push("## ID\u96C6\u8A08");
|
|
3075
|
+
lines.push("");
|
|
2719
3076
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2720
3077
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2721
3078
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2724,12 +3081,14 @@ function formatReportMarkdown(data) {
|
|
|
2724
3081
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2725
3082
|
lines.push("");
|
|
2726
3083
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
3084
|
+
lines.push("");
|
|
2727
3085
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2728
3086
|
lines.push(
|
|
2729
3087
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2730
3088
|
);
|
|
2731
3089
|
lines.push("");
|
|
2732
3090
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
3091
|
+
lines.push("");
|
|
2733
3092
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2734
3093
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2735
3094
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2738,6 +3097,7 @@ function formatReportMarkdown(data) {
|
|
|
2738
3097
|
);
|
|
2739
3098
|
lines.push("");
|
|
2740
3099
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
3100
|
+
lines.push("");
|
|
2741
3101
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2742
3102
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2743
3103
|
(a, b) => a.localeCompare(b)
|
|
@@ -2756,24 +3116,25 @@ function formatReportMarkdown(data) {
|
|
|
2756
3116
|
}
|
|
2757
3117
|
lines.push("");
|
|
2758
3118
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2759
|
-
|
|
3119
|
+
lines.push("");
|
|
3120
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2760
3121
|
const specIds = Object.keys(specToContracts).sort(
|
|
2761
3122
|
(a, b) => a.localeCompare(b)
|
|
2762
3123
|
);
|
|
2763
3124
|
if (specIds.length === 0) {
|
|
2764
3125
|
lines.push("- (none)");
|
|
2765
3126
|
} else {
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
}
|
|
3127
|
+
const rows = specIds.map((specId) => {
|
|
3128
|
+
const entry = specToContracts[specId];
|
|
3129
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
3130
|
+
const status = entry?.status ?? "missing";
|
|
3131
|
+
return [specId, status, contracts];
|
|
3132
|
+
});
|
|
3133
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2774
3134
|
}
|
|
2775
3135
|
lines.push("");
|
|
2776
3136
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
3137
|
+
lines.push("");
|
|
2777
3138
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2778
3139
|
if (missingRefSpecs.length === 0) {
|
|
2779
3140
|
lines.push("- (none)");
|
|
@@ -2784,6 +3145,7 @@ function formatReportMarkdown(data) {
|
|
|
2784
3145
|
}
|
|
2785
3146
|
lines.push("");
|
|
2786
3147
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
3148
|
+
lines.push("");
|
|
2787
3149
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2788
3150
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2789
3151
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2813,6 +3175,7 @@ function formatReportMarkdown(data) {
|
|
|
2813
3175
|
}
|
|
2814
3176
|
lines.push("");
|
|
2815
3177
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
3178
|
+
lines.push("");
|
|
2816
3179
|
const scRefs = data.traceability.sc.refs;
|
|
2817
3180
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2818
3181
|
if (scIds.length === 0) {
|
|
@@ -2829,6 +3192,7 @@ function formatReportMarkdown(data) {
|
|
|
2829
3192
|
}
|
|
2830
3193
|
lines.push("");
|
|
2831
3194
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
3195
|
+
lines.push("");
|
|
2832
3196
|
const specScIssues = data.issues.filter(
|
|
2833
3197
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2834
3198
|
);
|
|
@@ -2843,6 +3207,7 @@ function formatReportMarkdown(data) {
|
|
|
2843
3207
|
}
|
|
2844
3208
|
lines.push("");
|
|
2845
3209
|
lines.push("## Hotspots");
|
|
3210
|
+
lines.push("");
|
|
2846
3211
|
const hotspots = buildHotspots(data.issues);
|
|
2847
3212
|
if (hotspots.length === 0) {
|
|
2848
3213
|
lines.push("- (none)");
|
|
@@ -2855,6 +3220,7 @@ function formatReportMarkdown(data) {
|
|
|
2855
3220
|
}
|
|
2856
3221
|
lines.push("");
|
|
2857
3222
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
3223
|
+
lines.push("");
|
|
2858
3224
|
const traceIssues = data.issues.filter(
|
|
2859
3225
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2860
3226
|
);
|
|
@@ -2870,6 +3236,7 @@ function formatReportMarkdown(data) {
|
|
|
2870
3236
|
}
|
|
2871
3237
|
lines.push("");
|
|
2872
3238
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
3239
|
+
lines.push("");
|
|
2873
3240
|
if (data.issues.length === 0) {
|
|
2874
3241
|
lines.push("- (none)");
|
|
2875
3242
|
} else {
|
|
@@ -2887,7 +3254,7 @@ function formatReportJson(data) {
|
|
|
2887
3254
|
return JSON.stringify(data, null, 2);
|
|
2888
3255
|
}
|
|
2889
3256
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2890
|
-
const
|
|
3257
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2891
3258
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2892
3259
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2893
3260
|
for (const contractId of contractIdList) {
|
|
@@ -2896,24 +3263,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2896
3263
|
for (const file of specFiles) {
|
|
2897
3264
|
const text = await readFile11(file, "utf-8");
|
|
2898
3265
|
const parsed = parseSpec(text, file);
|
|
2899
|
-
const specKey = parsed.specId
|
|
3266
|
+
const specKey = parsed.specId;
|
|
3267
|
+
if (!specKey) {
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
2900
3270
|
const refs = parsed.contractRefs;
|
|
2901
3271
|
if (refs.lines.length === 0) {
|
|
2902
3272
|
missingRefSpecs.add(specKey);
|
|
3273
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2903
3274
|
continue;
|
|
2904
3275
|
}
|
|
2905
|
-
const
|
|
3276
|
+
const current = specToContracts.get(specKey) ?? {
|
|
3277
|
+
status: "declared",
|
|
3278
|
+
ids: /* @__PURE__ */ new Set()
|
|
3279
|
+
};
|
|
2906
3280
|
for (const id of refs.ids) {
|
|
2907
|
-
|
|
3281
|
+
current.ids.add(id);
|
|
2908
3282
|
const specs = idToSpecs.get(id);
|
|
2909
3283
|
if (specs) {
|
|
2910
3284
|
specs.add(specKey);
|
|
2911
3285
|
}
|
|
2912
3286
|
}
|
|
2913
|
-
|
|
3287
|
+
specToContracts.set(specKey, current);
|
|
2914
3288
|
}
|
|
2915
3289
|
return {
|
|
2916
|
-
|
|
3290
|
+
specToContracts,
|
|
2917
3291
|
idToSpecs,
|
|
2918
3292
|
missingRefSpecs
|
|
2919
3293
|
};
|
|
@@ -2990,6 +3364,20 @@ function formatList(values) {
|
|
|
2990
3364
|
}
|
|
2991
3365
|
return values.join(", ");
|
|
2992
3366
|
}
|
|
3367
|
+
function formatMarkdownTable(headers, rows) {
|
|
3368
|
+
const widths = headers.map((header, index) => {
|
|
3369
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
3370
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
3371
|
+
});
|
|
3372
|
+
const formatRow = (cells) => {
|
|
3373
|
+
const padded = cells.map(
|
|
3374
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
3375
|
+
);
|
|
3376
|
+
return `| ${padded.join(" | ")} |`;
|
|
3377
|
+
};
|
|
3378
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
3379
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3380
|
+
}
|
|
2993
3381
|
function toSortedArray2(values) {
|
|
2994
3382
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2995
3383
|
}
|
|
@@ -3000,6 +3388,27 @@ function mapToSortedRecord(values) {
|
|
|
3000
3388
|
}
|
|
3001
3389
|
return record2;
|
|
3002
3390
|
}
|
|
3391
|
+
function mapToSpecContractRecord(values) {
|
|
3392
|
+
const record2 = {};
|
|
3393
|
+
for (const [key, entry] of values.entries()) {
|
|
3394
|
+
record2[key] = {
|
|
3395
|
+
status: entry.status,
|
|
3396
|
+
ids: toSortedArray2(entry.ids)
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
return record2;
|
|
3400
|
+
}
|
|
3401
|
+
function normalizeScSources(root, sources) {
|
|
3402
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3403
|
+
for (const [id, files] of sources.entries()) {
|
|
3404
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3405
|
+
for (const file of files) {
|
|
3406
|
+
mapped.add(toRelativePath(root, file));
|
|
3407
|
+
}
|
|
3408
|
+
normalized.set(id, mapped);
|
|
3409
|
+
}
|
|
3410
|
+
return normalized;
|
|
3411
|
+
}
|
|
3003
3412
|
function buildHotspots(issues) {
|
|
3004
3413
|
const map = /* @__PURE__ */ new Map();
|
|
3005
3414
|
for (const issue7 of issues) {
|
|
@@ -3024,39 +3433,54 @@ function buildHotspots(issues) {
|
|
|
3024
3433
|
|
|
3025
3434
|
// src/cli/commands/report.ts
|
|
3026
3435
|
async function runReport(options) {
|
|
3027
|
-
const root =
|
|
3436
|
+
const root = path18.resolve(options.root);
|
|
3028
3437
|
const configResult = await loadConfig(root);
|
|
3029
|
-
const input = configResult.config.output.validateJsonPath;
|
|
3030
|
-
const inputPath = path15.isAbsolute(input) ? input : path15.resolve(root, input);
|
|
3031
3438
|
let validation;
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3439
|
+
if (options.runValidate) {
|
|
3440
|
+
if (options.inputPath) {
|
|
3441
|
+
warn("report: --run-validate \u304C\u6307\u5B9A\u3055\u308C\u305F\u305F\u3081 --in \u306F\u7121\u8996\u3057\u307E\u3059\u3002");
|
|
3442
|
+
}
|
|
3443
|
+
const result = await validateProject(root, configResult);
|
|
3444
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3445
|
+
await writeValidationResult(
|
|
3446
|
+
root,
|
|
3447
|
+
configResult.config.output.validateJsonPath,
|
|
3448
|
+
normalized
|
|
3449
|
+
);
|
|
3450
|
+
validation = normalized;
|
|
3451
|
+
} else {
|
|
3452
|
+
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3453
|
+
const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
|
|
3454
|
+
try {
|
|
3455
|
+
validation = await readValidationResult(inputPath);
|
|
3456
|
+
} catch (err) {
|
|
3457
|
+
if (isMissingFileError5(err)) {
|
|
3458
|
+
error(
|
|
3459
|
+
[
|
|
3460
|
+
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
3461
|
+
"",
|
|
3462
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
3463
|
+
" qfai validate",
|
|
3464
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
3465
|
+
"",
|
|
3466
|
+
"\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3467
|
+
"GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3468
|
+
].join("\n")
|
|
3469
|
+
);
|
|
3470
|
+
process.exitCode = 2;
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
throw err;
|
|
3049
3474
|
}
|
|
3050
|
-
throw err;
|
|
3051
3475
|
}
|
|
3052
3476
|
const data = await createReportData(root, validation, configResult);
|
|
3053
3477
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3054
3478
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3055
|
-
const defaultOut = options.format === "json" ?
|
|
3479
|
+
const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
|
|
3056
3480
|
const out = options.outPath ?? defaultOut;
|
|
3057
|
-
const outPath =
|
|
3058
|
-
await
|
|
3059
|
-
await
|
|
3481
|
+
const outPath = path18.isAbsolute(out) ? out : path18.resolve(root, out);
|
|
3482
|
+
await mkdir3(path18.dirname(outPath), { recursive: true });
|
|
3483
|
+
await writeFile2(outPath, `${output}
|
|
3060
3484
|
`, "utf-8");
|
|
3061
3485
|
info(
|
|
3062
3486
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -3119,10 +3543,16 @@ function isMissingFileError5(error2) {
|
|
|
3119
3543
|
const record2 = error2;
|
|
3120
3544
|
return record2.code === "ENOENT";
|
|
3121
3545
|
}
|
|
3546
|
+
async function writeValidationResult(root, outputPath, result) {
|
|
3547
|
+
const abs = path18.isAbsolute(outputPath) ? outputPath : path18.resolve(root, outputPath);
|
|
3548
|
+
await mkdir3(path18.dirname(abs), { recursive: true });
|
|
3549
|
+
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
3550
|
+
`, "utf-8");
|
|
3551
|
+
}
|
|
3122
3552
|
|
|
3123
3553
|
// src/cli/commands/validate.ts
|
|
3124
|
-
import { mkdir as
|
|
3125
|
-
import
|
|
3554
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
3555
|
+
import path19 from "path";
|
|
3126
3556
|
|
|
3127
3557
|
// src/cli/lib/failOn.ts
|
|
3128
3558
|
function shouldFail(result, failOn) {
|
|
@@ -3137,19 +3567,24 @@ function shouldFail(result, failOn) {
|
|
|
3137
3567
|
|
|
3138
3568
|
// src/cli/commands/validate.ts
|
|
3139
3569
|
async function runValidate(options) {
|
|
3140
|
-
const root =
|
|
3570
|
+
const root = path19.resolve(options.root);
|
|
3141
3571
|
const configResult = await loadConfig(root);
|
|
3142
3572
|
const result = await validateProject(root, configResult);
|
|
3573
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3143
3574
|
const format = options.format ?? "text";
|
|
3144
3575
|
if (format === "text") {
|
|
3145
|
-
emitText(
|
|
3576
|
+
emitText(normalized);
|
|
3146
3577
|
}
|
|
3147
3578
|
if (format === "github") {
|
|
3148
|
-
|
|
3579
|
+
const jsonPath = resolveJsonPath(
|
|
3580
|
+
root,
|
|
3581
|
+
configResult.config.output.validateJsonPath
|
|
3582
|
+
);
|
|
3583
|
+
emitGitHubOutput(normalized, root, jsonPath);
|
|
3149
3584
|
}
|
|
3150
|
-
await emitJson(
|
|
3585
|
+
await emitJson(normalized, root, configResult.config.output.validateJsonPath);
|
|
3151
3586
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
3152
|
-
return shouldFail(
|
|
3587
|
+
return shouldFail(normalized, failOn) ? 1 : 0;
|
|
3153
3588
|
}
|
|
3154
3589
|
function resolveFailOn(options, fallback) {
|
|
3155
3590
|
if (options.failOn) {
|
|
@@ -3174,6 +3609,22 @@ function emitText(result) {
|
|
|
3174
3609
|
`
|
|
3175
3610
|
);
|
|
3176
3611
|
}
|
|
3612
|
+
function emitGitHubOutput(result, root, jsonPath) {
|
|
3613
|
+
const deduped = dedupeIssues(result.issues);
|
|
3614
|
+
const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
|
|
3615
|
+
const dropped = Math.max(result.issues.length - deduped.length, 0);
|
|
3616
|
+
emitGitHubSummary(result, {
|
|
3617
|
+
total: deduped.length,
|
|
3618
|
+
omitted,
|
|
3619
|
+
dropped,
|
|
3620
|
+
jsonPath,
|
|
3621
|
+
root
|
|
3622
|
+
});
|
|
3623
|
+
const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
|
|
3624
|
+
for (const issue7 of issues) {
|
|
3625
|
+
emitGitHub(issue7);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3177
3628
|
function emitGitHub(issue7) {
|
|
3178
3629
|
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
3179
3630
|
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
@@ -3185,22 +3636,75 @@ function emitGitHub(issue7) {
|
|
|
3185
3636
|
`
|
|
3186
3637
|
);
|
|
3187
3638
|
}
|
|
3639
|
+
function emitGitHubSummary(result, options) {
|
|
3640
|
+
const summary = [
|
|
3641
|
+
"qfai validate summary:",
|
|
3642
|
+
`error=${result.counts.error}`,
|
|
3643
|
+
`warning=${result.counts.warning}`,
|
|
3644
|
+
`info=${result.counts.info}`,
|
|
3645
|
+
`annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`
|
|
3646
|
+
].join(" ");
|
|
3647
|
+
process.stdout.write(`${summary}
|
|
3648
|
+
`);
|
|
3649
|
+
if (options.dropped > 0 || options.omitted > 0) {
|
|
3650
|
+
const details = [
|
|
3651
|
+
"qfai validate note:",
|
|
3652
|
+
options.dropped > 0 ? `\u91CD\u8907\u9664\u5916=${options.dropped}` : null,
|
|
3653
|
+
options.omitted > 0 ? `\u4E0A\u9650\u7701\u7565=${options.omitted}` : null
|
|
3654
|
+
].filter(Boolean).join(" ");
|
|
3655
|
+
process.stdout.write(`${details}
|
|
3656
|
+
`);
|
|
3657
|
+
}
|
|
3658
|
+
const relative = toRelativePath(options.root, options.jsonPath);
|
|
3659
|
+
process.stdout.write(
|
|
3660
|
+
`qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
3661
|
+
`
|
|
3662
|
+
);
|
|
3663
|
+
}
|
|
3664
|
+
function dedupeIssues(issues) {
|
|
3665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3666
|
+
const deduped = [];
|
|
3667
|
+
for (const issue7 of issues) {
|
|
3668
|
+
const key = issueKey(issue7);
|
|
3669
|
+
if (seen.has(key)) {
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
seen.add(key);
|
|
3673
|
+
deduped.push(issue7);
|
|
3674
|
+
}
|
|
3675
|
+
return deduped;
|
|
3676
|
+
}
|
|
3677
|
+
function issueKey(issue7) {
|
|
3678
|
+
const file = issue7.file ?? "";
|
|
3679
|
+
const line = issue7.loc?.line ?? "";
|
|
3680
|
+
const column = issue7.loc?.column ?? "";
|
|
3681
|
+
return [issue7.code, issue7.severity, issue7.message, file, line, column].join(
|
|
3682
|
+
"|"
|
|
3683
|
+
);
|
|
3684
|
+
}
|
|
3188
3685
|
async function emitJson(result, root, jsonPath) {
|
|
3189
|
-
const abs =
|
|
3190
|
-
await
|
|
3191
|
-
await
|
|
3686
|
+
const abs = resolveJsonPath(root, jsonPath);
|
|
3687
|
+
await mkdir4(path19.dirname(abs), { recursive: true });
|
|
3688
|
+
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
3192
3689
|
`, "utf-8");
|
|
3193
3690
|
}
|
|
3691
|
+
function resolveJsonPath(root, jsonPath) {
|
|
3692
|
+
return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
|
|
3693
|
+
}
|
|
3694
|
+
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3194
3695
|
|
|
3195
3696
|
// src/cli/lib/args.ts
|
|
3196
3697
|
function parseArgs(argv, cwd) {
|
|
3197
3698
|
const options = {
|
|
3198
3699
|
root: cwd,
|
|
3700
|
+
rootExplicit: false,
|
|
3199
3701
|
dir: cwd,
|
|
3200
3702
|
force: false,
|
|
3201
3703
|
yes: false,
|
|
3202
3704
|
dryRun: false,
|
|
3203
3705
|
reportFormat: "md",
|
|
3706
|
+
reportRunValidate: false,
|
|
3707
|
+
doctorFormat: "text",
|
|
3204
3708
|
validateFormat: "text",
|
|
3205
3709
|
strict: false,
|
|
3206
3710
|
help: false
|
|
@@ -3216,6 +3720,7 @@ function parseArgs(argv, cwd) {
|
|
|
3216
3720
|
switch (arg) {
|
|
3217
3721
|
case "--root":
|
|
3218
3722
|
options.root = args[i + 1] ?? options.root;
|
|
3723
|
+
options.rootExplicit = true;
|
|
3219
3724
|
i += 1;
|
|
3220
3725
|
break;
|
|
3221
3726
|
case "--dir":
|
|
@@ -3252,11 +3757,27 @@ function parseArgs(argv, cwd) {
|
|
|
3252
3757
|
{
|
|
3253
3758
|
const next = args[i + 1];
|
|
3254
3759
|
if (next) {
|
|
3255
|
-
|
|
3760
|
+
if (command === "doctor") {
|
|
3761
|
+
options.doctorOut = next;
|
|
3762
|
+
} else {
|
|
3763
|
+
options.reportOut = next;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
i += 1;
|
|
3768
|
+
break;
|
|
3769
|
+
case "--in":
|
|
3770
|
+
{
|
|
3771
|
+
const next = args[i + 1];
|
|
3772
|
+
if (next) {
|
|
3773
|
+
options.reportIn = next;
|
|
3256
3774
|
}
|
|
3257
3775
|
}
|
|
3258
3776
|
i += 1;
|
|
3259
3777
|
break;
|
|
3778
|
+
case "--run-validate":
|
|
3779
|
+
options.reportRunValidate = true;
|
|
3780
|
+
break;
|
|
3260
3781
|
case "--help":
|
|
3261
3782
|
case "-h":
|
|
3262
3783
|
options.help = true;
|
|
@@ -3283,6 +3804,12 @@ function applyFormatOption(command, value, options) {
|
|
|
3283
3804
|
}
|
|
3284
3805
|
return;
|
|
3285
3806
|
}
|
|
3807
|
+
if (command === "doctor") {
|
|
3808
|
+
if (value === "text" || value === "json") {
|
|
3809
|
+
options.doctorFormat = value;
|
|
3810
|
+
}
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3286
3813
|
if (value === "md" || value === "json") {
|
|
3287
3814
|
options.reportFormat = value;
|
|
3288
3815
|
}
|
|
@@ -3308,18 +3835,34 @@ async function run(argv, cwd) {
|
|
|
3308
3835
|
});
|
|
3309
3836
|
return;
|
|
3310
3837
|
case "validate":
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3838
|
+
{
|
|
3839
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3840
|
+
process.exitCode = await runValidate({
|
|
3841
|
+
root: resolvedRoot,
|
|
3842
|
+
strict: options.strict,
|
|
3843
|
+
format: options.validateFormat,
|
|
3844
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3317
3847
|
return;
|
|
3318
3848
|
case "report":
|
|
3319
|
-
|
|
3849
|
+
{
|
|
3850
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3851
|
+
await runReport({
|
|
3852
|
+
root: resolvedRoot,
|
|
3853
|
+
format: options.reportFormat,
|
|
3854
|
+
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
|
|
3855
|
+
...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
|
|
3856
|
+
...options.reportRunValidate ? { runValidate: true } : {}
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
return;
|
|
3860
|
+
case "doctor":
|
|
3861
|
+
await runDoctor({
|
|
3320
3862
|
root: options.root,
|
|
3321
|
-
|
|
3322
|
-
|
|
3863
|
+
rootExplicit: options.rootExplicit,
|
|
3864
|
+
format: options.doctorFormat,
|
|
3865
|
+
...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
|
|
3323
3866
|
});
|
|
3324
3867
|
return;
|
|
3325
3868
|
default:
|
|
@@ -3335,6 +3878,7 @@ Commands:
|
|
|
3335
3878
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
3336
3879
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
3337
3880
|
report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
|
|
3881
|
+
doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
|
|
3338
3882
|
|
|
3339
3883
|
Options:
|
|
3340
3884
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -3344,12 +3888,27 @@ Options:
|
|
|
3344
3888
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
3345
3889
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3346
3890
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3891
|
+
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3347
3892
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3348
3893
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3349
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
3894
|
+
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
3895
|
+
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3896
|
+
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3350
3897
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
3351
3898
|
`;
|
|
3352
3899
|
}
|
|
3900
|
+
async function resolveRoot(options) {
|
|
3901
|
+
if (options.rootExplicit) {
|
|
3902
|
+
return options.root;
|
|
3903
|
+
}
|
|
3904
|
+
const search = await findConfigRoot(options.root);
|
|
3905
|
+
if (!search.found) {
|
|
3906
|
+
warn(
|
|
3907
|
+
`qfai: qfai.config.yaml \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081 defaultConfig \u3092\u4F7F\u7528\u3057\u307E\u3059 (root=${search.root})`
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3910
|
+
return search.root;
|
|
3911
|
+
}
|
|
3353
3912
|
|
|
3354
3913
|
// src/cli/index.ts
|
|
3355
3914
|
run(process.argv.slice(2), process.cwd()).catch((err) => {
|