qfai 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/assets/init/.qfai/README.md +6 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/steering/traceability.md +2 -1
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +1 -1
- package/assets/init/.qfai/specs/README.md +3 -1
- package/assets/init/.qfai/specs/spec-0001/scenario.md +2 -1
- package/assets/init/root/qfai.config.yaml +1 -1
- package/dist/cli/index.cjs +1263 -704
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +1246 -687
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +343 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +347 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/cli/index.cjs
CHANGED
|
@@ -23,166 +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 error(message) {
|
|
143
|
-
process.stderr.write(`${message}
|
|
144
|
-
`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// src/cli/commands/init.ts
|
|
148
|
-
async function runInit(options) {
|
|
149
|
-
const assetsRoot = getInitAssetsDir();
|
|
150
|
-
const rootAssets = import_node_path3.default.join(assetsRoot, "root");
|
|
151
|
-
const qfaiAssets = import_node_path3.default.join(assetsRoot, ".qfai");
|
|
152
|
-
const destRoot = import_node_path3.default.resolve(options.dir);
|
|
153
|
-
const destQfai = import_node_path3.default.join(destRoot, ".qfai");
|
|
154
|
-
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
155
|
-
force: options.force,
|
|
156
|
-
dryRun: options.dryRun
|
|
157
|
-
});
|
|
158
|
-
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
159
|
-
force: options.force,
|
|
160
|
-
dryRun: options.dryRun
|
|
161
|
-
});
|
|
162
|
-
report(
|
|
163
|
-
[...rootResult.copied, ...qfaiResult.copied],
|
|
164
|
-
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
165
|
-
options.dryRun,
|
|
166
|
-
"init"
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
function report(copied, skipped, dryRun, label) {
|
|
170
|
-
info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
|
|
171
|
-
if (copied.length > 0) {
|
|
172
|
-
info(` created: ${copied.length}`);
|
|
173
|
-
}
|
|
174
|
-
if (skipped.length > 0) {
|
|
175
|
-
info(` skipped: ${skipped.length}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
26
|
+
// src/cli/commands/doctor.ts
|
|
27
|
+
var import_promises8 = require("fs/promises");
|
|
28
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
178
29
|
|
|
179
|
-
// src/
|
|
180
|
-
var
|
|
181
|
-
var
|
|
30
|
+
// src/core/doctor.ts
|
|
31
|
+
var import_promises7 = require("fs/promises");
|
|
32
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
182
33
|
|
|
183
34
|
// src/core/config.ts
|
|
184
|
-
var
|
|
185
|
-
var
|
|
35
|
+
var import_promises = require("fs/promises");
|
|
36
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
186
37
|
var import_yaml = require("yaml");
|
|
187
38
|
var defaultConfig = {
|
|
188
39
|
paths: {
|
|
@@ -213,7 +64,7 @@ var defaultConfig = {
|
|
|
213
64
|
testFileGlobs: [],
|
|
214
65
|
testFileExcludeGlobs: [],
|
|
215
66
|
scNoTestSeverity: "error",
|
|
216
|
-
|
|
67
|
+
orphanContractsPolicy: "error",
|
|
217
68
|
unknownContractIdSeverity: "error"
|
|
218
69
|
}
|
|
219
70
|
},
|
|
@@ -222,14 +73,34 @@ var defaultConfig = {
|
|
|
222
73
|
}
|
|
223
74
|
};
|
|
224
75
|
function getConfigPath(root) {
|
|
225
|
-
return
|
|
76
|
+
return import_node_path.default.join(root, "qfai.config.yaml");
|
|
77
|
+
}
|
|
78
|
+
async function findConfigRoot(startDir) {
|
|
79
|
+
const resolvedStart = import_node_path.default.resolve(startDir);
|
|
80
|
+
let current = resolvedStart;
|
|
81
|
+
while (true) {
|
|
82
|
+
const configPath = getConfigPath(current);
|
|
83
|
+
if (await exists(configPath)) {
|
|
84
|
+
return { root: current, configPath, found: true };
|
|
85
|
+
}
|
|
86
|
+
const parent = import_node_path.default.dirname(current);
|
|
87
|
+
if (parent === current) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
current = parent;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
root: resolvedStart,
|
|
94
|
+
configPath: getConfigPath(resolvedStart),
|
|
95
|
+
found: false
|
|
96
|
+
};
|
|
226
97
|
}
|
|
227
98
|
async function loadConfig(root) {
|
|
228
99
|
const configPath = getConfigPath(root);
|
|
229
100
|
const issues = [];
|
|
230
101
|
let parsed;
|
|
231
102
|
try {
|
|
232
|
-
const raw = await (0,
|
|
103
|
+
const raw = await (0, import_promises.readFile)(configPath, "utf-8");
|
|
233
104
|
parsed = (0, import_yaml.parse)(raw);
|
|
234
105
|
} catch (error2) {
|
|
235
106
|
if (isMissingFile(error2)) {
|
|
@@ -242,7 +113,7 @@ async function loadConfig(root) {
|
|
|
242
113
|
return { config: normalized, issues, configPath };
|
|
243
114
|
}
|
|
244
115
|
function resolvePath(root, config, key) {
|
|
245
|
-
return
|
|
116
|
+
return import_node_path.default.resolve(root, config.paths[key]);
|
|
246
117
|
}
|
|
247
118
|
function normalizeConfig(raw, configPath, issues) {
|
|
248
119
|
if (!isRecord(raw)) {
|
|
@@ -413,10 +284,10 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
413
284
|
configPath,
|
|
414
285
|
issues
|
|
415
286
|
),
|
|
416
|
-
|
|
417
|
-
traceabilityRaw?.
|
|
418
|
-
base.traceability.
|
|
419
|
-
"validation.traceability.
|
|
287
|
+
orphanContractsPolicy: readOrphanContractsPolicy(
|
|
288
|
+
traceabilityRaw?.orphanContractsPolicy,
|
|
289
|
+
base.traceability.orphanContractsPolicy,
|
|
290
|
+
"validation.traceability.orphanContractsPolicy",
|
|
420
291
|
configPath,
|
|
421
292
|
issues
|
|
422
293
|
),
|
|
@@ -512,6 +383,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
512
383
|
}
|
|
513
384
|
return fallback;
|
|
514
385
|
}
|
|
386
|
+
function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
|
|
387
|
+
if (value === "error" || value === "warning" || value === "allow") {
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
if (value !== void 0) {
|
|
391
|
+
issues.push(
|
|
392
|
+
configIssue(
|
|
393
|
+
configPath,
|
|
394
|
+
`${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
395
|
+
)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return fallback;
|
|
399
|
+
}
|
|
515
400
|
function configIssue(file, message) {
|
|
516
401
|
return {
|
|
517
402
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -527,6 +412,14 @@ function isMissingFile(error2) {
|
|
|
527
412
|
}
|
|
528
413
|
return false;
|
|
529
414
|
}
|
|
415
|
+
async function exists(target) {
|
|
416
|
+
try {
|
|
417
|
+
await (0, import_promises.access)(target);
|
|
418
|
+
return true;
|
|
419
|
+
} catch {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
530
423
|
function formatError(error2) {
|
|
531
424
|
if (error2 instanceof Error) {
|
|
532
425
|
return error2.message;
|
|
@@ -537,20 +430,12 @@ function isRecord(value) {
|
|
|
537
430
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
538
431
|
}
|
|
539
432
|
|
|
540
|
-
// src/core/report.ts
|
|
541
|
-
var import_promises15 = require("fs/promises");
|
|
542
|
-
var import_node_path14 = __toESM(require("path"), 1);
|
|
543
|
-
|
|
544
|
-
// src/core/contractIndex.ts
|
|
545
|
-
var import_promises6 = require("fs/promises");
|
|
546
|
-
var import_node_path7 = __toESM(require("path"), 1);
|
|
547
|
-
|
|
548
433
|
// src/core/discovery.ts
|
|
549
|
-
var
|
|
434
|
+
var import_promises4 = require("fs/promises");
|
|
550
435
|
|
|
551
436
|
// src/core/fs.ts
|
|
552
|
-
var
|
|
553
|
-
var
|
|
437
|
+
var import_promises2 = require("fs/promises");
|
|
438
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
554
439
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
555
440
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
556
441
|
"node_modules",
|
|
@@ -586,9 +471,9 @@ async function collectFilesByGlobs(root, options) {
|
|
|
586
471
|
});
|
|
587
472
|
}
|
|
588
473
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
589
|
-
const items = await (0,
|
|
474
|
+
const items = await (0, import_promises2.readdir)(current, { withFileTypes: true });
|
|
590
475
|
for (const item of items) {
|
|
591
|
-
const fullPath =
|
|
476
|
+
const fullPath = import_node_path2.default.join(current, item.name);
|
|
592
477
|
if (item.isDirectory()) {
|
|
593
478
|
if (ignoreDirs.has(item.name)) {
|
|
594
479
|
continue;
|
|
@@ -598,7 +483,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
598
483
|
}
|
|
599
484
|
if (item.isFile()) {
|
|
600
485
|
if (extensions.length > 0) {
|
|
601
|
-
const ext =
|
|
486
|
+
const ext = import_node_path2.default.extname(item.name).toLowerCase();
|
|
602
487
|
if (!extensions.includes(ext)) {
|
|
603
488
|
continue;
|
|
604
489
|
}
|
|
@@ -609,7 +494,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
609
494
|
}
|
|
610
495
|
async function exists2(target) {
|
|
611
496
|
try {
|
|
612
|
-
await (0,
|
|
497
|
+
await (0, import_promises2.access)(target);
|
|
613
498
|
return true;
|
|
614
499
|
} catch {
|
|
615
500
|
return false;
|
|
@@ -617,23 +502,23 @@ async function exists2(target) {
|
|
|
617
502
|
}
|
|
618
503
|
|
|
619
504
|
// src/core/specLayout.ts
|
|
620
|
-
var
|
|
621
|
-
var
|
|
505
|
+
var import_promises3 = require("fs/promises");
|
|
506
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
622
507
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
623
508
|
async function collectSpecEntries(specsRoot) {
|
|
624
509
|
const dirs = await listSpecDirs(specsRoot);
|
|
625
510
|
const entries = dirs.map((dir) => ({
|
|
626
511
|
dir,
|
|
627
|
-
specPath:
|
|
628
|
-
deltaPath:
|
|
629
|
-
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")
|
|
630
515
|
}));
|
|
631
516
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
632
517
|
}
|
|
633
518
|
async function listSpecDirs(specsRoot) {
|
|
634
519
|
try {
|
|
635
|
-
const items = await (0,
|
|
636
|
-
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));
|
|
637
522
|
} catch (error2) {
|
|
638
523
|
if (isMissingFileError(error2)) {
|
|
639
524
|
return [];
|
|
@@ -689,314 +574,65 @@ async function filterExisting(files) {
|
|
|
689
574
|
}
|
|
690
575
|
async function exists3(target) {
|
|
691
576
|
try {
|
|
692
|
-
await (0,
|
|
577
|
+
await (0, import_promises4.access)(target);
|
|
693
578
|
return true;
|
|
694
579
|
} catch {
|
|
695
580
|
return false;
|
|
696
581
|
}
|
|
697
582
|
}
|
|
698
583
|
|
|
699
|
-
// src/core/
|
|
700
|
-
var
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
705
|
-
const id = match[1];
|
|
706
|
-
if (id) {
|
|
707
|
-
ids.push(id);
|
|
708
|
-
}
|
|
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;
|
|
709
589
|
}
|
|
710
|
-
|
|
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);
|
|
711
598
|
}
|
|
712
|
-
function
|
|
713
|
-
return
|
|
599
|
+
function toPosixPath(value) {
|
|
600
|
+
return value.replace(/\\/g, "/");
|
|
714
601
|
}
|
|
715
602
|
|
|
716
|
-
// src/core/
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
async function indexContractFiles(files, index) {
|
|
738
|
-
for (const file of files) {
|
|
739
|
-
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
740
|
-
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
603
|
+
// src/core/traceability.ts
|
|
604
|
+
var import_promises5 = require("fs/promises");
|
|
605
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
606
|
+
|
|
607
|
+
// src/core/gherkin/parse.ts
|
|
608
|
+
var import_gherkin = require("@cucumber/gherkin");
|
|
609
|
+
var import_node_crypto = require("crypto");
|
|
610
|
+
function parseGherkin(source, uri) {
|
|
611
|
+
const errors = [];
|
|
612
|
+
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
613
|
+
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
614
|
+
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
615
|
+
const parser = new import_gherkin.Parser(builder, matcher);
|
|
616
|
+
try {
|
|
617
|
+
const gherkinDocument = parser.parse(source);
|
|
618
|
+
gherkinDocument.uri = uri;
|
|
619
|
+
return { gherkinDocument, errors };
|
|
620
|
+
} catch (error2) {
|
|
621
|
+
errors.push(formatError2(error2));
|
|
622
|
+
return { gherkinDocument: null, errors };
|
|
741
623
|
}
|
|
742
624
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// src/core/ids.ts
|
|
751
|
-
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
752
|
-
var STRICT_ID_PATTERNS = {
|
|
753
|
-
SPEC: /\bSPEC-\d{4}\b/g,
|
|
754
|
-
BR: /\bBR-\d{4}\b/g,
|
|
755
|
-
SC: /\bSC-\d{4}\b/g,
|
|
756
|
-
UI: /\bUI-\d{4}\b/g,
|
|
757
|
-
API: /\bAPI-\d{4}\b/g,
|
|
758
|
-
DB: /\bDB-\d{4}\b/g,
|
|
759
|
-
ADR: /\bADR-\d{4}\b/g
|
|
760
|
-
};
|
|
761
|
-
var LOOSE_ID_PATTERNS = {
|
|
762
|
-
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
763
|
-
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
764
|
-
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
765
|
-
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
766
|
-
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
767
|
-
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
768
|
-
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
769
|
-
};
|
|
770
|
-
function extractIds(text, prefix) {
|
|
771
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
772
|
-
const matches = text.match(pattern);
|
|
773
|
-
return unique(matches ?? []);
|
|
774
|
-
}
|
|
775
|
-
function extractAllIds(text) {
|
|
776
|
-
const all = [];
|
|
777
|
-
ID_PREFIXES.forEach((prefix) => {
|
|
778
|
-
all.push(...extractIds(text, prefix));
|
|
779
|
-
});
|
|
780
|
-
return unique(all);
|
|
781
|
-
}
|
|
782
|
-
function extractInvalidIds(text, prefixes) {
|
|
783
|
-
const invalid = [];
|
|
784
|
-
for (const prefix of prefixes) {
|
|
785
|
-
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
786
|
-
for (const candidate of candidates) {
|
|
787
|
-
if (!isValidId(candidate, prefix)) {
|
|
788
|
-
invalid.push(candidate);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
return unique(invalid);
|
|
793
|
-
}
|
|
794
|
-
function unique(values) {
|
|
795
|
-
return Array.from(new Set(values));
|
|
796
|
-
}
|
|
797
|
-
function isValidId(value, prefix) {
|
|
798
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
799
|
-
const strict = new RegExp(pattern.source);
|
|
800
|
-
return strict.test(value);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// src/core/parse/markdown.ts
|
|
804
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
805
|
-
function parseHeadings(md) {
|
|
806
|
-
const lines = md.split(/\r?\n/);
|
|
807
|
-
const headings = [];
|
|
808
|
-
for (let i = 0; i < lines.length; i++) {
|
|
809
|
-
const line = lines[i] ?? "";
|
|
810
|
-
const match = line.match(HEADING_RE);
|
|
811
|
-
if (!match) continue;
|
|
812
|
-
const levelToken = match[1];
|
|
813
|
-
const title = match[2];
|
|
814
|
-
if (!levelToken || !title) continue;
|
|
815
|
-
headings.push({
|
|
816
|
-
level: levelToken.length,
|
|
817
|
-
title: title.trim(),
|
|
818
|
-
line: i + 1
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
return headings;
|
|
822
|
-
}
|
|
823
|
-
function extractH2Sections(md) {
|
|
824
|
-
const lines = md.split(/\r?\n/);
|
|
825
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
826
|
-
const sections = /* @__PURE__ */ new Map();
|
|
827
|
-
for (let i = 0; i < headings.length; i++) {
|
|
828
|
-
const current = headings[i];
|
|
829
|
-
if (!current) continue;
|
|
830
|
-
const next = headings[i + 1];
|
|
831
|
-
const startLine = current.line + 1;
|
|
832
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
833
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
834
|
-
sections.set(current.title.trim(), {
|
|
835
|
-
title: current.title.trim(),
|
|
836
|
-
startLine,
|
|
837
|
-
endLine,
|
|
838
|
-
body
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
return sections;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// src/core/parse/spec.ts
|
|
845
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
846
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
847
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
848
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
849
|
-
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
850
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
851
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
852
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
853
|
-
function parseSpec(md, file) {
|
|
854
|
-
const headings = parseHeadings(md);
|
|
855
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
856
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
857
|
-
const sections = extractH2Sections(md);
|
|
858
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
859
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
860
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
861
|
-
const startLine = brSection?.startLine ?? 1;
|
|
862
|
-
const brs = [];
|
|
863
|
-
const brsWithoutPriority = [];
|
|
864
|
-
const brsWithInvalidPriority = [];
|
|
865
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
866
|
-
const lineText = brLines[i] ?? "";
|
|
867
|
-
const lineNumber = startLine + i;
|
|
868
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
869
|
-
if (validMatch) {
|
|
870
|
-
const id = validMatch[1];
|
|
871
|
-
const priority = validMatch[2];
|
|
872
|
-
const text = validMatch[3];
|
|
873
|
-
if (!id || !priority || !text) continue;
|
|
874
|
-
brs.push({
|
|
875
|
-
id,
|
|
876
|
-
priority,
|
|
877
|
-
text: text.trim(),
|
|
878
|
-
line: lineNumber
|
|
879
|
-
});
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
883
|
-
if (anyPriorityMatch) {
|
|
884
|
-
const id = anyPriorityMatch[1];
|
|
885
|
-
const priority = anyPriorityMatch[2];
|
|
886
|
-
const text = anyPriorityMatch[3];
|
|
887
|
-
if (!id || !priority || !text) continue;
|
|
888
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
889
|
-
brsWithInvalidPriority.push({
|
|
890
|
-
id,
|
|
891
|
-
priority,
|
|
892
|
-
text: text.trim(),
|
|
893
|
-
line: lineNumber
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
899
|
-
if (noPriorityMatch) {
|
|
900
|
-
const id = noPriorityMatch[1];
|
|
901
|
-
const text = noPriorityMatch[2];
|
|
902
|
-
if (!id || !text) continue;
|
|
903
|
-
brsWithoutPriority.push({
|
|
904
|
-
id,
|
|
905
|
-
text: text.trim(),
|
|
906
|
-
line: lineNumber
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
const parsed = {
|
|
911
|
-
file,
|
|
912
|
-
sections: sectionNames,
|
|
913
|
-
brs,
|
|
914
|
-
brsWithoutPriority,
|
|
915
|
-
brsWithInvalidPriority,
|
|
916
|
-
contractRefs: parseContractRefs(md)
|
|
917
|
-
};
|
|
918
|
-
if (specId) {
|
|
919
|
-
parsed.specId = specId;
|
|
920
|
-
}
|
|
921
|
-
return parsed;
|
|
922
|
-
}
|
|
923
|
-
function parseContractRefs(md) {
|
|
924
|
-
const lines = [];
|
|
925
|
-
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
926
|
-
lines.push((match[1] ?? "").trim());
|
|
927
|
-
}
|
|
928
|
-
const ids = [];
|
|
929
|
-
const invalidTokens = [];
|
|
930
|
-
let hasNone = false;
|
|
931
|
-
for (const line of lines) {
|
|
932
|
-
if (line.length === 0) {
|
|
933
|
-
invalidTokens.push("(empty)");
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
937
|
-
for (const token of tokens) {
|
|
938
|
-
if (token.length === 0) {
|
|
939
|
-
invalidTokens.push("(empty)");
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
if (token === "none") {
|
|
943
|
-
hasNone = true;
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
947
|
-
ids.push(token);
|
|
948
|
-
continue;
|
|
949
|
-
}
|
|
950
|
-
invalidTokens.push(token);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return {
|
|
954
|
-
lines,
|
|
955
|
-
ids: unique2(ids),
|
|
956
|
-
invalidTokens: unique2(invalidTokens),
|
|
957
|
-
hasNone
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
function unique2(values) {
|
|
961
|
-
return Array.from(new Set(values));
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// src/core/traceability.ts
|
|
965
|
-
var import_promises7 = require("fs/promises");
|
|
966
|
-
var import_node_path8 = __toESM(require("path"), 1);
|
|
967
|
-
|
|
968
|
-
// src/core/gherkin/parse.ts
|
|
969
|
-
var import_gherkin = require("@cucumber/gherkin");
|
|
970
|
-
var import_node_crypto = require("crypto");
|
|
971
|
-
function parseGherkin(source, uri) {
|
|
972
|
-
const errors = [];
|
|
973
|
-
const uuidFn = () => (0, import_node_crypto.randomUUID)();
|
|
974
|
-
const builder = new import_gherkin.AstBuilder(uuidFn);
|
|
975
|
-
const matcher = new import_gherkin.GherkinClassicTokenMatcher();
|
|
976
|
-
const parser = new import_gherkin.Parser(builder, matcher);
|
|
977
|
-
try {
|
|
978
|
-
const gherkinDocument = parser.parse(source);
|
|
979
|
-
gherkinDocument.uri = uri;
|
|
980
|
-
return { gherkinDocument, errors };
|
|
981
|
-
} catch (error2) {
|
|
982
|
-
errors.push(formatError2(error2));
|
|
983
|
-
return { gherkinDocument: null, errors };
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
function formatError2(error2) {
|
|
987
|
-
if (error2 instanceof Error) {
|
|
988
|
-
return error2.message;
|
|
989
|
-
}
|
|
990
|
-
return String(error2);
|
|
625
|
+
function formatError2(error2) {
|
|
626
|
+
if (error2 instanceof Error) {
|
|
627
|
+
return error2.message;
|
|
628
|
+
}
|
|
629
|
+
return String(error2);
|
|
991
630
|
}
|
|
992
631
|
|
|
993
632
|
// src/core/scenarioModel.ts
|
|
994
633
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
995
634
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
996
635
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
997
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
998
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
999
|
-
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
1000
636
|
function parseScenarioDocument(text, uri) {
|
|
1001
637
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1002
638
|
if (!gherkinDocument) {
|
|
@@ -1021,31 +657,21 @@ function parseScenarioDocument(text, uri) {
|
|
|
1021
657
|
errors
|
|
1022
658
|
};
|
|
1023
659
|
}
|
|
1024
|
-
function buildScenarioAtoms(document) {
|
|
660
|
+
function buildScenarioAtoms(document, contractIds = []) {
|
|
661
|
+
const uniqueContractIds = unique(contractIds).sort(
|
|
662
|
+
(a, b) => a.localeCompare(b)
|
|
663
|
+
);
|
|
1025
664
|
return document.scenarios.map((scenario) => {
|
|
1026
665
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1027
666
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1028
|
-
const brIds =
|
|
1029
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1030
|
-
scenario.tags.forEach((tag) => {
|
|
1031
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
1032
|
-
contractIds.add(tag);
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
for (const step of scenario.steps) {
|
|
1036
|
-
for (const text of collectStepTexts(step)) {
|
|
1037
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1038
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1039
|
-
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
667
|
+
const brIds = unique(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1042
668
|
const atom = {
|
|
1043
669
|
uri: document.uri,
|
|
1044
670
|
featureName: document.featureName ?? "",
|
|
1045
671
|
scenarioName: scenario.name,
|
|
1046
672
|
kind: scenario.kind,
|
|
1047
673
|
brIds,
|
|
1048
|
-
contractIds:
|
|
674
|
+
contractIds: uniqueContractIds
|
|
1049
675
|
};
|
|
1050
676
|
if (scenario.line !== void 0) {
|
|
1051
677
|
atom.line = scenario.line;
|
|
@@ -1098,24 +724,7 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
1098
724
|
function collectTagNames(tags) {
|
|
1099
725
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1100
726
|
}
|
|
1101
|
-
function
|
|
1102
|
-
const texts = [];
|
|
1103
|
-
if (step.text) {
|
|
1104
|
-
texts.push(step.text);
|
|
1105
|
-
}
|
|
1106
|
-
if (step.docString?.content) {
|
|
1107
|
-
texts.push(step.docString.content);
|
|
1108
|
-
}
|
|
1109
|
-
if (step.dataTable?.rows) {
|
|
1110
|
-
for (const row of step.dataTable.rows) {
|
|
1111
|
-
for (const cell of row.cells) {
|
|
1112
|
-
texts.push(cell.value);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
return texts;
|
|
1117
|
-
}
|
|
1118
|
-
function unique3(values) {
|
|
727
|
+
function unique(values) {
|
|
1119
728
|
return Array.from(new Set(values));
|
|
1120
729
|
}
|
|
1121
730
|
|
|
@@ -1145,7 +754,7 @@ function extractAnnotatedScIds(text) {
|
|
|
1145
754
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
1146
755
|
const scIds = /* @__PURE__ */ new Set();
|
|
1147
756
|
for (const file of scenarioFiles) {
|
|
1148
|
-
const text = await (0,
|
|
757
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
1149
758
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1150
759
|
if (!document || errors.length > 0) {
|
|
1151
760
|
continue;
|
|
@@ -1163,7 +772,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
1163
772
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1164
773
|
const sources = /* @__PURE__ */ new Map();
|
|
1165
774
|
for (const file of scenarioFiles) {
|
|
1166
|
-
const text = await (0,
|
|
775
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
1167
776
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1168
777
|
if (!document || errors.length > 0) {
|
|
1169
778
|
continue;
|
|
@@ -1216,10 +825,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1216
825
|
};
|
|
1217
826
|
}
|
|
1218
827
|
const normalizedFiles = Array.from(
|
|
1219
|
-
new Set(files.map((file) =>
|
|
828
|
+
new Set(files.map((file) => import_node_path5.default.normalize(file)))
|
|
1220
829
|
);
|
|
1221
830
|
for (const file of normalizedFiles) {
|
|
1222
|
-
const text = await (0,
|
|
831
|
+
const text = await (0, import_promises5.readFile)(file, "utf-8");
|
|
1223
832
|
const scIds = extractAnnotatedScIds(text);
|
|
1224
833
|
if (scIds.length === 0) {
|
|
1225
834
|
continue;
|
|
@@ -1229,85 +838,772 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1229
838
|
current.add(file);
|
|
1230
839
|
refs.set(scId, current);
|
|
1231
840
|
}
|
|
1232
|
-
}
|
|
1233
|
-
return {
|
|
1234
|
-
refs,
|
|
1235
|
-
scan: {
|
|
1236
|
-
globs: normalizedGlobs,
|
|
1237
|
-
excludeGlobs: mergedExcludeGlobs,
|
|
1238
|
-
matchedFileCount: normalizedFiles.length
|
|
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)");
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
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);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
lines,
|
|
1463
|
+
ids: unique3(ids),
|
|
1464
|
+
invalidTokens: unique3(invalidTokens),
|
|
1465
|
+
hasNone
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
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
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
function unique3(values) {
|
|
1476
|
+
return Array.from(new Set(values));
|
|
1477
|
+
}
|
|
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;
|
|
1498
|
+
}
|
|
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
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
return sections;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
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;
|
|
1239
1571
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
refsRecord[scId] = sortedFiles;
|
|
1251
|
-
if (sortedFiles.length === 0) {
|
|
1252
|
-
missingIds.push(scId);
|
|
1253
|
-
} else {
|
|
1254
|
-
covered += 1;
|
|
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
|
+
});
|
|
1255
1582
|
}
|
|
1256
1583
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1584
|
+
const parsed = {
|
|
1585
|
+
file,
|
|
1586
|
+
sections: sectionNames,
|
|
1587
|
+
brs,
|
|
1588
|
+
brsWithoutPriority,
|
|
1589
|
+
brsWithInvalidPriority,
|
|
1590
|
+
contractRefs: parseContractRefs(md)
|
|
1263
1591
|
};
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
1267
|
-
}
|
|
1268
|
-
function normalizeGlobs(globs) {
|
|
1269
|
-
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1270
|
-
}
|
|
1271
|
-
function formatError3(error2) {
|
|
1272
|
-
if (error2 instanceof Error) {
|
|
1273
|
-
return error2.message;
|
|
1274
|
-
}
|
|
1275
|
-
return String(error2);
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// src/core/version.ts
|
|
1279
|
-
var import_promises8 = require("fs/promises");
|
|
1280
|
-
var import_node_path9 = __toESM(require("path"), 1);
|
|
1281
|
-
var import_node_url2 = require("url");
|
|
1282
|
-
async function resolveToolVersion() {
|
|
1283
|
-
if ("0.5.0".length > 0) {
|
|
1284
|
-
return "0.5.0";
|
|
1285
|
-
}
|
|
1286
|
-
try {
|
|
1287
|
-
const packagePath = resolvePackageJsonPath();
|
|
1288
|
-
const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
|
|
1289
|
-
const parsed = JSON.parse(raw);
|
|
1290
|
-
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
1291
|
-
return version.length > 0 ? version : "unknown";
|
|
1292
|
-
} catch {
|
|
1293
|
-
return "unknown";
|
|
1592
|
+
if (specId) {
|
|
1593
|
+
parsed.specId = specId;
|
|
1294
1594
|
}
|
|
1295
|
-
|
|
1296
|
-
function resolvePackageJsonPath() {
|
|
1297
|
-
const base = __filename;
|
|
1298
|
-
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1299
|
-
return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
|
|
1595
|
+
return parsed;
|
|
1300
1596
|
}
|
|
1301
1597
|
|
|
1302
1598
|
// src/core/validators/contracts.ts
|
|
1303
|
-
var
|
|
1304
|
-
var
|
|
1599
|
+
var import_promises11 = require("fs/promises");
|
|
1600
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
1305
1601
|
|
|
1306
1602
|
// src/core/contracts.ts
|
|
1307
|
-
var
|
|
1603
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1308
1604
|
var import_yaml2 = require("yaml");
|
|
1309
1605
|
function parseStructuredContract(file, text) {
|
|
1310
|
-
const ext =
|
|
1606
|
+
const ext = import_node_path13.default.extname(file).toLowerCase();
|
|
1311
1607
|
if (ext === ".json") {
|
|
1312
1608
|
return JSON.parse(text);
|
|
1313
1609
|
}
|
|
@@ -1327,9 +1623,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1327
1623
|
async function validateContracts(root, config) {
|
|
1328
1624
|
const issues = [];
|
|
1329
1625
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1330
|
-
issues.push(...await validateUiContracts(
|
|
1331
|
-
issues.push(...await validateApiContracts(
|
|
1332
|
-
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")));
|
|
1333
1629
|
const contractIndex = await buildContractIndex(root, config);
|
|
1334
1630
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1335
1631
|
return issues;
|
|
@@ -1349,7 +1645,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
1349
1645
|
}
|
|
1350
1646
|
const issues = [];
|
|
1351
1647
|
for (const file of files) {
|
|
1352
|
-
const text = await (0,
|
|
1648
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1353
1649
|
const invalidIds = extractInvalidIds(text, [
|
|
1354
1650
|
"SPEC",
|
|
1355
1651
|
"BR",
|
|
@@ -1404,7 +1700,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1404
1700
|
}
|
|
1405
1701
|
const issues = [];
|
|
1406
1702
|
for (const file of files) {
|
|
1407
|
-
const text = await (0,
|
|
1703
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1408
1704
|
const invalidIds = extractInvalidIds(text, [
|
|
1409
1705
|
"SPEC",
|
|
1410
1706
|
"BR",
|
|
@@ -1472,7 +1768,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1472
1768
|
}
|
|
1473
1769
|
const issues = [];
|
|
1474
1770
|
for (const file of files) {
|
|
1475
|
-
const text = await (0,
|
|
1771
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
1476
1772
|
const invalidIds = extractInvalidIds(text, [
|
|
1477
1773
|
"SPEC",
|
|
1478
1774
|
"BR",
|
|
@@ -1611,8 +1907,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1611
1907
|
}
|
|
1612
1908
|
|
|
1613
1909
|
// src/core/validators/delta.ts
|
|
1614
|
-
var
|
|
1615
|
-
var
|
|
1910
|
+
var import_promises12 = require("fs/promises");
|
|
1911
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
1616
1912
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1617
1913
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1618
1914
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1626,10 +1922,10 @@ async function validateDeltas(root, config) {
|
|
|
1626
1922
|
}
|
|
1627
1923
|
const issues = [];
|
|
1628
1924
|
for (const pack of packs) {
|
|
1629
|
-
const deltaPath =
|
|
1925
|
+
const deltaPath = import_node_path15.default.join(pack, "delta.md");
|
|
1630
1926
|
let text;
|
|
1631
1927
|
try {
|
|
1632
|
-
text = await (0,
|
|
1928
|
+
text = await (0, import_promises12.readFile)(deltaPath, "utf-8");
|
|
1633
1929
|
} catch (error2) {
|
|
1634
1930
|
if (isMissingFileError2(error2)) {
|
|
1635
1931
|
issues.push(
|
|
@@ -1701,8 +1997,8 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1701
1997
|
}
|
|
1702
1998
|
|
|
1703
1999
|
// src/core/validators/ids.ts
|
|
1704
|
-
var
|
|
1705
|
-
var
|
|
2000
|
+
var import_promises13 = require("fs/promises");
|
|
2001
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
1706
2002
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1707
2003
|
async function validateDefinedIds(root, config) {
|
|
1708
2004
|
const issues = [];
|
|
@@ -1737,7 +2033,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1737
2033
|
}
|
|
1738
2034
|
async function collectSpecDefinitionIds(files, out) {
|
|
1739
2035
|
for (const file of files) {
|
|
1740
|
-
const text = await (0,
|
|
2036
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1741
2037
|
const parsed = parseSpec(text, file);
|
|
1742
2038
|
if (parsed.specId) {
|
|
1743
2039
|
recordId(out, parsed.specId, file);
|
|
@@ -1747,7 +2043,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1747
2043
|
}
|
|
1748
2044
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1749
2045
|
for (const file of files) {
|
|
1750
|
-
const text = await (0,
|
|
2046
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
1751
2047
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1752
2048
|
if (!document || errors.length > 0) {
|
|
1753
2049
|
continue;
|
|
@@ -1768,7 +2064,7 @@ function recordId(out, id, file) {
|
|
|
1768
2064
|
}
|
|
1769
2065
|
function formatFileList(files, root) {
|
|
1770
2066
|
return files.map((file) => {
|
|
1771
|
-
const relative =
|
|
2067
|
+
const relative = import_node_path16.default.relative(root, file);
|
|
1772
2068
|
return relative.length > 0 ? relative : file;
|
|
1773
2069
|
}).join(", ");
|
|
1774
2070
|
}
|
|
@@ -1791,7 +2087,7 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1791
2087
|
}
|
|
1792
2088
|
|
|
1793
2089
|
// src/core/validators/scenario.ts
|
|
1794
|
-
var
|
|
2090
|
+
var import_promises14 = require("fs/promises");
|
|
1795
2091
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1796
2092
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1797
2093
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -1817,7 +2113,7 @@ async function validateScenarios(root, config) {
|
|
|
1817
2113
|
for (const entry of entries) {
|
|
1818
2114
|
let text;
|
|
1819
2115
|
try {
|
|
1820
|
-
text = await (0,
|
|
2116
|
+
text = await (0, import_promises14.readFile)(entry.scenarioPath, "utf-8");
|
|
1821
2117
|
} catch (error2) {
|
|
1822
2118
|
if (isMissingFileError3(error2)) {
|
|
1823
2119
|
issues.push(
|
|
@@ -1987,7 +2283,7 @@ function isMissingFileError3(error2) {
|
|
|
1987
2283
|
}
|
|
1988
2284
|
|
|
1989
2285
|
// src/core/validators/spec.ts
|
|
1990
|
-
var
|
|
2286
|
+
var import_promises15 = require("fs/promises");
|
|
1991
2287
|
async function validateSpecs(root, config) {
|
|
1992
2288
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1993
2289
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -2008,7 +2304,7 @@ async function validateSpecs(root, config) {
|
|
|
2008
2304
|
for (const entry of entries) {
|
|
2009
2305
|
let text;
|
|
2010
2306
|
try {
|
|
2011
|
-
text = await (0,
|
|
2307
|
+
text = await (0, import_promises15.readFile)(entry.specPath, "utf-8");
|
|
2012
2308
|
} catch (error2) {
|
|
2013
2309
|
if (isMissingFileError4(error2)) {
|
|
2014
2310
|
issues.push(
|
|
@@ -2157,7 +2453,7 @@ function isMissingFileError4(error2) {
|
|
|
2157
2453
|
}
|
|
2158
2454
|
|
|
2159
2455
|
// src/core/validators/traceability.ts
|
|
2160
|
-
var
|
|
2456
|
+
var import_promises16 = require("fs/promises");
|
|
2161
2457
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2162
2458
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2163
2459
|
async function validateTraceability(root, config) {
|
|
@@ -2177,7 +2473,7 @@ async function validateTraceability(root, config) {
|
|
|
2177
2473
|
const contractIndex = await buildContractIndex(root, config);
|
|
2178
2474
|
const contractIds = contractIndex.ids;
|
|
2179
2475
|
for (const file of specFiles) {
|
|
2180
|
-
const text = await (0,
|
|
2476
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2181
2477
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2182
2478
|
const parsed = parseSpec(text, file);
|
|
2183
2479
|
if (parsed.specId) {
|
|
@@ -2205,7 +2501,7 @@ async function validateTraceability(root, config) {
|
|
|
2205
2501
|
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2206
2502
|
issues.push(
|
|
2207
2503
|
issue6(
|
|
2208
|
-
"QFAI-TRACE-
|
|
2504
|
+
"QFAI-TRACE-023",
|
|
2209
2505
|
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2210
2506
|
"error",
|
|
2211
2507
|
file,
|
|
@@ -2237,7 +2533,7 @@ async function validateTraceability(root, config) {
|
|
|
2237
2533
|
if (unknownContractIds.length > 0) {
|
|
2238
2534
|
issues.push(
|
|
2239
2535
|
issue6(
|
|
2240
|
-
"QFAI-TRACE-
|
|
2536
|
+
"QFAI-TRACE-024",
|
|
2241
2537
|
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2242
2538
|
", "
|
|
2243
2539
|
)}`,
|
|
@@ -2250,13 +2546,64 @@ async function validateTraceability(root, config) {
|
|
|
2250
2546
|
}
|
|
2251
2547
|
}
|
|
2252
2548
|
for (const file of scenarioFiles) {
|
|
2253
|
-
const text = await (0,
|
|
2549
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2254
2550
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2551
|
+
const scenarioContractRefs = parseContractRefs(text, {
|
|
2552
|
+
allowCommentPrefix: true
|
|
2553
|
+
});
|
|
2554
|
+
if (scenarioContractRefs.lines.length === 0) {
|
|
2555
|
+
issues.push(
|
|
2556
|
+
issue6(
|
|
2557
|
+
"QFAI-TRACE-031",
|
|
2558
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2559
|
+
"error",
|
|
2560
|
+
file,
|
|
2561
|
+
"traceability.scenarioContractRefRequired"
|
|
2562
|
+
)
|
|
2563
|
+
);
|
|
2564
|
+
} else {
|
|
2565
|
+
if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
2566
|
+
issues.push(
|
|
2567
|
+
issue6(
|
|
2568
|
+
"QFAI-TRACE-033",
|
|
2569
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2570
|
+
"error",
|
|
2571
|
+
file,
|
|
2572
|
+
"traceability.scenarioContractRefFormat"
|
|
2573
|
+
)
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
if (scenarioContractRefs.invalidTokens.length > 0) {
|
|
2577
|
+
issues.push(
|
|
2578
|
+
issue6(
|
|
2579
|
+
"QFAI-TRACE-032",
|
|
2580
|
+
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2581
|
+
", "
|
|
2582
|
+
)}`,
|
|
2583
|
+
"error",
|
|
2584
|
+
file,
|
|
2585
|
+
"traceability.scenarioContractRefFormat",
|
|
2586
|
+
scenarioContractRefs.invalidTokens
|
|
2587
|
+
)
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2255
2591
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2256
2592
|
if (!document || errors.length > 0) {
|
|
2257
2593
|
continue;
|
|
2258
2594
|
}
|
|
2259
|
-
|
|
2595
|
+
if (document.scenarios.length !== 1) {
|
|
2596
|
+
issues.push(
|
|
2597
|
+
issue6(
|
|
2598
|
+
"QFAI-TRACE-030",
|
|
2599
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306F 1\u30D5\u30A1\u30A4\u30EB=1\u30B7\u30CA\u30EA\u30AA\u3067\u3059\u3002\u73FE\u5728: ${document.scenarios.length}\u4EF6 (file=${file})`,
|
|
2600
|
+
"error",
|
|
2601
|
+
file,
|
|
2602
|
+
"traceability.scenarioOnePerFile"
|
|
2603
|
+
)
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
|
|
2260
2607
|
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2261
2608
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2262
2609
|
const atom = atoms[index];
|
|
@@ -2401,7 +2748,7 @@ async function validateTraceability(root, config) {
|
|
|
2401
2748
|
if (orphanBrIds.length > 0) {
|
|
2402
2749
|
issues.push(
|
|
2403
2750
|
issue6(
|
|
2404
|
-
"
|
|
2751
|
+
"QFAI-TRACE-009",
|
|
2405
2752
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
2406
2753
|
"error",
|
|
2407
2754
|
specsRoot,
|
|
@@ -2471,17 +2818,19 @@ async function validateTraceability(root, config) {
|
|
|
2471
2818
|
);
|
|
2472
2819
|
}
|
|
2473
2820
|
}
|
|
2474
|
-
|
|
2821
|
+
const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
|
|
2822
|
+
if (orphanPolicy !== "allow") {
|
|
2475
2823
|
if (contractIds.size > 0) {
|
|
2476
2824
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2477
2825
|
(id) => !specContractIds.has(id)
|
|
2478
2826
|
);
|
|
2479
2827
|
if (orphanContracts.length > 0) {
|
|
2828
|
+
const severity = orphanPolicy === "warning" ? "warning" : "error";
|
|
2480
2829
|
issues.push(
|
|
2481
2830
|
issue6(
|
|
2482
2831
|
"QFAI-TRACE-022",
|
|
2483
2832
|
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2484
|
-
|
|
2833
|
+
severity,
|
|
2485
2834
|
specsRoot,
|
|
2486
2835
|
"traceability.contractCoverage",
|
|
2487
2836
|
orphanContracts
|
|
@@ -2519,7 +2868,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2519
2868
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2520
2869
|
let found = false;
|
|
2521
2870
|
for (const file of targetFiles) {
|
|
2522
|
-
const text = await (0,
|
|
2871
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
2523
2872
|
if (pattern.test(text)) {
|
|
2524
2873
|
found = true;
|
|
2525
2874
|
break;
|
|
@@ -2606,16 +2955,17 @@ function countIssues(issues) {
|
|
|
2606
2955
|
// src/core/report.ts
|
|
2607
2956
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2608
2957
|
async function createReportData(root, validation, configResult) {
|
|
2609
|
-
const
|
|
2958
|
+
const resolvedRoot = import_node_path17.default.resolve(root);
|
|
2959
|
+
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2610
2960
|
const config = resolved.config;
|
|
2611
2961
|
const configPath = resolved.configPath;
|
|
2612
|
-
const specsRoot = resolvePath(
|
|
2613
|
-
const contractsRoot = resolvePath(
|
|
2614
|
-
const apiRoot =
|
|
2615
|
-
const uiRoot =
|
|
2616
|
-
const dbRoot =
|
|
2617
|
-
const srcRoot = resolvePath(
|
|
2618
|
-
const testsRoot = resolvePath(
|
|
2962
|
+
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2963
|
+
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
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");
|
|
2967
|
+
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2968
|
+
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2619
2969
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
2620
2970
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2621
2971
|
const {
|
|
@@ -2623,15 +2973,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2623
2973
|
ui: uiFiles,
|
|
2624
2974
|
db: dbFiles
|
|
2625
2975
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2626
|
-
const contractIndex = await buildContractIndex(
|
|
2976
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2627
2977
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2628
2978
|
const specContractRefs = await collectSpecContractRefs(
|
|
2629
2979
|
specFiles,
|
|
2630
2980
|
contractIdList
|
|
2631
2981
|
);
|
|
2632
2982
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2633
|
-
for (const
|
|
2634
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2983
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2984
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2635
2985
|
}
|
|
2636
2986
|
const referencedContractCount = contractIdList.filter(
|
|
2637
2987
|
(id) => referencedContracts.has(id)
|
|
@@ -2640,8 +2990,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2640
2990
|
(id) => !referencedContracts.has(id)
|
|
2641
2991
|
).length;
|
|
2642
2992
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2643
|
-
const
|
|
2644
|
-
specContractRefs.
|
|
2993
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2994
|
+
specContractRefs.specToContracts
|
|
2645
2995
|
);
|
|
2646
2996
|
const idsByPrefix = await collectIds([
|
|
2647
2997
|
...specFiles,
|
|
@@ -2659,24 +3009,28 @@ async function createReportData(root, validation, configResult) {
|
|
|
2659
3009
|
srcRoot,
|
|
2660
3010
|
testsRoot
|
|
2661
3011
|
);
|
|
2662
|
-
const
|
|
2663
|
-
const
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
3012
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
3013
|
+
const normalizedValidation = normalizeValidationResult(
|
|
3014
|
+
resolvedRoot,
|
|
3015
|
+
resolvedValidationRaw
|
|
2667
3016
|
);
|
|
2668
|
-
const scCoverage =
|
|
2669
|
-
const testFiles =
|
|
3017
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
3018
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2670
3019
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2671
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2672
|
-
|
|
3020
|
+
const scSourceRecord = mapToSortedRecord(
|
|
3021
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
3022
|
+
);
|
|
2673
3023
|
const version = await resolveToolVersion();
|
|
3024
|
+
const reportFormatVersion = 1;
|
|
3025
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
3026
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2674
3027
|
return {
|
|
2675
3028
|
tool: "qfai",
|
|
2676
3029
|
version,
|
|
3030
|
+
reportFormatVersion,
|
|
2677
3031
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2678
|
-
root,
|
|
2679
|
-
configPath,
|
|
3032
|
+
root: displayRoot,
|
|
3033
|
+
configPath: displayConfigPath,
|
|
2680
3034
|
summary: {
|
|
2681
3035
|
specs: specFiles.length,
|
|
2682
3036
|
scenarios: scenarioFiles.length,
|
|
@@ -2685,7 +3039,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2685
3039
|
ui: uiFiles.length,
|
|
2686
3040
|
db: dbFiles.length
|
|
2687
3041
|
},
|
|
2688
|
-
counts:
|
|
3042
|
+
counts: normalizedValidation.counts
|
|
2689
3043
|
},
|
|
2690
3044
|
ids: {
|
|
2691
3045
|
spec: idsByPrefix.SPEC,
|
|
@@ -2710,21 +3064,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2710
3064
|
specs: {
|
|
2711
3065
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2712
3066
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2713
|
-
|
|
3067
|
+
specToContracts: specToContractsRecord
|
|
2714
3068
|
}
|
|
2715
3069
|
},
|
|
2716
|
-
issues:
|
|
3070
|
+
issues: normalizedValidation.issues
|
|
2717
3071
|
};
|
|
2718
3072
|
}
|
|
2719
3073
|
function formatReportMarkdown(data) {
|
|
2720
3074
|
const lines = [];
|
|
2721
3075
|
lines.push("# QFAI Report");
|
|
3076
|
+
lines.push("");
|
|
2722
3077
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2723
3078
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2724
3079
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2725
3080
|
lines.push(`- \u7248: ${data.version}`);
|
|
2726
3081
|
lines.push("");
|
|
2727
3082
|
lines.push("## \u6982\u8981");
|
|
3083
|
+
lines.push("");
|
|
2728
3084
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2729
3085
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2730
3086
|
lines.push(
|
|
@@ -2735,6 +3091,7 @@ function formatReportMarkdown(data) {
|
|
|
2735
3091
|
);
|
|
2736
3092
|
lines.push("");
|
|
2737
3093
|
lines.push("## ID\u96C6\u8A08");
|
|
3094
|
+
lines.push("");
|
|
2738
3095
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2739
3096
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2740
3097
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2743,12 +3100,14 @@ function formatReportMarkdown(data) {
|
|
|
2743
3100
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2744
3101
|
lines.push("");
|
|
2745
3102
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
3103
|
+
lines.push("");
|
|
2746
3104
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2747
3105
|
lines.push(
|
|
2748
3106
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2749
3107
|
);
|
|
2750
3108
|
lines.push("");
|
|
2751
3109
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
3110
|
+
lines.push("");
|
|
2752
3111
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2753
3112
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2754
3113
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2757,6 +3116,7 @@ function formatReportMarkdown(data) {
|
|
|
2757
3116
|
);
|
|
2758
3117
|
lines.push("");
|
|
2759
3118
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
3119
|
+
lines.push("");
|
|
2760
3120
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2761
3121
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2762
3122
|
(a, b) => a.localeCompare(b)
|
|
@@ -2775,24 +3135,25 @@ function formatReportMarkdown(data) {
|
|
|
2775
3135
|
}
|
|
2776
3136
|
lines.push("");
|
|
2777
3137
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2778
|
-
|
|
3138
|
+
lines.push("");
|
|
3139
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2779
3140
|
const specIds = Object.keys(specToContracts).sort(
|
|
2780
3141
|
(a, b) => a.localeCompare(b)
|
|
2781
3142
|
);
|
|
2782
3143
|
if (specIds.length === 0) {
|
|
2783
3144
|
lines.push("- (none)");
|
|
2784
3145
|
} else {
|
|
2785
|
-
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
}
|
|
3146
|
+
const rows = specIds.map((specId) => {
|
|
3147
|
+
const entry = specToContracts[specId];
|
|
3148
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
3149
|
+
const status = entry?.status ?? "missing";
|
|
3150
|
+
return [specId, status, contracts];
|
|
3151
|
+
});
|
|
3152
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2793
3153
|
}
|
|
2794
3154
|
lines.push("");
|
|
2795
3155
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
3156
|
+
lines.push("");
|
|
2796
3157
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2797
3158
|
if (missingRefSpecs.length === 0) {
|
|
2798
3159
|
lines.push("- (none)");
|
|
@@ -2803,6 +3164,7 @@ function formatReportMarkdown(data) {
|
|
|
2803
3164
|
}
|
|
2804
3165
|
lines.push("");
|
|
2805
3166
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
3167
|
+
lines.push("");
|
|
2806
3168
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2807
3169
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2808
3170
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2832,6 +3194,7 @@ function formatReportMarkdown(data) {
|
|
|
2832
3194
|
}
|
|
2833
3195
|
lines.push("");
|
|
2834
3196
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
3197
|
+
lines.push("");
|
|
2835
3198
|
const scRefs = data.traceability.sc.refs;
|
|
2836
3199
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2837
3200
|
if (scIds.length === 0) {
|
|
@@ -2848,6 +3211,7 @@ function formatReportMarkdown(data) {
|
|
|
2848
3211
|
}
|
|
2849
3212
|
lines.push("");
|
|
2850
3213
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
3214
|
+
lines.push("");
|
|
2851
3215
|
const specScIssues = data.issues.filter(
|
|
2852
3216
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2853
3217
|
);
|
|
@@ -2862,6 +3226,7 @@ function formatReportMarkdown(data) {
|
|
|
2862
3226
|
}
|
|
2863
3227
|
lines.push("");
|
|
2864
3228
|
lines.push("## Hotspots");
|
|
3229
|
+
lines.push("");
|
|
2865
3230
|
const hotspots = buildHotspots(data.issues);
|
|
2866
3231
|
if (hotspots.length === 0) {
|
|
2867
3232
|
lines.push("- (none)");
|
|
@@ -2874,6 +3239,7 @@ function formatReportMarkdown(data) {
|
|
|
2874
3239
|
}
|
|
2875
3240
|
lines.push("");
|
|
2876
3241
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
3242
|
+
lines.push("");
|
|
2877
3243
|
const traceIssues = data.issues.filter(
|
|
2878
3244
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2879
3245
|
);
|
|
@@ -2889,6 +3255,7 @@ function formatReportMarkdown(data) {
|
|
|
2889
3255
|
}
|
|
2890
3256
|
lines.push("");
|
|
2891
3257
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
3258
|
+
lines.push("");
|
|
2892
3259
|
if (data.issues.length === 0) {
|
|
2893
3260
|
lines.push("- (none)");
|
|
2894
3261
|
} else {
|
|
@@ -2906,33 +3273,40 @@ function formatReportJson(data) {
|
|
|
2906
3273
|
return JSON.stringify(data, null, 2);
|
|
2907
3274
|
}
|
|
2908
3275
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2909
|
-
const
|
|
3276
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2910
3277
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2911
3278
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2912
3279
|
for (const contractId of contractIdList) {
|
|
2913
3280
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
2914
3281
|
}
|
|
2915
3282
|
for (const file of specFiles) {
|
|
2916
|
-
const text = await (0,
|
|
3283
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
2917
3284
|
const parsed = parseSpec(text, file);
|
|
2918
|
-
const specKey = parsed.specId
|
|
3285
|
+
const specKey = parsed.specId;
|
|
3286
|
+
if (!specKey) {
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
2919
3289
|
const refs = parsed.contractRefs;
|
|
2920
3290
|
if (refs.lines.length === 0) {
|
|
2921
3291
|
missingRefSpecs.add(specKey);
|
|
3292
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2922
3293
|
continue;
|
|
2923
3294
|
}
|
|
2924
|
-
const
|
|
3295
|
+
const current = specToContracts.get(specKey) ?? {
|
|
3296
|
+
status: "declared",
|
|
3297
|
+
ids: /* @__PURE__ */ new Set()
|
|
3298
|
+
};
|
|
2925
3299
|
for (const id of refs.ids) {
|
|
2926
|
-
|
|
3300
|
+
current.ids.add(id);
|
|
2927
3301
|
const specs = idToSpecs.get(id);
|
|
2928
3302
|
if (specs) {
|
|
2929
3303
|
specs.add(specKey);
|
|
2930
3304
|
}
|
|
2931
3305
|
}
|
|
2932
|
-
|
|
3306
|
+
specToContracts.set(specKey, current);
|
|
2933
3307
|
}
|
|
2934
3308
|
return {
|
|
2935
|
-
|
|
3309
|
+
specToContracts,
|
|
2936
3310
|
idToSpecs,
|
|
2937
3311
|
missingRefSpecs
|
|
2938
3312
|
};
|
|
@@ -2947,7 +3321,7 @@ async function collectIds(files) {
|
|
|
2947
3321
|
DB: /* @__PURE__ */ new Set()
|
|
2948
3322
|
};
|
|
2949
3323
|
for (const file of files) {
|
|
2950
|
-
const text = await (0,
|
|
3324
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
2951
3325
|
for (const prefix of ID_PREFIXES2) {
|
|
2952
3326
|
const ids = extractIds(text, prefix);
|
|
2953
3327
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -2965,7 +3339,7 @@ async function collectIds(files) {
|
|
|
2965
3339
|
async function collectUpstreamIds(files) {
|
|
2966
3340
|
const ids = /* @__PURE__ */ new Set();
|
|
2967
3341
|
for (const file of files) {
|
|
2968
|
-
const text = await (0,
|
|
3342
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
2969
3343
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2970
3344
|
}
|
|
2971
3345
|
return ids;
|
|
@@ -2986,7 +3360,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2986
3360
|
}
|
|
2987
3361
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2988
3362
|
for (const file of targetFiles) {
|
|
2989
|
-
const text = await (0,
|
|
3363
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
2990
3364
|
if (pattern.test(text)) {
|
|
2991
3365
|
return true;
|
|
2992
3366
|
}
|
|
@@ -3009,6 +3383,20 @@ function formatList(values) {
|
|
|
3009
3383
|
}
|
|
3010
3384
|
return values.join(", ");
|
|
3011
3385
|
}
|
|
3386
|
+
function formatMarkdownTable(headers, rows) {
|
|
3387
|
+
const widths = headers.map((header, index) => {
|
|
3388
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
3389
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
3390
|
+
});
|
|
3391
|
+
const formatRow = (cells) => {
|
|
3392
|
+
const padded = cells.map(
|
|
3393
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
3394
|
+
);
|
|
3395
|
+
return `| ${padded.join(" | ")} |`;
|
|
3396
|
+
};
|
|
3397
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
3398
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3399
|
+
}
|
|
3012
3400
|
function toSortedArray2(values) {
|
|
3013
3401
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
3014
3402
|
}
|
|
@@ -3019,6 +3407,27 @@ function mapToSortedRecord(values) {
|
|
|
3019
3407
|
}
|
|
3020
3408
|
return record2;
|
|
3021
3409
|
}
|
|
3410
|
+
function mapToSpecContractRecord(values) {
|
|
3411
|
+
const record2 = {};
|
|
3412
|
+
for (const [key, entry] of values.entries()) {
|
|
3413
|
+
record2[key] = {
|
|
3414
|
+
status: entry.status,
|
|
3415
|
+
ids: toSortedArray2(entry.ids)
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
return record2;
|
|
3419
|
+
}
|
|
3420
|
+
function normalizeScSources(root, sources) {
|
|
3421
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3422
|
+
for (const [id, files] of sources.entries()) {
|
|
3423
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3424
|
+
for (const file of files) {
|
|
3425
|
+
mapped.add(toRelativePath(root, file));
|
|
3426
|
+
}
|
|
3427
|
+
normalized.set(id, mapped);
|
|
3428
|
+
}
|
|
3429
|
+
return normalized;
|
|
3430
|
+
}
|
|
3022
3431
|
function buildHotspots(issues) {
|
|
3023
3432
|
const map = /* @__PURE__ */ new Map();
|
|
3024
3433
|
for (const issue7 of issues) {
|
|
@@ -3043,39 +3452,54 @@ function buildHotspots(issues) {
|
|
|
3043
3452
|
|
|
3044
3453
|
// src/cli/commands/report.ts
|
|
3045
3454
|
async function runReport(options) {
|
|
3046
|
-
const root =
|
|
3455
|
+
const root = import_node_path18.default.resolve(options.root);
|
|
3047
3456
|
const configResult = await loadConfig(root);
|
|
3048
|
-
const input = configResult.config.output.validateJsonPath;
|
|
3049
|
-
const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
|
|
3050
3457
|
let validation;
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3458
|
+
if (options.runValidate) {
|
|
3459
|
+
if (options.inputPath) {
|
|
3460
|
+
warn("report: --run-validate \u304C\u6307\u5B9A\u3055\u308C\u305F\u305F\u3081 --in \u306F\u7121\u8996\u3057\u307E\u3059\u3002");
|
|
3461
|
+
}
|
|
3462
|
+
const result = await validateProject(root, configResult);
|
|
3463
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3464
|
+
await writeValidationResult(
|
|
3465
|
+
root,
|
|
3466
|
+
configResult.config.output.validateJsonPath,
|
|
3467
|
+
normalized
|
|
3468
|
+
);
|
|
3469
|
+
validation = normalized;
|
|
3470
|
+
} else {
|
|
3471
|
+
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3472
|
+
const inputPath = import_node_path18.default.isAbsolute(input) ? input : import_node_path18.default.resolve(root, input);
|
|
3473
|
+
try {
|
|
3474
|
+
validation = await readValidationResult(inputPath);
|
|
3475
|
+
} catch (err) {
|
|
3476
|
+
if (isMissingFileError5(err)) {
|
|
3477
|
+
error(
|
|
3478
|
+
[
|
|
3479
|
+
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
3480
|
+
"",
|
|
3481
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
3482
|
+
" qfai validate",
|
|
3483
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
3484
|
+
"",
|
|
3485
|
+
"\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3486
|
+
"GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3487
|
+
].join("\n")
|
|
3488
|
+
);
|
|
3489
|
+
process.exitCode = 2;
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
throw err;
|
|
3068
3493
|
}
|
|
3069
|
-
throw err;
|
|
3070
3494
|
}
|
|
3071
3495
|
const data = await createReportData(root, validation, configResult);
|
|
3072
3496
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3073
3497
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3074
|
-
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");
|
|
3075
3499
|
const out = options.outPath ?? defaultOut;
|
|
3076
|
-
const outPath =
|
|
3077
|
-
await (0,
|
|
3078
|
-
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}
|
|
3079
3503
|
`, "utf-8");
|
|
3080
3504
|
info(
|
|
3081
3505
|
`report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
|
|
@@ -3083,7 +3507,7 @@ async function runReport(options) {
|
|
|
3083
3507
|
info(`wrote report: ${outPath}`);
|
|
3084
3508
|
}
|
|
3085
3509
|
async function readValidationResult(inputPath) {
|
|
3086
|
-
const raw = await (0,
|
|
3510
|
+
const raw = await (0, import_promises18.readFile)(inputPath, "utf-8");
|
|
3087
3511
|
const parsed = JSON.parse(raw);
|
|
3088
3512
|
if (!isValidationResult(parsed)) {
|
|
3089
3513
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -3138,10 +3562,16 @@ function isMissingFileError5(error2) {
|
|
|
3138
3562
|
const record2 = error2;
|
|
3139
3563
|
return record2.code === "ENOENT";
|
|
3140
3564
|
}
|
|
3565
|
+
async function writeValidationResult(root, outputPath, result) {
|
|
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)}
|
|
3569
|
+
`, "utf-8");
|
|
3570
|
+
}
|
|
3141
3571
|
|
|
3142
3572
|
// src/cli/commands/validate.ts
|
|
3143
|
-
var
|
|
3144
|
-
var
|
|
3573
|
+
var import_promises19 = require("fs/promises");
|
|
3574
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3145
3575
|
|
|
3146
3576
|
// src/cli/lib/failOn.ts
|
|
3147
3577
|
function shouldFail(result, failOn) {
|
|
@@ -3156,19 +3586,24 @@ function shouldFail(result, failOn) {
|
|
|
3156
3586
|
|
|
3157
3587
|
// src/cli/commands/validate.ts
|
|
3158
3588
|
async function runValidate(options) {
|
|
3159
|
-
const root =
|
|
3589
|
+
const root = import_node_path19.default.resolve(options.root);
|
|
3160
3590
|
const configResult = await loadConfig(root);
|
|
3161
3591
|
const result = await validateProject(root, configResult);
|
|
3592
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3162
3593
|
const format = options.format ?? "text";
|
|
3163
3594
|
if (format === "text") {
|
|
3164
|
-
emitText(
|
|
3595
|
+
emitText(normalized);
|
|
3165
3596
|
}
|
|
3166
3597
|
if (format === "github") {
|
|
3167
|
-
|
|
3598
|
+
const jsonPath = resolveJsonPath(
|
|
3599
|
+
root,
|
|
3600
|
+
configResult.config.output.validateJsonPath
|
|
3601
|
+
);
|
|
3602
|
+
emitGitHubOutput(normalized, root, jsonPath);
|
|
3168
3603
|
}
|
|
3169
|
-
await emitJson(
|
|
3604
|
+
await emitJson(normalized, root, configResult.config.output.validateJsonPath);
|
|
3170
3605
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
3171
|
-
return shouldFail(
|
|
3606
|
+
return shouldFail(normalized, failOn) ? 1 : 0;
|
|
3172
3607
|
}
|
|
3173
3608
|
function resolveFailOn(options, fallback) {
|
|
3174
3609
|
if (options.failOn) {
|
|
@@ -3193,6 +3628,22 @@ function emitText(result) {
|
|
|
3193
3628
|
`
|
|
3194
3629
|
);
|
|
3195
3630
|
}
|
|
3631
|
+
function emitGitHubOutput(result, root, jsonPath) {
|
|
3632
|
+
const deduped = dedupeIssues(result.issues);
|
|
3633
|
+
const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
|
|
3634
|
+
const dropped = Math.max(result.issues.length - deduped.length, 0);
|
|
3635
|
+
emitGitHubSummary(result, {
|
|
3636
|
+
total: deduped.length,
|
|
3637
|
+
omitted,
|
|
3638
|
+
dropped,
|
|
3639
|
+
jsonPath,
|
|
3640
|
+
root
|
|
3641
|
+
});
|
|
3642
|
+
const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
|
|
3643
|
+
for (const issue7 of issues) {
|
|
3644
|
+
emitGitHub(issue7);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3196
3647
|
function emitGitHub(issue7) {
|
|
3197
3648
|
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
3198
3649
|
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
@@ -3204,22 +3655,75 @@ function emitGitHub(issue7) {
|
|
|
3204
3655
|
`
|
|
3205
3656
|
);
|
|
3206
3657
|
}
|
|
3658
|
+
function emitGitHubSummary(result, options) {
|
|
3659
|
+
const summary = [
|
|
3660
|
+
"qfai validate summary:",
|
|
3661
|
+
`error=${result.counts.error}`,
|
|
3662
|
+
`warning=${result.counts.warning}`,
|
|
3663
|
+
`info=${result.counts.info}`,
|
|
3664
|
+
`annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`
|
|
3665
|
+
].join(" ");
|
|
3666
|
+
process.stdout.write(`${summary}
|
|
3667
|
+
`);
|
|
3668
|
+
if (options.dropped > 0 || options.omitted > 0) {
|
|
3669
|
+
const details = [
|
|
3670
|
+
"qfai validate note:",
|
|
3671
|
+
options.dropped > 0 ? `\u91CD\u8907\u9664\u5916=${options.dropped}` : null,
|
|
3672
|
+
options.omitted > 0 ? `\u4E0A\u9650\u7701\u7565=${options.omitted}` : null
|
|
3673
|
+
].filter(Boolean).join(" ");
|
|
3674
|
+
process.stdout.write(`${details}
|
|
3675
|
+
`);
|
|
3676
|
+
}
|
|
3677
|
+
const relative = toRelativePath(options.root, options.jsonPath);
|
|
3678
|
+
process.stdout.write(
|
|
3679
|
+
`qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
3680
|
+
`
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
function dedupeIssues(issues) {
|
|
3684
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3685
|
+
const deduped = [];
|
|
3686
|
+
for (const issue7 of issues) {
|
|
3687
|
+
const key = issueKey(issue7);
|
|
3688
|
+
if (seen.has(key)) {
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
seen.add(key);
|
|
3692
|
+
deduped.push(issue7);
|
|
3693
|
+
}
|
|
3694
|
+
return deduped;
|
|
3695
|
+
}
|
|
3696
|
+
function issueKey(issue7) {
|
|
3697
|
+
const file = issue7.file ?? "";
|
|
3698
|
+
const line = issue7.loc?.line ?? "";
|
|
3699
|
+
const column = issue7.loc?.column ?? "";
|
|
3700
|
+
return [issue7.code, issue7.severity, issue7.message, file, line, column].join(
|
|
3701
|
+
"|"
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3207
3704
|
async function emitJson(result, root, jsonPath) {
|
|
3208
|
-
const abs =
|
|
3209
|
-
await (0,
|
|
3210
|
-
await (0,
|
|
3705
|
+
const abs = resolveJsonPath(root, jsonPath);
|
|
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)}
|
|
3211
3708
|
`, "utf-8");
|
|
3212
3709
|
}
|
|
3710
|
+
function resolveJsonPath(root, jsonPath) {
|
|
3711
|
+
return import_node_path19.default.isAbsolute(jsonPath) ? jsonPath : import_node_path19.default.resolve(root, jsonPath);
|
|
3712
|
+
}
|
|
3713
|
+
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3213
3714
|
|
|
3214
3715
|
// src/cli/lib/args.ts
|
|
3215
3716
|
function parseArgs(argv, cwd) {
|
|
3216
3717
|
const options = {
|
|
3217
3718
|
root: cwd,
|
|
3719
|
+
rootExplicit: false,
|
|
3218
3720
|
dir: cwd,
|
|
3219
3721
|
force: false,
|
|
3220
3722
|
yes: false,
|
|
3221
3723
|
dryRun: false,
|
|
3222
3724
|
reportFormat: "md",
|
|
3725
|
+
reportRunValidate: false,
|
|
3726
|
+
doctorFormat: "text",
|
|
3223
3727
|
validateFormat: "text",
|
|
3224
3728
|
strict: false,
|
|
3225
3729
|
help: false
|
|
@@ -3235,6 +3739,7 @@ function parseArgs(argv, cwd) {
|
|
|
3235
3739
|
switch (arg) {
|
|
3236
3740
|
case "--root":
|
|
3237
3741
|
options.root = args[i + 1] ?? options.root;
|
|
3742
|
+
options.rootExplicit = true;
|
|
3238
3743
|
i += 1;
|
|
3239
3744
|
break;
|
|
3240
3745
|
case "--dir":
|
|
@@ -3271,11 +3776,27 @@ function parseArgs(argv, cwd) {
|
|
|
3271
3776
|
{
|
|
3272
3777
|
const next = args[i + 1];
|
|
3273
3778
|
if (next) {
|
|
3274
|
-
|
|
3779
|
+
if (command === "doctor") {
|
|
3780
|
+
options.doctorOut = next;
|
|
3781
|
+
} else {
|
|
3782
|
+
options.reportOut = next;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
i += 1;
|
|
3787
|
+
break;
|
|
3788
|
+
case "--in":
|
|
3789
|
+
{
|
|
3790
|
+
const next = args[i + 1];
|
|
3791
|
+
if (next) {
|
|
3792
|
+
options.reportIn = next;
|
|
3275
3793
|
}
|
|
3276
3794
|
}
|
|
3277
3795
|
i += 1;
|
|
3278
3796
|
break;
|
|
3797
|
+
case "--run-validate":
|
|
3798
|
+
options.reportRunValidate = true;
|
|
3799
|
+
break;
|
|
3279
3800
|
case "--help":
|
|
3280
3801
|
case "-h":
|
|
3281
3802
|
options.help = true;
|
|
@@ -3302,6 +3823,12 @@ function applyFormatOption(command, value, options) {
|
|
|
3302
3823
|
}
|
|
3303
3824
|
return;
|
|
3304
3825
|
}
|
|
3826
|
+
if (command === "doctor") {
|
|
3827
|
+
if (value === "text" || value === "json") {
|
|
3828
|
+
options.doctorFormat = value;
|
|
3829
|
+
}
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3305
3832
|
if (value === "md" || value === "json") {
|
|
3306
3833
|
options.reportFormat = value;
|
|
3307
3834
|
}
|
|
@@ -3327,18 +3854,34 @@ async function run(argv, cwd) {
|
|
|
3327
3854
|
});
|
|
3328
3855
|
return;
|
|
3329
3856
|
case "validate":
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3857
|
+
{
|
|
3858
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3859
|
+
process.exitCode = await runValidate({
|
|
3860
|
+
root: resolvedRoot,
|
|
3861
|
+
strict: options.strict,
|
|
3862
|
+
format: options.validateFormat,
|
|
3863
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3336
3866
|
return;
|
|
3337
3867
|
case "report":
|
|
3338
|
-
|
|
3868
|
+
{
|
|
3869
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3870
|
+
await runReport({
|
|
3871
|
+
root: resolvedRoot,
|
|
3872
|
+
format: options.reportFormat,
|
|
3873
|
+
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
|
|
3874
|
+
...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
|
|
3875
|
+
...options.reportRunValidate ? { runValidate: true } : {}
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
return;
|
|
3879
|
+
case "doctor":
|
|
3880
|
+
await runDoctor({
|
|
3339
3881
|
root: options.root,
|
|
3340
|
-
|
|
3341
|
-
|
|
3882
|
+
rootExplicit: options.rootExplicit,
|
|
3883
|
+
format: options.doctorFormat,
|
|
3884
|
+
...options.doctorOut !== void 0 ? { outPath: options.doctorOut } : {}
|
|
3342
3885
|
});
|
|
3343
3886
|
return;
|
|
3344
3887
|
default:
|
|
@@ -3354,6 +3897,7 @@ Commands:
|
|
|
3354
3897
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
3355
3898
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
3356
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
|
|
3357
3901
|
|
|
3358
3902
|
Options:
|
|
3359
3903
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -3363,12 +3907,27 @@ Options:
|
|
|
3363
3907
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
3364
3908
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3365
3909
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3910
|
+
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|
|
3366
3911
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3367
3912
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3368
|
-
--out <path> report: \u51FA\u529B\u5148
|
|
3913
|
+
--out <path> report/doctor: \u51FA\u529B\u5148
|
|
3914
|
+
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3915
|
+
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3369
3916
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
3370
3917
|
`;
|
|
3371
3918
|
}
|
|
3919
|
+
async function resolveRoot(options) {
|
|
3920
|
+
if (options.rootExplicit) {
|
|
3921
|
+
return options.root;
|
|
3922
|
+
}
|
|
3923
|
+
const search = await findConfigRoot(options.root);
|
|
3924
|
+
if (!search.found) {
|
|
3925
|
+
warn(
|
|
3926
|
+
`qfai: qfai.config.yaml \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081 defaultConfig \u3092\u4F7F\u7528\u3057\u307E\u3059 (root=${search.root})`
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3929
|
+
return search.root;
|
|
3930
|
+
}
|
|
3372
3931
|
|
|
3373
3932
|
// src/cli/index.ts
|
|
3374
3933
|
run(process.argv.slice(2), process.cwd()).catch((err) => {
|