qfai 0.5.2 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cli/index.cjs +987 -649
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +980 -642
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,169 +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 warn(message) {
|
|
120
|
-
process.stdout.write(`${message}
|
|
121
|
-
`);
|
|
122
|
-
}
|
|
123
|
-
function error(message) {
|
|
124
|
-
process.stderr.write(`${message}
|
|
125
|
-
`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// src/cli/commands/init.ts
|
|
129
|
-
async function runInit(options) {
|
|
130
|
-
const assetsRoot = getInitAssetsDir();
|
|
131
|
-
const rootAssets = path3.join(assetsRoot, "root");
|
|
132
|
-
const qfaiAssets = path3.join(assetsRoot, ".qfai");
|
|
133
|
-
const destRoot = path3.resolve(options.dir);
|
|
134
|
-
const destQfai = path3.join(destRoot, ".qfai");
|
|
135
|
-
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
136
|
-
force: options.force,
|
|
137
|
-
dryRun: options.dryRun
|
|
138
|
-
});
|
|
139
|
-
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
140
|
-
force: options.force,
|
|
141
|
-
dryRun: options.dryRun
|
|
142
|
-
});
|
|
143
|
-
report(
|
|
144
|
-
[...rootResult.copied, ...qfaiResult.copied],
|
|
145
|
-
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
146
|
-
options.dryRun,
|
|
147
|
-
"init"
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
function report(copied, skipped, dryRun, label) {
|
|
151
|
-
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
152
|
-
if (copied.length > 0) {
|
|
153
|
-
info(` created: ${copied.length}`);
|
|
154
|
-
}
|
|
155
|
-
if (skipped.length > 0) {
|
|
156
|
-
info(` skipped: ${skipped.length}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
3
|
+
// src/cli/commands/doctor.ts
|
|
4
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
+
import path8 from "path";
|
|
159
6
|
|
|
160
|
-
// src/
|
|
161
|
-
import {
|
|
162
|
-
import
|
|
7
|
+
// src/core/doctor.ts
|
|
8
|
+
import { access as access4 } from "fs/promises";
|
|
9
|
+
import path7 from "path";
|
|
163
10
|
|
|
164
11
|
// src/core/config.ts
|
|
165
|
-
import { access
|
|
166
|
-
import
|
|
12
|
+
import { access, readFile } from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
167
14
|
import { parse as parseYaml } from "yaml";
|
|
168
15
|
var defaultConfig = {
|
|
169
16
|
paths: {
|
|
@@ -203,17 +50,17 @@ var defaultConfig = {
|
|
|
203
50
|
}
|
|
204
51
|
};
|
|
205
52
|
function getConfigPath(root) {
|
|
206
|
-
return
|
|
53
|
+
return path.join(root, "qfai.config.yaml");
|
|
207
54
|
}
|
|
208
55
|
async function findConfigRoot(startDir) {
|
|
209
|
-
const resolvedStart =
|
|
56
|
+
const resolvedStart = path.resolve(startDir);
|
|
210
57
|
let current = resolvedStart;
|
|
211
58
|
while (true) {
|
|
212
59
|
const configPath = getConfigPath(current);
|
|
213
|
-
if (await
|
|
60
|
+
if (await exists(configPath)) {
|
|
214
61
|
return { root: current, configPath, found: true };
|
|
215
62
|
}
|
|
216
|
-
const parent =
|
|
63
|
+
const parent = path.dirname(current);
|
|
217
64
|
if (parent === current) {
|
|
218
65
|
break;
|
|
219
66
|
}
|
|
@@ -243,7 +90,7 @@ async function loadConfig(root) {
|
|
|
243
90
|
return { config: normalized, issues, configPath };
|
|
244
91
|
}
|
|
245
92
|
function resolvePath(root, config, key) {
|
|
246
|
-
return
|
|
93
|
+
return path.resolve(root, config.paths[key]);
|
|
247
94
|
}
|
|
248
95
|
function normalizeConfig(raw, configPath, issues) {
|
|
249
96
|
if (!isRecord(raw)) {
|
|
@@ -542,9 +389,9 @@ function isMissingFile(error2) {
|
|
|
542
389
|
}
|
|
543
390
|
return false;
|
|
544
391
|
}
|
|
545
|
-
async function
|
|
392
|
+
async function exists(target) {
|
|
546
393
|
try {
|
|
547
|
-
await
|
|
394
|
+
await access(target);
|
|
548
395
|
return true;
|
|
549
396
|
} catch {
|
|
550
397
|
return false;
|
|
@@ -560,76 +407,12 @@ function isRecord(value) {
|
|
|
560
407
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
561
408
|
}
|
|
562
409
|
|
|
563
|
-
// src/core/paths.ts
|
|
564
|
-
import path5 from "path";
|
|
565
|
-
function toRelativePath(root, target) {
|
|
566
|
-
if (!target) {
|
|
567
|
-
return target;
|
|
568
|
-
}
|
|
569
|
-
if (!path5.isAbsolute(target)) {
|
|
570
|
-
return toPosixPath(target);
|
|
571
|
-
}
|
|
572
|
-
const relative = path5.relative(root, target);
|
|
573
|
-
if (!relative) {
|
|
574
|
-
return ".";
|
|
575
|
-
}
|
|
576
|
-
return toPosixPath(relative);
|
|
577
|
-
}
|
|
578
|
-
function toPosixPath(value) {
|
|
579
|
-
return value.replace(/\\/g, "/");
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// src/core/normalize.ts
|
|
583
|
-
function normalizeIssuePaths(root, issues) {
|
|
584
|
-
return issues.map((issue7) => {
|
|
585
|
-
if (!issue7.file) {
|
|
586
|
-
return issue7;
|
|
587
|
-
}
|
|
588
|
-
const normalized = toRelativePath(root, issue7.file);
|
|
589
|
-
if (normalized === issue7.file) {
|
|
590
|
-
return issue7;
|
|
591
|
-
}
|
|
592
|
-
return {
|
|
593
|
-
...issue7,
|
|
594
|
-
file: normalized
|
|
595
|
-
};
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
function normalizeScCoverage(root, sc) {
|
|
599
|
-
const refs = {};
|
|
600
|
-
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
601
|
-
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
602
|
-
}
|
|
603
|
-
return {
|
|
604
|
-
...sc,
|
|
605
|
-
refs
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
function normalizeValidationResult(root, result) {
|
|
609
|
-
return {
|
|
610
|
-
...result,
|
|
611
|
-
issues: normalizeIssuePaths(root, result.issues),
|
|
612
|
-
traceability: {
|
|
613
|
-
...result.traceability,
|
|
614
|
-
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
615
|
-
}
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// src/core/report.ts
|
|
620
|
-
import { readFile as readFile11 } from "fs/promises";
|
|
621
|
-
import path15 from "path";
|
|
622
|
-
|
|
623
|
-
// src/core/contractIndex.ts
|
|
624
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
625
|
-
import path8 from "path";
|
|
626
|
-
|
|
627
410
|
// src/core/discovery.ts
|
|
628
|
-
import { access as
|
|
411
|
+
import { access as access3 } from "fs/promises";
|
|
629
412
|
|
|
630
413
|
// src/core/fs.ts
|
|
631
|
-
import { access as
|
|
632
|
-
import
|
|
414
|
+
import { access as access2, readdir } from "fs/promises";
|
|
415
|
+
import path2 from "path";
|
|
633
416
|
import fg from "fast-glob";
|
|
634
417
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
635
418
|
"node_modules",
|
|
@@ -641,7 +424,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
641
424
|
]);
|
|
642
425
|
async function collectFiles(root, options = {}) {
|
|
643
426
|
const entries = [];
|
|
644
|
-
if (!await
|
|
427
|
+
if (!await exists2(root)) {
|
|
645
428
|
return entries;
|
|
646
429
|
}
|
|
647
430
|
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
@@ -665,9 +448,9 @@ async function collectFilesByGlobs(root, options) {
|
|
|
665
448
|
});
|
|
666
449
|
}
|
|
667
450
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
668
|
-
const items = await
|
|
451
|
+
const items = await readdir(current, { withFileTypes: true });
|
|
669
452
|
for (const item of items) {
|
|
670
|
-
const fullPath =
|
|
453
|
+
const fullPath = path2.join(current, item.name);
|
|
671
454
|
if (item.isDirectory()) {
|
|
672
455
|
if (ignoreDirs.has(item.name)) {
|
|
673
456
|
continue;
|
|
@@ -677,7 +460,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
677
460
|
}
|
|
678
461
|
if (item.isFile()) {
|
|
679
462
|
if (extensions.length > 0) {
|
|
680
|
-
const ext =
|
|
463
|
+
const ext = path2.extname(item.name).toLowerCase();
|
|
681
464
|
if (!extensions.includes(ext)) {
|
|
682
465
|
continue;
|
|
683
466
|
}
|
|
@@ -686,9 +469,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
686
469
|
}
|
|
687
470
|
}
|
|
688
471
|
}
|
|
689
|
-
async function
|
|
472
|
+
async function exists2(target) {
|
|
690
473
|
try {
|
|
691
|
-
await
|
|
474
|
+
await access2(target);
|
|
692
475
|
return true;
|
|
693
476
|
} catch {
|
|
694
477
|
return false;
|
|
@@ -696,23 +479,23 @@ async function exists3(target) {
|
|
|
696
479
|
}
|
|
697
480
|
|
|
698
481
|
// src/core/specLayout.ts
|
|
699
|
-
import { readdir as
|
|
700
|
-
import
|
|
482
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
483
|
+
import path3 from "path";
|
|
701
484
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
702
485
|
async function collectSpecEntries(specsRoot) {
|
|
703
486
|
const dirs = await listSpecDirs(specsRoot);
|
|
704
487
|
const entries = dirs.map((dir) => ({
|
|
705
488
|
dir,
|
|
706
|
-
specPath:
|
|
707
|
-
deltaPath:
|
|
708
|
-
scenarioPath:
|
|
489
|
+
specPath: path3.join(dir, "spec.md"),
|
|
490
|
+
deltaPath: path3.join(dir, "delta.md"),
|
|
491
|
+
scenarioPath: path3.join(dir, "scenario.md")
|
|
709
492
|
}));
|
|
710
493
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
711
494
|
}
|
|
712
495
|
async function listSpecDirs(specsRoot) {
|
|
713
496
|
try {
|
|
714
|
-
const items = await
|
|
715
|
-
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));
|
|
716
499
|
} catch (error2) {
|
|
717
500
|
if (isMissingFileError(error2)) {
|
|
718
501
|
return [];
|
|
@@ -760,298 +543,43 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
|
760
543
|
async function filterExisting(files) {
|
|
761
544
|
const existing = [];
|
|
762
545
|
for (const file of files) {
|
|
763
|
-
if (await
|
|
546
|
+
if (await exists3(file)) {
|
|
764
547
|
existing.push(file);
|
|
765
548
|
}
|
|
766
549
|
}
|
|
767
550
|
return existing;
|
|
768
551
|
}
|
|
769
|
-
async function
|
|
552
|
+
async function exists3(target) {
|
|
770
553
|
try {
|
|
771
|
-
await
|
|
554
|
+
await access3(target);
|
|
772
555
|
return true;
|
|
773
556
|
} catch {
|
|
774
557
|
return false;
|
|
775
558
|
}
|
|
776
559
|
}
|
|
777
560
|
|
|
778
|
-
// src/core/
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
784
|
-
const id = match[1];
|
|
785
|
-
if (id) {
|
|
786
|
-
ids.push(id);
|
|
787
|
-
}
|
|
561
|
+
// src/core/paths.ts
|
|
562
|
+
import path4 from "path";
|
|
563
|
+
function toRelativePath(root, target) {
|
|
564
|
+
if (!target) {
|
|
565
|
+
return target;
|
|
788
566
|
}
|
|
789
|
-
|
|
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);
|
|
790
575
|
}
|
|
791
|
-
function
|
|
792
|
-
return
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// src/core/contractIndex.ts
|
|
796
|
-
async function buildContractIndex(root, config) {
|
|
797
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
798
|
-
const uiRoot = path8.join(contractsRoot, "ui");
|
|
799
|
-
const apiRoot = path8.join(contractsRoot, "api");
|
|
800
|
-
const dbRoot = path8.join(contractsRoot, "db");
|
|
801
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
802
|
-
collectUiContractFiles(uiRoot),
|
|
803
|
-
collectApiContractFiles(apiRoot),
|
|
804
|
-
collectDbContractFiles(dbRoot)
|
|
805
|
-
]);
|
|
806
|
-
const index = {
|
|
807
|
-
ids: /* @__PURE__ */ new Set(),
|
|
808
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
809
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
810
|
-
};
|
|
811
|
-
await indexContractFiles(uiFiles, index);
|
|
812
|
-
await indexContractFiles(apiFiles, index);
|
|
813
|
-
await indexContractFiles(dbFiles, index);
|
|
814
|
-
return index;
|
|
815
|
-
}
|
|
816
|
-
async function indexContractFiles(files, index) {
|
|
817
|
-
for (const file of files) {
|
|
818
|
-
const text = await readFile2(file, "utf-8");
|
|
819
|
-
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
function record(index, id, file) {
|
|
823
|
-
index.ids.add(id);
|
|
824
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
825
|
-
current.add(file);
|
|
826
|
-
index.idToFiles.set(id, current);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// src/core/ids.ts
|
|
830
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
831
|
-
var STRICT_ID_PATTERNS = {
|
|
832
|
-
SPEC: /\bSPEC-\d{4}\b/g,
|
|
833
|
-
BR: /\bBR-\d{4}\b/g,
|
|
834
|
-
SC: /\bSC-\d{4}\b/g,
|
|
835
|
-
UI: /\bUI-\d{4}\b/g,
|
|
836
|
-
API: /\bAPI-\d{4}\b/g,
|
|
837
|
-
DB: /\bDB-\d{4}\b/g,
|
|
838
|
-
ADR: /\bADR-\d{4}\b/g
|
|
839
|
-
};
|
|
840
|
-
var LOOSE_ID_PATTERNS = {
|
|
841
|
-
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
842
|
-
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
843
|
-
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
844
|
-
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
845
|
-
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
846
|
-
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
847
|
-
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
848
|
-
};
|
|
849
|
-
function extractIds(text, prefix) {
|
|
850
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
851
|
-
const matches = text.match(pattern);
|
|
852
|
-
return unique(matches ?? []);
|
|
853
|
-
}
|
|
854
|
-
function extractAllIds(text) {
|
|
855
|
-
const all = [];
|
|
856
|
-
ID_PREFIXES.forEach((prefix) => {
|
|
857
|
-
all.push(...extractIds(text, prefix));
|
|
858
|
-
});
|
|
859
|
-
return unique(all);
|
|
860
|
-
}
|
|
861
|
-
function extractInvalidIds(text, prefixes) {
|
|
862
|
-
const invalid = [];
|
|
863
|
-
for (const prefix of prefixes) {
|
|
864
|
-
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
865
|
-
for (const candidate of candidates) {
|
|
866
|
-
if (!isValidId(candidate, prefix)) {
|
|
867
|
-
invalid.push(candidate);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return unique(invalid);
|
|
872
|
-
}
|
|
873
|
-
function unique(values) {
|
|
874
|
-
return Array.from(new Set(values));
|
|
875
|
-
}
|
|
876
|
-
function isValidId(value, prefix) {
|
|
877
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
878
|
-
const strict = new RegExp(pattern.source);
|
|
879
|
-
return strict.test(value);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// src/core/parse/contractRefs.ts
|
|
883
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
884
|
-
function parseContractRefs(text, options = {}) {
|
|
885
|
-
const linePattern = buildLinePattern(options);
|
|
886
|
-
const lines = [];
|
|
887
|
-
for (const match of text.matchAll(linePattern)) {
|
|
888
|
-
lines.push((match[1] ?? "").trim());
|
|
889
|
-
}
|
|
890
|
-
const ids = [];
|
|
891
|
-
const invalidTokens = [];
|
|
892
|
-
let hasNone = false;
|
|
893
|
-
for (const line of lines) {
|
|
894
|
-
if (line.length === 0) {
|
|
895
|
-
invalidTokens.push("(empty)");
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
899
|
-
for (const token of tokens) {
|
|
900
|
-
if (token.length === 0) {
|
|
901
|
-
invalidTokens.push("(empty)");
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
if (token === "none") {
|
|
905
|
-
hasNone = true;
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
909
|
-
ids.push(token);
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
invalidTokens.push(token);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return {
|
|
916
|
-
lines,
|
|
917
|
-
ids: unique2(ids),
|
|
918
|
-
invalidTokens: unique2(invalidTokens),
|
|
919
|
-
hasNone
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
function buildLinePattern(options) {
|
|
923
|
-
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
924
|
-
return new RegExp(
|
|
925
|
-
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
926
|
-
"gm"
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
function unique2(values) {
|
|
930
|
-
return Array.from(new Set(values));
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// src/core/parse/markdown.ts
|
|
934
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
935
|
-
function parseHeadings(md) {
|
|
936
|
-
const lines = md.split(/\r?\n/);
|
|
937
|
-
const headings = [];
|
|
938
|
-
for (let i = 0; i < lines.length; i++) {
|
|
939
|
-
const line = lines[i] ?? "";
|
|
940
|
-
const match = line.match(HEADING_RE);
|
|
941
|
-
if (!match) continue;
|
|
942
|
-
const levelToken = match[1];
|
|
943
|
-
const title = match[2];
|
|
944
|
-
if (!levelToken || !title) continue;
|
|
945
|
-
headings.push({
|
|
946
|
-
level: levelToken.length,
|
|
947
|
-
title: title.trim(),
|
|
948
|
-
line: i + 1
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
return headings;
|
|
952
|
-
}
|
|
953
|
-
function extractH2Sections(md) {
|
|
954
|
-
const lines = md.split(/\r?\n/);
|
|
955
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
956
|
-
const sections = /* @__PURE__ */ new Map();
|
|
957
|
-
for (let i = 0; i < headings.length; i++) {
|
|
958
|
-
const current = headings[i];
|
|
959
|
-
if (!current) continue;
|
|
960
|
-
const next = headings[i + 1];
|
|
961
|
-
const startLine = current.line + 1;
|
|
962
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
963
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
964
|
-
sections.set(current.title.trim(), {
|
|
965
|
-
title: current.title.trim(),
|
|
966
|
-
startLine,
|
|
967
|
-
endLine,
|
|
968
|
-
body
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
return sections;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// src/core/parse/spec.ts
|
|
975
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
976
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
977
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
978
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
979
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
980
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
981
|
-
function parseSpec(md, file) {
|
|
982
|
-
const headings = parseHeadings(md);
|
|
983
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
984
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
985
|
-
const sections = extractH2Sections(md);
|
|
986
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
987
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
988
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
989
|
-
const startLine = brSection?.startLine ?? 1;
|
|
990
|
-
const brs = [];
|
|
991
|
-
const brsWithoutPriority = [];
|
|
992
|
-
const brsWithInvalidPriority = [];
|
|
993
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
994
|
-
const lineText = brLines[i] ?? "";
|
|
995
|
-
const lineNumber = startLine + i;
|
|
996
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
997
|
-
if (validMatch) {
|
|
998
|
-
const id = validMatch[1];
|
|
999
|
-
const priority = validMatch[2];
|
|
1000
|
-
const text = validMatch[3];
|
|
1001
|
-
if (!id || !priority || !text) continue;
|
|
1002
|
-
brs.push({
|
|
1003
|
-
id,
|
|
1004
|
-
priority,
|
|
1005
|
-
text: text.trim(),
|
|
1006
|
-
line: lineNumber
|
|
1007
|
-
});
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1011
|
-
if (anyPriorityMatch) {
|
|
1012
|
-
const id = anyPriorityMatch[1];
|
|
1013
|
-
const priority = anyPriorityMatch[2];
|
|
1014
|
-
const text = anyPriorityMatch[3];
|
|
1015
|
-
if (!id || !priority || !text) continue;
|
|
1016
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1017
|
-
brsWithInvalidPriority.push({
|
|
1018
|
-
id,
|
|
1019
|
-
priority,
|
|
1020
|
-
text: text.trim(),
|
|
1021
|
-
line: lineNumber
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1027
|
-
if (noPriorityMatch) {
|
|
1028
|
-
const id = noPriorityMatch[1];
|
|
1029
|
-
const text = noPriorityMatch[2];
|
|
1030
|
-
if (!id || !text) continue;
|
|
1031
|
-
brsWithoutPriority.push({
|
|
1032
|
-
id,
|
|
1033
|
-
text: text.trim(),
|
|
1034
|
-
line: lineNumber
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
const parsed = {
|
|
1039
|
-
file,
|
|
1040
|
-
sections: sectionNames,
|
|
1041
|
-
brs,
|
|
1042
|
-
brsWithoutPriority,
|
|
1043
|
-
brsWithInvalidPriority,
|
|
1044
|
-
contractRefs: parseContractRefs(md)
|
|
1045
|
-
};
|
|
1046
|
-
if (specId) {
|
|
1047
|
-
parsed.specId = specId;
|
|
1048
|
-
}
|
|
1049
|
-
return parsed;
|
|
576
|
+
function toPosixPath(value) {
|
|
577
|
+
return value.replace(/\\/g, "/");
|
|
1050
578
|
}
|
|
1051
579
|
|
|
1052
580
|
// src/core/traceability.ts
|
|
1053
|
-
import { readFile as
|
|
1054
|
-
import
|
|
581
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
582
|
+
import path5 from "path";
|
|
1055
583
|
|
|
1056
584
|
// src/core/gherkin/parse.ts
|
|
1057
585
|
import {
|
|
@@ -1111,13 +639,13 @@ function parseScenarioDocument(text, uri) {
|
|
|
1111
639
|
};
|
|
1112
640
|
}
|
|
1113
641
|
function buildScenarioAtoms(document, contractIds = []) {
|
|
1114
|
-
const uniqueContractIds =
|
|
642
|
+
const uniqueContractIds = unique(contractIds).sort(
|
|
1115
643
|
(a, b) => a.localeCompare(b)
|
|
1116
644
|
);
|
|
1117
645
|
return document.scenarios.map((scenario) => {
|
|
1118
646
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1119
647
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1120
|
-
const brIds =
|
|
648
|
+
const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1121
649
|
const atom = {
|
|
1122
650
|
uri: document.uri,
|
|
1123
651
|
featureName: document.featureName ?? "",
|
|
@@ -1177,7 +705,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
1177
705
|
function collectTagNames(tags) {
|
|
1178
706
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1179
707
|
}
|
|
1180
|
-
function
|
|
708
|
+
function unique(values) {
|
|
1181
709
|
return Array.from(new Set(values));
|
|
1182
710
|
}
|
|
1183
711
|
|
|
@@ -1207,7 +735,7 @@ function extractAnnotatedScIds(text) {
|
|
|
1207
735
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
1208
736
|
const scIds = /* @__PURE__ */ new Set();
|
|
1209
737
|
for (const file of scenarioFiles) {
|
|
1210
|
-
const text = await
|
|
738
|
+
const text = await readFile2(file, "utf-8");
|
|
1211
739
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1212
740
|
if (!document || errors.length > 0) {
|
|
1213
741
|
continue;
|
|
@@ -1225,7 +753,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
1225
753
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1226
754
|
const sources = /* @__PURE__ */ new Map();
|
|
1227
755
|
for (const file of scenarioFiles) {
|
|
1228
|
-
const text = await
|
|
756
|
+
const text = await readFile2(file, "utf-8");
|
|
1229
757
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1230
758
|
if (!document || errors.length > 0) {
|
|
1231
759
|
continue;
|
|
@@ -1260,116 +788,900 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1260
788
|
}
|
|
1261
789
|
};
|
|
1262
790
|
}
|
|
1263
|
-
let files = [];
|
|
1264
|
-
try {
|
|
1265
|
-
files = await collectFilesByGlobs(root, {
|
|
1266
|
-
globs: normalizedGlobs,
|
|
1267
|
-
ignore: mergedExcludeGlobs
|
|
1268
|
-
});
|
|
1269
|
-
} catch (error2) {
|
|
1270
|
-
return {
|
|
1271
|
-
refs,
|
|
1272
|
-
scan: {
|
|
1273
|
-
globs: normalizedGlobs,
|
|
1274
|
-
excludeGlobs: mergedExcludeGlobs,
|
|
1275
|
-
matchedFileCount: 0
|
|
1276
|
-
},
|
|
1277
|
-
error: formatError3(error2)
|
|
1278
|
-
};
|
|
791
|
+
let files = [];
|
|
792
|
+
try {
|
|
793
|
+
files = await collectFilesByGlobs(root, {
|
|
794
|
+
globs: normalizedGlobs,
|
|
795
|
+
ignore: mergedExcludeGlobs
|
|
796
|
+
});
|
|
797
|
+
} catch (error2) {
|
|
798
|
+
return {
|
|
799
|
+
refs,
|
|
800
|
+
scan: {
|
|
801
|
+
globs: normalizedGlobs,
|
|
802
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
803
|
+
matchedFileCount: 0
|
|
804
|
+
},
|
|
805
|
+
error: formatError3(error2)
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const normalizedFiles = Array.from(
|
|
809
|
+
new Set(files.map((file) => path5.normalize(file)))
|
|
810
|
+
);
|
|
811
|
+
for (const file of normalizedFiles) {
|
|
812
|
+
const text = await readFile2(file, "utf-8");
|
|
813
|
+
const scIds = extractAnnotatedScIds(text);
|
|
814
|
+
if (scIds.length === 0) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
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.2".length > 0) {
|
|
874
|
+
return "0.6.2";
|
|
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 outDirAbs = resolvePath(root, config, "outDir");
|
|
1006
|
+
const rel = path7.relative(outDirAbs, validateJsonAbs);
|
|
1007
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
1008
|
+
addCheck(checks, {
|
|
1009
|
+
id: "output.pathAlignment",
|
|
1010
|
+
severity: inside ? "ok" : "warning",
|
|
1011
|
+
title: "Output path alignment",
|
|
1012
|
+
message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
|
|
1013
|
+
details: {
|
|
1014
|
+
outDir: toRelativePath(root, outDirAbs),
|
|
1015
|
+
validateJsonPath: toRelativePath(root, validateJsonAbs)
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
if (options.rootExplicit) {
|
|
1019
|
+
addCheck(checks, await buildOutDirCollisionCheck(root));
|
|
1020
|
+
}
|
|
1021
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1022
|
+
const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
|
|
1023
|
+
const exclude = normalizeGlobs2([
|
|
1024
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
1025
|
+
...config.validation.traceability.testFileExcludeGlobs
|
|
1026
|
+
]);
|
|
1027
|
+
try {
|
|
1028
|
+
const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
|
|
1029
|
+
const matchedCount = matched.length;
|
|
1030
|
+
const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
|
|
1031
|
+
addCheck(checks, {
|
|
1032
|
+
id: "traceability.testGlobs",
|
|
1033
|
+
severity,
|
|
1034
|
+
title: "Test file globs",
|
|
1035
|
+
message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
|
|
1036
|
+
details: {
|
|
1037
|
+
globs,
|
|
1038
|
+
excludeGlobs: exclude,
|
|
1039
|
+
scenarioFiles: scenarioFiles.length,
|
|
1040
|
+
scMustHaveTest: config.validation.traceability.scMustHaveTest
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
} catch (error2) {
|
|
1044
|
+
addCheck(checks, {
|
|
1045
|
+
id: "traceability.testGlobs",
|
|
1046
|
+
severity: "error",
|
|
1047
|
+
title: "Test file globs",
|
|
1048
|
+
message: "Glob scan failed (invalid pattern or filesystem error)",
|
|
1049
|
+
details: { globs, excludeGlobs: exclude, error: String(error2) }
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
tool: "qfai",
|
|
1054
|
+
version,
|
|
1055
|
+
generatedAt,
|
|
1056
|
+
root: toRelativePath(process.cwd(), root),
|
|
1057
|
+
config: {
|
|
1058
|
+
startDir: toRelativePath(process.cwd(), startDir),
|
|
1059
|
+
found: search.found,
|
|
1060
|
+
configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
|
|
1061
|
+
},
|
|
1062
|
+
summary: summarize(checks),
|
|
1063
|
+
checks
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
var DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS = [
|
|
1067
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
1068
|
+
"**/.pnpm/**",
|
|
1069
|
+
"**/tmp/**",
|
|
1070
|
+
"**/.mcp-tools/**"
|
|
1071
|
+
];
|
|
1072
|
+
async function buildOutDirCollisionCheck(root) {
|
|
1073
|
+
try {
|
|
1074
|
+
const result = await detectOutDirCollisions(root);
|
|
1075
|
+
const relativeRoot = toRelativePath(process.cwd(), result.monorepoRoot);
|
|
1076
|
+
const configRoots = result.configRoots.map((configRoot) => toRelativePath(result.monorepoRoot, configRoot)).sort((a, b) => a.localeCompare(b));
|
|
1077
|
+
const collisions = result.collisions.map((item) => ({
|
|
1078
|
+
outDir: toRelativePath(result.monorepoRoot, item.outDir),
|
|
1079
|
+
roots: item.roots.map(
|
|
1080
|
+
(collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
|
|
1081
|
+
).sort((a, b) => a.localeCompare(b))
|
|
1082
|
+
})).sort((a, b) => a.outDir.localeCompare(b.outDir));
|
|
1083
|
+
const severity = collisions.length > 0 ? "warning" : "ok";
|
|
1084
|
+
const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
|
|
1085
|
+
return {
|
|
1086
|
+
id: "output.outDirCollision",
|
|
1087
|
+
severity,
|
|
1088
|
+
title: "OutDir collision",
|
|
1089
|
+
message,
|
|
1090
|
+
details: {
|
|
1091
|
+
monorepoRoot: relativeRoot,
|
|
1092
|
+
configRoots,
|
|
1093
|
+
collisions
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
} catch (error2) {
|
|
1097
|
+
return {
|
|
1098
|
+
id: "output.outDirCollision",
|
|
1099
|
+
severity: "error",
|
|
1100
|
+
title: "OutDir collision",
|
|
1101
|
+
message: "OutDir collision scan failed",
|
|
1102
|
+
details: { error: String(error2) }
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function detectOutDirCollisions(root) {
|
|
1107
|
+
const monorepoRoot = await findMonorepoRoot(root);
|
|
1108
|
+
const configPaths = await collectFilesByGlobs(monorepoRoot, {
|
|
1109
|
+
globs: ["**/qfai.config.yaml"],
|
|
1110
|
+
ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
|
|
1111
|
+
});
|
|
1112
|
+
const configRoots = Array.from(
|
|
1113
|
+
new Set(configPaths.map((configPath) => path7.dirname(configPath)))
|
|
1114
|
+
).sort((a, b) => a.localeCompare(b));
|
|
1115
|
+
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1116
|
+
for (const configRoot of configRoots) {
|
|
1117
|
+
const { config } = await loadConfig(configRoot);
|
|
1118
|
+
const outDir = path7.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1119
|
+
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1120
|
+
roots.add(configRoot);
|
|
1121
|
+
outDirToRoots.set(outDir, roots);
|
|
1122
|
+
}
|
|
1123
|
+
const collisions = [];
|
|
1124
|
+
for (const [outDir, roots] of outDirToRoots.entries()) {
|
|
1125
|
+
if (roots.size > 1) {
|
|
1126
|
+
collisions.push({
|
|
1127
|
+
outDir,
|
|
1128
|
+
roots: Array.from(roots).sort((a, b) => a.localeCompare(b))
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return { monorepoRoot, configRoots, collisions };
|
|
1133
|
+
}
|
|
1134
|
+
async function findMonorepoRoot(startDir) {
|
|
1135
|
+
let current = path7.resolve(startDir);
|
|
1136
|
+
while (true) {
|
|
1137
|
+
const gitPath = path7.join(current, ".git");
|
|
1138
|
+
const workspacePath = path7.join(current, "pnpm-workspace.yaml");
|
|
1139
|
+
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1140
|
+
return current;
|
|
1141
|
+
}
|
|
1142
|
+
const parent = path7.dirname(current);
|
|
1143
|
+
if (parent === current) {
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
current = parent;
|
|
1147
|
+
}
|
|
1148
|
+
return path7.resolve(startDir);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/cli/lib/logger.ts
|
|
1152
|
+
function info(message) {
|
|
1153
|
+
process.stdout.write(`${message}
|
|
1154
|
+
`);
|
|
1155
|
+
}
|
|
1156
|
+
function warn(message) {
|
|
1157
|
+
process.stdout.write(`${message}
|
|
1158
|
+
`);
|
|
1159
|
+
}
|
|
1160
|
+
function error(message) {
|
|
1161
|
+
process.stderr.write(`${message}
|
|
1162
|
+
`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/cli/commands/doctor.ts
|
|
1166
|
+
function formatDoctorText(data) {
|
|
1167
|
+
const lines = [];
|
|
1168
|
+
lines.push(
|
|
1169
|
+
`qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
|
|
1170
|
+
);
|
|
1171
|
+
for (const check of data.checks) {
|
|
1172
|
+
lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
|
|
1173
|
+
}
|
|
1174
|
+
lines.push(
|
|
1175
|
+
`summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
|
|
1176
|
+
);
|
|
1177
|
+
return lines.join("\n");
|
|
1178
|
+
}
|
|
1179
|
+
function formatDoctorJson(data) {
|
|
1180
|
+
return JSON.stringify(data, null, 2);
|
|
1181
|
+
}
|
|
1182
|
+
async function runDoctor(options) {
|
|
1183
|
+
const data = await createDoctorData({
|
|
1184
|
+
startDir: options.root,
|
|
1185
|
+
rootExplicit: options.rootExplicit
|
|
1186
|
+
});
|
|
1187
|
+
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1188
|
+
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1189
|
+
if (options.outPath) {
|
|
1190
|
+
const outAbs = path8.isAbsolute(options.outPath) ? options.outPath : path8.resolve(process.cwd(), options.outPath);
|
|
1191
|
+
await mkdir(path8.dirname(outAbs), { recursive: true });
|
|
1192
|
+
await writeFile(outAbs, `${output}
|
|
1193
|
+
`, "utf-8");
|
|
1194
|
+
info(`doctor: wrote ${outAbs}`);
|
|
1195
|
+
return exitCode;
|
|
1196
|
+
}
|
|
1197
|
+
info(output);
|
|
1198
|
+
return exitCode;
|
|
1199
|
+
}
|
|
1200
|
+
function shouldFailDoctor(summary, failOn) {
|
|
1201
|
+
if (!failOn) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
if (failOn === "error") {
|
|
1205
|
+
return summary.error > 0;
|
|
1206
|
+
}
|
|
1207
|
+
return summary.warning + summary.error > 0;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/cli/commands/init.ts
|
|
1211
|
+
import path11 from "path";
|
|
1212
|
+
|
|
1213
|
+
// src/cli/lib/fs.ts
|
|
1214
|
+
import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
|
|
1215
|
+
import path9 from "path";
|
|
1216
|
+
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1217
|
+
const files = await collectTemplateFiles(sourceRoot);
|
|
1218
|
+
return copyFiles(files, sourceRoot, destRoot, options);
|
|
1219
|
+
}
|
|
1220
|
+
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
1221
|
+
const copied = [];
|
|
1222
|
+
const skipped = [];
|
|
1223
|
+
const conflicts = [];
|
|
1224
|
+
if (!options.force) {
|
|
1225
|
+
for (const file of files) {
|
|
1226
|
+
const relative = path9.relative(sourceRoot, file);
|
|
1227
|
+
const dest = path9.join(destRoot, relative);
|
|
1228
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1229
|
+
conflicts.push(dest);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (conflicts.length > 0) {
|
|
1233
|
+
throw new Error(formatConflictMessage(conflicts));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
for (const file of files) {
|
|
1237
|
+
const relative = path9.relative(sourceRoot, file);
|
|
1238
|
+
const dest = path9.join(destRoot, relative);
|
|
1239
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1240
|
+
skipped.push(dest);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (!options.dryRun) {
|
|
1244
|
+
await mkdir2(path9.dirname(dest), { recursive: true });
|
|
1245
|
+
await copyFile(file, dest);
|
|
1246
|
+
}
|
|
1247
|
+
copied.push(dest);
|
|
1248
|
+
}
|
|
1249
|
+
return { copied, skipped };
|
|
1250
|
+
}
|
|
1251
|
+
function formatConflictMessage(conflicts) {
|
|
1252
|
+
return [
|
|
1253
|
+
"\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",
|
|
1254
|
+
"",
|
|
1255
|
+
"\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
|
|
1256
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
1257
|
+
"",
|
|
1258
|
+
"\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"
|
|
1259
|
+
].join("\n");
|
|
1260
|
+
}
|
|
1261
|
+
async function collectTemplateFiles(root) {
|
|
1262
|
+
const entries = [];
|
|
1263
|
+
if (!await exists5(root)) {
|
|
1264
|
+
return entries;
|
|
1265
|
+
}
|
|
1266
|
+
const items = await readdir3(root, { withFileTypes: true });
|
|
1267
|
+
for (const item of items) {
|
|
1268
|
+
const fullPath = path9.join(root, item.name);
|
|
1269
|
+
if (item.isDirectory()) {
|
|
1270
|
+
const nested = await collectTemplateFiles(fullPath);
|
|
1271
|
+
entries.push(...nested);
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (item.isFile()) {
|
|
1275
|
+
entries.push(fullPath);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return entries;
|
|
1279
|
+
}
|
|
1280
|
+
async function shouldWrite(target, force) {
|
|
1281
|
+
if (force) {
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
return !await exists5(target);
|
|
1285
|
+
}
|
|
1286
|
+
async function exists5(target) {
|
|
1287
|
+
try {
|
|
1288
|
+
await access5(target);
|
|
1289
|
+
return true;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/cli/lib/assets.ts
|
|
1296
|
+
import { existsSync } from "fs";
|
|
1297
|
+
import path10 from "path";
|
|
1298
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1299
|
+
function getInitAssetsDir() {
|
|
1300
|
+
const base = import.meta.url;
|
|
1301
|
+
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1302
|
+
const baseDir = path10.dirname(basePath);
|
|
1303
|
+
const candidates = [
|
|
1304
|
+
path10.resolve(baseDir, "../../../assets/init"),
|
|
1305
|
+
path10.resolve(baseDir, "../../assets/init")
|
|
1306
|
+
];
|
|
1307
|
+
for (const candidate of candidates) {
|
|
1308
|
+
if (existsSync(candidate)) {
|
|
1309
|
+
return candidate;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
[
|
|
1314
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1315
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1316
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
1317
|
+
].join("\n")
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/cli/commands/init.ts
|
|
1322
|
+
async function runInit(options) {
|
|
1323
|
+
const assetsRoot = getInitAssetsDir();
|
|
1324
|
+
const rootAssets = path11.join(assetsRoot, "root");
|
|
1325
|
+
const qfaiAssets = path11.join(assetsRoot, ".qfai");
|
|
1326
|
+
const destRoot = path11.resolve(options.dir);
|
|
1327
|
+
const destQfai = path11.join(destRoot, ".qfai");
|
|
1328
|
+
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
1329
|
+
force: options.force,
|
|
1330
|
+
dryRun: options.dryRun
|
|
1331
|
+
});
|
|
1332
|
+
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
1333
|
+
force: options.force,
|
|
1334
|
+
dryRun: options.dryRun
|
|
1335
|
+
});
|
|
1336
|
+
report(
|
|
1337
|
+
[...rootResult.copied, ...qfaiResult.copied],
|
|
1338
|
+
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
1339
|
+
options.dryRun,
|
|
1340
|
+
"init"
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
function report(copied, skipped, dryRun, label) {
|
|
1344
|
+
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
1345
|
+
if (copied.length > 0) {
|
|
1346
|
+
info(` created: ${copied.length}`);
|
|
1347
|
+
}
|
|
1348
|
+
if (skipped.length > 0) {
|
|
1349
|
+
info(` skipped: ${skipped.length}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/cli/commands/report.ts
|
|
1354
|
+
import { mkdir as mkdir3, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
|
|
1355
|
+
import path18 from "path";
|
|
1356
|
+
|
|
1357
|
+
// src/core/normalize.ts
|
|
1358
|
+
function normalizeIssuePaths(root, issues) {
|
|
1359
|
+
return issues.map((issue7) => {
|
|
1360
|
+
if (!issue7.file) {
|
|
1361
|
+
return issue7;
|
|
1362
|
+
}
|
|
1363
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
1364
|
+
if (normalized === issue7.file) {
|
|
1365
|
+
return issue7;
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
...issue7,
|
|
1369
|
+
file: normalized
|
|
1370
|
+
};
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
function normalizeScCoverage(root, sc) {
|
|
1374
|
+
const refs = {};
|
|
1375
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
1376
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
1377
|
+
}
|
|
1378
|
+
return {
|
|
1379
|
+
...sc,
|
|
1380
|
+
refs
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function normalizeValidationResult(root, result) {
|
|
1384
|
+
return {
|
|
1385
|
+
...result,
|
|
1386
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
1387
|
+
traceability: {
|
|
1388
|
+
...result.traceability,
|
|
1389
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/core/report.ts
|
|
1395
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1396
|
+
import path17 from "path";
|
|
1397
|
+
|
|
1398
|
+
// src/core/contractIndex.ts
|
|
1399
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1400
|
+
import path12 from "path";
|
|
1401
|
+
|
|
1402
|
+
// src/core/contractsDecl.ts
|
|
1403
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
1404
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
1405
|
+
function extractDeclaredContractIds(text) {
|
|
1406
|
+
const ids = [];
|
|
1407
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
1408
|
+
const id = match[1];
|
|
1409
|
+
if (id) {
|
|
1410
|
+
ids.push(id);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return ids;
|
|
1414
|
+
}
|
|
1415
|
+
function stripContractDeclarationLines(text) {
|
|
1416
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/core/contractIndex.ts
|
|
1420
|
+
async function buildContractIndex(root, config) {
|
|
1421
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1422
|
+
const uiRoot = path12.join(contractsRoot, "ui");
|
|
1423
|
+
const apiRoot = path12.join(contractsRoot, "api");
|
|
1424
|
+
const dbRoot = path12.join(contractsRoot, "db");
|
|
1425
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1426
|
+
collectUiContractFiles(uiRoot),
|
|
1427
|
+
collectApiContractFiles(apiRoot),
|
|
1428
|
+
collectDbContractFiles(dbRoot)
|
|
1429
|
+
]);
|
|
1430
|
+
const index = {
|
|
1431
|
+
ids: /* @__PURE__ */ new Set(),
|
|
1432
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
1433
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1434
|
+
};
|
|
1435
|
+
await indexContractFiles(uiFiles, index);
|
|
1436
|
+
await indexContractFiles(apiFiles, index);
|
|
1437
|
+
await indexContractFiles(dbFiles, index);
|
|
1438
|
+
return index;
|
|
1439
|
+
}
|
|
1440
|
+
async function indexContractFiles(files, index) {
|
|
1441
|
+
for (const file of files) {
|
|
1442
|
+
const text = await readFile4(file, "utf-8");
|
|
1443
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function record(index, id, file) {
|
|
1447
|
+
index.ids.add(id);
|
|
1448
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1449
|
+
current.add(file);
|
|
1450
|
+
index.idToFiles.set(id, current);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/core/ids.ts
|
|
1454
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
1455
|
+
var STRICT_ID_PATTERNS = {
|
|
1456
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1457
|
+
BR: /\bBR-\d{4}\b/g,
|
|
1458
|
+
SC: /\bSC-\d{4}\b/g,
|
|
1459
|
+
UI: /\bUI-\d{4}\b/g,
|
|
1460
|
+
API: /\bAPI-\d{4}\b/g,
|
|
1461
|
+
DB: /\bDB-\d{4}\b/g,
|
|
1462
|
+
ADR: /\bADR-\d{4}\b/g
|
|
1463
|
+
};
|
|
1464
|
+
var LOOSE_ID_PATTERNS = {
|
|
1465
|
+
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
1466
|
+
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
1467
|
+
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
1468
|
+
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1469
|
+
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1470
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1471
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1472
|
+
};
|
|
1473
|
+
function extractIds(text, prefix) {
|
|
1474
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1475
|
+
const matches = text.match(pattern);
|
|
1476
|
+
return unique2(matches ?? []);
|
|
1477
|
+
}
|
|
1478
|
+
function extractAllIds(text) {
|
|
1479
|
+
const all = [];
|
|
1480
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
1481
|
+
all.push(...extractIds(text, prefix));
|
|
1482
|
+
});
|
|
1483
|
+
return unique2(all);
|
|
1484
|
+
}
|
|
1485
|
+
function extractInvalidIds(text, prefixes) {
|
|
1486
|
+
const invalid = [];
|
|
1487
|
+
for (const prefix of prefixes) {
|
|
1488
|
+
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
1489
|
+
for (const candidate of candidates) {
|
|
1490
|
+
if (!isValidId(candidate, prefix)) {
|
|
1491
|
+
invalid.push(candidate);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return unique2(invalid);
|
|
1496
|
+
}
|
|
1497
|
+
function unique2(values) {
|
|
1498
|
+
return Array.from(new Set(values));
|
|
1499
|
+
}
|
|
1500
|
+
function isValidId(value, prefix) {
|
|
1501
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1502
|
+
const strict = new RegExp(pattern.source);
|
|
1503
|
+
return strict.test(value);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/core/parse/contractRefs.ts
|
|
1507
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1508
|
+
function parseContractRefs(text, options = {}) {
|
|
1509
|
+
const linePattern = buildLinePattern(options);
|
|
1510
|
+
const lines = [];
|
|
1511
|
+
for (const match of text.matchAll(linePattern)) {
|
|
1512
|
+
lines.push((match[1] ?? "").trim());
|
|
1279
1513
|
}
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
for (const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (scIds.length === 0) {
|
|
1514
|
+
const ids = [];
|
|
1515
|
+
const invalidTokens = [];
|
|
1516
|
+
let hasNone = false;
|
|
1517
|
+
for (const line of lines) {
|
|
1518
|
+
if (line.length === 0) {
|
|
1519
|
+
invalidTokens.push("(empty)");
|
|
1287
1520
|
continue;
|
|
1288
1521
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1522
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
1523
|
+
for (const token of tokens) {
|
|
1524
|
+
if (token.length === 0) {
|
|
1525
|
+
invalidTokens.push("(empty)");
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
if (token === "none") {
|
|
1529
|
+
hasNone = true;
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
1533
|
+
ids.push(token);
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
invalidTokens.push(token);
|
|
1293
1537
|
}
|
|
1294
1538
|
}
|
|
1295
1539
|
return {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
matchedFileCount: normalizedFiles.length
|
|
1301
|
-
}
|
|
1540
|
+
lines,
|
|
1541
|
+
ids: unique3(ids),
|
|
1542
|
+
invalidTokens: unique3(invalidTokens),
|
|
1543
|
+
hasNone
|
|
1302
1544
|
};
|
|
1303
1545
|
}
|
|
1304
|
-
function
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const files = refs.get(scId);
|
|
1311
|
-
const sortedFiles = files ? toSortedArray(files) : [];
|
|
1312
|
-
refsRecord[scId] = sortedFiles;
|
|
1313
|
-
if (sortedFiles.length === 0) {
|
|
1314
|
-
missingIds.push(scId);
|
|
1315
|
-
} else {
|
|
1316
|
-
covered += 1;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
return {
|
|
1320
|
-
total: sortedScIds.length,
|
|
1321
|
-
covered,
|
|
1322
|
-
missing: missingIds.length,
|
|
1323
|
-
missingIds,
|
|
1324
|
-
refs: refsRecord
|
|
1325
|
-
};
|
|
1546
|
+
function buildLinePattern(options) {
|
|
1547
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
1548
|
+
return new RegExp(
|
|
1549
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
1550
|
+
"gm"
|
|
1551
|
+
);
|
|
1326
1552
|
}
|
|
1327
|
-
function
|
|
1328
|
-
return Array.from(new Set(values))
|
|
1553
|
+
function unique3(values) {
|
|
1554
|
+
return Array.from(new Set(values));
|
|
1329
1555
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1556
|
+
|
|
1557
|
+
// src/core/parse/markdown.ts
|
|
1558
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1559
|
+
function parseHeadings(md) {
|
|
1560
|
+
const lines = md.split(/\r?\n/);
|
|
1561
|
+
const headings = [];
|
|
1562
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1563
|
+
const line = lines[i] ?? "";
|
|
1564
|
+
const match = line.match(HEADING_RE);
|
|
1565
|
+
if (!match) continue;
|
|
1566
|
+
const levelToken = match[1];
|
|
1567
|
+
const title = match[2];
|
|
1568
|
+
if (!levelToken || !title) continue;
|
|
1569
|
+
headings.push({
|
|
1570
|
+
level: levelToken.length,
|
|
1571
|
+
title: title.trim(),
|
|
1572
|
+
line: i + 1
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
return headings;
|
|
1332
1576
|
}
|
|
1333
|
-
function
|
|
1334
|
-
|
|
1335
|
-
|
|
1577
|
+
function extractH2Sections(md) {
|
|
1578
|
+
const lines = md.split(/\r?\n/);
|
|
1579
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1580
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1581
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1582
|
+
const current = headings[i];
|
|
1583
|
+
if (!current) continue;
|
|
1584
|
+
const next = headings[i + 1];
|
|
1585
|
+
const startLine = current.line + 1;
|
|
1586
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1587
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1588
|
+
sections.set(current.title.trim(), {
|
|
1589
|
+
title: current.title.trim(),
|
|
1590
|
+
startLine,
|
|
1591
|
+
endLine,
|
|
1592
|
+
body
|
|
1593
|
+
});
|
|
1336
1594
|
}
|
|
1337
|
-
return
|
|
1595
|
+
return sections;
|
|
1338
1596
|
}
|
|
1339
1597
|
|
|
1340
|
-
// src/core/
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1598
|
+
// src/core/parse/spec.ts
|
|
1599
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1600
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1601
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1602
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1603
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1604
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1605
|
+
function parseSpec(md, file) {
|
|
1606
|
+
const headings = parseHeadings(md);
|
|
1607
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1608
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1609
|
+
const sections = extractH2Sections(md);
|
|
1610
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1611
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1612
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1613
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1614
|
+
const brs = [];
|
|
1615
|
+
const brsWithoutPriority = [];
|
|
1616
|
+
const brsWithInvalidPriority = [];
|
|
1617
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1618
|
+
const lineText = brLines[i] ?? "";
|
|
1619
|
+
const lineNumber = startLine + i;
|
|
1620
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1621
|
+
if (validMatch) {
|
|
1622
|
+
const id = validMatch[1];
|
|
1623
|
+
const priority = validMatch[2];
|
|
1624
|
+
const text = validMatch[3];
|
|
1625
|
+
if (!id || !priority || !text) continue;
|
|
1626
|
+
brs.push({
|
|
1627
|
+
id,
|
|
1628
|
+
priority,
|
|
1629
|
+
text: text.trim(),
|
|
1630
|
+
line: lineNumber
|
|
1631
|
+
});
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1635
|
+
if (anyPriorityMatch) {
|
|
1636
|
+
const id = anyPriorityMatch[1];
|
|
1637
|
+
const priority = anyPriorityMatch[2];
|
|
1638
|
+
const text = anyPriorityMatch[3];
|
|
1639
|
+
if (!id || !priority || !text) continue;
|
|
1640
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1641
|
+
brsWithInvalidPriority.push({
|
|
1642
|
+
id,
|
|
1643
|
+
priority,
|
|
1644
|
+
text: text.trim(),
|
|
1645
|
+
line: lineNumber
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1651
|
+
if (noPriorityMatch) {
|
|
1652
|
+
const id = noPriorityMatch[1];
|
|
1653
|
+
const text = noPriorityMatch[2];
|
|
1654
|
+
if (!id || !text) continue;
|
|
1655
|
+
brsWithoutPriority.push({
|
|
1656
|
+
id,
|
|
1657
|
+
text: text.trim(),
|
|
1658
|
+
line: lineNumber
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1347
1661
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1662
|
+
const parsed = {
|
|
1663
|
+
file,
|
|
1664
|
+
sections: sectionNames,
|
|
1665
|
+
brs,
|
|
1666
|
+
brsWithoutPriority,
|
|
1667
|
+
brsWithInvalidPriority,
|
|
1668
|
+
contractRefs: parseContractRefs(md)
|
|
1669
|
+
};
|
|
1670
|
+
if (specId) {
|
|
1671
|
+
parsed.specId = specId;
|
|
1356
1672
|
}
|
|
1357
|
-
|
|
1358
|
-
function resolvePackageJsonPath() {
|
|
1359
|
-
const base = import.meta.url;
|
|
1360
|
-
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1361
|
-
return path10.resolve(path10.dirname(basePath), "../../package.json");
|
|
1673
|
+
return parsed;
|
|
1362
1674
|
}
|
|
1363
1675
|
|
|
1364
1676
|
// src/core/validators/contracts.ts
|
|
1365
1677
|
import { readFile as readFile5 } from "fs/promises";
|
|
1366
|
-
import
|
|
1678
|
+
import path14 from "path";
|
|
1367
1679
|
|
|
1368
1680
|
// src/core/contracts.ts
|
|
1369
|
-
import
|
|
1681
|
+
import path13 from "path";
|
|
1370
1682
|
import { parse as parseYaml2 } from "yaml";
|
|
1371
1683
|
function parseStructuredContract(file, text) {
|
|
1372
|
-
const ext =
|
|
1684
|
+
const ext = path13.extname(file).toLowerCase();
|
|
1373
1685
|
if (ext === ".json") {
|
|
1374
1686
|
return JSON.parse(text);
|
|
1375
1687
|
}
|
|
@@ -1389,9 +1701,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1389
1701
|
async function validateContracts(root, config) {
|
|
1390
1702
|
const issues = [];
|
|
1391
1703
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1392
|
-
issues.push(...await validateUiContracts(
|
|
1393
|
-
issues.push(...await validateApiContracts(
|
|
1394
|
-
issues.push(...await validateDbContracts(
|
|
1704
|
+
issues.push(...await validateUiContracts(path14.join(contractsRoot, "ui")));
|
|
1705
|
+
issues.push(...await validateApiContracts(path14.join(contractsRoot, "api")));
|
|
1706
|
+
issues.push(...await validateDbContracts(path14.join(contractsRoot, "db")));
|
|
1395
1707
|
const contractIndex = await buildContractIndex(root, config);
|
|
1396
1708
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1397
1709
|
return issues;
|
|
@@ -1674,7 +1986,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1674
1986
|
|
|
1675
1987
|
// src/core/validators/delta.ts
|
|
1676
1988
|
import { readFile as readFile6 } from "fs/promises";
|
|
1677
|
-
import
|
|
1989
|
+
import path15 from "path";
|
|
1678
1990
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1679
1991
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1680
1992
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1688,7 +2000,7 @@ async function validateDeltas(root, config) {
|
|
|
1688
2000
|
}
|
|
1689
2001
|
const issues = [];
|
|
1690
2002
|
for (const pack of packs) {
|
|
1691
|
-
const deltaPath =
|
|
2003
|
+
const deltaPath = path15.join(pack, "delta.md");
|
|
1692
2004
|
let text;
|
|
1693
2005
|
try {
|
|
1694
2006
|
text = await readFile6(deltaPath, "utf-8");
|
|
@@ -1764,7 +2076,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1764
2076
|
|
|
1765
2077
|
// src/core/validators/ids.ts
|
|
1766
2078
|
import { readFile as readFile7 } from "fs/promises";
|
|
1767
|
-
import
|
|
2079
|
+
import path16 from "path";
|
|
1768
2080
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1769
2081
|
async function validateDefinedIds(root, config) {
|
|
1770
2082
|
const issues = [];
|
|
@@ -1830,7 +2142,7 @@ function recordId(out, id, file) {
|
|
|
1830
2142
|
}
|
|
1831
2143
|
function formatFileList(files, root) {
|
|
1832
2144
|
return files.map((file) => {
|
|
1833
|
-
const relative =
|
|
2145
|
+
const relative = path16.relative(root, file);
|
|
1834
2146
|
return relative.length > 0 ? relative : file;
|
|
1835
2147
|
}).join(", ");
|
|
1836
2148
|
}
|
|
@@ -2721,15 +3033,15 @@ function countIssues(issues) {
|
|
|
2721
3033
|
// src/core/report.ts
|
|
2722
3034
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2723
3035
|
async function createReportData(root, validation, configResult) {
|
|
2724
|
-
const resolvedRoot =
|
|
3036
|
+
const resolvedRoot = path17.resolve(root);
|
|
2725
3037
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2726
3038
|
const config = resolved.config;
|
|
2727
3039
|
const configPath = resolved.configPath;
|
|
2728
3040
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2729
3041
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2730
|
-
const apiRoot =
|
|
2731
|
-
const uiRoot =
|
|
2732
|
-
const dbRoot =
|
|
3042
|
+
const apiRoot = path17.join(contractsRoot, "api");
|
|
3043
|
+
const uiRoot = path17.join(contractsRoot, "ui");
|
|
3044
|
+
const dbRoot = path17.join(contractsRoot, "db");
|
|
2733
3045
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2734
3046
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2735
3047
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3216,7 +3528,7 @@ function buildHotspots(issues) {
|
|
|
3216
3528
|
|
|
3217
3529
|
// src/cli/commands/report.ts
|
|
3218
3530
|
async function runReport(options) {
|
|
3219
|
-
const root =
|
|
3531
|
+
const root = path18.resolve(options.root);
|
|
3220
3532
|
const configResult = await loadConfig(root);
|
|
3221
3533
|
let validation;
|
|
3222
3534
|
if (options.runValidate) {
|
|
@@ -3233,7 +3545,7 @@ async function runReport(options) {
|
|
|
3233
3545
|
validation = normalized;
|
|
3234
3546
|
} else {
|
|
3235
3547
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3236
|
-
const inputPath =
|
|
3548
|
+
const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
|
|
3237
3549
|
try {
|
|
3238
3550
|
validation = await readValidationResult(inputPath);
|
|
3239
3551
|
} catch (err) {
|
|
@@ -3259,11 +3571,11 @@ async function runReport(options) {
|
|
|
3259
3571
|
const data = await createReportData(root, validation, configResult);
|
|
3260
3572
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3261
3573
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3262
|
-
const defaultOut = options.format === "json" ?
|
|
3574
|
+
const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
|
|
3263
3575
|
const out = options.outPath ?? defaultOut;
|
|
3264
|
-
const outPath =
|
|
3265
|
-
await
|
|
3266
|
-
await
|
|
3576
|
+
const outPath = path18.isAbsolute(out) ? out : path18.resolve(root, out);
|
|
3577
|
+
await mkdir3(path18.dirname(outPath), { recursive: true });
|
|
3578
|
+
await writeFile2(outPath, `${output}
|
|
3267
3579
|
`, "utf-8");
|
|
3268
3580
|
info(
|
|
3269
3581
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -3327,15 +3639,15 @@ function isMissingFileError5(error2) {
|
|
|
3327
3639
|
return record2.code === "ENOENT";
|
|
3328
3640
|
}
|
|
3329
3641
|
async function writeValidationResult(root, outputPath, result) {
|
|
3330
|
-
const abs =
|
|
3331
|
-
await
|
|
3332
|
-
await
|
|
3642
|
+
const abs = path18.isAbsolute(outputPath) ? outputPath : path18.resolve(root, outputPath);
|
|
3643
|
+
await mkdir3(path18.dirname(abs), { recursive: true });
|
|
3644
|
+
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
3333
3645
|
`, "utf-8");
|
|
3334
3646
|
}
|
|
3335
3647
|
|
|
3336
3648
|
// src/cli/commands/validate.ts
|
|
3337
|
-
import { mkdir as
|
|
3338
|
-
import
|
|
3649
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
3650
|
+
import path19 from "path";
|
|
3339
3651
|
|
|
3340
3652
|
// src/cli/lib/failOn.ts
|
|
3341
3653
|
function shouldFail(result, failOn) {
|
|
@@ -3350,7 +3662,7 @@ function shouldFail(result, failOn) {
|
|
|
3350
3662
|
|
|
3351
3663
|
// src/cli/commands/validate.ts
|
|
3352
3664
|
async function runValidate(options) {
|
|
3353
|
-
const root =
|
|
3665
|
+
const root = path19.resolve(options.root);
|
|
3354
3666
|
const configResult = await loadConfig(root);
|
|
3355
3667
|
const result = await validateProject(root, configResult);
|
|
3356
3668
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -3467,12 +3779,12 @@ function issueKey(issue7) {
|
|
|
3467
3779
|
}
|
|
3468
3780
|
async function emitJson(result, root, jsonPath) {
|
|
3469
3781
|
const abs = resolveJsonPath(root, jsonPath);
|
|
3470
|
-
await
|
|
3471
|
-
await
|
|
3782
|
+
await mkdir4(path19.dirname(abs), { recursive: true });
|
|
3783
|
+
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
3472
3784
|
`, "utf-8");
|
|
3473
3785
|
}
|
|
3474
3786
|
function resolveJsonPath(root, jsonPath) {
|
|
3475
|
-
return
|
|
3787
|
+
return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
|
|
3476
3788
|
}
|
|
3477
3789
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3478
3790
|
|
|
@@ -3487,6 +3799,7 @@ function parseArgs(argv, cwd) {
|
|
|
3487
3799
|
dryRun: false,
|
|
3488
3800
|
reportFormat: "md",
|
|
3489
3801
|
reportRunValidate: false,
|
|
3802
|
+
doctorFormat: "text",
|
|
3490
3803
|
validateFormat: "text",
|
|
3491
3804
|
strict: false,
|
|
3492
3805
|
help: false
|
|
@@ -3539,7 +3852,11 @@ function parseArgs(argv, cwd) {
|
|
|
3539
3852
|
{
|
|
3540
3853
|
const next = args[i + 1];
|
|
3541
3854
|
if (next) {
|
|
3542
|
-
|
|
3855
|
+
if (command === "doctor") {
|
|
3856
|
+
options.doctorOut = next;
|
|
3857
|
+
} else {
|
|
3858
|
+
options.reportOut = next;
|
|
3859
|
+
}
|
|
3543
3860
|
}
|
|
3544
3861
|
}
|
|
3545
3862
|
i += 1;
|
|
@@ -3582,6 +3899,12 @@ function applyFormatOption(command, value, options) {
|
|
|
3582
3899
|
}
|
|
3583
3900
|
return;
|
|
3584
3901
|
}
|
|
3902
|
+
if (command === "doctor") {
|
|
3903
|
+
if (value === "text" || value === "json") {
|
|
3904
|
+
options.doctorFormat = value;
|
|
3905
|
+
}
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3585
3908
|
if (value === "md" || value === "json") {
|
|
3586
3909
|
options.reportFormat = value;
|
|
3587
3910
|
}
|
|
@@ -3629,6 +3952,18 @@ async function run(argv, cwd) {
|
|
|
3629
3952
|
});
|
|
3630
3953
|
}
|
|
3631
3954
|
return;
|
|
3955
|
+
case "doctor":
|
|
3956
|
+
{
|
|
3957
|
+
const exitCode = await runDoctor({
|
|
3958
|
+
root: options.root,
|
|
3959
|
+
rootExplicit: options.rootExplicit,
|
|
3960
|
+
format: options.doctorFormat,
|
|
3961
|
+
...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {},
|
|
3962
|
+
...options.failOn && options.failOn !== "never" ? { failOn: options.failOn } : {}
|
|
3963
|
+
});
|
|
3964
|
+
process.exitCode = exitCode;
|
|
3965
|
+
}
|
|
3966
|
+
return;
|
|
3632
3967
|
default:
|
|
3633
3968
|
error(`Unknown command: ${command}`);
|
|
3634
3969
|
info(usage());
|
|
@@ -3642,6 +3977,7 @@ Commands:
|
|
|
3642
3977
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
3643
3978
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
3644
3979
|
report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
|
|
3980
|
+
doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
|
|
3645
3981
|
|
|
3646
3982
|
Options:
|
|
3647
3983
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -3651,9 +3987,11 @@ Options:
|
|
|
3651
3987
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
3652
3988
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3653
3989
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3990
|
+
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3654
3991
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3655
3992
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3656
|
-
--
|
|
3993
|
+
--fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
|
|
3994
|
+
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
3657
3995
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3658
3996
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3659
3997
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|