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.cjs
CHANGED
|
@@ -23,170 +23,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
-
// src/cli/commands/
|
|
27
|
-
var
|
|
28
|
-
|
|
29
|
-
// src/cli/lib/fs.ts
|
|
30
|
-
var import_promises = require("fs/promises");
|
|
31
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
32
|
-
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
33
|
-
const files = await collectTemplateFiles(sourceRoot);
|
|
34
|
-
return copyFiles(files, sourceRoot, destRoot, options);
|
|
35
|
-
}
|
|
36
|
-
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
37
|
-
const copied = [];
|
|
38
|
-
const skipped = [];
|
|
39
|
-
const conflicts = [];
|
|
40
|
-
if (!options.force) {
|
|
41
|
-
for (const file of files) {
|
|
42
|
-
const relative = import_node_path.default.relative(sourceRoot, file);
|
|
43
|
-
const dest = import_node_path.default.join(destRoot, relative);
|
|
44
|
-
if (!await shouldWrite(dest, options.force)) {
|
|
45
|
-
conflicts.push(dest);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (conflicts.length > 0) {
|
|
49
|
-
throw new Error(formatConflictMessage(conflicts));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
for (const file of files) {
|
|
53
|
-
const relative = import_node_path.default.relative(sourceRoot, file);
|
|
54
|
-
const dest = import_node_path.default.join(destRoot, relative);
|
|
55
|
-
if (!await shouldWrite(dest, options.force)) {
|
|
56
|
-
skipped.push(dest);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (!options.dryRun) {
|
|
60
|
-
await (0, import_promises.mkdir)(import_node_path.default.dirname(dest), { recursive: true });
|
|
61
|
-
await (0, import_promises.copyFile)(file, dest);
|
|
62
|
-
}
|
|
63
|
-
copied.push(dest);
|
|
64
|
-
}
|
|
65
|
-
return { copied, skipped };
|
|
66
|
-
}
|
|
67
|
-
function formatConflictMessage(conflicts) {
|
|
68
|
-
return [
|
|
69
|
-
"\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",
|
|
70
|
-
"",
|
|
71
|
-
"\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
|
|
72
|
-
...conflicts.map((conflict) => `- ${conflict}`),
|
|
73
|
-
"",
|
|
74
|
-
"\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"
|
|
75
|
-
].join("\n");
|
|
76
|
-
}
|
|
77
|
-
async function collectTemplateFiles(root) {
|
|
78
|
-
const entries = [];
|
|
79
|
-
if (!await exists(root)) {
|
|
80
|
-
return entries;
|
|
81
|
-
}
|
|
82
|
-
const items = await (0, import_promises.readdir)(root, { withFileTypes: true });
|
|
83
|
-
for (const item of items) {
|
|
84
|
-
const fullPath = import_node_path.default.join(root, item.name);
|
|
85
|
-
if (item.isDirectory()) {
|
|
86
|
-
const nested = await collectTemplateFiles(fullPath);
|
|
87
|
-
entries.push(...nested);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (item.isFile()) {
|
|
91
|
-
entries.push(fullPath);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return entries;
|
|
95
|
-
}
|
|
96
|
-
async function shouldWrite(target, force) {
|
|
97
|
-
if (force) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
return !await exists(target);
|
|
101
|
-
}
|
|
102
|
-
async function exists(target) {
|
|
103
|
-
try {
|
|
104
|
-
await (0, import_promises.access)(target);
|
|
105
|
-
return true;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/cli/lib/assets.ts
|
|
112
|
-
var import_node_fs = require("fs");
|
|
113
|
-
var import_node_path2 = __toESM(require("path"), 1);
|
|
114
|
-
var import_node_url = require("url");
|
|
115
|
-
function getInitAssetsDir() {
|
|
116
|
-
const base = __filename;
|
|
117
|
-
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
118
|
-
const baseDir = import_node_path2.default.dirname(basePath);
|
|
119
|
-
const candidates = [
|
|
120
|
-
import_node_path2.default.resolve(baseDir, "../../../assets/init"),
|
|
121
|
-
import_node_path2.default.resolve(baseDir, "../../assets/init")
|
|
122
|
-
];
|
|
123
|
-
for (const candidate of candidates) {
|
|
124
|
-
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
125
|
-
return candidate;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
throw new Error(
|
|
129
|
-
[
|
|
130
|
-
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
131
|
-
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
132
|
-
...candidates.map((candidate) => `- ${candidate}`)
|
|
133
|
-
].join("\n")
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// src/cli/lib/logger.ts
|
|
138
|
-
function info(message) {
|
|
139
|
-
process.stdout.write(`${message}
|
|
140
|
-
`);
|
|
141
|
-
}
|
|
142
|
-
function warn(message) {
|
|
143
|
-
process.stdout.write(`${message}
|
|
144
|
-
`);
|
|
145
|
-
}
|
|
146
|
-
function error(message) {
|
|
147
|
-
process.stderr.write(`${message}
|
|
148
|
-
`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// src/cli/commands/init.ts
|
|
152
|
-
async function runInit(options) {
|
|
153
|
-
const assetsRoot = getInitAssetsDir();
|
|
154
|
-
const rootAssets = import_node_path3.default.join(assetsRoot, "root");
|
|
155
|
-
const qfaiAssets = import_node_path3.default.join(assetsRoot, ".qfai");
|
|
156
|
-
const destRoot = import_node_path3.default.resolve(options.dir);
|
|
157
|
-
const destQfai = import_node_path3.default.join(destRoot, ".qfai");
|
|
158
|
-
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
159
|
-
force: options.force,
|
|
160
|
-
dryRun: options.dryRun
|
|
161
|
-
});
|
|
162
|
-
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
163
|
-
force: options.force,
|
|
164
|
-
dryRun: options.dryRun
|
|
165
|
-
});
|
|
166
|
-
report(
|
|
167
|
-
[...rootResult.copied, ...qfaiResult.copied],
|
|
168
|
-
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
169
|
-
options.dryRun,
|
|
170
|
-
"init"
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
function report(copied, skipped, dryRun, label) {
|
|
174
|
-
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
175
|
-
if (copied.length > 0) {
|
|
176
|
-
info(` created: ${copied.length}`);
|
|
177
|
-
}
|
|
178
|
-
if (skipped.length > 0) {
|
|
179
|
-
info(` skipped: ${skipped.length}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
26
|
+
// src/cli/commands/doctor.ts
|
|
27
|
+
var import_promises8 = require("fs/promises");
|
|
28
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
182
29
|
|
|
183
|
-
// src/
|
|
184
|
-
var
|
|
185
|
-
var
|
|
30
|
+
// src/core/doctor.ts
|
|
31
|
+
var import_promises7 = require("fs/promises");
|
|
32
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
186
33
|
|
|
187
34
|
// src/core/config.ts
|
|
188
|
-
var
|
|
189
|
-
var
|
|
35
|
+
var import_promises = require("fs/promises");
|
|
36
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
190
37
|
var import_yaml = require("yaml");
|
|
191
38
|
var defaultConfig = {
|
|
192
39
|
paths: {
|
|
@@ -226,17 +73,17 @@ var defaultConfig = {
|
|
|
226
73
|
}
|
|
227
74
|
};
|
|
228
75
|
function getConfigPath(root) {
|
|
229
|
-
return
|
|
76
|
+
return import_node_path.default.join(root, "qfai.config.yaml");
|
|
230
77
|
}
|
|
231
78
|
async function findConfigRoot(startDir) {
|
|
232
|
-
const resolvedStart =
|
|
79
|
+
const resolvedStart = import_node_path.default.resolve(startDir);
|
|
233
80
|
let current = resolvedStart;
|
|
234
81
|
while (true) {
|
|
235
82
|
const configPath = getConfigPath(current);
|
|
236
|
-
if (await
|
|
83
|
+
if (await exists(configPath)) {
|
|
237
84
|
return { root: current, configPath, found: true };
|
|
238
85
|
}
|
|
239
|
-
const parent =
|
|
86
|
+
const parent = import_node_path.default.dirname(current);
|
|
240
87
|
if (parent === current) {
|
|
241
88
|
break;
|
|
242
89
|
}
|
|
@@ -253,7 +100,7 @@ async function loadConfig(root) {
|
|
|
253
100
|
const issues = [];
|
|
254
101
|
let parsed;
|
|
255
102
|
try {
|
|
256
|
-
const raw = await (0,
|
|
103
|
+
const raw = await (0, import_promises.readFile)(configPath, "utf-8");
|
|
257
104
|
parsed = (0, import_yaml.parse)(raw);
|
|
258
105
|
} catch (error2) {
|
|
259
106
|
if (isMissingFile(error2)) {
|
|
@@ -266,7 +113,7 @@ async function loadConfig(root) {
|
|
|
266
113
|
return { config: normalized, issues, configPath };
|
|
267
114
|
}
|
|
268
115
|
function resolvePath(root, config, key) {
|
|
269
|
-
return
|
|
116
|
+
return import_node_path.default.resolve(root, config.paths[key]);
|
|
270
117
|
}
|
|
271
118
|
function normalizeConfig(raw, configPath, issues) {
|
|
272
119
|
if (!isRecord(raw)) {
|
|
@@ -565,9 +412,9 @@ function isMissingFile(error2) {
|
|
|
565
412
|
}
|
|
566
413
|
return false;
|
|
567
414
|
}
|
|
568
|
-
async function
|
|
415
|
+
async function exists(target) {
|
|
569
416
|
try {
|
|
570
|
-
await (0,
|
|
417
|
+
await (0, import_promises.access)(target);
|
|
571
418
|
return true;
|
|
572
419
|
} catch {
|
|
573
420
|
return false;
|
|
@@ -583,76 +430,12 @@ function isRecord(value) {
|
|
|
583
430
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
584
431
|
}
|
|
585
432
|
|
|
586
|
-
// src/core/paths.ts
|
|
587
|
-
var import_node_path5 = __toESM(require("path"), 1);
|
|
588
|
-
function toRelativePath(root, target) {
|
|
589
|
-
if (!target) {
|
|
590
|
-
return target;
|
|
591
|
-
}
|
|
592
|
-
if (!import_node_path5.default.isAbsolute(target)) {
|
|
593
|
-
return toPosixPath(target);
|
|
594
|
-
}
|
|
595
|
-
const relative = import_node_path5.default.relative(root, target);
|
|
596
|
-
if (!relative) {
|
|
597
|
-
return ".";
|
|
598
|
-
}
|
|
599
|
-
return toPosixPath(relative);
|
|
600
|
-
}
|
|
601
|
-
function toPosixPath(value) {
|
|
602
|
-
return value.replace(/\\/g, "/");
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// src/core/normalize.ts
|
|
606
|
-
function normalizeIssuePaths(root, issues) {
|
|
607
|
-
return issues.map((issue7) => {
|
|
608
|
-
if (!issue7.file) {
|
|
609
|
-
return issue7;
|
|
610
|
-
}
|
|
611
|
-
const normalized = toRelativePath(root, issue7.file);
|
|
612
|
-
if (normalized === issue7.file) {
|
|
613
|
-
return issue7;
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
...issue7,
|
|
617
|
-
file: normalized
|
|
618
|
-
};
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
function normalizeScCoverage(root, sc) {
|
|
622
|
-
const refs = {};
|
|
623
|
-
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
624
|
-
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
625
|
-
}
|
|
626
|
-
return {
|
|
627
|
-
...sc,
|
|
628
|
-
refs
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
function normalizeValidationResult(root, result) {
|
|
632
|
-
return {
|
|
633
|
-
...result,
|
|
634
|
-
issues: normalizeIssuePaths(root, result.issues),
|
|
635
|
-
traceability: {
|
|
636
|
-
...result.traceability,
|
|
637
|
-
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// src/core/report.ts
|
|
643
|
-
var import_promises15 = require("fs/promises");
|
|
644
|
-
var import_node_path15 = __toESM(require("path"), 1);
|
|
645
|
-
|
|
646
|
-
// src/core/contractIndex.ts
|
|
647
|
-
var import_promises6 = require("fs/promises");
|
|
648
|
-
var import_node_path8 = __toESM(require("path"), 1);
|
|
649
|
-
|
|
650
433
|
// src/core/discovery.ts
|
|
651
|
-
var
|
|
434
|
+
var import_promises4 = require("fs/promises");
|
|
652
435
|
|
|
653
436
|
// src/core/fs.ts
|
|
654
|
-
var
|
|
655
|
-
var
|
|
437
|
+
var import_promises2 = require("fs/promises");
|
|
438
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
656
439
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
657
440
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
658
441
|
"node_modules",
|
|
@@ -664,7 +447,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
664
447
|
]);
|
|
665
448
|
async function collectFiles(root, options = {}) {
|
|
666
449
|
const entries = [];
|
|
667
|
-
if (!await
|
|
450
|
+
if (!await exists2(root)) {
|
|
668
451
|
return entries;
|
|
669
452
|
}
|
|
670
453
|
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
@@ -688,9 +471,9 @@ async function collectFilesByGlobs(root, options) {
|
|
|
688
471
|
});
|
|
689
472
|
}
|
|
690
473
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
691
|
-
const items = await (0,
|
|
474
|
+
const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
|
|
692
475
|
for (const item of items) {
|
|
693
|
-
const fullPath =
|
|
476
|
+
const fullPath = import_node_path2.default.join(current, item.name);
|
|
694
477
|
if (item.isDirectory()) {
|
|
695
478
|
if (ignoreDirs.has(item.name)) {
|
|
696
479
|
continue;
|
|
@@ -700,7 +483,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
700
483
|
}
|
|
701
484
|
if (item.isFile()) {
|
|
702
485
|
if (extensions.length > 0) {
|
|
703
|
-
const ext =
|
|
486
|
+
const ext = import_node_path2.default.extname(item.name).toLowerCase();
|
|
704
487
|
if (!extensions.includes(ext)) {
|
|
705
488
|
continue;
|
|
706
489
|
}
|
|
@@ -709,9 +492,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
709
492
|
}
|
|
710
493
|
}
|
|
711
494
|
}
|
|
712
|
-
async function
|
|
495
|
+
async function exists2(target) {
|
|
713
496
|
try {
|
|
714
|
-
await (0,
|
|
497
|
+
await (0, import_promises2.access)(target);
|
|
715
498
|
return true;
|
|
716
499
|
} catch {
|
|
717
500
|
return false;
|
|
@@ -719,23 +502,23 @@ async function exists3(target) {
|
|
|
719
502
|
}
|
|
720
503
|
|
|
721
504
|
// src/core/specLayout.ts
|
|
722
|
-
var
|
|
723
|
-
var
|
|
505
|
+
var import_promises3 = require("fs/promises");
|
|
506
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
724
507
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
725
508
|
async function collectSpecEntries(specsRoot) {
|
|
726
509
|
const dirs = await listSpecDirs(specsRoot);
|
|
727
510
|
const entries = dirs.map((dir) => ({
|
|
728
511
|
dir,
|
|
729
|
-
specPath:
|
|
730
|
-
deltaPath:
|
|
731
|
-
scenarioPath:
|
|
512
|
+
specPath: import_node_path3.default.join(dir, "spec.md"),
|
|
513
|
+
deltaPath: import_node_path3.default.join(dir, "delta.md"),
|
|
514
|
+
scenarioPath: import_node_path3.default.join(dir, "scenario.md")
|
|
732
515
|
}));
|
|
733
516
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
734
517
|
}
|
|
735
518
|
async function listSpecDirs(specsRoot) {
|
|
736
519
|
try {
|
|
737
|
-
const items = await (0,
|
|
738
|
-
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) =>
|
|
520
|
+
const items = await (0, import_promises3.readdir)(specsRoot, { withFileTypes: true });
|
|
521
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path3.default.join(specsRoot, name));
|
|
739
522
|
} catch (error2) {
|
|
740
523
|
if (isMissingFileError(error2)) {
|
|
741
524
|
return [];
|
|
@@ -783,298 +566,43 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
|
783
566
|
async function filterExisting(files) {
|
|
784
567
|
const existing = [];
|
|
785
568
|
for (const file of files) {
|
|
786
|
-
if (await
|
|
569
|
+
if (await exists3(file)) {
|
|
787
570
|
existing.push(file);
|
|
788
571
|
}
|
|
789
572
|
}
|
|
790
573
|
return existing;
|
|
791
574
|
}
|
|
792
|
-
async function
|
|
575
|
+
async function exists3(target) {
|
|
793
576
|
try {
|
|
794
|
-
await (0,
|
|
577
|
+
await (0, import_promises4.access)(target);
|
|
795
578
|
return true;
|
|
796
579
|
} catch {
|
|
797
580
|
return false;
|
|
798
581
|
}
|
|
799
582
|
}
|
|
800
583
|
|
|
801
|
-
// src/core/
|
|
802
|
-
var
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
807
|
-
const id = match[1];
|
|
808
|
-
if (id) {
|
|
809
|
-
ids.push(id);
|
|
810
|
-
}
|
|
584
|
+
// src/core/paths.ts
|
|
585
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
586
|
+
function toRelativePath(root, target) {
|
|
587
|
+
if (!target) {
|
|
588
|
+
return target;
|
|
811
589
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
590
|
+
if (!import_node_path4.default.isAbsolute(target)) {
|
|
591
|
+
return toPosixPath(target);
|
|
592
|
+
}
|
|
593
|
+
const relative = import_node_path4.default.relative(root, target);
|
|
594
|
+
if (!relative) {
|
|
595
|
+
return ".";
|
|
596
|
+
}
|
|
597
|
+
return toPosixPath(relative);
|
|
816
598
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
async function buildContractIndex(root, config) {
|
|
820
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
821
|
-
const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
|
|
822
|
-
const apiRoot = import_node_path8.default.join(contractsRoot, "api");
|
|
823
|
-
const dbRoot = import_node_path8.default.join(contractsRoot, "db");
|
|
824
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
825
|
-
collectUiContractFiles(uiRoot),
|
|
826
|
-
collectApiContractFiles(apiRoot),
|
|
827
|
-
collectDbContractFiles(dbRoot)
|
|
828
|
-
]);
|
|
829
|
-
const index = {
|
|
830
|
-
ids: /* @__PURE__ */ new Set(),
|
|
831
|
-
idToFiles: /* @__PURE__ */ new Map(),
|
|
832
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
833
|
-
};
|
|
834
|
-
await indexContractFiles(uiFiles, index);
|
|
835
|
-
await indexContractFiles(apiFiles, index);
|
|
836
|
-
await indexContractFiles(dbFiles, index);
|
|
837
|
-
return index;
|
|
838
|
-
}
|
|
839
|
-
async function indexContractFiles(files, index) {
|
|
840
|
-
for (const file of files) {
|
|
841
|
-
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
842
|
-
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
function record(index, id, file) {
|
|
846
|
-
index.ids.add(id);
|
|
847
|
-
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
848
|
-
current.add(file);
|
|
849
|
-
index.idToFiles.set(id, current);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/core/ids.ts
|
|
853
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
854
|
-
var STRICT_ID_PATTERNS = {
|
|
855
|
-
SPEC: /\bSPEC-\d{4}\b/g,
|
|
856
|
-
BR: /\bBR-\d{4}\b/g,
|
|
857
|
-
SC: /\bSC-\d{4}\b/g,
|
|
858
|
-
UI: /\bUI-\d{4}\b/g,
|
|
859
|
-
API: /\bAPI-\d{4}\b/g,
|
|
860
|
-
DB: /\bDB-\d{4}\b/g,
|
|
861
|
-
ADR: /\bADR-\d{4}\b/g
|
|
862
|
-
};
|
|
863
|
-
var LOOSE_ID_PATTERNS = {
|
|
864
|
-
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
865
|
-
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
866
|
-
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
867
|
-
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
868
|
-
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
869
|
-
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
870
|
-
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
871
|
-
};
|
|
872
|
-
function extractIds(text, prefix) {
|
|
873
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
874
|
-
const matches = text.match(pattern);
|
|
875
|
-
return unique(matches ?? []);
|
|
876
|
-
}
|
|
877
|
-
function extractAllIds(text) {
|
|
878
|
-
const all = [];
|
|
879
|
-
ID_PREFIXES.forEach((prefix) => {
|
|
880
|
-
all.push(...extractIds(text, prefix));
|
|
881
|
-
});
|
|
882
|
-
return unique(all);
|
|
883
|
-
}
|
|
884
|
-
function extractInvalidIds(text, prefixes) {
|
|
885
|
-
const invalid = [];
|
|
886
|
-
for (const prefix of prefixes) {
|
|
887
|
-
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
888
|
-
for (const candidate of candidates) {
|
|
889
|
-
if (!isValidId(candidate, prefix)) {
|
|
890
|
-
invalid.push(candidate);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
return unique(invalid);
|
|
895
|
-
}
|
|
896
|
-
function unique(values) {
|
|
897
|
-
return Array.from(new Set(values));
|
|
898
|
-
}
|
|
899
|
-
function isValidId(value, prefix) {
|
|
900
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
901
|
-
const strict = new RegExp(pattern.source);
|
|
902
|
-
return strict.test(value);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// src/core/parse/contractRefs.ts
|
|
906
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
907
|
-
function parseContractRefs(text, options = {}) {
|
|
908
|
-
const linePattern = buildLinePattern(options);
|
|
909
|
-
const lines = [];
|
|
910
|
-
for (const match of text.matchAll(linePattern)) {
|
|
911
|
-
lines.push((match[1] ?? "").trim());
|
|
912
|
-
}
|
|
913
|
-
const ids = [];
|
|
914
|
-
const invalidTokens = [];
|
|
915
|
-
let hasNone = false;
|
|
916
|
-
for (const line of lines) {
|
|
917
|
-
if (line.length === 0) {
|
|
918
|
-
invalidTokens.push("(empty)");
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
922
|
-
for (const token of tokens) {
|
|
923
|
-
if (token.length === 0) {
|
|
924
|
-
invalidTokens.push("(empty)");
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
if (token === "none") {
|
|
928
|
-
hasNone = true;
|
|
929
|
-
continue;
|
|
930
|
-
}
|
|
931
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
932
|
-
ids.push(token);
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
invalidTokens.push(token);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return {
|
|
939
|
-
lines,
|
|
940
|
-
ids: unique2(ids),
|
|
941
|
-
invalidTokens: unique2(invalidTokens),
|
|
942
|
-
hasNone
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
function buildLinePattern(options) {
|
|
946
|
-
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
947
|
-
return new RegExp(
|
|
948
|
-
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
949
|
-
"gm"
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
function unique2(values) {
|
|
953
|
-
return Array.from(new Set(values));
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// src/core/parse/markdown.ts
|
|
957
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
958
|
-
function parseHeadings(md) {
|
|
959
|
-
const lines = md.split(/\r?\n/);
|
|
960
|
-
const headings = [];
|
|
961
|
-
for (let i = 0; i < lines.length; i++) {
|
|
962
|
-
const line = lines[i] ?? "";
|
|
963
|
-
const match = line.match(HEADING_RE);
|
|
964
|
-
if (!match) continue;
|
|
965
|
-
const levelToken = match[1];
|
|
966
|
-
const title = match[2];
|
|
967
|
-
if (!levelToken || !title) continue;
|
|
968
|
-
headings.push({
|
|
969
|
-
level: levelToken.length,
|
|
970
|
-
title: title.trim(),
|
|
971
|
-
line: i + 1
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
return headings;
|
|
975
|
-
}
|
|
976
|
-
function extractH2Sections(md) {
|
|
977
|
-
const lines = md.split(/\r?\n/);
|
|
978
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
979
|
-
const sections = /* @__PURE__ */ new Map();
|
|
980
|
-
for (let i = 0; i < headings.length; i++) {
|
|
981
|
-
const current = headings[i];
|
|
982
|
-
if (!current) continue;
|
|
983
|
-
const next = headings[i + 1];
|
|
984
|
-
const startLine = current.line + 1;
|
|
985
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
986
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
987
|
-
sections.set(current.title.trim(), {
|
|
988
|
-
title: current.title.trim(),
|
|
989
|
-
startLine,
|
|
990
|
-
endLine,
|
|
991
|
-
body
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
return sections;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// src/core/parse/spec.ts
|
|
998
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
999
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1000
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1001
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1002
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1003
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1004
|
-
function parseSpec(md, file) {
|
|
1005
|
-
const headings = parseHeadings(md);
|
|
1006
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1007
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1008
|
-
const sections = extractH2Sections(md);
|
|
1009
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1010
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1011
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1012
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1013
|
-
const brs = [];
|
|
1014
|
-
const brsWithoutPriority = [];
|
|
1015
|
-
const brsWithInvalidPriority = [];
|
|
1016
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1017
|
-
const lineText = brLines[i] ?? "";
|
|
1018
|
-
const lineNumber = startLine + i;
|
|
1019
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1020
|
-
if (validMatch) {
|
|
1021
|
-
const id = validMatch[1];
|
|
1022
|
-
const priority = validMatch[2];
|
|
1023
|
-
const text = validMatch[3];
|
|
1024
|
-
if (!id || !priority || !text) continue;
|
|
1025
|
-
brs.push({
|
|
1026
|
-
id,
|
|
1027
|
-
priority,
|
|
1028
|
-
text: text.trim(),
|
|
1029
|
-
line: lineNumber
|
|
1030
|
-
});
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1034
|
-
if (anyPriorityMatch) {
|
|
1035
|
-
const id = anyPriorityMatch[1];
|
|
1036
|
-
const priority = anyPriorityMatch[2];
|
|
1037
|
-
const text = anyPriorityMatch[3];
|
|
1038
|
-
if (!id || !priority || !text) continue;
|
|
1039
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1040
|
-
brsWithInvalidPriority.push({
|
|
1041
|
-
id,
|
|
1042
|
-
priority,
|
|
1043
|
-
text: text.trim(),
|
|
1044
|
-
line: lineNumber
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
continue;
|
|
1048
|
-
}
|
|
1049
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1050
|
-
if (noPriorityMatch) {
|
|
1051
|
-
const id = noPriorityMatch[1];
|
|
1052
|
-
const text = noPriorityMatch[2];
|
|
1053
|
-
if (!id || !text) continue;
|
|
1054
|
-
brsWithoutPriority.push({
|
|
1055
|
-
id,
|
|
1056
|
-
text: text.trim(),
|
|
1057
|
-
line: lineNumber
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
const parsed = {
|
|
1062
|
-
file,
|
|
1063
|
-
sections: sectionNames,
|
|
1064
|
-
brs,
|
|
1065
|
-
brsWithoutPriority,
|
|
1066
|
-
brsWithInvalidPriority,
|
|
1067
|
-
contractRefs: parseContractRefs(md)
|
|
1068
|
-
};
|
|
1069
|
-
if (specId) {
|
|
1070
|
-
parsed.specId = specId;
|
|
1071
|
-
}
|
|
1072
|
-
return parsed;
|
|
599
|
+
function toPosixPath(value) {
|
|
600
|
+
return value.replace(/\\/g, "/");
|
|
1073
601
|
}
|
|
1074
602
|
|
|
1075
603
|
// src/core/traceability.ts
|
|
1076
|
-
var
|
|
1077
|
-
var
|
|
604
|
+
var import_promises5 = require("fs/promises");
|
|
605
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1078
606
|
|
|
1079
607
|
// src/core/gherkin/parse.ts
|
|
1080
608
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -1130,13 +658,13 @@ function parseScenarioDocument(text, uri) {
|
|
|
1130
658
|
};
|
|
1131
659
|
}
|
|
1132
660
|
function buildScenarioAtoms(document, contractIds = []) {
|
|
1133
|
-
const uniqueContractIds =
|
|
661
|
+
const uniqueContractIds = unique(contractIds).sort(
|
|
1134
662
|
(a, b) => a.localeCompare(b)
|
|
1135
663
|
);
|
|
1136
664
|
return document.scenarios.map((scenario) => {
|
|
1137
665
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1138
666
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1139
|
-
const brIds =
|
|
667
|
+
const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1140
668
|
const atom = {
|
|
1141
669
|
uri: document.uri,
|
|
1142
670
|
featureName: document.featureName ?? "",
|
|
@@ -1196,7 +724,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
1196
724
|
function collectTagNames(tags) {
|
|
1197
725
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1198
726
|
}
|
|
1199
|
-
function
|
|
727
|
+
function unique(values) {
|
|
1200
728
|
return Array.from(new Set(values));
|
|
1201
729
|
}
|
|
1202
730
|
|
|
@@ -1226,7 +754,7 @@ function extractAnnotatedScIds(text) {
|
|
|
1226
754
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
1227
755
|
const scIds = /* @__PURE__ */ new Set();
|
|
1228
756
|
for (const file of scenarioFiles) {
|
|
1229
|
-
const text = await (0,
|
|
757
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
1230
758
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1231
759
|
if (!document || errors.length > 0) {
|
|
1232
760
|
continue;
|
|
@@ -1244,7 +772,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
1244
772
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1245
773
|
const sources = /* @__PURE__ */ new Map();
|
|
1246
774
|
for (const file of scenarioFiles) {
|
|
1247
|
-
const text = await (0,
|
|
775
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
1248
776
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1249
777
|
if (!document || errors.length > 0) {
|
|
1250
778
|
continue;
|
|
@@ -1296,99 +824,883 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1296
824
|
error: formatError3(error2)
|
|
1297
825
|
};
|
|
1298
826
|
}
|
|
1299
|
-
const normalizedFiles = Array.from(
|
|
1300
|
-
new Set(files.map((file) =>
|
|
1301
|
-
);
|
|
1302
|
-
for (const file of normalizedFiles) {
|
|
1303
|
-
const text = await (0,
|
|
1304
|
-
const scIds = extractAnnotatedScIds(text);
|
|
1305
|
-
if (scIds.length === 0) {
|
|
827
|
+
const normalizedFiles = Array.from(
|
|
828
|
+
new Set(files.map((file) => import_node_path5.default.normalize(file)))
|
|
829
|
+
);
|
|
830
|
+
for (const file of normalizedFiles) {
|
|
831
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
832
|
+
const scIds = extractAnnotatedScIds(text);
|
|
833
|
+
if (scIds.length === 0) {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
for (const scId of scIds) {
|
|
837
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
838
|
+
current.add(file);
|
|
839
|
+
refs.set(scId, current);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
refs,
|
|
844
|
+
scan: {
|
|
845
|
+
globs: normalizedGlobs,
|
|
846
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
847
|
+
matchedFileCount: normalizedFiles.length
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function buildScCoverage(scIds, refs) {
|
|
852
|
+
const sortedScIds = toSortedArray(scIds);
|
|
853
|
+
const refsRecord = {};
|
|
854
|
+
const missingIds = [];
|
|
855
|
+
let covered = 0;
|
|
856
|
+
for (const scId of sortedScIds) {
|
|
857
|
+
const files = refs.get(scId);
|
|
858
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
859
|
+
refsRecord[scId] = sortedFiles;
|
|
860
|
+
if (sortedFiles.length === 0) {
|
|
861
|
+
missingIds.push(scId);
|
|
862
|
+
} else {
|
|
863
|
+
covered += 1;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
total: sortedScIds.length,
|
|
868
|
+
covered,
|
|
869
|
+
missing: missingIds.length,
|
|
870
|
+
missingIds,
|
|
871
|
+
refs: refsRecord
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function toSortedArray(values) {
|
|
875
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
876
|
+
}
|
|
877
|
+
function normalizeGlobs(globs) {
|
|
878
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
879
|
+
}
|
|
880
|
+
function formatError3(error2) {
|
|
881
|
+
if (error2 instanceof Error) {
|
|
882
|
+
return error2.message;
|
|
883
|
+
}
|
|
884
|
+
return String(error2);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/core/version.ts
|
|
888
|
+
var import_promises6 = require("fs/promises");
|
|
889
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
890
|
+
var import_node_url = require("url");
|
|
891
|
+
async function resolveToolVersion() {
|
|
892
|
+
if ("0.6.2".length > 0) {
|
|
893
|
+
return "0.6.2";
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const packagePath = resolvePackageJsonPath();
|
|
897
|
+
const raw = await (0, import_promises6.readFile)(packagePath, "utf-8");
|
|
898
|
+
const parsed = JSON.parse(raw);
|
|
899
|
+
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
900
|
+
return version.length > 0 ? version : "unknown";
|
|
901
|
+
} catch {
|
|
902
|
+
return "unknown";
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function resolvePackageJsonPath() {
|
|
906
|
+
const base = __filename;
|
|
907
|
+
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
908
|
+
return import_node_path6.default.resolve(import_node_path6.default.dirname(basePath), "../../package.json");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/core/doctor.ts
|
|
912
|
+
async function exists4(target) {
|
|
913
|
+
try {
|
|
914
|
+
await (0, import_promises7.access)(target);
|
|
915
|
+
return true;
|
|
916
|
+
} catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function addCheck(checks, check) {
|
|
921
|
+
checks.push(check);
|
|
922
|
+
}
|
|
923
|
+
function summarize(checks) {
|
|
924
|
+
const summary = { ok: 0, warning: 0, error: 0 };
|
|
925
|
+
for (const check of checks) {
|
|
926
|
+
summary[check.severity] += 1;
|
|
927
|
+
}
|
|
928
|
+
return summary;
|
|
929
|
+
}
|
|
930
|
+
function normalizeGlobs2(values) {
|
|
931
|
+
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
932
|
+
}
|
|
933
|
+
async function createDoctorData(options) {
|
|
934
|
+
const startDir = import_node_path7.default.resolve(options.startDir);
|
|
935
|
+
const checks = [];
|
|
936
|
+
const configPath = getConfigPath(startDir);
|
|
937
|
+
const search = options.rootExplicit ? {
|
|
938
|
+
root: startDir,
|
|
939
|
+
configPath,
|
|
940
|
+
found: await exists4(configPath)
|
|
941
|
+
} : await findConfigRoot(startDir);
|
|
942
|
+
const root = search.root;
|
|
943
|
+
const version = await resolveToolVersion();
|
|
944
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
945
|
+
addCheck(checks, {
|
|
946
|
+
id: "config.search",
|
|
947
|
+
severity: search.found ? "ok" : "warning",
|
|
948
|
+
title: "Config search",
|
|
949
|
+
message: search.found ? "qfai.config.yaml found" : "qfai.config.yaml not found (default config will be used)",
|
|
950
|
+
details: { configPath: toRelativePath(root, search.configPath) }
|
|
951
|
+
});
|
|
952
|
+
const {
|
|
953
|
+
config,
|
|
954
|
+
issues,
|
|
955
|
+
configPath: resolvedConfigPath
|
|
956
|
+
} = await loadConfig(root);
|
|
957
|
+
if (issues.length === 0) {
|
|
958
|
+
addCheck(checks, {
|
|
959
|
+
id: "config.load",
|
|
960
|
+
severity: "ok",
|
|
961
|
+
title: "Config load",
|
|
962
|
+
message: "Loaded and normalized with 0 issues",
|
|
963
|
+
details: { configPath: toRelativePath(root, resolvedConfigPath) }
|
|
964
|
+
});
|
|
965
|
+
} else {
|
|
966
|
+
addCheck(checks, {
|
|
967
|
+
id: "config.load",
|
|
968
|
+
severity: "warning",
|
|
969
|
+
title: "Config load",
|
|
970
|
+
message: `Loaded with ${issues.length} issue(s) (normalized with defaults when needed)`,
|
|
971
|
+
details: {
|
|
972
|
+
configPath: toRelativePath(root, resolvedConfigPath),
|
|
973
|
+
issues
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
const pathKeys = [
|
|
978
|
+
"specsDir",
|
|
979
|
+
"contractsDir",
|
|
980
|
+
"outDir",
|
|
981
|
+
"srcDir",
|
|
982
|
+
"testsDir",
|
|
983
|
+
"rulesDir",
|
|
984
|
+
"promptsDir"
|
|
985
|
+
];
|
|
986
|
+
for (const key of pathKeys) {
|
|
987
|
+
const resolved = resolvePath(root, config, key);
|
|
988
|
+
const ok = await exists4(resolved);
|
|
989
|
+
addCheck(checks, {
|
|
990
|
+
id: `paths.${key}`,
|
|
991
|
+
severity: ok ? "ok" : "warning",
|
|
992
|
+
title: `Path exists: ${key}`,
|
|
993
|
+
message: ok ? `${key} exists` : `${key} is missing (did you run 'qfai init'?)`,
|
|
994
|
+
details: { path: toRelativePath(root, resolved) }
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
998
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
999
|
+
let missingFiles = 0;
|
|
1000
|
+
for (const entry of entries) {
|
|
1001
|
+
const requiredFiles = [entry.specPath, entry.deltaPath, entry.scenarioPath];
|
|
1002
|
+
for (const filePath of requiredFiles) {
|
|
1003
|
+
if (!await exists4(filePath)) {
|
|
1004
|
+
missingFiles += 1;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
addCheck(checks, {
|
|
1009
|
+
id: "spec.layout",
|
|
1010
|
+
severity: missingFiles === 0 ? "ok" : "warning",
|
|
1011
|
+
title: "Spec pack shape",
|
|
1012
|
+
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1013
|
+
details: { specPacks: entries.length, missingFiles }
|
|
1014
|
+
});
|
|
1015
|
+
const validateJsonAbs = import_node_path7.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path7.default.resolve(root, config.output.validateJsonPath);
|
|
1016
|
+
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1017
|
+
addCheck(checks, {
|
|
1018
|
+
id: "output.validateJson",
|
|
1019
|
+
severity: validateJsonExists ? "ok" : "warning",
|
|
1020
|
+
title: "validate.json",
|
|
1021
|
+
message: validateJsonExists ? "validate.json exists (report can run)" : "validate.json is missing (run 'qfai validate' before 'qfai report')",
|
|
1022
|
+
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1023
|
+
});
|
|
1024
|
+
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1025
|
+
const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
|
|
1026
|
+
const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
|
|
1027
|
+
addCheck(checks, {
|
|
1028
|
+
id: "output.pathAlignment",
|
|
1029
|
+
severity: inside ? "ok" : "warning",
|
|
1030
|
+
title: "Output path alignment",
|
|
1031
|
+
message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
|
|
1032
|
+
details: {
|
|
1033
|
+
outDir: toRelativePath(root, outDirAbs),
|
|
1034
|
+
validateJsonPath: toRelativePath(root, validateJsonAbs)
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
if (options.rootExplicit) {
|
|
1038
|
+
addCheck(checks, await buildOutDirCollisionCheck(root));
|
|
1039
|
+
}
|
|
1040
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1041
|
+
const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
|
|
1042
|
+
const exclude = normalizeGlobs2([
|
|
1043
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
1044
|
+
...config.validation.traceability.testFileExcludeGlobs
|
|
1045
|
+
]);
|
|
1046
|
+
try {
|
|
1047
|
+
const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
|
|
1048
|
+
const matchedCount = matched.length;
|
|
1049
|
+
const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
|
|
1050
|
+
addCheck(checks, {
|
|
1051
|
+
id: "traceability.testGlobs",
|
|
1052
|
+
severity,
|
|
1053
|
+
title: "Test file globs",
|
|
1054
|
+
message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
|
|
1055
|
+
details: {
|
|
1056
|
+
globs,
|
|
1057
|
+
excludeGlobs: exclude,
|
|
1058
|
+
scenarioFiles: scenarioFiles.length,
|
|
1059
|
+
scMustHaveTest: config.validation.traceability.scMustHaveTest
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error2) {
|
|
1063
|
+
addCheck(checks, {
|
|
1064
|
+
id: "traceability.testGlobs",
|
|
1065
|
+
severity: "error",
|
|
1066
|
+
title: "Test file globs",
|
|
1067
|
+
message: "Glob scan failed (invalid pattern or filesystem error)",
|
|
1068
|
+
details: { globs, excludeGlobs: exclude, error: String(error2) }
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
tool: "qfai",
|
|
1073
|
+
version,
|
|
1074
|
+
generatedAt,
|
|
1075
|
+
root: toRelativePath(process.cwd(), root),
|
|
1076
|
+
config: {
|
|
1077
|
+
startDir: toRelativePath(process.cwd(), startDir),
|
|
1078
|
+
found: search.found,
|
|
1079
|
+
configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
|
|
1080
|
+
},
|
|
1081
|
+
summary: summarize(checks),
|
|
1082
|
+
checks
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
var DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS = [
|
|
1086
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
1087
|
+
"**/.pnpm/**",
|
|
1088
|
+
"**/tmp/**",
|
|
1089
|
+
"**/.mcp-tools/**"
|
|
1090
|
+
];
|
|
1091
|
+
async function buildOutDirCollisionCheck(root) {
|
|
1092
|
+
try {
|
|
1093
|
+
const result = await detectOutDirCollisions(root);
|
|
1094
|
+
const relativeRoot = toRelativePath(process.cwd(), result.monorepoRoot);
|
|
1095
|
+
const configRoots = result.configRoots.map((configRoot) => toRelativePath(result.monorepoRoot, configRoot)).sort((a, b) => a.localeCompare(b));
|
|
1096
|
+
const collisions = result.collisions.map((item) => ({
|
|
1097
|
+
outDir: toRelativePath(result.monorepoRoot, item.outDir),
|
|
1098
|
+
roots: item.roots.map(
|
|
1099
|
+
(collisionRoot) => toRelativePath(result.monorepoRoot, collisionRoot)
|
|
1100
|
+
).sort((a, b) => a.localeCompare(b))
|
|
1101
|
+
})).sort((a, b) => a.outDir.localeCompare(b.outDir));
|
|
1102
|
+
const severity = collisions.length > 0 ? "warning" : "ok";
|
|
1103
|
+
const message = collisions.length > 0 ? `outDir collision detected (count=${collisions.length})` : `outDir collision not detected (configs=${configRoots.length})`;
|
|
1104
|
+
return {
|
|
1105
|
+
id: "output.outDirCollision",
|
|
1106
|
+
severity,
|
|
1107
|
+
title: "OutDir collision",
|
|
1108
|
+
message,
|
|
1109
|
+
details: {
|
|
1110
|
+
monorepoRoot: relativeRoot,
|
|
1111
|
+
configRoots,
|
|
1112
|
+
collisions
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
} catch (error2) {
|
|
1116
|
+
return {
|
|
1117
|
+
id: "output.outDirCollision",
|
|
1118
|
+
severity: "error",
|
|
1119
|
+
title: "OutDir collision",
|
|
1120
|
+
message: "OutDir collision scan failed",
|
|
1121
|
+
details: { error: String(error2) }
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async function detectOutDirCollisions(root) {
|
|
1126
|
+
const monorepoRoot = await findMonorepoRoot(root);
|
|
1127
|
+
const configPaths = await collectFilesByGlobs(monorepoRoot, {
|
|
1128
|
+
globs: ["**/qfai.config.yaml"],
|
|
1129
|
+
ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
|
|
1130
|
+
});
|
|
1131
|
+
const configRoots = Array.from(
|
|
1132
|
+
new Set(configPaths.map((configPath) => import_node_path7.default.dirname(configPath)))
|
|
1133
|
+
).sort((a, b) => a.localeCompare(b));
|
|
1134
|
+
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const configRoot of configRoots) {
|
|
1136
|
+
const { config } = await loadConfig(configRoot);
|
|
1137
|
+
const outDir = import_node_path7.default.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1138
|
+
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1139
|
+
roots.add(configRoot);
|
|
1140
|
+
outDirToRoots.set(outDir, roots);
|
|
1141
|
+
}
|
|
1142
|
+
const collisions = [];
|
|
1143
|
+
for (const [outDir, roots] of outDirToRoots.entries()) {
|
|
1144
|
+
if (roots.size > 1) {
|
|
1145
|
+
collisions.push({
|
|
1146
|
+
outDir,
|
|
1147
|
+
roots: Array.from(roots).sort((a, b) => a.localeCompare(b))
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return { monorepoRoot, configRoots, collisions };
|
|
1152
|
+
}
|
|
1153
|
+
async function findMonorepoRoot(startDir) {
|
|
1154
|
+
let current = import_node_path7.default.resolve(startDir);
|
|
1155
|
+
while (true) {
|
|
1156
|
+
const gitPath = import_node_path7.default.join(current, ".git");
|
|
1157
|
+
const workspacePath = import_node_path7.default.join(current, "pnpm-workspace.yaml");
|
|
1158
|
+
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1159
|
+
return current;
|
|
1160
|
+
}
|
|
1161
|
+
const parent = import_node_path7.default.dirname(current);
|
|
1162
|
+
if (parent === current) {
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
current = parent;
|
|
1166
|
+
}
|
|
1167
|
+
return import_node_path7.default.resolve(startDir);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/cli/lib/logger.ts
|
|
1171
|
+
function info(message) {
|
|
1172
|
+
process.stdout.write(`${message}
|
|
1173
|
+
`);
|
|
1174
|
+
}
|
|
1175
|
+
function warn(message) {
|
|
1176
|
+
process.stdout.write(`${message}
|
|
1177
|
+
`);
|
|
1178
|
+
}
|
|
1179
|
+
function error(message) {
|
|
1180
|
+
process.stderr.write(`${message}
|
|
1181
|
+
`);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/cli/commands/doctor.ts
|
|
1185
|
+
function formatDoctorText(data) {
|
|
1186
|
+
const lines = [];
|
|
1187
|
+
lines.push(
|
|
1188
|
+
`qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
|
|
1189
|
+
);
|
|
1190
|
+
for (const check of data.checks) {
|
|
1191
|
+
lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
|
|
1192
|
+
}
|
|
1193
|
+
lines.push(
|
|
1194
|
+
`summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
|
|
1195
|
+
);
|
|
1196
|
+
return lines.join("\n");
|
|
1197
|
+
}
|
|
1198
|
+
function formatDoctorJson(data) {
|
|
1199
|
+
return JSON.stringify(data, null, 2);
|
|
1200
|
+
}
|
|
1201
|
+
async function runDoctor(options) {
|
|
1202
|
+
const data = await createDoctorData({
|
|
1203
|
+
startDir: options.root,
|
|
1204
|
+
rootExplicit: options.rootExplicit
|
|
1205
|
+
});
|
|
1206
|
+
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1207
|
+
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1208
|
+
if (options.outPath) {
|
|
1209
|
+
const outAbs = import_node_path8.default.isAbsolute(options.outPath) ? options.outPath : import_node_path8.default.resolve(process.cwd(), options.outPath);
|
|
1210
|
+
await (0, import_promises8.mkdir)(import_node_path8.default.dirname(outAbs), { recursive: true });
|
|
1211
|
+
await (0, import_promises8.writeFile)(outAbs, `${output}
|
|
1212
|
+
`, "utf-8");
|
|
1213
|
+
info(`doctor: wrote ${outAbs}`);
|
|
1214
|
+
return exitCode;
|
|
1215
|
+
}
|
|
1216
|
+
info(output);
|
|
1217
|
+
return exitCode;
|
|
1218
|
+
}
|
|
1219
|
+
function shouldFailDoctor(summary, failOn) {
|
|
1220
|
+
if (!failOn) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
if (failOn === "error") {
|
|
1224
|
+
return summary.error > 0;
|
|
1225
|
+
}
|
|
1226
|
+
return summary.warning + summary.error > 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/cli/commands/init.ts
|
|
1230
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1231
|
+
|
|
1232
|
+
// src/cli/lib/fs.ts
|
|
1233
|
+
var import_promises9 = require("fs/promises");
|
|
1234
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1235
|
+
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1236
|
+
const files = await collectTemplateFiles(sourceRoot);
|
|
1237
|
+
return copyFiles(files, sourceRoot, destRoot, options);
|
|
1238
|
+
}
|
|
1239
|
+
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
1240
|
+
const copied = [];
|
|
1241
|
+
const skipped = [];
|
|
1242
|
+
const conflicts = [];
|
|
1243
|
+
if (!options.force) {
|
|
1244
|
+
for (const file of files) {
|
|
1245
|
+
const relative = import_node_path9.default.relative(sourceRoot, file);
|
|
1246
|
+
const dest = import_node_path9.default.join(destRoot, relative);
|
|
1247
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1248
|
+
conflicts.push(dest);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (conflicts.length > 0) {
|
|
1252
|
+
throw new Error(formatConflictMessage(conflicts));
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
for (const file of files) {
|
|
1256
|
+
const relative = import_node_path9.default.relative(sourceRoot, file);
|
|
1257
|
+
const dest = import_node_path9.default.join(destRoot, relative);
|
|
1258
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1259
|
+
skipped.push(dest);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (!options.dryRun) {
|
|
1263
|
+
await (0, import_promises9.mkdir)(import_node_path9.default.dirname(dest), { recursive: true });
|
|
1264
|
+
await (0, import_promises9.copyFile)(file, dest);
|
|
1265
|
+
}
|
|
1266
|
+
copied.push(dest);
|
|
1267
|
+
}
|
|
1268
|
+
return { copied, skipped };
|
|
1269
|
+
}
|
|
1270
|
+
function formatConflictMessage(conflicts) {
|
|
1271
|
+
return [
|
|
1272
|
+
"\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",
|
|
1273
|
+
"",
|
|
1274
|
+
"\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
|
|
1275
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
1276
|
+
"",
|
|
1277
|
+
"\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"
|
|
1278
|
+
].join("\n");
|
|
1279
|
+
}
|
|
1280
|
+
async function collectTemplateFiles(root) {
|
|
1281
|
+
const entries = [];
|
|
1282
|
+
if (!await exists5(root)) {
|
|
1283
|
+
return entries;
|
|
1284
|
+
}
|
|
1285
|
+
const items = await (0, import_promises9.readdir)(root, { withFileTypes: true });
|
|
1286
|
+
for (const item of items) {
|
|
1287
|
+
const fullPath = import_node_path9.default.join(root, item.name);
|
|
1288
|
+
if (item.isDirectory()) {
|
|
1289
|
+
const nested = await collectTemplateFiles(fullPath);
|
|
1290
|
+
entries.push(...nested);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (item.isFile()) {
|
|
1294
|
+
entries.push(fullPath);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return entries;
|
|
1298
|
+
}
|
|
1299
|
+
async function shouldWrite(target, force) {
|
|
1300
|
+
if (force) {
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
return !await exists5(target);
|
|
1304
|
+
}
|
|
1305
|
+
async function exists5(target) {
|
|
1306
|
+
try {
|
|
1307
|
+
await (0, import_promises9.access)(target);
|
|
1308
|
+
return true;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/cli/lib/assets.ts
|
|
1315
|
+
var import_node_fs = require("fs");
|
|
1316
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1317
|
+
var import_node_url2 = require("url");
|
|
1318
|
+
function getInitAssetsDir() {
|
|
1319
|
+
const base = __filename;
|
|
1320
|
+
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1321
|
+
const baseDir = import_node_path10.default.dirname(basePath);
|
|
1322
|
+
const candidates = [
|
|
1323
|
+
import_node_path10.default.resolve(baseDir, "../../../assets/init"),
|
|
1324
|
+
import_node_path10.default.resolve(baseDir, "../../assets/init")
|
|
1325
|
+
];
|
|
1326
|
+
for (const candidate of candidates) {
|
|
1327
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
1328
|
+
return candidate;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
[
|
|
1333
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1334
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1335
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
1336
|
+
].join("\n")
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/cli/commands/init.ts
|
|
1341
|
+
async function runInit(options) {
|
|
1342
|
+
const assetsRoot = getInitAssetsDir();
|
|
1343
|
+
const rootAssets = import_node_path11.default.join(assetsRoot, "root");
|
|
1344
|
+
const qfaiAssets = import_node_path11.default.join(assetsRoot, ".qfai");
|
|
1345
|
+
const destRoot = import_node_path11.default.resolve(options.dir);
|
|
1346
|
+
const destQfai = import_node_path11.default.join(destRoot, ".qfai");
|
|
1347
|
+
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
1348
|
+
force: options.force,
|
|
1349
|
+
dryRun: options.dryRun
|
|
1350
|
+
});
|
|
1351
|
+
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
1352
|
+
force: options.force,
|
|
1353
|
+
dryRun: options.dryRun
|
|
1354
|
+
});
|
|
1355
|
+
report(
|
|
1356
|
+
[...rootResult.copied, ...qfaiResult.copied],
|
|
1357
|
+
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
1358
|
+
options.dryRun,
|
|
1359
|
+
"init"
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
function report(copied, skipped, dryRun, label) {
|
|
1363
|
+
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
1364
|
+
if (copied.length > 0) {
|
|
1365
|
+
info(` created: ${copied.length}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (skipped.length > 0) {
|
|
1368
|
+
info(` skipped: ${skipped.length}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/cli/commands/report.ts
|
|
1373
|
+
var import_promises18 = require("fs/promises");
|
|
1374
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
1375
|
+
|
|
1376
|
+
// src/core/normalize.ts
|
|
1377
|
+
function normalizeIssuePaths(root, issues) {
|
|
1378
|
+
return issues.map((issue7) => {
|
|
1379
|
+
if (!issue7.file) {
|
|
1380
|
+
return issue7;
|
|
1381
|
+
}
|
|
1382
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
1383
|
+
if (normalized === issue7.file) {
|
|
1384
|
+
return issue7;
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
...issue7,
|
|
1388
|
+
file: normalized
|
|
1389
|
+
};
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function normalizeScCoverage(root, sc) {
|
|
1393
|
+
const refs = {};
|
|
1394
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
1395
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
...sc,
|
|
1399
|
+
refs
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function normalizeValidationResult(root, result) {
|
|
1403
|
+
return {
|
|
1404
|
+
...result,
|
|
1405
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
1406
|
+
traceability: {
|
|
1407
|
+
...result.traceability,
|
|
1408
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/core/report.ts
|
|
1414
|
+
var import_promises17 = require("fs/promises");
|
|
1415
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
1416
|
+
|
|
1417
|
+
// src/core/contractIndex.ts
|
|
1418
|
+
var import_promises10 = require("fs/promises");
|
|
1419
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1420
|
+
|
|
1421
|
+
// src/core/contractsDecl.ts
|
|
1422
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
1423
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
1424
|
+
function extractDeclaredContractIds(text) {
|
|
1425
|
+
const ids = [];
|
|
1426
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
1427
|
+
const id = match[1];
|
|
1428
|
+
if (id) {
|
|
1429
|
+
ids.push(id);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return ids;
|
|
1433
|
+
}
|
|
1434
|
+
function stripContractDeclarationLines(text) {
|
|
1435
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/core/contractIndex.ts
|
|
1439
|
+
async function buildContractIndex(root, config) {
|
|
1440
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1441
|
+
const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
|
|
1442
|
+
const apiRoot = import_node_path12.default.join(contractsRoot, "api");
|
|
1443
|
+
const dbRoot = import_node_path12.default.join(contractsRoot, "db");
|
|
1444
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1445
|
+
collectUiContractFiles(uiRoot),
|
|
1446
|
+
collectApiContractFiles(apiRoot),
|
|
1447
|
+
collectDbContractFiles(dbRoot)
|
|
1448
|
+
]);
|
|
1449
|
+
const index = {
|
|
1450
|
+
ids: /* @__PURE__ */ new Set(),
|
|
1451
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
1452
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1453
|
+
};
|
|
1454
|
+
await indexContractFiles(uiFiles, index);
|
|
1455
|
+
await indexContractFiles(apiFiles, index);
|
|
1456
|
+
await indexContractFiles(dbFiles, index);
|
|
1457
|
+
return index;
|
|
1458
|
+
}
|
|
1459
|
+
async function indexContractFiles(files, index) {
|
|
1460
|
+
for (const file of files) {
|
|
1461
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1462
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function record(index, id, file) {
|
|
1466
|
+
index.ids.add(id);
|
|
1467
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1468
|
+
current.add(file);
|
|
1469
|
+
index.idToFiles.set(id, current);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// src/core/ids.ts
|
|
1473
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
1474
|
+
var STRICT_ID_PATTERNS = {
|
|
1475
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1476
|
+
BR: /\bBR-\d{4}\b/g,
|
|
1477
|
+
SC: /\bSC-\d{4}\b/g,
|
|
1478
|
+
UI: /\bUI-\d{4}\b/g,
|
|
1479
|
+
API: /\bAPI-\d{4}\b/g,
|
|
1480
|
+
DB: /\bDB-\d{4}\b/g,
|
|
1481
|
+
ADR: /\bADR-\d{4}\b/g
|
|
1482
|
+
};
|
|
1483
|
+
var LOOSE_ID_PATTERNS = {
|
|
1484
|
+
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
1485
|
+
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
1486
|
+
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
1487
|
+
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1488
|
+
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1489
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1490
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1491
|
+
};
|
|
1492
|
+
function extractIds(text, prefix) {
|
|
1493
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1494
|
+
const matches = text.match(pattern);
|
|
1495
|
+
return unique2(matches ?? []);
|
|
1496
|
+
}
|
|
1497
|
+
function extractAllIds(text) {
|
|
1498
|
+
const all = [];
|
|
1499
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
1500
|
+
all.push(...extractIds(text, prefix));
|
|
1501
|
+
});
|
|
1502
|
+
return unique2(all);
|
|
1503
|
+
}
|
|
1504
|
+
function extractInvalidIds(text, prefixes) {
|
|
1505
|
+
const invalid = [];
|
|
1506
|
+
for (const prefix of prefixes) {
|
|
1507
|
+
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
1508
|
+
for (const candidate of candidates) {
|
|
1509
|
+
if (!isValidId(candidate, prefix)) {
|
|
1510
|
+
invalid.push(candidate);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return unique2(invalid);
|
|
1515
|
+
}
|
|
1516
|
+
function unique2(values) {
|
|
1517
|
+
return Array.from(new Set(values));
|
|
1518
|
+
}
|
|
1519
|
+
function isValidId(value, prefix) {
|
|
1520
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1521
|
+
const strict = new RegExp(pattern.source);
|
|
1522
|
+
return strict.test(value);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/core/parse/contractRefs.ts
|
|
1526
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1527
|
+
function parseContractRefs(text, options = {}) {
|
|
1528
|
+
const linePattern = buildLinePattern(options);
|
|
1529
|
+
const lines = [];
|
|
1530
|
+
for (const match of text.matchAll(linePattern)) {
|
|
1531
|
+
lines.push((match[1] ?? "").trim());
|
|
1532
|
+
}
|
|
1533
|
+
const ids = [];
|
|
1534
|
+
const invalidTokens = [];
|
|
1535
|
+
let hasNone = false;
|
|
1536
|
+
for (const line of lines) {
|
|
1537
|
+
if (line.length === 0) {
|
|
1538
|
+
invalidTokens.push("(empty)");
|
|
1306
1539
|
continue;
|
|
1307
1540
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1541
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
1542
|
+
for (const token of tokens) {
|
|
1543
|
+
if (token.length === 0) {
|
|
1544
|
+
invalidTokens.push("(empty)");
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (token === "none") {
|
|
1548
|
+
hasNone = true;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
1552
|
+
ids.push(token);
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
invalidTokens.push(token);
|
|
1312
1556
|
}
|
|
1313
1557
|
}
|
|
1314
1558
|
return {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
matchedFileCount: normalizedFiles.length
|
|
1320
|
-
}
|
|
1559
|
+
lines,
|
|
1560
|
+
ids: unique3(ids),
|
|
1561
|
+
invalidTokens: unique3(invalidTokens),
|
|
1562
|
+
hasNone
|
|
1321
1563
|
};
|
|
1322
1564
|
}
|
|
1323
|
-
function
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const files = refs.get(scId);
|
|
1330
|
-
const sortedFiles = files ? toSortedArray(files) : [];
|
|
1331
|
-
refsRecord[scId] = sortedFiles;
|
|
1332
|
-
if (sortedFiles.length === 0) {
|
|
1333
|
-
missingIds.push(scId);
|
|
1334
|
-
} else {
|
|
1335
|
-
covered += 1;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
return {
|
|
1339
|
-
total: sortedScIds.length,
|
|
1340
|
-
covered,
|
|
1341
|
-
missing: missingIds.length,
|
|
1342
|
-
missingIds,
|
|
1343
|
-
refs: refsRecord
|
|
1344
|
-
};
|
|
1565
|
+
function buildLinePattern(options) {
|
|
1566
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
1567
|
+
return new RegExp(
|
|
1568
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
1569
|
+
"gm"
|
|
1570
|
+
);
|
|
1345
1571
|
}
|
|
1346
|
-
function
|
|
1347
|
-
return Array.from(new Set(values))
|
|
1572
|
+
function unique3(values) {
|
|
1573
|
+
return Array.from(new Set(values));
|
|
1348
1574
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1575
|
+
|
|
1576
|
+
// src/core/parse/markdown.ts
|
|
1577
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1578
|
+
function parseHeadings(md) {
|
|
1579
|
+
const lines = md.split(/\r?\n/);
|
|
1580
|
+
const headings = [];
|
|
1581
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1582
|
+
const line = lines[i] ?? "";
|
|
1583
|
+
const match = line.match(HEADING_RE);
|
|
1584
|
+
if (!match) continue;
|
|
1585
|
+
const levelToken = match[1];
|
|
1586
|
+
const title = match[2];
|
|
1587
|
+
if (!levelToken || !title) continue;
|
|
1588
|
+
headings.push({
|
|
1589
|
+
level: levelToken.length,
|
|
1590
|
+
title: title.trim(),
|
|
1591
|
+
line: i + 1
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return headings;
|
|
1351
1595
|
}
|
|
1352
|
-
function
|
|
1353
|
-
|
|
1354
|
-
|
|
1596
|
+
function extractH2Sections(md) {
|
|
1597
|
+
const lines = md.split(/\r?\n/);
|
|
1598
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1599
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1601
|
+
const current = headings[i];
|
|
1602
|
+
if (!current) continue;
|
|
1603
|
+
const next = headings[i + 1];
|
|
1604
|
+
const startLine = current.line + 1;
|
|
1605
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1606
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1607
|
+
sections.set(current.title.trim(), {
|
|
1608
|
+
title: current.title.trim(),
|
|
1609
|
+
startLine,
|
|
1610
|
+
endLine,
|
|
1611
|
+
body
|
|
1612
|
+
});
|
|
1355
1613
|
}
|
|
1356
|
-
return
|
|
1614
|
+
return sections;
|
|
1357
1615
|
}
|
|
1358
1616
|
|
|
1359
|
-
// src/core/
|
|
1360
|
-
var
|
|
1361
|
-
var
|
|
1362
|
-
var
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1617
|
+
// src/core/parse/spec.ts
|
|
1618
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1619
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1620
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1621
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1622
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1623
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1624
|
+
function parseSpec(md, file) {
|
|
1625
|
+
const headings = parseHeadings(md);
|
|
1626
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1627
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1628
|
+
const sections = extractH2Sections(md);
|
|
1629
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1630
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1631
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1632
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1633
|
+
const brs = [];
|
|
1634
|
+
const brsWithoutPriority = [];
|
|
1635
|
+
const brsWithInvalidPriority = [];
|
|
1636
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1637
|
+
const lineText = brLines[i] ?? "";
|
|
1638
|
+
const lineNumber = startLine + i;
|
|
1639
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1640
|
+
if (validMatch) {
|
|
1641
|
+
const id = validMatch[1];
|
|
1642
|
+
const priority = validMatch[2];
|
|
1643
|
+
const text = validMatch[3];
|
|
1644
|
+
if (!id || !priority || !text) continue;
|
|
1645
|
+
brs.push({
|
|
1646
|
+
id,
|
|
1647
|
+
priority,
|
|
1648
|
+
text: text.trim(),
|
|
1649
|
+
line: lineNumber
|
|
1650
|
+
});
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1654
|
+
if (anyPriorityMatch) {
|
|
1655
|
+
const id = anyPriorityMatch[1];
|
|
1656
|
+
const priority = anyPriorityMatch[2];
|
|
1657
|
+
const text = anyPriorityMatch[3];
|
|
1658
|
+
if (!id || !priority || !text) continue;
|
|
1659
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1660
|
+
brsWithInvalidPriority.push({
|
|
1661
|
+
id,
|
|
1662
|
+
priority,
|
|
1663
|
+
text: text.trim(),
|
|
1664
|
+
line: lineNumber
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1670
|
+
if (noPriorityMatch) {
|
|
1671
|
+
const id = noPriorityMatch[1];
|
|
1672
|
+
const text = noPriorityMatch[2];
|
|
1673
|
+
if (!id || !text) continue;
|
|
1674
|
+
brsWithoutPriority.push({
|
|
1675
|
+
id,
|
|
1676
|
+
text: text.trim(),
|
|
1677
|
+
line: lineNumber
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1366
1680
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1681
|
+
const parsed = {
|
|
1682
|
+
file,
|
|
1683
|
+
sections: sectionNames,
|
|
1684
|
+
brs,
|
|
1685
|
+
brsWithoutPriority,
|
|
1686
|
+
brsWithInvalidPriority,
|
|
1687
|
+
contractRefs: parseContractRefs(md)
|
|
1688
|
+
};
|
|
1689
|
+
if (specId) {
|
|
1690
|
+
parsed.specId = specId;
|
|
1375
1691
|
}
|
|
1376
|
-
|
|
1377
|
-
function resolvePackageJsonPath() {
|
|
1378
|
-
const base = __filename;
|
|
1379
|
-
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1380
|
-
return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
|
|
1692
|
+
return parsed;
|
|
1381
1693
|
}
|
|
1382
1694
|
|
|
1383
1695
|
// src/core/validators/contracts.ts
|
|
1384
|
-
var
|
|
1385
|
-
var
|
|
1696
|
+
var import_promises11 = require("fs/promises");
|
|
1697
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
1386
1698
|
|
|
1387
1699
|
// src/core/contracts.ts
|
|
1388
|
-
var
|
|
1700
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1389
1701
|
var import_yaml2 = require("yaml");
|
|
1390
1702
|
function parseStructuredContract(file, text) {
|
|
1391
|
-
const ext =
|
|
1703
|
+
const ext = import_node_path13.default.extname(file).toLowerCase();
|
|
1392
1704
|
if (ext === ".json") {
|
|
1393
1705
|
return JSON.parse(text);
|
|
1394
1706
|
}
|
|
@@ -1408,9 +1720,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1408
1720
|
async function validateContracts(root, config) {
|
|
1409
1721
|
const issues = [];
|
|
1410
1722
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1411
|
-
issues.push(...await validateUiContracts(
|
|
1412
|
-
issues.push(...await validateApiContracts(
|
|
1413
|
-
issues.push(...await validateDbContracts(
|
|
1723
|
+
issues.push(...await validateUiContracts(import_node_path14.default.join(contractsRoot, "ui")));
|
|
1724
|
+
issues.push(...await validateApiContracts(import_node_path14.default.join(contractsRoot, "api")));
|
|
1725
|
+
issues.push(...await validateDbContracts(import_node_path14.default.join(contractsRoot, "db")));
|
|
1414
1726
|
const contractIndex = await buildContractIndex(root, config);
|
|
1415
1727
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1416
1728
|
return issues;
|
|
@@ -1430,7 +1742,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
1430
1742
|
}
|
|
1431
1743
|
const issues = [];
|
|
1432
1744
|
for (const file of files) {
|
|
1433
|
-
const text = await (0,
|
|
1745
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1434
1746
|
const invalidIds = extractInvalidIds(text, [
|
|
1435
1747
|
"SPEC",
|
|
1436
1748
|
"BR",
|
|
@@ -1485,7 +1797,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1485
1797
|
}
|
|
1486
1798
|
const issues = [];
|
|
1487
1799
|
for (const file of files) {
|
|
1488
|
-
const text = await (0,
|
|
1800
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1489
1801
|
const invalidIds = extractInvalidIds(text, [
|
|
1490
1802
|
"SPEC",
|
|
1491
1803
|
"BR",
|
|
@@ -1553,7 +1865,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1553
1865
|
}
|
|
1554
1866
|
const issues = [];
|
|
1555
1867
|
for (const file of files) {
|
|
1556
|
-
const text = await (0,
|
|
1868
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1557
1869
|
const invalidIds = extractInvalidIds(text, [
|
|
1558
1870
|
"SPEC",
|
|
1559
1871
|
"BR",
|
|
@@ -1692,8 +2004,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1692
2004
|
}
|
|
1693
2005
|
|
|
1694
2006
|
// src/core/validators/delta.ts
|
|
1695
|
-
var
|
|
1696
|
-
var
|
|
2007
|
+
var import_promises12 = require("fs/promises");
|
|
2008
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
1697
2009
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1698
2010
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1699
2011
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1707,10 +2019,10 @@ async function validateDeltas(root, config) {
|
|
|
1707
2019
|
}
|
|
1708
2020
|
const issues = [];
|
|
1709
2021
|
for (const pack of packs) {
|
|
1710
|
-
const deltaPath =
|
|
2022
|
+
const deltaPath = import_node_path15.default.join(pack, "delta.md");
|
|
1711
2023
|
let text;
|
|
1712
2024
|
try {
|
|
1713
|
-
text = await (0,
|
|
2025
|
+
text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
|
|
1714
2026
|
} catch (error2) {
|
|
1715
2027
|
if (isMissingFileError2(error2)) {
|
|
1716
2028
|
issues.push(
|
|
@@ -1782,8 +2094,8 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1782
2094
|
}
|
|
1783
2095
|
|
|
1784
2096
|
// src/core/validators/ids.ts
|
|
1785
|
-
var
|
|
1786
|
-
var
|
|
2097
|
+
var import_promises13 = require("fs/promises");
|
|
2098
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
1787
2099
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1788
2100
|
async function validateDefinedIds(root, config) {
|
|
1789
2101
|
const issues = [];
|
|
@@ -1818,7 +2130,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1818
2130
|
}
|
|
1819
2131
|
async function collectSpecDefinitionIds(files, out) {
|
|
1820
2132
|
for (const file of files) {
|
|
1821
|
-
const text = await (0,
|
|
2133
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1822
2134
|
const parsed = parseSpec(text, file);
|
|
1823
2135
|
if (parsed.specId) {
|
|
1824
2136
|
recordId(out, parsed.specId, file);
|
|
@@ -1828,7 +2140,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1828
2140
|
}
|
|
1829
2141
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1830
2142
|
for (const file of files) {
|
|
1831
|
-
const text = await (0,
|
|
2143
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1832
2144
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1833
2145
|
if (!document || errors.length > 0) {
|
|
1834
2146
|
continue;
|
|
@@ -1849,7 +2161,7 @@ function recordId(out, id, file) {
|
|
|
1849
2161
|
}
|
|
1850
2162
|
function formatFileList(files, root) {
|
|
1851
2163
|
return files.map((file) => {
|
|
1852
|
-
const relative =
|
|
2164
|
+
const relative = import_node_path16.default.relative(root, file);
|
|
1853
2165
|
return relative.length > 0 ? relative : file;
|
|
1854
2166
|
}).join(", ");
|
|
1855
2167
|
}
|
|
@@ -1872,7 +2184,7 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1872
2184
|
}
|
|
1873
2185
|
|
|
1874
2186
|
// src/core/validators/scenario.ts
|
|
1875
|
-
var
|
|
2187
|
+
var import_promises14 = require("fs/promises");
|
|
1876
2188
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1877
2189
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1878
2190
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -1898,7 +2210,7 @@ async function validateScenarios(root, config) {
|
|
|
1898
2210
|
for (const entry of entries) {
|
|
1899
2211
|
let text;
|
|
1900
2212
|
try {
|
|
1901
|
-
text = await (0,
|
|
2213
|
+
text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
|
|
1902
2214
|
} catch (error2) {
|
|
1903
2215
|
if (isMissingFileError3(error2)) {
|
|
1904
2216
|
issues.push(
|
|
@@ -2068,7 +2380,7 @@ function isMissingFileError3(error2) {
|
|
|
2068
2380
|
}
|
|
2069
2381
|
|
|
2070
2382
|
// src/core/validators/spec.ts
|
|
2071
|
-
var
|
|
2383
|
+
var import_promises15 = require("fs/promises");
|
|
2072
2384
|
async function validateSpecs(root, config) {
|
|
2073
2385
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2074
2386
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -2089,7 +2401,7 @@ async function validateSpecs(root, config) {
|
|
|
2089
2401
|
for (const entry of entries) {
|
|
2090
2402
|
let text;
|
|
2091
2403
|
try {
|
|
2092
|
-
text = await (0,
|
|
2404
|
+
text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
|
|
2093
2405
|
} catch (error2) {
|
|
2094
2406
|
if (isMissingFileError4(error2)) {
|
|
2095
2407
|
issues.push(
|
|
@@ -2238,7 +2550,7 @@ function isMissingFileError4(error2) {
|
|
|
2238
2550
|
}
|
|
2239
2551
|
|
|
2240
2552
|
// src/core/validators/traceability.ts
|
|
2241
|
-
var
|
|
2553
|
+
var import_promises16 = require("fs/promises");
|
|
2242
2554
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2243
2555
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2244
2556
|
async function validateTraceability(root, config) {
|
|
@@ -2258,7 +2570,7 @@ async function validateTraceability(root, config) {
|
|
|
2258
2570
|
const contractIndex = await buildContractIndex(root, config);
|
|
2259
2571
|
const contractIds = contractIndex.ids;
|
|
2260
2572
|
for (const file of specFiles) {
|
|
2261
|
-
const text = await (0,
|
|
2573
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2262
2574
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2263
2575
|
const parsed = parseSpec(text, file);
|
|
2264
2576
|
if (parsed.specId) {
|
|
@@ -2331,7 +2643,7 @@ async function validateTraceability(root, config) {
|
|
|
2331
2643
|
}
|
|
2332
2644
|
}
|
|
2333
2645
|
for (const file of scenarioFiles) {
|
|
2334
|
-
const text = await (0,
|
|
2646
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2335
2647
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2336
2648
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2337
2649
|
allowCommentPrefix: true
|
|
@@ -2653,7 +2965,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2653
2965
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2654
2966
|
let found = false;
|
|
2655
2967
|
for (const file of targetFiles) {
|
|
2656
|
-
const text = await (0,
|
|
2968
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2657
2969
|
if (pattern.test(text)) {
|
|
2658
2970
|
found = true;
|
|
2659
2971
|
break;
|
|
@@ -2740,15 +3052,15 @@ function countIssues(issues) {
|
|
|
2740
3052
|
// src/core/report.ts
|
|
2741
3053
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2742
3054
|
async function createReportData(root, validation, configResult) {
|
|
2743
|
-
const resolvedRoot =
|
|
3055
|
+
const resolvedRoot = import_node_path17.default.resolve(root);
|
|
2744
3056
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2745
3057
|
const config = resolved.config;
|
|
2746
3058
|
const configPath = resolved.configPath;
|
|
2747
3059
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2748
3060
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2749
|
-
const apiRoot =
|
|
2750
|
-
const uiRoot =
|
|
2751
|
-
const dbRoot =
|
|
3061
|
+
const apiRoot = import_node_path17.default.join(contractsRoot, "api");
|
|
3062
|
+
const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
|
|
3063
|
+
const dbRoot = import_node_path17.default.join(contractsRoot, "db");
|
|
2752
3064
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2753
3065
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2754
3066
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3063,7 +3375,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
3063
3375
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
3064
3376
|
}
|
|
3065
3377
|
for (const file of specFiles) {
|
|
3066
|
-
const text = await (0,
|
|
3378
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3067
3379
|
const parsed = parseSpec(text, file);
|
|
3068
3380
|
const specKey = parsed.specId;
|
|
3069
3381
|
if (!specKey) {
|
|
@@ -3104,7 +3416,7 @@ async function collectIds(files) {
|
|
|
3104
3416
|
DB: /* @__PURE__ */ new Set()
|
|
3105
3417
|
};
|
|
3106
3418
|
for (const file of files) {
|
|
3107
|
-
const text = await (0,
|
|
3419
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3108
3420
|
for (const prefix of ID_PREFIXES2) {
|
|
3109
3421
|
const ids = extractIds(text, prefix);
|
|
3110
3422
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3122,7 +3434,7 @@ async function collectIds(files) {
|
|
|
3122
3434
|
async function collectUpstreamIds(files) {
|
|
3123
3435
|
const ids = /* @__PURE__ */ new Set();
|
|
3124
3436
|
for (const file of files) {
|
|
3125
|
-
const text = await (0,
|
|
3437
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3126
3438
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3127
3439
|
}
|
|
3128
3440
|
return ids;
|
|
@@ -3143,7 +3455,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3143
3455
|
}
|
|
3144
3456
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3145
3457
|
for (const file of targetFiles) {
|
|
3146
|
-
const text = await (0,
|
|
3458
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3147
3459
|
if (pattern.test(text)) {
|
|
3148
3460
|
return true;
|
|
3149
3461
|
}
|
|
@@ -3235,7 +3547,7 @@ function buildHotspots(issues) {
|
|
|
3235
3547
|
|
|
3236
3548
|
// src/cli/commands/report.ts
|
|
3237
3549
|
async function runReport(options) {
|
|
3238
|
-
const root =
|
|
3550
|
+
const root = import_node_path18.default.resolve(options.root);
|
|
3239
3551
|
const configResult = await loadConfig(root);
|
|
3240
3552
|
let validation;
|
|
3241
3553
|
if (options.runValidate) {
|
|
@@ -3252,7 +3564,7 @@ async function runReport(options) {
|
|
|
3252
3564
|
validation = normalized;
|
|
3253
3565
|
} else {
|
|
3254
3566
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3255
|
-
const inputPath =
|
|
3567
|
+
const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
|
|
3256
3568
|
try {
|
|
3257
3569
|
validation = await readValidationResult(inputPath);
|
|
3258
3570
|
} catch (err) {
|
|
@@ -3278,11 +3590,11 @@ async function runReport(options) {
|
|
|
3278
3590
|
const data = await createReportData(root, validation, configResult);
|
|
3279
3591
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3280
3592
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3281
|
-
const defaultOut = options.format === "json" ?
|
|
3593
|
+
const defaultOut = options.format === "json" ? import_node_path18.default.join(outRoot, "report.json") : import_node_path18.default.join(outRoot, "report.md");
|
|
3282
3594
|
const out = options.outPath ?? defaultOut;
|
|
3283
|
-
const outPath =
|
|
3284
|
-
await (0,
|
|
3285
|
-
await (0,
|
|
3595
|
+
const outPath = import_node_path18.default.isAbsolute(out) ? out : import_node_path18.default.resolve(root, out);
|
|
3596
|
+
await (0, import_promises18.mkdir)(import_node_path18.default.dirname(outPath), { recursive: true });
|
|
3597
|
+
await (0, import_promises18.writeFile)(outPath, `${output}
|
|
3286
3598
|
`, "utf-8");
|
|
3287
3599
|
info(
|
|
3288
3600
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -3290,7 +3602,7 @@ async function runReport(options) {
|
|
|
3290
3602
|
info(`wrote report: ${outPath}`);
|
|
3291
3603
|
}
|
|
3292
3604
|
async function readValidationResult(inputPath) {
|
|
3293
|
-
const raw = await (0,
|
|
3605
|
+
const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
|
|
3294
3606
|
const parsed = JSON.parse(raw);
|
|
3295
3607
|
if (!isValidationResult(parsed)) {
|
|
3296
3608
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -3346,15 +3658,15 @@ function isMissingFileError5(error2) {
|
|
|
3346
3658
|
return record2.code === "ENOENT";
|
|
3347
3659
|
}
|
|
3348
3660
|
async function writeValidationResult(root, outputPath, result) {
|
|
3349
|
-
const abs =
|
|
3350
|
-
await (0,
|
|
3351
|
-
await (0,
|
|
3661
|
+
const abs = import_node_path18.default.isAbsolute(outputPath) ? outputPath : import_node_path18.default.resolve(root, outputPath);
|
|
3662
|
+
await (0, import_promises18.mkdir)(import_node_path18.default.dirname(abs), { recursive: true });
|
|
3663
|
+
await (0, import_promises18.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3352
3664
|
`, "utf-8");
|
|
3353
3665
|
}
|
|
3354
3666
|
|
|
3355
3667
|
// src/cli/commands/validate.ts
|
|
3356
|
-
var
|
|
3357
|
-
var
|
|
3668
|
+
var import_promises19 = require("fs/promises");
|
|
3669
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3358
3670
|
|
|
3359
3671
|
// src/cli/lib/failOn.ts
|
|
3360
3672
|
function shouldFail(result, failOn) {
|
|
@@ -3369,7 +3681,7 @@ function shouldFail(result, failOn) {
|
|
|
3369
3681
|
|
|
3370
3682
|
// src/cli/commands/validate.ts
|
|
3371
3683
|
async function runValidate(options) {
|
|
3372
|
-
const root =
|
|
3684
|
+
const root = import_node_path19.default.resolve(options.root);
|
|
3373
3685
|
const configResult = await loadConfig(root);
|
|
3374
3686
|
const result = await validateProject(root, configResult);
|
|
3375
3687
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -3486,12 +3798,12 @@ function issueKey(issue7) {
|
|
|
3486
3798
|
}
|
|
3487
3799
|
async function emitJson(result, root, jsonPath) {
|
|
3488
3800
|
const abs = resolveJsonPath(root, jsonPath);
|
|
3489
|
-
await (0,
|
|
3490
|
-
await (0,
|
|
3801
|
+
await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
|
|
3802
|
+
await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3491
3803
|
`, "utf-8");
|
|
3492
3804
|
}
|
|
3493
3805
|
function resolveJsonPath(root, jsonPath) {
|
|
3494
|
-
return
|
|
3806
|
+
return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
|
|
3495
3807
|
}
|
|
3496
3808
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3497
3809
|
|
|
@@ -3506,6 +3818,7 @@ function parseArgs(argv, cwd) {
|
|
|
3506
3818
|
dryRun: false,
|
|
3507
3819
|
reportFormat: "md",
|
|
3508
3820
|
reportRunValidate: false,
|
|
3821
|
+
doctorFormat: "text",
|
|
3509
3822
|
validateFormat: "text",
|
|
3510
3823
|
strict: false,
|
|
3511
3824
|
help: false
|
|
@@ -3558,7 +3871,11 @@ function parseArgs(argv, cwd) {
|
|
|
3558
3871
|
{
|
|
3559
3872
|
const next = args[i + 1];
|
|
3560
3873
|
if (next) {
|
|
3561
|
-
|
|
3874
|
+
if (command === "doctor") {
|
|
3875
|
+
options.doctorOut = next;
|
|
3876
|
+
} else {
|
|
3877
|
+
options.reportOut = next;
|
|
3878
|
+
}
|
|
3562
3879
|
}
|
|
3563
3880
|
}
|
|
3564
3881
|
i += 1;
|
|
@@ -3601,6 +3918,12 @@ function applyFormatOption(command, value, options) {
|
|
|
3601
3918
|
}
|
|
3602
3919
|
return;
|
|
3603
3920
|
}
|
|
3921
|
+
if (command === "doctor") {
|
|
3922
|
+
if (value === "text" || value === "json") {
|
|
3923
|
+
options.doctorFormat = value;
|
|
3924
|
+
}
|
|
3925
|
+
return;
|
|
3926
|
+
}
|
|
3604
3927
|
if (value === "md" || value === "json") {
|
|
3605
3928
|
options.reportFormat = value;
|
|
3606
3929
|
}
|
|
@@ -3648,6 +3971,18 @@ async function run(argv, cwd) {
|
|
|
3648
3971
|
});
|
|
3649
3972
|
}
|
|
3650
3973
|
return;
|
|
3974
|
+
case "doctor":
|
|
3975
|
+
{
|
|
3976
|
+
const exitCode = await runDoctor({
|
|
3977
|
+
root: options.root,
|
|
3978
|
+
rootExplicit: options.rootExplicit,
|
|
3979
|
+
format: options.doctorFormat,
|
|
3980
|
+
...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {},
|
|
3981
|
+
...options.failOn && options.failOn !== "never" ? { failOn: options.failOn } : {}
|
|
3982
|
+
});
|
|
3983
|
+
process.exitCode = exitCode;
|
|
3984
|
+
}
|
|
3985
|
+
return;
|
|
3651
3986
|
default:
|
|
3652
3987
|
error(`Unknown command: ${command}`);
|
|
3653
3988
|
info(usage());
|
|
@@ -3661,6 +3996,7 @@ Commands:
|
|
|
3661
3996
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
3662
3997
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
3663
3998
|
report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
|
|
3999
|
+
doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
|
|
3664
4000
|
|
|
3665
4001
|
Options:
|
|
3666
4002
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -3670,9 +4006,11 @@ Options:
|
|
|
3670
4006
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
3671
4007
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3672
4008
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
4009
|
+
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3673
4010
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3674
4011
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3675
|
-
--
|
|
4012
|
+
--fail-on <error|warning> doctor: \u5931\u6557\u6761\u4EF6
|
|
4013
|
+
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
3676
4014
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3677
4015
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3678
4016
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|