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.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,786 @@ 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.0".length > 0) {
|
|
893
|
+
return "0.6.0";
|
|
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 scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
1025
|
+
const globs = normalizeGlobs2(config.validation.traceability.testFileGlobs);
|
|
1026
|
+
const exclude = normalizeGlobs2([
|
|
1027
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
1028
|
+
...config.validation.traceability.testFileExcludeGlobs
|
|
1029
|
+
]);
|
|
1030
|
+
try {
|
|
1031
|
+
const matched = globs.length === 0 ? [] : await collectFilesByGlobs(root, { globs, ignore: exclude });
|
|
1032
|
+
const matchedCount = matched.length;
|
|
1033
|
+
const severity = globs.length === 0 ? "warning" : scenarioFiles.length > 0 && config.validation.traceability.scMustHaveTest && matchedCount === 0 ? "warning" : "ok";
|
|
1034
|
+
addCheck(checks, {
|
|
1035
|
+
id: "traceability.testGlobs",
|
|
1036
|
+
severity,
|
|
1037
|
+
title: "Test file globs",
|
|
1038
|
+
message: globs.length === 0 ? "testFileGlobs is empty (SC\u2192Test cannot be verified)" : `matchedFileCount=${matchedCount}`,
|
|
1039
|
+
details: {
|
|
1040
|
+
globs,
|
|
1041
|
+
excludeGlobs: exclude,
|
|
1042
|
+
scenarioFiles: scenarioFiles.length,
|
|
1043
|
+
scMustHaveTest: config.validation.traceability.scMustHaveTest
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
} catch (error2) {
|
|
1047
|
+
addCheck(checks, {
|
|
1048
|
+
id: "traceability.testGlobs",
|
|
1049
|
+
severity: "error",
|
|
1050
|
+
title: "Test file globs",
|
|
1051
|
+
message: "Glob scan failed (invalid pattern or filesystem error)",
|
|
1052
|
+
details: { globs, excludeGlobs: exclude, error: String(error2) }
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1056
|
+
const rel = import_node_path7.default.relative(outDirAbs, validateJsonAbs);
|
|
1057
|
+
const inside = rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
|
|
1058
|
+
addCheck(checks, {
|
|
1059
|
+
id: "output.pathAlignment",
|
|
1060
|
+
severity: inside ? "ok" : "warning",
|
|
1061
|
+
title: "Output path alignment",
|
|
1062
|
+
message: inside ? "validateJsonPath is under outDir" : "validateJsonPath is not under outDir (may be intended, but check configuration)",
|
|
1063
|
+
details: {
|
|
1064
|
+
outDir: toRelativePath(root, outDirAbs),
|
|
1065
|
+
validateJsonPath: toRelativePath(root, validateJsonAbs)
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
tool: "qfai",
|
|
1070
|
+
version,
|
|
1071
|
+
doctorFormatVersion: 1,
|
|
1072
|
+
generatedAt,
|
|
1073
|
+
root: toRelativePath(process.cwd(), root),
|
|
1074
|
+
config: {
|
|
1075
|
+
startDir: toRelativePath(process.cwd(), startDir),
|
|
1076
|
+
found: search.found,
|
|
1077
|
+
configPath: toRelativePath(root, search.configPath) || "qfai.config.yaml"
|
|
1078
|
+
},
|
|
1079
|
+
summary: summarize(checks),
|
|
1080
|
+
checks
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/cli/lib/logger.ts
|
|
1085
|
+
function info(message) {
|
|
1086
|
+
process.stdout.write(`${message}
|
|
1087
|
+
`);
|
|
1088
|
+
}
|
|
1089
|
+
function warn(message) {
|
|
1090
|
+
process.stdout.write(`${message}
|
|
1091
|
+
`);
|
|
1092
|
+
}
|
|
1093
|
+
function error(message) {
|
|
1094
|
+
process.stderr.write(`${message}
|
|
1095
|
+
`);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/cli/commands/doctor.ts
|
|
1099
|
+
function formatDoctorText(data) {
|
|
1100
|
+
const lines = [];
|
|
1101
|
+
lines.push(
|
|
1102
|
+
`qfai doctor: root=${data.root} config=${data.config.configPath} (${data.config.found ? "found" : "missing"})`
|
|
1103
|
+
);
|
|
1104
|
+
for (const check of data.checks) {
|
|
1105
|
+
lines.push(`[${check.severity}] ${check.id}: ${check.message}`);
|
|
1106
|
+
}
|
|
1107
|
+
lines.push(
|
|
1108
|
+
`summary: ok=${data.summary.ok} warning=${data.summary.warning} error=${data.summary.error}`
|
|
1109
|
+
);
|
|
1110
|
+
return lines.join("\n");
|
|
1111
|
+
}
|
|
1112
|
+
function formatDoctorJson(data) {
|
|
1113
|
+
return JSON.stringify(data, null, 2);
|
|
1114
|
+
}
|
|
1115
|
+
async function runDoctor(options) {
|
|
1116
|
+
const data = await createDoctorData({
|
|
1117
|
+
startDir: options.root,
|
|
1118
|
+
rootExplicit: options.rootExplicit
|
|
1119
|
+
});
|
|
1120
|
+
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1121
|
+
if (options.outPath) {
|
|
1122
|
+
const outAbs = import_node_path8.default.isAbsolute(options.outPath) ? options.outPath : import_node_path8.default.resolve(process.cwd(), options.outPath);
|
|
1123
|
+
await (0, import_promises8.mkdir)(import_node_path8.default.dirname(outAbs), { recursive: true });
|
|
1124
|
+
await (0, import_promises8.writeFile)(outAbs, `${output}
|
|
1125
|
+
`, "utf-8");
|
|
1126
|
+
info(`doctor: wrote ${outAbs}`);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
info(output);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/cli/commands/init.ts
|
|
1133
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1134
|
+
|
|
1135
|
+
// src/cli/lib/fs.ts
|
|
1136
|
+
var import_promises9 = require("fs/promises");
|
|
1137
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1138
|
+
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1139
|
+
const files = await collectTemplateFiles(sourceRoot);
|
|
1140
|
+
return copyFiles(files, sourceRoot, destRoot, options);
|
|
1141
|
+
}
|
|
1142
|
+
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
1143
|
+
const copied = [];
|
|
1144
|
+
const skipped = [];
|
|
1145
|
+
const conflicts = [];
|
|
1146
|
+
if (!options.force) {
|
|
1147
|
+
for (const file of files) {
|
|
1148
|
+
const relative = import_node_path9.default.relative(sourceRoot, file);
|
|
1149
|
+
const dest = import_node_path9.default.join(destRoot, relative);
|
|
1150
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1151
|
+
conflicts.push(dest);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (conflicts.length > 0) {
|
|
1155
|
+
throw new Error(formatConflictMessage(conflicts));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
for (const file of files) {
|
|
1159
|
+
const relative = import_node_path9.default.relative(sourceRoot, file);
|
|
1160
|
+
const dest = import_node_path9.default.join(destRoot, relative);
|
|
1161
|
+
if (!await shouldWrite(dest, options.force)) {
|
|
1162
|
+
skipped.push(dest);
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (!options.dryRun) {
|
|
1166
|
+
await (0, import_promises9.mkdir)(import_node_path9.default.dirname(dest), { recursive: true });
|
|
1167
|
+
await (0, import_promises9.copyFile)(file, dest);
|
|
1168
|
+
}
|
|
1169
|
+
copied.push(dest);
|
|
1170
|
+
}
|
|
1171
|
+
return { copied, skipped };
|
|
1172
|
+
}
|
|
1173
|
+
function formatConflictMessage(conflicts) {
|
|
1174
|
+
return [
|
|
1175
|
+
"\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",
|
|
1176
|
+
"",
|
|
1177
|
+
"\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
|
|
1178
|
+
...conflicts.map((conflict) => `- ${conflict}`),
|
|
1179
|
+
"",
|
|
1180
|
+
"\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"
|
|
1181
|
+
].join("\n");
|
|
1182
|
+
}
|
|
1183
|
+
async function collectTemplateFiles(root) {
|
|
1184
|
+
const entries = [];
|
|
1185
|
+
if (!await exists5(root)) {
|
|
1186
|
+
return entries;
|
|
1187
|
+
}
|
|
1188
|
+
const items = await (0, import_promises9.readdir)(root, { withFileTypes: true });
|
|
1189
|
+
for (const item of items) {
|
|
1190
|
+
const fullPath = import_node_path9.default.join(root, item.name);
|
|
1191
|
+
if (item.isDirectory()) {
|
|
1192
|
+
const nested = await collectTemplateFiles(fullPath);
|
|
1193
|
+
entries.push(...nested);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (item.isFile()) {
|
|
1197
|
+
entries.push(fullPath);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return entries;
|
|
1201
|
+
}
|
|
1202
|
+
async function shouldWrite(target, force) {
|
|
1203
|
+
if (force) {
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
return !await exists5(target);
|
|
1207
|
+
}
|
|
1208
|
+
async function exists5(target) {
|
|
1209
|
+
try {
|
|
1210
|
+
await (0, import_promises9.access)(target);
|
|
1211
|
+
return true;
|
|
1212
|
+
} catch {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/cli/lib/assets.ts
|
|
1218
|
+
var import_node_fs = require("fs");
|
|
1219
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1220
|
+
var import_node_url2 = require("url");
|
|
1221
|
+
function getInitAssetsDir() {
|
|
1222
|
+
const base = __filename;
|
|
1223
|
+
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1224
|
+
const baseDir = import_node_path10.default.dirname(basePath);
|
|
1225
|
+
const candidates = [
|
|
1226
|
+
import_node_path10.default.resolve(baseDir, "../../../assets/init"),
|
|
1227
|
+
import_node_path10.default.resolve(baseDir, "../../assets/init")
|
|
1228
|
+
];
|
|
1229
|
+
for (const candidate of candidates) {
|
|
1230
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
1231
|
+
return candidate;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
[
|
|
1236
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1237
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1238
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
1239
|
+
].join("\n")
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/cli/commands/init.ts
|
|
1244
|
+
async function runInit(options) {
|
|
1245
|
+
const assetsRoot = getInitAssetsDir();
|
|
1246
|
+
const rootAssets = import_node_path11.default.join(assetsRoot, "root");
|
|
1247
|
+
const qfaiAssets = import_node_path11.default.join(assetsRoot, ".qfai");
|
|
1248
|
+
const destRoot = import_node_path11.default.resolve(options.dir);
|
|
1249
|
+
const destQfai = import_node_path11.default.join(destRoot, ".qfai");
|
|
1250
|
+
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
1251
|
+
force: options.force,
|
|
1252
|
+
dryRun: options.dryRun
|
|
1253
|
+
});
|
|
1254
|
+
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
1255
|
+
force: options.force,
|
|
1256
|
+
dryRun: options.dryRun
|
|
1257
|
+
});
|
|
1258
|
+
report(
|
|
1259
|
+
[...rootResult.copied, ...qfaiResult.copied],
|
|
1260
|
+
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
1261
|
+
options.dryRun,
|
|
1262
|
+
"init"
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
function report(copied, skipped, dryRun, label) {
|
|
1266
|
+
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
1267
|
+
if (copied.length > 0) {
|
|
1268
|
+
info(` created: ${copied.length}`);
|
|
1269
|
+
}
|
|
1270
|
+
if (skipped.length > 0) {
|
|
1271
|
+
info(` skipped: ${skipped.length}`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/cli/commands/report.ts
|
|
1276
|
+
var import_promises18 = require("fs/promises");
|
|
1277
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
1278
|
+
|
|
1279
|
+
// src/core/normalize.ts
|
|
1280
|
+
function normalizeIssuePaths(root, issues) {
|
|
1281
|
+
return issues.map((issue7) => {
|
|
1282
|
+
if (!issue7.file) {
|
|
1283
|
+
return issue7;
|
|
1284
|
+
}
|
|
1285
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
1286
|
+
if (normalized === issue7.file) {
|
|
1287
|
+
return issue7;
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
...issue7,
|
|
1291
|
+
file: normalized
|
|
1292
|
+
};
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
function normalizeScCoverage(root, sc) {
|
|
1296
|
+
const refs = {};
|
|
1297
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
1298
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
...sc,
|
|
1302
|
+
refs
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function normalizeValidationResult(root, result) {
|
|
1306
|
+
return {
|
|
1307
|
+
...result,
|
|
1308
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
1309
|
+
traceability: {
|
|
1310
|
+
...result.traceability,
|
|
1311
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// src/core/report.ts
|
|
1317
|
+
var import_promises17 = require("fs/promises");
|
|
1318
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
1319
|
+
|
|
1320
|
+
// src/core/contractIndex.ts
|
|
1321
|
+
var import_promises10 = require("fs/promises");
|
|
1322
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1323
|
+
|
|
1324
|
+
// src/core/contractsDecl.ts
|
|
1325
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
1326
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
1327
|
+
function extractDeclaredContractIds(text) {
|
|
1328
|
+
const ids = [];
|
|
1329
|
+
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
1330
|
+
const id = match[1];
|
|
1331
|
+
if (id) {
|
|
1332
|
+
ids.push(id);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return ids;
|
|
1336
|
+
}
|
|
1337
|
+
function stripContractDeclarationLines(text) {
|
|
1338
|
+
return text.split(/\r?\n/).filter((line) => !CONTRACT_DECLARATION_LINE_RE.test(line)).join("\n");
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/core/contractIndex.ts
|
|
1342
|
+
async function buildContractIndex(root, config) {
|
|
1343
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1344
|
+
const uiRoot = import_node_path12.default.join(contractsRoot, "ui");
|
|
1345
|
+
const apiRoot = import_node_path12.default.join(contractsRoot, "api");
|
|
1346
|
+
const dbRoot = import_node_path12.default.join(contractsRoot, "db");
|
|
1347
|
+
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1348
|
+
collectUiContractFiles(uiRoot),
|
|
1349
|
+
collectApiContractFiles(apiRoot),
|
|
1350
|
+
collectDbContractFiles(dbRoot)
|
|
1351
|
+
]);
|
|
1352
|
+
const index = {
|
|
1353
|
+
ids: /* @__PURE__ */ new Set(),
|
|
1354
|
+
idToFiles: /* @__PURE__ */ new Map(),
|
|
1355
|
+
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1356
|
+
};
|
|
1357
|
+
await indexContractFiles(uiFiles, index);
|
|
1358
|
+
await indexContractFiles(apiFiles, index);
|
|
1359
|
+
await indexContractFiles(dbFiles, index);
|
|
1360
|
+
return index;
|
|
1361
|
+
}
|
|
1362
|
+
async function indexContractFiles(files, index) {
|
|
1363
|
+
for (const file of files) {
|
|
1364
|
+
const text = await (0, import_promises10.readFile)(file, "utf-8");
|
|
1365
|
+
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function record(index, id, file) {
|
|
1369
|
+
index.ids.add(id);
|
|
1370
|
+
const current = index.idToFiles.get(id) ?? /* @__PURE__ */ new Set();
|
|
1371
|
+
current.add(file);
|
|
1372
|
+
index.idToFiles.set(id, current);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/core/ids.ts
|
|
1376
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
1377
|
+
var STRICT_ID_PATTERNS = {
|
|
1378
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1379
|
+
BR: /\bBR-\d{4}\b/g,
|
|
1380
|
+
SC: /\bSC-\d{4}\b/g,
|
|
1381
|
+
UI: /\bUI-\d{4}\b/g,
|
|
1382
|
+
API: /\bAPI-\d{4}\b/g,
|
|
1383
|
+
DB: /\bDB-\d{4}\b/g,
|
|
1384
|
+
ADR: /\bADR-\d{4}\b/g
|
|
1385
|
+
};
|
|
1386
|
+
var LOOSE_ID_PATTERNS = {
|
|
1387
|
+
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
1388
|
+
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
1389
|
+
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
1390
|
+
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1391
|
+
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1392
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1393
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1394
|
+
};
|
|
1395
|
+
function extractIds(text, prefix) {
|
|
1396
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1397
|
+
const matches = text.match(pattern);
|
|
1398
|
+
return unique2(matches ?? []);
|
|
1399
|
+
}
|
|
1400
|
+
function extractAllIds(text) {
|
|
1401
|
+
const all = [];
|
|
1402
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
1403
|
+
all.push(...extractIds(text, prefix));
|
|
1404
|
+
});
|
|
1405
|
+
return unique2(all);
|
|
1406
|
+
}
|
|
1407
|
+
function extractInvalidIds(text, prefixes) {
|
|
1408
|
+
const invalid = [];
|
|
1409
|
+
for (const prefix of prefixes) {
|
|
1410
|
+
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
1411
|
+
for (const candidate of candidates) {
|
|
1412
|
+
if (!isValidId(candidate, prefix)) {
|
|
1413
|
+
invalid.push(candidate);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return unique2(invalid);
|
|
1418
|
+
}
|
|
1419
|
+
function unique2(values) {
|
|
1420
|
+
return Array.from(new Set(values));
|
|
1421
|
+
}
|
|
1422
|
+
function isValidId(value, prefix) {
|
|
1423
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1424
|
+
const strict = new RegExp(pattern.source);
|
|
1425
|
+
return strict.test(value);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/core/parse/contractRefs.ts
|
|
1429
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1430
|
+
function parseContractRefs(text, options = {}) {
|
|
1431
|
+
const linePattern = buildLinePattern(options);
|
|
1432
|
+
const lines = [];
|
|
1433
|
+
for (const match of text.matchAll(linePattern)) {
|
|
1434
|
+
lines.push((match[1] ?? "").trim());
|
|
1435
|
+
}
|
|
1436
|
+
const ids = [];
|
|
1437
|
+
const invalidTokens = [];
|
|
1438
|
+
let hasNone = false;
|
|
1439
|
+
for (const line of lines) {
|
|
1440
|
+
if (line.length === 0) {
|
|
1441
|
+
invalidTokens.push("(empty)");
|
|
1306
1442
|
continue;
|
|
1307
1443
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1444
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
1445
|
+
for (const token of tokens) {
|
|
1446
|
+
if (token.length === 0) {
|
|
1447
|
+
invalidTokens.push("(empty)");
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (token === "none") {
|
|
1451
|
+
hasNone = true;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
1455
|
+
ids.push(token);
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
invalidTokens.push(token);
|
|
1312
1459
|
}
|
|
1313
1460
|
}
|
|
1314
1461
|
return {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
matchedFileCount: normalizedFiles.length
|
|
1320
|
-
}
|
|
1462
|
+
lines,
|
|
1463
|
+
ids: unique3(ids),
|
|
1464
|
+
invalidTokens: unique3(invalidTokens),
|
|
1465
|
+
hasNone
|
|
1321
1466
|
};
|
|
1322
1467
|
}
|
|
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
|
-
};
|
|
1468
|
+
function buildLinePattern(options) {
|
|
1469
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
1470
|
+
return new RegExp(
|
|
1471
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
1472
|
+
"gm"
|
|
1473
|
+
);
|
|
1345
1474
|
}
|
|
1346
|
-
function
|
|
1347
|
-
return Array.from(new Set(values))
|
|
1475
|
+
function unique3(values) {
|
|
1476
|
+
return Array.from(new Set(values));
|
|
1348
1477
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1478
|
+
|
|
1479
|
+
// src/core/parse/markdown.ts
|
|
1480
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1481
|
+
function parseHeadings(md) {
|
|
1482
|
+
const lines = md.split(/\r?\n/);
|
|
1483
|
+
const headings = [];
|
|
1484
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1485
|
+
const line = lines[i] ?? "";
|
|
1486
|
+
const match = line.match(HEADING_RE);
|
|
1487
|
+
if (!match) continue;
|
|
1488
|
+
const levelToken = match[1];
|
|
1489
|
+
const title = match[2];
|
|
1490
|
+
if (!levelToken || !title) continue;
|
|
1491
|
+
headings.push({
|
|
1492
|
+
level: levelToken.length,
|
|
1493
|
+
title: title.trim(),
|
|
1494
|
+
line: i + 1
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return headings;
|
|
1351
1498
|
}
|
|
1352
|
-
function
|
|
1353
|
-
|
|
1354
|
-
|
|
1499
|
+
function extractH2Sections(md) {
|
|
1500
|
+
const lines = md.split(/\r?\n/);
|
|
1501
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1502
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1503
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1504
|
+
const current = headings[i];
|
|
1505
|
+
if (!current) continue;
|
|
1506
|
+
const next = headings[i + 1];
|
|
1507
|
+
const startLine = current.line + 1;
|
|
1508
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1509
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1510
|
+
sections.set(current.title.trim(), {
|
|
1511
|
+
title: current.title.trim(),
|
|
1512
|
+
startLine,
|
|
1513
|
+
endLine,
|
|
1514
|
+
body
|
|
1515
|
+
});
|
|
1355
1516
|
}
|
|
1356
|
-
return
|
|
1517
|
+
return sections;
|
|
1357
1518
|
}
|
|
1358
1519
|
|
|
1359
|
-
// src/core/
|
|
1360
|
-
var
|
|
1361
|
-
var
|
|
1362
|
-
var
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1520
|
+
// src/core/parse/spec.ts
|
|
1521
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1522
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1523
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1524
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1525
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1526
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1527
|
+
function parseSpec(md, file) {
|
|
1528
|
+
const headings = parseHeadings(md);
|
|
1529
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1530
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1531
|
+
const sections = extractH2Sections(md);
|
|
1532
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1533
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1534
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1535
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1536
|
+
const brs = [];
|
|
1537
|
+
const brsWithoutPriority = [];
|
|
1538
|
+
const brsWithInvalidPriority = [];
|
|
1539
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1540
|
+
const lineText = brLines[i] ?? "";
|
|
1541
|
+
const lineNumber = startLine + i;
|
|
1542
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1543
|
+
if (validMatch) {
|
|
1544
|
+
const id = validMatch[1];
|
|
1545
|
+
const priority = validMatch[2];
|
|
1546
|
+
const text = validMatch[3];
|
|
1547
|
+
if (!id || !priority || !text) continue;
|
|
1548
|
+
brs.push({
|
|
1549
|
+
id,
|
|
1550
|
+
priority,
|
|
1551
|
+
text: text.trim(),
|
|
1552
|
+
line: lineNumber
|
|
1553
|
+
});
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1557
|
+
if (anyPriorityMatch) {
|
|
1558
|
+
const id = anyPriorityMatch[1];
|
|
1559
|
+
const priority = anyPriorityMatch[2];
|
|
1560
|
+
const text = anyPriorityMatch[3];
|
|
1561
|
+
if (!id || !priority || !text) continue;
|
|
1562
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1563
|
+
brsWithInvalidPriority.push({
|
|
1564
|
+
id,
|
|
1565
|
+
priority,
|
|
1566
|
+
text: text.trim(),
|
|
1567
|
+
line: lineNumber
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1573
|
+
if (noPriorityMatch) {
|
|
1574
|
+
const id = noPriorityMatch[1];
|
|
1575
|
+
const text = noPriorityMatch[2];
|
|
1576
|
+
if (!id || !text) continue;
|
|
1577
|
+
brsWithoutPriority.push({
|
|
1578
|
+
id,
|
|
1579
|
+
text: text.trim(),
|
|
1580
|
+
line: lineNumber
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1366
1583
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1584
|
+
const parsed = {
|
|
1585
|
+
file,
|
|
1586
|
+
sections: sectionNames,
|
|
1587
|
+
brs,
|
|
1588
|
+
brsWithoutPriority,
|
|
1589
|
+
brsWithInvalidPriority,
|
|
1590
|
+
contractRefs: parseContractRefs(md)
|
|
1591
|
+
};
|
|
1592
|
+
if (specId) {
|
|
1593
|
+
parsed.specId = specId;
|
|
1375
1594
|
}
|
|
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");
|
|
1595
|
+
return parsed;
|
|
1381
1596
|
}
|
|
1382
1597
|
|
|
1383
1598
|
// src/core/validators/contracts.ts
|
|
1384
|
-
var
|
|
1385
|
-
var
|
|
1599
|
+
var import_promises11 = require("fs/promises");
|
|
1600
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
1386
1601
|
|
|
1387
1602
|
// src/core/contracts.ts
|
|
1388
|
-
var
|
|
1603
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1389
1604
|
var import_yaml2 = require("yaml");
|
|
1390
1605
|
function parseStructuredContract(file, text) {
|
|
1391
|
-
const ext =
|
|
1606
|
+
const ext = import_node_path13.default.extname(file).toLowerCase();
|
|
1392
1607
|
if (ext === ".json") {
|
|
1393
1608
|
return JSON.parse(text);
|
|
1394
1609
|
}
|
|
@@ -1408,9 +1623,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1408
1623
|
async function validateContracts(root, config) {
|
|
1409
1624
|
const issues = [];
|
|
1410
1625
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1411
|
-
issues.push(...await validateUiContracts(
|
|
1412
|
-
issues.push(...await validateApiContracts(
|
|
1413
|
-
issues.push(...await validateDbContracts(
|
|
1626
|
+
issues.push(...await validateUiContracts(import_node_path14.default.join(contractsRoot, "ui")));
|
|
1627
|
+
issues.push(...await validateApiContracts(import_node_path14.default.join(contractsRoot, "api")));
|
|
1628
|
+
issues.push(...await validateDbContracts(import_node_path14.default.join(contractsRoot, "db")));
|
|
1414
1629
|
const contractIndex = await buildContractIndex(root, config);
|
|
1415
1630
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1416
1631
|
return issues;
|
|
@@ -1430,7 +1645,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
1430
1645
|
}
|
|
1431
1646
|
const issues = [];
|
|
1432
1647
|
for (const file of files) {
|
|
1433
|
-
const text = await (0,
|
|
1648
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1434
1649
|
const invalidIds = extractInvalidIds(text, [
|
|
1435
1650
|
"SPEC",
|
|
1436
1651
|
"BR",
|
|
@@ -1485,7 +1700,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1485
1700
|
}
|
|
1486
1701
|
const issues = [];
|
|
1487
1702
|
for (const file of files) {
|
|
1488
|
-
const text = await (0,
|
|
1703
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1489
1704
|
const invalidIds = extractInvalidIds(text, [
|
|
1490
1705
|
"SPEC",
|
|
1491
1706
|
"BR",
|
|
@@ -1553,7 +1768,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1553
1768
|
}
|
|
1554
1769
|
const issues = [];
|
|
1555
1770
|
for (const file of files) {
|
|
1556
|
-
const text = await (0,
|
|
1771
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1557
1772
|
const invalidIds = extractInvalidIds(text, [
|
|
1558
1773
|
"SPEC",
|
|
1559
1774
|
"BR",
|
|
@@ -1692,8 +1907,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1692
1907
|
}
|
|
1693
1908
|
|
|
1694
1909
|
// src/core/validators/delta.ts
|
|
1695
|
-
var
|
|
1696
|
-
var
|
|
1910
|
+
var import_promises12 = require("fs/promises");
|
|
1911
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
1697
1912
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1698
1913
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1699
1914
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1707,10 +1922,10 @@ async function validateDeltas(root, config) {
|
|
|
1707
1922
|
}
|
|
1708
1923
|
const issues = [];
|
|
1709
1924
|
for (const pack of packs) {
|
|
1710
|
-
const deltaPath =
|
|
1925
|
+
const deltaPath = import_node_path15.default.join(pack, "delta.md");
|
|
1711
1926
|
let text;
|
|
1712
1927
|
try {
|
|
1713
|
-
text = await (0,
|
|
1928
|
+
text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
|
|
1714
1929
|
} catch (error2) {
|
|
1715
1930
|
if (isMissingFileError2(error2)) {
|
|
1716
1931
|
issues.push(
|
|
@@ -1782,8 +1997,8 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1782
1997
|
}
|
|
1783
1998
|
|
|
1784
1999
|
// src/core/validators/ids.ts
|
|
1785
|
-
var
|
|
1786
|
-
var
|
|
2000
|
+
var import_promises13 = require("fs/promises");
|
|
2001
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
1787
2002
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1788
2003
|
async function validateDefinedIds(root, config) {
|
|
1789
2004
|
const issues = [];
|
|
@@ -1818,7 +2033,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1818
2033
|
}
|
|
1819
2034
|
async function collectSpecDefinitionIds(files, out) {
|
|
1820
2035
|
for (const file of files) {
|
|
1821
|
-
const text = await (0,
|
|
2036
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1822
2037
|
const parsed = parseSpec(text, file);
|
|
1823
2038
|
if (parsed.specId) {
|
|
1824
2039
|
recordId(out, parsed.specId, file);
|
|
@@ -1828,7 +2043,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1828
2043
|
}
|
|
1829
2044
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1830
2045
|
for (const file of files) {
|
|
1831
|
-
const text = await (0,
|
|
2046
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1832
2047
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1833
2048
|
if (!document || errors.length > 0) {
|
|
1834
2049
|
continue;
|
|
@@ -1849,7 +2064,7 @@ function recordId(out, id, file) {
|
|
|
1849
2064
|
}
|
|
1850
2065
|
function formatFileList(files, root) {
|
|
1851
2066
|
return files.map((file) => {
|
|
1852
|
-
const relative =
|
|
2067
|
+
const relative = import_node_path16.default.relative(root, file);
|
|
1853
2068
|
return relative.length > 0 ? relative : file;
|
|
1854
2069
|
}).join(", ");
|
|
1855
2070
|
}
|
|
@@ -1872,7 +2087,7 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1872
2087
|
}
|
|
1873
2088
|
|
|
1874
2089
|
// src/core/validators/scenario.ts
|
|
1875
|
-
var
|
|
2090
|
+
var import_promises14 = require("fs/promises");
|
|
1876
2091
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1877
2092
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1878
2093
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -1898,7 +2113,7 @@ async function validateScenarios(root, config) {
|
|
|
1898
2113
|
for (const entry of entries) {
|
|
1899
2114
|
let text;
|
|
1900
2115
|
try {
|
|
1901
|
-
text = await (0,
|
|
2116
|
+
text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
|
|
1902
2117
|
} catch (error2) {
|
|
1903
2118
|
if (isMissingFileError3(error2)) {
|
|
1904
2119
|
issues.push(
|
|
@@ -2068,7 +2283,7 @@ function isMissingFileError3(error2) {
|
|
|
2068
2283
|
}
|
|
2069
2284
|
|
|
2070
2285
|
// src/core/validators/spec.ts
|
|
2071
|
-
var
|
|
2286
|
+
var import_promises15 = require("fs/promises");
|
|
2072
2287
|
async function validateSpecs(root, config) {
|
|
2073
2288
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2074
2289
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -2089,7 +2304,7 @@ async function validateSpecs(root, config) {
|
|
|
2089
2304
|
for (const entry of entries) {
|
|
2090
2305
|
let text;
|
|
2091
2306
|
try {
|
|
2092
|
-
text = await (0,
|
|
2307
|
+
text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
|
|
2093
2308
|
} catch (error2) {
|
|
2094
2309
|
if (isMissingFileError4(error2)) {
|
|
2095
2310
|
issues.push(
|
|
@@ -2238,7 +2453,7 @@ function isMissingFileError4(error2) {
|
|
|
2238
2453
|
}
|
|
2239
2454
|
|
|
2240
2455
|
// src/core/validators/traceability.ts
|
|
2241
|
-
var
|
|
2456
|
+
var import_promises16 = require("fs/promises");
|
|
2242
2457
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2243
2458
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2244
2459
|
async function validateTraceability(root, config) {
|
|
@@ -2258,7 +2473,7 @@ async function validateTraceability(root, config) {
|
|
|
2258
2473
|
const contractIndex = await buildContractIndex(root, config);
|
|
2259
2474
|
const contractIds = contractIndex.ids;
|
|
2260
2475
|
for (const file of specFiles) {
|
|
2261
|
-
const text = await (0,
|
|
2476
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2262
2477
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2263
2478
|
const parsed = parseSpec(text, file);
|
|
2264
2479
|
if (parsed.specId) {
|
|
@@ -2331,7 +2546,7 @@ async function validateTraceability(root, config) {
|
|
|
2331
2546
|
}
|
|
2332
2547
|
}
|
|
2333
2548
|
for (const file of scenarioFiles) {
|
|
2334
|
-
const text = await (0,
|
|
2549
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2335
2550
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2336
2551
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2337
2552
|
allowCommentPrefix: true
|
|
@@ -2653,7 +2868,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2653
2868
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2654
2869
|
let found = false;
|
|
2655
2870
|
for (const file of targetFiles) {
|
|
2656
|
-
const text = await (0,
|
|
2871
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2657
2872
|
if (pattern.test(text)) {
|
|
2658
2873
|
found = true;
|
|
2659
2874
|
break;
|
|
@@ -2740,15 +2955,15 @@ function countIssues(issues) {
|
|
|
2740
2955
|
// src/core/report.ts
|
|
2741
2956
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2742
2957
|
async function createReportData(root, validation, configResult) {
|
|
2743
|
-
const resolvedRoot =
|
|
2958
|
+
const resolvedRoot = import_node_path17.default.resolve(root);
|
|
2744
2959
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2745
2960
|
const config = resolved.config;
|
|
2746
2961
|
const configPath = resolved.configPath;
|
|
2747
2962
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2748
2963
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2749
|
-
const apiRoot =
|
|
2750
|
-
const uiRoot =
|
|
2751
|
-
const dbRoot =
|
|
2964
|
+
const apiRoot = import_node_path17.default.join(contractsRoot, "api");
|
|
2965
|
+
const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
|
|
2966
|
+
const dbRoot = import_node_path17.default.join(contractsRoot, "db");
|
|
2752
2967
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2753
2968
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2754
2969
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2806,11 +3021,13 @@ async function createReportData(root, validation, configResult) {
|
|
|
2806
3021
|
normalizeScSources(resolvedRoot, scSources)
|
|
2807
3022
|
);
|
|
2808
3023
|
const version = await resolveToolVersion();
|
|
3024
|
+
const reportFormatVersion = 1;
|
|
2809
3025
|
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2810
3026
|
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2811
3027
|
return {
|
|
2812
3028
|
tool: "qfai",
|
|
2813
3029
|
version,
|
|
3030
|
+
reportFormatVersion,
|
|
2814
3031
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2815
3032
|
root: displayRoot,
|
|
2816
3033
|
configPath: displayConfigPath,
|
|
@@ -3063,7 +3280,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
3063
3280
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
3064
3281
|
}
|
|
3065
3282
|
for (const file of specFiles) {
|
|
3066
|
-
const text = await (0,
|
|
3283
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3067
3284
|
const parsed = parseSpec(text, file);
|
|
3068
3285
|
const specKey = parsed.specId;
|
|
3069
3286
|
if (!specKey) {
|
|
@@ -3104,7 +3321,7 @@ async function collectIds(files) {
|
|
|
3104
3321
|
DB: /* @__PURE__ */ new Set()
|
|
3105
3322
|
};
|
|
3106
3323
|
for (const file of files) {
|
|
3107
|
-
const text = await (0,
|
|
3324
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3108
3325
|
for (const prefix of ID_PREFIXES2) {
|
|
3109
3326
|
const ids = extractIds(text, prefix);
|
|
3110
3327
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3122,7 +3339,7 @@ async function collectIds(files) {
|
|
|
3122
3339
|
async function collectUpstreamIds(files) {
|
|
3123
3340
|
const ids = /* @__PURE__ */ new Set();
|
|
3124
3341
|
for (const file of files) {
|
|
3125
|
-
const text = await (0,
|
|
3342
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3126
3343
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3127
3344
|
}
|
|
3128
3345
|
return ids;
|
|
@@ -3143,7 +3360,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3143
3360
|
}
|
|
3144
3361
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3145
3362
|
for (const file of targetFiles) {
|
|
3146
|
-
const text = await (0,
|
|
3363
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3147
3364
|
if (pattern.test(text)) {
|
|
3148
3365
|
return true;
|
|
3149
3366
|
}
|
|
@@ -3235,7 +3452,7 @@ function buildHotspots(issues) {
|
|
|
3235
3452
|
|
|
3236
3453
|
// src/cli/commands/report.ts
|
|
3237
3454
|
async function runReport(options) {
|
|
3238
|
-
const root =
|
|
3455
|
+
const root = import_node_path18.default.resolve(options.root);
|
|
3239
3456
|
const configResult = await loadConfig(root);
|
|
3240
3457
|
let validation;
|
|
3241
3458
|
if (options.runValidate) {
|
|
@@ -3252,7 +3469,7 @@ async function runReport(options) {
|
|
|
3252
3469
|
validation = normalized;
|
|
3253
3470
|
} else {
|
|
3254
3471
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3255
|
-
const inputPath =
|
|
3472
|
+
const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
|
|
3256
3473
|
try {
|
|
3257
3474
|
validation = await readValidationResult(inputPath);
|
|
3258
3475
|
} catch (err) {
|
|
@@ -3278,11 +3495,11 @@ async function runReport(options) {
|
|
|
3278
3495
|
const data = await createReportData(root, validation, configResult);
|
|
3279
3496
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3280
3497
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3281
|
-
const defaultOut = options.format === "json" ?
|
|
3498
|
+
const defaultOut = options.format === "json" ? import_node_path18.default.join(outRoot, "report.json") : import_node_path18.default.join(outRoot, "report.md");
|
|
3282
3499
|
const out = options.outPath ?? defaultOut;
|
|
3283
|
-
const outPath =
|
|
3284
|
-
await (0,
|
|
3285
|
-
await (0,
|
|
3500
|
+
const outPath = import_node_path18.default.isAbsolute(out) ? out : import_node_path18.default.resolve(root, out);
|
|
3501
|
+
await (0, import_promises18.mkdir)(import_node_path18.default.dirname(outPath), { recursive: true });
|
|
3502
|
+
await (0, import_promises18.writeFile)(outPath, `${output}
|
|
3286
3503
|
`, "utf-8");
|
|
3287
3504
|
info(
|
|
3288
3505
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -3290,7 +3507,7 @@ async function runReport(options) {
|
|
|
3290
3507
|
info(`wrote report: ${outPath}`);
|
|
3291
3508
|
}
|
|
3292
3509
|
async function readValidationResult(inputPath) {
|
|
3293
|
-
const raw = await (0,
|
|
3510
|
+
const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
|
|
3294
3511
|
const parsed = JSON.parse(raw);
|
|
3295
3512
|
if (!isValidationResult(parsed)) {
|
|
3296
3513
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -3346,15 +3563,15 @@ function isMissingFileError5(error2) {
|
|
|
3346
3563
|
return record2.code === "ENOENT";
|
|
3347
3564
|
}
|
|
3348
3565
|
async function writeValidationResult(root, outputPath, result) {
|
|
3349
|
-
const abs =
|
|
3350
|
-
await (0,
|
|
3351
|
-
await (0,
|
|
3566
|
+
const abs = import_node_path18.default.isAbsolute(outputPath) ? outputPath : import_node_path18.default.resolve(root, outputPath);
|
|
3567
|
+
await (0, import_promises18.mkdir)(import_node_path18.default.dirname(abs), { recursive: true });
|
|
3568
|
+
await (0, import_promises18.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3352
3569
|
`, "utf-8");
|
|
3353
3570
|
}
|
|
3354
3571
|
|
|
3355
3572
|
// src/cli/commands/validate.ts
|
|
3356
|
-
var
|
|
3357
|
-
var
|
|
3573
|
+
var import_promises19 = require("fs/promises");
|
|
3574
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3358
3575
|
|
|
3359
3576
|
// src/cli/lib/failOn.ts
|
|
3360
3577
|
function shouldFail(result, failOn) {
|
|
@@ -3369,7 +3586,7 @@ function shouldFail(result, failOn) {
|
|
|
3369
3586
|
|
|
3370
3587
|
// src/cli/commands/validate.ts
|
|
3371
3588
|
async function runValidate(options) {
|
|
3372
|
-
const root =
|
|
3589
|
+
const root = import_node_path19.default.resolve(options.root);
|
|
3373
3590
|
const configResult = await loadConfig(root);
|
|
3374
3591
|
const result = await validateProject(root, configResult);
|
|
3375
3592
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -3486,12 +3703,12 @@ function issueKey(issue7) {
|
|
|
3486
3703
|
}
|
|
3487
3704
|
async function emitJson(result, root, jsonPath) {
|
|
3488
3705
|
const abs = resolveJsonPath(root, jsonPath);
|
|
3489
|
-
await (0,
|
|
3490
|
-
await (0,
|
|
3706
|
+
await (0, import_promises19.mkdir)(import_node_path19.default.dirname(abs), { recursive: true });
|
|
3707
|
+
await (0, import_promises19.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3491
3708
|
`, "utf-8");
|
|
3492
3709
|
}
|
|
3493
3710
|
function resolveJsonPath(root, jsonPath) {
|
|
3494
|
-
return
|
|
3711
|
+
return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
|
|
3495
3712
|
}
|
|
3496
3713
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3497
3714
|
|
|
@@ -3506,6 +3723,7 @@ function parseArgs(argv, cwd) {
|
|
|
3506
3723
|
dryRun: false,
|
|
3507
3724
|
reportFormat: "md",
|
|
3508
3725
|
reportRunValidate: false,
|
|
3726
|
+
doctorFormat: "text",
|
|
3509
3727
|
validateFormat: "text",
|
|
3510
3728
|
strict: false,
|
|
3511
3729
|
help: false
|
|
@@ -3558,7 +3776,11 @@ function parseArgs(argv, cwd) {
|
|
|
3558
3776
|
{
|
|
3559
3777
|
const next = args[i + 1];
|
|
3560
3778
|
if (next) {
|
|
3561
|
-
|
|
3779
|
+
if (command === "doctor") {
|
|
3780
|
+
options.doctorOut = next;
|
|
3781
|
+
} else {
|
|
3782
|
+
options.reportOut = next;
|
|
3783
|
+
}
|
|
3562
3784
|
}
|
|
3563
3785
|
}
|
|
3564
3786
|
i += 1;
|
|
@@ -3601,6 +3823,12 @@ function applyFormatOption(command, value, options) {
|
|
|
3601
3823
|
}
|
|
3602
3824
|
return;
|
|
3603
3825
|
}
|
|
3826
|
+
if (command === "doctor") {
|
|
3827
|
+
if (value === "text" || value === "json") {
|
|
3828
|
+
options.doctorFormat = value;
|
|
3829
|
+
}
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3604
3832
|
if (value === "md" || value === "json") {
|
|
3605
3833
|
options.reportFormat = value;
|
|
3606
3834
|
}
|
|
@@ -3648,6 +3876,14 @@ async function run(argv, cwd) {
|
|
|
3648
3876
|
});
|
|
3649
3877
|
}
|
|
3650
3878
|
return;
|
|
3879
|
+
case "doctor":
|
|
3880
|
+
await runDoctor({
|
|
3881
|
+
root: options.root,
|
|
3882
|
+
rootExplicit: options.rootExplicit,
|
|
3883
|
+
format: options.doctorFormat,
|
|
3884
|
+
...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
|
|
3885
|
+
});
|
|
3886
|
+
return;
|
|
3651
3887
|
default:
|
|
3652
3888
|
error(`Unknown command: ${command}`);
|
|
3653
3889
|
info(usage());
|
|
@@ -3661,6 +3897,7 @@ Commands:
|
|
|
3661
3897
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
3662
3898
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
3663
3899
|
report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
|
|
3900
|
+
doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
|
|
3664
3901
|
|
|
3665
3902
|
Options:
|
|
3666
3903
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -3670,9 +3907,10 @@ Options:
|
|
|
3670
3907
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
3671
3908
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3672
3909
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3910
|
+
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3673
3911
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3674
3912
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3675
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
3913
|
+
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
3676
3914
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3677
3915
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3678
3916
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|