qfai 0.5.2 → 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 +3 -0
- package/dist/cli/index.cjs +887 -649
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +881 -643
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- 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;
|
|
@@ -1258,118 +786,805 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1258
786
|
excludeGlobs: mergedExcludeGlobs,
|
|
1259
787
|
matchedFileCount: 0
|
|
1260
788
|
}
|
|
1261
|
-
};
|
|
789
|
+
};
|
|
790
|
+
}
|
|
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.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
|
+
}
|
|
1262
1397
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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());
|
|
1279
1416
|
}
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
for (const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (scIds.length === 0) {
|
|
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)");
|
|
1287
1423
|
continue;
|
|
1288
1424
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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);
|
|
1293
1440
|
}
|
|
1294
1441
|
}
|
|
1295
1442
|
return {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
matchedFileCount: normalizedFiles.length
|
|
1301
|
-
}
|
|
1443
|
+
lines,
|
|
1444
|
+
ids: unique3(ids),
|
|
1445
|
+
invalidTokens: unique3(invalidTokens),
|
|
1446
|
+
hasNone
|
|
1302
1447
|
};
|
|
1303
1448
|
}
|
|
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
|
-
};
|
|
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
|
+
);
|
|
1326
1455
|
}
|
|
1327
|
-
function
|
|
1328
|
-
return Array.from(new Set(values))
|
|
1456
|
+
function unique3(values) {
|
|
1457
|
+
return Array.from(new Set(values));
|
|
1329
1458
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
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;
|
|
1332
1479
|
}
|
|
1333
|
-
function
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
+
});
|
|
1336
1497
|
}
|
|
1337
|
-
return
|
|
1498
|
+
return sections;
|
|
1338
1499
|
}
|
|
1339
1500
|
|
|
1340
|
-
// src/core/
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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;
|
|
1552
|
+
}
|
|
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
|
+
});
|
|
1563
|
+
}
|
|
1347
1564
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1565
|
+
const parsed = {
|
|
1566
|
+
file,
|
|
1567
|
+
sections: sectionNames,
|
|
1568
|
+
brs,
|
|
1569
|
+
brsWithoutPriority,
|
|
1570
|
+
brsWithInvalidPriority,
|
|
1571
|
+
contractRefs: parseContractRefs(md)
|
|
1572
|
+
};
|
|
1573
|
+
if (specId) {
|
|
1574
|
+
parsed.specId = specId;
|
|
1356
1575
|
}
|
|
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");
|
|
1576
|
+
return parsed;
|
|
1362
1577
|
}
|
|
1363
1578
|
|
|
1364
1579
|
// src/core/validators/contracts.ts
|
|
1365
1580
|
import { readFile as readFile5 } from "fs/promises";
|
|
1366
|
-
import
|
|
1581
|
+
import path14 from "path";
|
|
1367
1582
|
|
|
1368
1583
|
// src/core/contracts.ts
|
|
1369
|
-
import
|
|
1584
|
+
import path13 from "path";
|
|
1370
1585
|
import { parse as parseYaml2 } from "yaml";
|
|
1371
1586
|
function parseStructuredContract(file, text) {
|
|
1372
|
-
const ext =
|
|
1587
|
+
const ext = path13.extname(file).toLowerCase();
|
|
1373
1588
|
if (ext === ".json") {
|
|
1374
1589
|
return JSON.parse(text);
|
|
1375
1590
|
}
|
|
@@ -1389,9 +1604,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1389
1604
|
async function validateContracts(root, config) {
|
|
1390
1605
|
const issues = [];
|
|
1391
1606
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1392
|
-
issues.push(...await validateUiContracts(
|
|
1393
|
-
issues.push(...await validateApiContracts(
|
|
1394
|
-
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")));
|
|
1395
1610
|
const contractIndex = await buildContractIndex(root, config);
|
|
1396
1611
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1397
1612
|
return issues;
|
|
@@ -1674,7 +1889,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1674
1889
|
|
|
1675
1890
|
// src/core/validators/delta.ts
|
|
1676
1891
|
import { readFile as readFile6 } from "fs/promises";
|
|
1677
|
-
import
|
|
1892
|
+
import path15 from "path";
|
|
1678
1893
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1679
1894
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1680
1895
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1688,7 +1903,7 @@ async function validateDeltas(root, config) {
|
|
|
1688
1903
|
}
|
|
1689
1904
|
const issues = [];
|
|
1690
1905
|
for (const pack of packs) {
|
|
1691
|
-
const deltaPath =
|
|
1906
|
+
const deltaPath = path15.join(pack, "delta.md");
|
|
1692
1907
|
let text;
|
|
1693
1908
|
try {
|
|
1694
1909
|
text = await readFile6(deltaPath, "utf-8");
|
|
@@ -1764,7 +1979,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1764
1979
|
|
|
1765
1980
|
// src/core/validators/ids.ts
|
|
1766
1981
|
import { readFile as readFile7 } from "fs/promises";
|
|
1767
|
-
import
|
|
1982
|
+
import path16 from "path";
|
|
1768
1983
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1769
1984
|
async function validateDefinedIds(root, config) {
|
|
1770
1985
|
const issues = [];
|
|
@@ -1830,7 +2045,7 @@ function recordId(out, id, file) {
|
|
|
1830
2045
|
}
|
|
1831
2046
|
function formatFileList(files, root) {
|
|
1832
2047
|
return files.map((file) => {
|
|
1833
|
-
const relative =
|
|
2048
|
+
const relative = path16.relative(root, file);
|
|
1834
2049
|
return relative.length > 0 ? relative : file;
|
|
1835
2050
|
}).join(", ");
|
|
1836
2051
|
}
|
|
@@ -2721,15 +2936,15 @@ function countIssues(issues) {
|
|
|
2721
2936
|
// src/core/report.ts
|
|
2722
2937
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2723
2938
|
async function createReportData(root, validation, configResult) {
|
|
2724
|
-
const resolvedRoot =
|
|
2939
|
+
const resolvedRoot = path17.resolve(root);
|
|
2725
2940
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2726
2941
|
const config = resolved.config;
|
|
2727
2942
|
const configPath = resolved.configPath;
|
|
2728
2943
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2729
2944
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2730
|
-
const apiRoot =
|
|
2731
|
-
const uiRoot =
|
|
2732
|
-
const dbRoot =
|
|
2945
|
+
const apiRoot = path17.join(contractsRoot, "api");
|
|
2946
|
+
const uiRoot = path17.join(contractsRoot, "ui");
|
|
2947
|
+
const dbRoot = path17.join(contractsRoot, "db");
|
|
2733
2948
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2734
2949
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2735
2950
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2787,11 +3002,13 @@ async function createReportData(root, validation, configResult) {
|
|
|
2787
3002
|
normalizeScSources(resolvedRoot, scSources)
|
|
2788
3003
|
);
|
|
2789
3004
|
const version = await resolveToolVersion();
|
|
3005
|
+
const reportFormatVersion = 1;
|
|
2790
3006
|
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2791
3007
|
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2792
3008
|
return {
|
|
2793
3009
|
tool: "qfai",
|
|
2794
3010
|
version,
|
|
3011
|
+
reportFormatVersion,
|
|
2795
3012
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2796
3013
|
root: displayRoot,
|
|
2797
3014
|
configPath: displayConfigPath,
|
|
@@ -3216,7 +3433,7 @@ function buildHotspots(issues) {
|
|
|
3216
3433
|
|
|
3217
3434
|
// src/cli/commands/report.ts
|
|
3218
3435
|
async function runReport(options) {
|
|
3219
|
-
const root =
|
|
3436
|
+
const root = path18.resolve(options.root);
|
|
3220
3437
|
const configResult = await loadConfig(root);
|
|
3221
3438
|
let validation;
|
|
3222
3439
|
if (options.runValidate) {
|
|
@@ -3233,7 +3450,7 @@ async function runReport(options) {
|
|
|
3233
3450
|
validation = normalized;
|
|
3234
3451
|
} else {
|
|
3235
3452
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3236
|
-
const inputPath =
|
|
3453
|
+
const inputPath = path18.isAbsolute(input) ? input : path18.resolve(root, input);
|
|
3237
3454
|
try {
|
|
3238
3455
|
validation = await readValidationResult(inputPath);
|
|
3239
3456
|
} catch (err) {
|
|
@@ -3259,11 +3476,11 @@ async function runReport(options) {
|
|
|
3259
3476
|
const data = await createReportData(root, validation, configResult);
|
|
3260
3477
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3261
3478
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3262
|
-
const defaultOut = options.format === "json" ?
|
|
3479
|
+
const defaultOut = options.format === "json" ? path18.join(outRoot, "report.json") : path18.join(outRoot, "report.md");
|
|
3263
3480
|
const out = options.outPath ?? defaultOut;
|
|
3264
|
-
const outPath =
|
|
3265
|
-
await
|
|
3266
|
-
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}
|
|
3267
3484
|
`, "utf-8");
|
|
3268
3485
|
info(
|
|
3269
3486
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -3327,15 +3544,15 @@ function isMissingFileError5(error2) {
|
|
|
3327
3544
|
return record2.code === "ENOENT";
|
|
3328
3545
|
}
|
|
3329
3546
|
async function writeValidationResult(root, outputPath, result) {
|
|
3330
|
-
const abs =
|
|
3331
|
-
await
|
|
3332
|
-
await
|
|
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)}
|
|
3333
3550
|
`, "utf-8");
|
|
3334
3551
|
}
|
|
3335
3552
|
|
|
3336
3553
|
// src/cli/commands/validate.ts
|
|
3337
|
-
import { mkdir as
|
|
3338
|
-
import
|
|
3554
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
3555
|
+
import path19 from "path";
|
|
3339
3556
|
|
|
3340
3557
|
// src/cli/lib/failOn.ts
|
|
3341
3558
|
function shouldFail(result, failOn) {
|
|
@@ -3350,7 +3567,7 @@ function shouldFail(result, failOn) {
|
|
|
3350
3567
|
|
|
3351
3568
|
// src/cli/commands/validate.ts
|
|
3352
3569
|
async function runValidate(options) {
|
|
3353
|
-
const root =
|
|
3570
|
+
const root = path19.resolve(options.root);
|
|
3354
3571
|
const configResult = await loadConfig(root);
|
|
3355
3572
|
const result = await validateProject(root, configResult);
|
|
3356
3573
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -3467,12 +3684,12 @@ function issueKey(issue7) {
|
|
|
3467
3684
|
}
|
|
3468
3685
|
async function emitJson(result, root, jsonPath) {
|
|
3469
3686
|
const abs = resolveJsonPath(root, jsonPath);
|
|
3470
|
-
await
|
|
3471
|
-
await
|
|
3687
|
+
await mkdir4(path19.dirname(abs), { recursive: true });
|
|
3688
|
+
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
3472
3689
|
`, "utf-8");
|
|
3473
3690
|
}
|
|
3474
3691
|
function resolveJsonPath(root, jsonPath) {
|
|
3475
|
-
return
|
|
3692
|
+
return path19.isAbsolute(jsonPath) ? jsonPath : path19.resolve(root, jsonPath);
|
|
3476
3693
|
}
|
|
3477
3694
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3478
3695
|
|
|
@@ -3487,6 +3704,7 @@ function parseArgs(argv, cwd) {
|
|
|
3487
3704
|
dryRun: false,
|
|
3488
3705
|
reportFormat: "md",
|
|
3489
3706
|
reportRunValidate: false,
|
|
3707
|
+
doctorFormat: "text",
|
|
3490
3708
|
validateFormat: "text",
|
|
3491
3709
|
strict: false,
|
|
3492
3710
|
help: false
|
|
@@ -3539,7 +3757,11 @@ function parseArgs(argv, cwd) {
|
|
|
3539
3757
|
{
|
|
3540
3758
|
const next = args[i + 1];
|
|
3541
3759
|
if (next) {
|
|
3542
|
-
|
|
3760
|
+
if (command === "doctor") {
|
|
3761
|
+
options.doctorOut = next;
|
|
3762
|
+
} else {
|
|
3763
|
+
options.reportOut = next;
|
|
3764
|
+
}
|
|
3543
3765
|
}
|
|
3544
3766
|
}
|
|
3545
3767
|
i += 1;
|
|
@@ -3582,6 +3804,12 @@ function applyFormatOption(command, value, options) {
|
|
|
3582
3804
|
}
|
|
3583
3805
|
return;
|
|
3584
3806
|
}
|
|
3807
|
+
if (command === "doctor") {
|
|
3808
|
+
if (value === "text" || value === "json") {
|
|
3809
|
+
options.doctorFormat = value;
|
|
3810
|
+
}
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3585
3813
|
if (value === "md" || value === "json") {
|
|
3586
3814
|
options.reportFormat = value;
|
|
3587
3815
|
}
|
|
@@ -3629,6 +3857,14 @@ async function run(argv, cwd) {
|
|
|
3629
3857
|
});
|
|
3630
3858
|
}
|
|
3631
3859
|
return;
|
|
3860
|
+
case "doctor":
|
|
3861
|
+
await runDoctor({
|
|
3862
|
+
root: options.root,
|
|
3863
|
+
rootExplicit: options.rootExplicit,
|
|
3864
|
+
format: options.doctorFormat,
|
|
3865
|
+
...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
|
|
3866
|
+
});
|
|
3867
|
+
return;
|
|
3632
3868
|
default:
|
|
3633
3869
|
error(`Unknown command: ${command}`);
|
|
3634
3870
|
info(usage());
|
|
@@ -3642,6 +3878,7 @@ Commands:
|
|
|
3642
3878
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
3643
3879
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
3644
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
|
|
3645
3882
|
|
|
3646
3883
|
Options:
|
|
3647
3884
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -3651,9 +3888,10 @@ Options:
|
|
|
3651
3888
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
3652
3889
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3653
3890
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3891
|
+
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3654
3892
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3655
3893
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3656
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
3894
|
+
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
3657
3895
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3658
3896
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3659
3897
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|