qfai 0.2.5

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.
Files changed (120) hide show
  1. package/README.md +28 -0
  2. package/assets/init/qfai/README.md +6 -0
  3. package/assets/init/qfai/contracts/api/api-0001-sample.yaml +14 -0
  4. package/assets/init/qfai/contracts/db/db-0001-sample.sql +5 -0
  5. package/assets/init/qfai/contracts/ui/ui-0001-sample.yaml +4 -0
  6. package/assets/init/qfai/prompts/makeBusinessFlow.md +34 -0
  7. package/assets/init/qfai/prompts/makeOverview.md +27 -0
  8. package/assets/init/qfai/spec/decisions/ADR-0001.md +7 -0
  9. package/assets/init/qfai/spec/scenarios.feature +6 -0
  10. package/assets/init/qfai/spec/spec-0001-sample.md +29 -0
  11. package/assets/init/root/.github/workflows/qfai.yml +22 -0
  12. package/assets/init/root/qfai.config.yaml +29 -0
  13. package/dist/cli/commands/init.d.ts +8 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -0
  15. package/dist/cli/commands/init.js +30 -0
  16. package/dist/cli/commands/init.js.map +1 -0
  17. package/dist/cli/commands/report.d.ts +8 -0
  18. package/dist/cli/commands/report.d.ts.map +1 -0
  19. package/dist/cli/commands/report.js +83 -0
  20. package/dist/cli/commands/report.js.map +1 -0
  21. package/dist/cli/commands/validate.d.ts +10 -0
  22. package/dist/cli/commands/validate.d.ts.map +1 -0
  23. package/dist/cli/commands/validate.js +66 -0
  24. package/dist/cli/commands/validate.js.map +1 -0
  25. package/dist/cli/index.cjs +2003 -0
  26. package/dist/cli/index.cjs.map +1 -0
  27. package/dist/cli/index.d.cts +1 -0
  28. package/dist/cli/index.d.ts +3 -0
  29. package/dist/cli/index.d.ts.map +1 -0
  30. package/dist/cli/index.js +7 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/index.mjs +1980 -0
  33. package/dist/cli/index.mjs.map +1 -0
  34. package/dist/cli/lib/args.d.ts +19 -0
  35. package/dist/cli/lib/args.d.ts.map +1 -0
  36. package/dist/cli/lib/args.js +107 -0
  37. package/dist/cli/lib/args.js.map +1 -0
  38. package/dist/cli/lib/assets.d.ts +2 -0
  39. package/dist/cli/lib/assets.d.ts.map +1 -0
  40. package/dist/cli/lib/assets.js +8 -0
  41. package/dist/cli/lib/assets.js.map +1 -0
  42. package/dist/cli/lib/failOn.d.ts +5 -0
  43. package/dist/cli/lib/failOn.d.ts.map +1 -0
  44. package/dist/cli/lib/failOn.js +10 -0
  45. package/dist/cli/lib/failOn.js.map +1 -0
  46. package/dist/cli/lib/fs.d.ts +11 -0
  47. package/dist/cli/lib/fs.d.ts.map +1 -0
  48. package/dist/cli/lib/fs.js +91 -0
  49. package/dist/cli/lib/fs.js.map +1 -0
  50. package/dist/cli/lib/logger.d.ts +4 -0
  51. package/dist/cli/lib/logger.d.ts.map +1 -0
  52. package/dist/cli/lib/logger.js +10 -0
  53. package/dist/cli/lib/logger.js.map +1 -0
  54. package/dist/cli/main.d.ts +2 -0
  55. package/dist/cli/main.d.ts.map +1 -0
  56. package/dist/cli/main.js +73 -0
  57. package/dist/cli/main.js.map +1 -0
  58. package/dist/core/config.d.ts +46 -0
  59. package/dist/core/config.d.ts.map +1 -0
  60. package/dist/core/config.js +224 -0
  61. package/dist/core/config.js.map +1 -0
  62. package/dist/core/discovery.d.ts +11 -0
  63. package/dist/core/discovery.d.ts.map +1 -0
  64. package/dist/core/discovery.js +31 -0
  65. package/dist/core/discovery.js.map +1 -0
  66. package/dist/core/fs.d.ts +6 -0
  67. package/dist/core/fs.d.ts.map +1 -0
  68. package/dist/core/fs.js +55 -0
  69. package/dist/core/fs.js.map +1 -0
  70. package/dist/core/ids.d.ts +5 -0
  71. package/dist/core/ids.d.ts.map +1 -0
  72. package/dist/core/ids.js +49 -0
  73. package/dist/core/ids.js.map +1 -0
  74. package/dist/core/index.d.ts +11 -0
  75. package/dist/core/index.d.ts.map +1 -0
  76. package/dist/core/index.js +11 -0
  77. package/dist/core/index.js.map +1 -0
  78. package/dist/core/report.d.ts +41 -0
  79. package/dist/core/report.d.ts.map +1 -0
  80. package/dist/core/report.js +238 -0
  81. package/dist/core/report.js.map +1 -0
  82. package/dist/core/types.d.ts +27 -0
  83. package/dist/core/types.d.ts.map +1 -0
  84. package/dist/core/types.js +2 -0
  85. package/dist/core/types.js.map +1 -0
  86. package/dist/core/validate.d.ts +4 -0
  87. package/dist/core/validate.d.ts.map +1 -0
  88. package/dist/core/validate.js +32 -0
  89. package/dist/core/validate.js.map +1 -0
  90. package/dist/core/validators/contracts.d.ts +5 -0
  91. package/dist/core/validators/contracts.d.ts.map +1 -0
  92. package/dist/core/validators/contracts.js +157 -0
  93. package/dist/core/validators/contracts.js.map +1 -0
  94. package/dist/core/validators/scenario.d.ts +5 -0
  95. package/dist/core/validators/scenario.d.ts.map +1 -0
  96. package/dist/core/validators/scenario.js +82 -0
  97. package/dist/core/validators/scenario.js.map +1 -0
  98. package/dist/core/validators/spec.d.ts +5 -0
  99. package/dist/core/validators/spec.d.ts.map +1 -0
  100. package/dist/core/validators/spec.js +69 -0
  101. package/dist/core/validators/spec.js.map +1 -0
  102. package/dist/core/validators/traceability.d.ts +4 -0
  103. package/dist/core/validators/traceability.d.ts.map +1 -0
  104. package/dist/core/validators/traceability.js +148 -0
  105. package/dist/core/validators/traceability.js.map +1 -0
  106. package/dist/core/version.d.ts +2 -0
  107. package/dist/core/version.d.ts.map +1 -0
  108. package/dist/core/version.js +25 -0
  109. package/dist/core/version.js.map +1 -0
  110. package/dist/index.cjs +1579 -0
  111. package/dist/index.cjs.map +1 -0
  112. package/dist/index.d.cts +132 -0
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.d.ts.map +1 -0
  115. package/dist/index.js +2 -0
  116. package/dist/index.js.map +1 -0
  117. package/dist/index.mjs +1523 -0
  118. package/dist/index.mjs.map +1 -0
  119. package/dist/tsconfig.tsbuildinfo +1 -0
  120. package/package.json +38 -0
@@ -0,0 +1,1980 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/commands/init.ts
4
+ import path3 from "path";
5
+
6
+ // src/cli/lib/fs.ts
7
+ import { access, copyFile, mkdir, readdir } from "fs/promises";
8
+ import path from "path";
9
+ async function copyTemplateTree(sourceRoot, destRoot, options) {
10
+ const files = await collectTemplateFiles(sourceRoot);
11
+ return copyFiles(files, sourceRoot, destRoot, options);
12
+ }
13
+ async function copyFiles(files, sourceRoot, destRoot, options) {
14
+ const copied = [];
15
+ const skipped = [];
16
+ const conflicts = [];
17
+ if (!options.force) {
18
+ for (const file of files) {
19
+ const relative = path.relative(sourceRoot, file);
20
+ const dest = path.join(destRoot, relative);
21
+ if (!await shouldWrite(dest, options.force)) {
22
+ conflicts.push(dest);
23
+ }
24
+ }
25
+ if (conflicts.length > 0) {
26
+ throw new Error(formatConflictMessage(conflicts));
27
+ }
28
+ }
29
+ for (const file of files) {
30
+ const relative = path.relative(sourceRoot, file);
31
+ const dest = path.join(destRoot, relative);
32
+ if (!await shouldWrite(dest, options.force)) {
33
+ skipped.push(dest);
34
+ continue;
35
+ }
36
+ if (!options.dryRun) {
37
+ await mkdir(path.dirname(dest), { recursive: true });
38
+ await copyFile(file, dest);
39
+ }
40
+ copied.push(dest);
41
+ }
42
+ return { copied, skipped };
43
+ }
44
+ function formatConflictMessage(conflicts) {
45
+ return [
46
+ "\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3068\u885D\u7A81\u3057\u307E\u3057\u305F\u3002\u5B89\u5168\u306E\u305F\u3081\u505C\u6B62\u3057\u307E\u3059\u3002",
47
+ "",
48
+ "\u885D\u7A81\u30D5\u30A1\u30A4\u30EB:",
49
+ ...conflicts.map((conflict) => `- ${conflict}`),
50
+ "",
51
+ "\u4E0A\u66F8\u304D\u3057\u3066\u7D9A\u884C\u3059\u308B\u5834\u5408\u306F --force \u3092\u4ED8\u3051\u3066\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
52
+ ].join("\n");
53
+ }
54
+ async function collectTemplateFiles(root) {
55
+ const entries = [];
56
+ if (!await exists(root)) {
57
+ return entries;
58
+ }
59
+ const items = await readdir(root, { withFileTypes: true });
60
+ for (const item of items) {
61
+ const fullPath = path.join(root, item.name);
62
+ if (item.isDirectory()) {
63
+ const nested = await collectTemplateFiles(fullPath);
64
+ entries.push(...nested);
65
+ continue;
66
+ }
67
+ if (item.isFile()) {
68
+ entries.push(fullPath);
69
+ }
70
+ }
71
+ return entries;
72
+ }
73
+ async function shouldWrite(target, force) {
74
+ if (force) {
75
+ return true;
76
+ }
77
+ return !await exists(target);
78
+ }
79
+ async function exists(target) {
80
+ try {
81
+ await access(target);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ // src/cli/lib/assets.ts
89
+ import path2 from "path";
90
+ import { fileURLToPath } from "url";
91
+ function getInitAssetsDir() {
92
+ const base = import.meta.url;
93
+ const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
94
+ return path2.resolve(path2.dirname(basePath), "../../assets/init");
95
+ }
96
+
97
+ // src/cli/lib/logger.ts
98
+ function info(message) {
99
+ process.stdout.write(`${message}
100
+ `);
101
+ }
102
+ function error(message) {
103
+ process.stderr.write(`${message}
104
+ `);
105
+ }
106
+
107
+ // src/cli/commands/init.ts
108
+ async function runInit(options) {
109
+ const assetsRoot = getInitAssetsDir();
110
+ const rootAssets = path3.join(assetsRoot, "root");
111
+ const qfaiAssets = path3.join(assetsRoot, "qfai");
112
+ const destRoot = path3.resolve(options.dir);
113
+ const destQfai = path3.join(destRoot, "qfai");
114
+ const rootResult = await copyTemplateTree(rootAssets, destRoot, {
115
+ force: options.force,
116
+ dryRun: options.dryRun
117
+ });
118
+ const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
119
+ force: options.force,
120
+ dryRun: options.dryRun
121
+ });
122
+ report(
123
+ [...rootResult.copied, ...qfaiResult.copied],
124
+ [...rootResult.skipped, ...qfaiResult.skipped],
125
+ options.dryRun,
126
+ "init"
127
+ );
128
+ }
129
+ function report(copied, skipped, dryRun, label) {
130
+ info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
131
+ if (copied.length > 0) {
132
+ info(` created: ${copied.length}`);
133
+ }
134
+ if (skipped.length > 0) {
135
+ info(` skipped: ${skipped.length}`);
136
+ }
137
+ }
138
+
139
+ // src/cli/commands/report.ts
140
+ import { mkdir as mkdir2, readFile as readFile8, writeFile } from "fs/promises";
141
+ import path9 from "path";
142
+
143
+ // src/core/config.ts
144
+ import { readFile } from "fs/promises";
145
+ import path4 from "path";
146
+ import { parse as parseYaml } from "yaml";
147
+ var defaultConfig = {
148
+ paths: {
149
+ specDir: "qfai/spec",
150
+ decisionsDir: "qfai/spec/decisions",
151
+ scenariosDir: "qfai/spec",
152
+ rulesDir: "qfai/rules",
153
+ contractsDir: "qfai/contracts",
154
+ uiContractsDir: "qfai/contracts/ui",
155
+ apiContractsDir: "qfai/contracts/api",
156
+ dataContractsDir: "qfai/contracts/db",
157
+ srcDir: "src",
158
+ testsDir: "tests"
159
+ },
160
+ validation: {
161
+ failOn: "error",
162
+ require: {
163
+ specSections: [
164
+ "\u80CC\u666F",
165
+ "\u30B9\u30B3\u30FC\u30D7",
166
+ "\u975E\u30B4\u30FC\u30EB",
167
+ "\u7528\u8A9E",
168
+ "\u524D\u63D0",
169
+ "\u6C7A\u5B9A\u4E8B\u9805",
170
+ "\u696D\u52D9\u30EB\u30FC\u30EB"
171
+ ]
172
+ },
173
+ traceability: {
174
+ brMustHaveSc: true,
175
+ scMustTouchContracts: true,
176
+ allowOrphanContracts: false
177
+ }
178
+ },
179
+ output: {
180
+ format: "text",
181
+ jsonPath: ".qfai/out/validate.json"
182
+ }
183
+ };
184
+ function getConfigPath(root) {
185
+ return path4.join(root, "qfai.config.yaml");
186
+ }
187
+ async function loadConfig(root) {
188
+ const configPath = getConfigPath(root);
189
+ const issues = [];
190
+ let parsed;
191
+ try {
192
+ const raw = await readFile(configPath, "utf-8");
193
+ parsed = parseYaml(raw);
194
+ } catch (error2) {
195
+ if (isMissingFile(error2)) {
196
+ return { config: defaultConfig, issues, configPath };
197
+ }
198
+ issues.push(configIssue(configPath, formatError(error2)));
199
+ return { config: defaultConfig, issues, configPath };
200
+ }
201
+ const normalized = normalizeConfig(parsed, configPath, issues);
202
+ return { config: normalized, issues, configPath };
203
+ }
204
+ function resolvePath(root, config, key) {
205
+ return path4.resolve(root, config.paths[key]);
206
+ }
207
+ function normalizeConfig(raw, configPath, issues) {
208
+ if (!isRecord(raw)) {
209
+ issues.push(configIssue(configPath, "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059\u3002"));
210
+ return defaultConfig;
211
+ }
212
+ return {
213
+ paths: normalizePaths(raw.paths, configPath, issues),
214
+ validation: normalizeValidation(raw.validation, configPath, issues),
215
+ output: normalizeOutput(raw.output, configPath, issues)
216
+ };
217
+ }
218
+ function normalizePaths(raw, configPath, issues) {
219
+ const base = defaultConfig.paths;
220
+ if (!raw) {
221
+ return base;
222
+ }
223
+ if (!isRecord(raw)) {
224
+ issues.push(
225
+ configIssue(configPath, "paths \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
226
+ );
227
+ return base;
228
+ }
229
+ return {
230
+ specDir: readString(
231
+ raw.specDir,
232
+ base.specDir,
233
+ "paths.specDir",
234
+ configPath,
235
+ issues
236
+ ),
237
+ decisionsDir: readString(
238
+ raw.decisionsDir,
239
+ base.decisionsDir,
240
+ "paths.decisionsDir",
241
+ configPath,
242
+ issues
243
+ ),
244
+ scenariosDir: readString(
245
+ raw.scenariosDir,
246
+ base.scenariosDir,
247
+ "paths.scenariosDir",
248
+ configPath,
249
+ issues
250
+ ),
251
+ rulesDir: readString(
252
+ raw.rulesDir,
253
+ base.rulesDir,
254
+ "paths.rulesDir",
255
+ configPath,
256
+ issues
257
+ ),
258
+ contractsDir: readString(
259
+ raw.contractsDir,
260
+ base.contractsDir,
261
+ "paths.contractsDir",
262
+ configPath,
263
+ issues
264
+ ),
265
+ uiContractsDir: readString(
266
+ raw.uiContractsDir,
267
+ base.uiContractsDir,
268
+ "paths.uiContractsDir",
269
+ configPath,
270
+ issues
271
+ ),
272
+ apiContractsDir: readString(
273
+ raw.apiContractsDir,
274
+ base.apiContractsDir,
275
+ "paths.apiContractsDir",
276
+ configPath,
277
+ issues
278
+ ),
279
+ dataContractsDir: readString(
280
+ raw.dataContractsDir,
281
+ base.dataContractsDir,
282
+ "paths.dataContractsDir",
283
+ configPath,
284
+ issues
285
+ ),
286
+ srcDir: readString(
287
+ raw.srcDir,
288
+ base.srcDir,
289
+ "paths.srcDir",
290
+ configPath,
291
+ issues
292
+ ),
293
+ testsDir: readString(
294
+ raw.testsDir,
295
+ base.testsDir,
296
+ "paths.testsDir",
297
+ configPath,
298
+ issues
299
+ )
300
+ };
301
+ }
302
+ function normalizeValidation(raw, configPath, issues) {
303
+ const base = defaultConfig.validation;
304
+ if (!raw) {
305
+ return base;
306
+ }
307
+ if (!isRecord(raw)) {
308
+ issues.push(
309
+ configIssue(
310
+ configPath,
311
+ "validation \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
312
+ )
313
+ );
314
+ return base;
315
+ }
316
+ let requireRaw;
317
+ if (raw.require === void 0) {
318
+ requireRaw = void 0;
319
+ } else if (isRecord(raw.require)) {
320
+ requireRaw = raw.require;
321
+ } else {
322
+ issues.push(
323
+ configIssue(
324
+ configPath,
325
+ "validation.require \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
326
+ )
327
+ );
328
+ requireRaw = void 0;
329
+ }
330
+ let traceabilityRaw;
331
+ if (raw.traceability === void 0) {
332
+ traceabilityRaw = void 0;
333
+ } else if (isRecord(raw.traceability)) {
334
+ traceabilityRaw = raw.traceability;
335
+ } else {
336
+ issues.push(
337
+ configIssue(
338
+ configPath,
339
+ "validation.traceability \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
340
+ )
341
+ );
342
+ traceabilityRaw = void 0;
343
+ }
344
+ return {
345
+ failOn: readFailOn(
346
+ raw.failOn,
347
+ base.failOn,
348
+ "validation.failOn",
349
+ configPath,
350
+ issues
351
+ ),
352
+ require: {
353
+ specSections: readStringArray(
354
+ requireRaw?.specSections,
355
+ base.require.specSections,
356
+ "validation.require.specSections",
357
+ configPath,
358
+ issues
359
+ )
360
+ },
361
+ traceability: {
362
+ brMustHaveSc: readBoolean(
363
+ traceabilityRaw?.brMustHaveSc,
364
+ base.traceability.brMustHaveSc,
365
+ "validation.traceability.brMustHaveSc",
366
+ configPath,
367
+ issues
368
+ ),
369
+ scMustTouchContracts: readBoolean(
370
+ traceabilityRaw?.scMustTouchContracts,
371
+ base.traceability.scMustTouchContracts,
372
+ "validation.traceability.scMustTouchContracts",
373
+ configPath,
374
+ issues
375
+ ),
376
+ allowOrphanContracts: readBoolean(
377
+ traceabilityRaw?.allowOrphanContracts,
378
+ base.traceability.allowOrphanContracts,
379
+ "validation.traceability.allowOrphanContracts",
380
+ configPath,
381
+ issues
382
+ )
383
+ }
384
+ };
385
+ }
386
+ function normalizeOutput(raw, configPath, issues) {
387
+ const base = defaultConfig.output;
388
+ if (!raw) {
389
+ return base;
390
+ }
391
+ if (!isRecord(raw)) {
392
+ issues.push(
393
+ configIssue(configPath, "output \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
394
+ );
395
+ return base;
396
+ }
397
+ return {
398
+ format: readOutputFormat(
399
+ raw.format,
400
+ base.format,
401
+ "output.format",
402
+ configPath,
403
+ issues
404
+ ),
405
+ jsonPath: readString(
406
+ raw.jsonPath,
407
+ base.jsonPath,
408
+ "output.jsonPath",
409
+ configPath,
410
+ issues
411
+ )
412
+ };
413
+ }
414
+ function readString(value, fallback, label, configPath, issues) {
415
+ if (typeof value === "string" && value.trim().length > 0) {
416
+ return value;
417
+ }
418
+ if (value !== void 0) {
419
+ issues.push(
420
+ configIssue(configPath, `${label} \u306F\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
421
+ );
422
+ }
423
+ return fallback;
424
+ }
425
+ function readStringArray(value, fallback, label, configPath, issues) {
426
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
427
+ return value;
428
+ }
429
+ if (value !== void 0) {
430
+ issues.push(
431
+ configIssue(configPath, `${label} \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
432
+ );
433
+ }
434
+ return fallback;
435
+ }
436
+ function readBoolean(value, fallback, label, configPath, issues) {
437
+ if (typeof value === "boolean") {
438
+ return value;
439
+ }
440
+ if (value !== void 0) {
441
+ issues.push(
442
+ configIssue(configPath, `${label} \u306F\u771F\u507D\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
443
+ );
444
+ }
445
+ return fallback;
446
+ }
447
+ function readFailOn(value, fallback, label, configPath, issues) {
448
+ if (value === "never" || value === "warning" || value === "error") {
449
+ return value;
450
+ }
451
+ if (value !== void 0) {
452
+ issues.push(
453
+ configIssue(
454
+ configPath,
455
+ `${label} \u306F never|warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
456
+ )
457
+ );
458
+ }
459
+ return fallback;
460
+ }
461
+ function readOutputFormat(value, fallback, label, configPath, issues) {
462
+ if (value === "text" || value === "json" || value === "github") {
463
+ return value;
464
+ }
465
+ if (value !== void 0) {
466
+ issues.push(
467
+ configIssue(
468
+ configPath,
469
+ `${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
470
+ )
471
+ );
472
+ }
473
+ return fallback;
474
+ }
475
+ function configIssue(file, message) {
476
+ return {
477
+ code: "QFAI_CONFIG_INVALID",
478
+ severity: "error",
479
+ message,
480
+ file,
481
+ rule: "config.invalid"
482
+ };
483
+ }
484
+ function isMissingFile(error2) {
485
+ if (error2 && typeof error2 === "object" && "code" in error2) {
486
+ return error2.code === "ENOENT";
487
+ }
488
+ return false;
489
+ }
490
+ function formatError(error2) {
491
+ if (error2 instanceof Error) {
492
+ return error2.message;
493
+ }
494
+ return String(error2);
495
+ }
496
+ function isRecord(value) {
497
+ return value !== null && typeof value === "object" && !Array.isArray(value);
498
+ }
499
+
500
+ // src/core/report.ts
501
+ import { readFile as readFile7 } from "fs/promises";
502
+
503
+ // src/core/discovery.ts
504
+ import path6 from "path";
505
+
506
+ // src/core/fs.ts
507
+ import { access as access2, readdir as readdir2 } from "fs/promises";
508
+ import path5 from "path";
509
+ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
510
+ "node_modules",
511
+ ".git",
512
+ "dist",
513
+ ".pnpm",
514
+ "tmp",
515
+ ".mcp-tools"
516
+ ]);
517
+ async function collectFiles(root, options = {}) {
518
+ const entries = [];
519
+ if (!await exists2(root)) {
520
+ return entries;
521
+ }
522
+ const ignoreDirs = /* @__PURE__ */ new Set([
523
+ ...DEFAULT_IGNORE_DIRS,
524
+ ...options.ignoreDirs ?? []
525
+ ]);
526
+ const extensions = options.extensions?.map((ext) => ext.toLowerCase()) ?? [];
527
+ await walk(root, root, ignoreDirs, extensions, entries);
528
+ return entries;
529
+ }
530
+ async function walk(base, current, ignoreDirs, extensions, out) {
531
+ const items = await readdir2(current, { withFileTypes: true });
532
+ for (const item of items) {
533
+ const fullPath = path5.join(current, item.name);
534
+ if (item.isDirectory()) {
535
+ if (ignoreDirs.has(item.name)) {
536
+ continue;
537
+ }
538
+ await walk(base, fullPath, ignoreDirs, extensions, out);
539
+ continue;
540
+ }
541
+ if (item.isFile()) {
542
+ if (extensions.length > 0) {
543
+ const ext = path5.extname(item.name).toLowerCase();
544
+ if (!extensions.includes(ext)) {
545
+ continue;
546
+ }
547
+ }
548
+ out.push(fullPath);
549
+ }
550
+ }
551
+ }
552
+ async function exists2(target) {
553
+ try {
554
+ await access2(target);
555
+ return true;
556
+ } catch {
557
+ return false;
558
+ }
559
+ }
560
+
561
+ // src/core/discovery.ts
562
+ var LEGACY_SPEC_NAME = "spec.md";
563
+ var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/i;
564
+ async function collectSpecFiles(specRoot) {
565
+ const files = await collectFiles(specRoot, { extensions: [".md"] });
566
+ return files.filter((file) => isSpecFile(file));
567
+ }
568
+ async function collectUiContractFiles(uiRoot) {
569
+ return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
570
+ }
571
+ async function collectApiContractFiles(apiRoot) {
572
+ return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
573
+ }
574
+ async function collectDataContractFiles(dataRoot) {
575
+ return collectFiles(dataRoot, { extensions: [".sql"] });
576
+ }
577
+ async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
578
+ const [ui, api, db] = await Promise.all([
579
+ collectUiContractFiles(uiRoot),
580
+ collectApiContractFiles(apiRoot),
581
+ collectDataContractFiles(dataRoot)
582
+ ]);
583
+ return { ui, api, db };
584
+ }
585
+ function isSpecFile(filePath) {
586
+ const name = path6.basename(filePath).toLowerCase();
587
+ return name === LEGACY_SPEC_NAME || SPEC_NAMED_PATTERN.test(name);
588
+ }
589
+
590
+ // src/core/ids.ts
591
+ var ID_PATTERNS = {
592
+ SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
593
+ BR: /\bBR-[A-Z0-9-]+\b/g,
594
+ SC: /\bSC-[A-Z0-9-]+\b/g,
595
+ UI: /\bUI-[A-Z0-9-]+\b/g,
596
+ API: /\bAPI-[A-Z0-9-]+\b/g,
597
+ DATA: /\bDATA-[A-Z0-9-]+\b/g
598
+ };
599
+ var LOOSE_ID_PATTERNS = {
600
+ SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
601
+ BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
602
+ SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
603
+ UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
604
+ API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
605
+ DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
606
+ };
607
+ function extractIds(text, prefix) {
608
+ const pattern = ID_PATTERNS[prefix];
609
+ const matches = text.match(pattern);
610
+ return unique(matches ?? []);
611
+ }
612
+ function extractAllIds(text) {
613
+ const all = [];
614
+ Object.keys(ID_PATTERNS).forEach((prefix) => {
615
+ all.push(...extractIds(text, prefix));
616
+ });
617
+ return unique(all);
618
+ }
619
+ function extractInvalidIds(text, prefixes) {
620
+ const invalid = [];
621
+ for (const prefix of prefixes) {
622
+ const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
623
+ for (const candidate of candidates) {
624
+ if (!isValidId(candidate, prefix)) {
625
+ invalid.push(candidate);
626
+ }
627
+ }
628
+ }
629
+ return unique(invalid);
630
+ }
631
+ function unique(values) {
632
+ return Array.from(new Set(values));
633
+ }
634
+ function isValidId(value, prefix) {
635
+ const pattern = ID_PATTERNS[prefix];
636
+ const strict = new RegExp(pattern.source);
637
+ return strict.test(value);
638
+ }
639
+
640
+ // src/core/types.ts
641
+ var VALIDATION_SCHEMA_VERSION = "0.2";
642
+
643
+ // src/core/version.ts
644
+ import { readFile as readFile2 } from "fs/promises";
645
+ import path7 from "path";
646
+ import { fileURLToPath as fileURLToPath2 } from "url";
647
+ async function resolveToolVersion() {
648
+ if ("0.2.5".length > 0) {
649
+ return "0.2.5";
650
+ }
651
+ try {
652
+ const packagePath = resolvePackageJsonPath();
653
+ const raw = await readFile2(packagePath, "utf-8");
654
+ const parsed = JSON.parse(raw);
655
+ const version = typeof parsed.version === "string" ? parsed.version : "";
656
+ return version.length > 0 ? version : "unknown";
657
+ } catch {
658
+ return "unknown";
659
+ }
660
+ }
661
+ function resolvePackageJsonPath() {
662
+ const base = import.meta.url;
663
+ const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
664
+ return path7.resolve(path7.dirname(basePath), "../../package.json");
665
+ }
666
+
667
+ // src/core/validators/contracts.ts
668
+ import { readFile as readFile3 } from "fs/promises";
669
+ import path8 from "path";
670
+ import { parse as parseYaml2 } from "yaml";
671
+ var SQL_DANGEROUS_PATTERNS = [
672
+ { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
673
+ { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
674
+ { pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
675
+ {
676
+ pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
677
+ label: "ALTER TABLE ... DROP"
678
+ }
679
+ ];
680
+ async function validateContracts(root, config) {
681
+ const issues = [];
682
+ issues.push(
683
+ ...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
684
+ );
685
+ issues.push(
686
+ ...await validateApiContracts(
687
+ resolvePath(root, config, "apiContractsDir")
688
+ )
689
+ );
690
+ issues.push(
691
+ ...await validateDataContracts(
692
+ resolvePath(root, config, "dataContractsDir")
693
+ )
694
+ );
695
+ return issues;
696
+ }
697
+ async function validateUiContracts(uiRoot) {
698
+ const files = await collectUiContractFiles(uiRoot);
699
+ if (files.length === 0) {
700
+ return [
701
+ issue(
702
+ "QFAI-UI-000",
703
+ "UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
704
+ "info",
705
+ uiRoot,
706
+ "contracts.ui.files"
707
+ )
708
+ ];
709
+ }
710
+ const issues = [];
711
+ for (const file of files) {
712
+ const text = await readFile3(file, "utf-8");
713
+ const invalidIds = extractInvalidIds(text, [
714
+ "SPEC",
715
+ "BR",
716
+ "SC",
717
+ "UI",
718
+ "API",
719
+ "DATA"
720
+ ]);
721
+ if (invalidIds.length > 0) {
722
+ issues.push(
723
+ issue(
724
+ "QFAI_ID_INVALID_FORMAT",
725
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
726
+ "error",
727
+ file,
728
+ "id.format",
729
+ invalidIds
730
+ )
731
+ );
732
+ }
733
+ try {
734
+ const doc = parseYaml2(text);
735
+ const id = typeof doc.id === "string" ? doc.id : "";
736
+ if (!id.startsWith("UI-")) {
737
+ issues.push(
738
+ issue(
739
+ "QFAI-UI-001",
740
+ "UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
741
+ "error",
742
+ file,
743
+ "contracts.ui.id"
744
+ )
745
+ );
746
+ }
747
+ } catch (error2) {
748
+ issues.push(
749
+ issue(
750
+ "QFAI-UI-002",
751
+ `UI YAML \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error2)}`,
752
+ "error",
753
+ file,
754
+ "contracts.ui.parse"
755
+ )
756
+ );
757
+ }
758
+ }
759
+ return issues;
760
+ }
761
+ async function validateApiContracts(apiRoot) {
762
+ const files = await collectApiContractFiles(apiRoot);
763
+ if (files.length === 0) {
764
+ return [
765
+ issue(
766
+ "QFAI-API-000",
767
+ "API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
768
+ "info",
769
+ apiRoot,
770
+ "contracts.api.files"
771
+ )
772
+ ];
773
+ }
774
+ const issues = [];
775
+ for (const file of files) {
776
+ const text = await readFile3(file, "utf-8");
777
+ const invalidIds = extractInvalidIds(text, [
778
+ "SPEC",
779
+ "BR",
780
+ "SC",
781
+ "UI",
782
+ "API",
783
+ "DATA"
784
+ ]);
785
+ if (invalidIds.length > 0) {
786
+ issues.push(
787
+ issue(
788
+ "QFAI_ID_INVALID_FORMAT",
789
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
790
+ "error",
791
+ file,
792
+ "id.format",
793
+ invalidIds
794
+ )
795
+ );
796
+ }
797
+ try {
798
+ const doc = parseStructured(file, text);
799
+ if (!doc || !hasOpenApi(doc)) {
800
+ issues.push(
801
+ issue(
802
+ "QFAI-API-001",
803
+ "OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
804
+ "error",
805
+ file,
806
+ "contracts.api.openapi"
807
+ )
808
+ );
809
+ }
810
+ } catch (error2) {
811
+ issues.push(
812
+ issue(
813
+ "QFAI-API-002",
814
+ `API \u5B9A\u7FA9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error2)}`,
815
+ "error",
816
+ file,
817
+ "contracts.api.parse"
818
+ )
819
+ );
820
+ }
821
+ }
822
+ return issues;
823
+ }
824
+ async function validateDataContracts(dataRoot) {
825
+ const files = await collectDataContractFiles(dataRoot);
826
+ if (files.length === 0) {
827
+ return [
828
+ issue(
829
+ "QFAI-DATA-000",
830
+ "DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
831
+ "info",
832
+ dataRoot,
833
+ "contracts.data.files"
834
+ )
835
+ ];
836
+ }
837
+ const issues = [];
838
+ for (const file of files) {
839
+ const text = await readFile3(file, "utf-8");
840
+ const invalidIds = extractInvalidIds(text, [
841
+ "SPEC",
842
+ "BR",
843
+ "SC",
844
+ "UI",
845
+ "API",
846
+ "DATA"
847
+ ]);
848
+ if (invalidIds.length > 0) {
849
+ issues.push(
850
+ issue(
851
+ "QFAI_ID_INVALID_FORMAT",
852
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
853
+ "error",
854
+ file,
855
+ "id.format",
856
+ invalidIds
857
+ )
858
+ );
859
+ }
860
+ issues.push(...lintSql(text, file));
861
+ }
862
+ return issues;
863
+ }
864
+ function lintSql(text, file) {
865
+ const issues = [];
866
+ for (const { pattern, label } of SQL_DANGEROUS_PATTERNS) {
867
+ if (pattern.test(text)) {
868
+ issues.push(
869
+ issue(
870
+ "QFAI-DATA-001",
871
+ `\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
872
+ "warning",
873
+ file,
874
+ "contracts.data.sql"
875
+ )
876
+ );
877
+ }
878
+ }
879
+ return issues;
880
+ }
881
+ function parseStructured(file, text) {
882
+ const ext = path8.extname(file).toLowerCase();
883
+ if (ext === ".json") {
884
+ return JSON.parse(text);
885
+ }
886
+ return parseYaml2(text);
887
+ }
888
+ function hasOpenApi(doc) {
889
+ return typeof doc.openapi === "string" && doc.openapi.length > 0;
890
+ }
891
+ function formatError2(error2) {
892
+ if (error2 instanceof Error) {
893
+ return error2.message;
894
+ }
895
+ return String(error2);
896
+ }
897
+ function issue(code, message, severity, file, rule, refs) {
898
+ const issue5 = {
899
+ code,
900
+ severity,
901
+ message
902
+ };
903
+ if (file) {
904
+ issue5.file = file;
905
+ }
906
+ if (rule) {
907
+ issue5.rule = rule;
908
+ }
909
+ if (refs && refs.length > 0) {
910
+ issue5.refs = refs;
911
+ }
912
+ return issue5;
913
+ }
914
+
915
+ // src/core/validators/scenario.ts
916
+ import { readFile as readFile4 } from "fs/promises";
917
+ var GIVEN_PATTERN = /\bGiven\b/;
918
+ var WHEN_PATTERN = /\bWhen\b/;
919
+ var THEN_PATTERN = /\bThen\b/;
920
+ async function validateScenarios(root, config) {
921
+ const scenariosRoot = resolvePath(root, config, "scenariosDir");
922
+ const files = await collectFiles(scenariosRoot, {
923
+ extensions: [".feature"]
924
+ });
925
+ if (files.length === 0) {
926
+ return [
927
+ issue2(
928
+ "QFAI-SC-000",
929
+ "Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
930
+ "info",
931
+ scenariosRoot,
932
+ "scenario.files"
933
+ )
934
+ ];
935
+ }
936
+ const issues = [];
937
+ for (const file of files) {
938
+ const text = await readFile4(file, "utf-8");
939
+ issues.push(...validateScenarioContent(text, file));
940
+ }
941
+ return issues;
942
+ }
943
+ function validateScenarioContent(text, file) {
944
+ const issues = [];
945
+ const invalidIds = extractInvalidIds(text, [
946
+ "SPEC",
947
+ "BR",
948
+ "SC",
949
+ "UI",
950
+ "API",
951
+ "DATA"
952
+ ]);
953
+ if (invalidIds.length > 0) {
954
+ issues.push(
955
+ issue2(
956
+ "QFAI_ID_INVALID_FORMAT",
957
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
958
+ "error",
959
+ file,
960
+ "id.format",
961
+ invalidIds
962
+ )
963
+ );
964
+ }
965
+ const scIds = extractIds(text, "SC");
966
+ if (scIds.length === 0) {
967
+ issues.push(
968
+ issue2(
969
+ "QFAI-SC-001",
970
+ "SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
971
+ "error",
972
+ file,
973
+ "scenario.id"
974
+ )
975
+ );
976
+ }
977
+ const specIds = extractIds(text, "SPEC");
978
+ if (specIds.length === 0) {
979
+ issues.push(
980
+ issue2(
981
+ "QFAI-SC-002",
982
+ "SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
983
+ "error",
984
+ file,
985
+ "scenario.spec"
986
+ )
987
+ );
988
+ }
989
+ const brIds = extractIds(text, "BR");
990
+ if (brIds.length === 0) {
991
+ issues.push(
992
+ issue2(
993
+ "QFAI-SC-003",
994
+ "SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
995
+ "error",
996
+ file,
997
+ "scenario.br"
998
+ )
999
+ );
1000
+ }
1001
+ const missingSteps = [];
1002
+ if (!GIVEN_PATTERN.test(text)) {
1003
+ missingSteps.push("Given");
1004
+ }
1005
+ if (!WHEN_PATTERN.test(text)) {
1006
+ missingSteps.push("When");
1007
+ }
1008
+ if (!THEN_PATTERN.test(text)) {
1009
+ missingSteps.push("Then");
1010
+ }
1011
+ if (missingSteps.length > 0) {
1012
+ issues.push(
1013
+ issue2(
1014
+ "QFAI-SC-005",
1015
+ `Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
1016
+ "warning",
1017
+ file,
1018
+ "scenario.steps"
1019
+ )
1020
+ );
1021
+ }
1022
+ return issues;
1023
+ }
1024
+ function issue2(code, message, severity, file, rule, refs) {
1025
+ const issue5 = {
1026
+ code,
1027
+ severity,
1028
+ message
1029
+ };
1030
+ if (file) {
1031
+ issue5.file = file;
1032
+ }
1033
+ if (rule) {
1034
+ issue5.rule = rule;
1035
+ }
1036
+ if (refs && refs.length > 0) {
1037
+ issue5.refs = refs;
1038
+ }
1039
+ return issue5;
1040
+ }
1041
+
1042
+ // src/core/validators/spec.ts
1043
+ import { readFile as readFile5 } from "fs/promises";
1044
+ async function validateSpecs(root, config) {
1045
+ const specsRoot = resolvePath(root, config, "specDir");
1046
+ const files = await collectSpecFiles(specsRoot);
1047
+ if (files.length === 0) {
1048
+ return [
1049
+ issue3(
1050
+ "QFAI-SPEC-000",
1051
+ "Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1052
+ "info",
1053
+ specsRoot,
1054
+ "spec.files"
1055
+ )
1056
+ ];
1057
+ }
1058
+ const issues = [];
1059
+ for (const file of files) {
1060
+ const text = await readFile5(file, "utf-8");
1061
+ issues.push(
1062
+ ...validateSpecContent(
1063
+ text,
1064
+ file,
1065
+ config.validation.require.specSections
1066
+ )
1067
+ );
1068
+ }
1069
+ return issues;
1070
+ }
1071
+ function validateSpecContent(text, file, requiredSections) {
1072
+ const issues = [];
1073
+ const invalidIds = extractInvalidIds(text, [
1074
+ "SPEC",
1075
+ "BR",
1076
+ "SC",
1077
+ "UI",
1078
+ "API",
1079
+ "DATA"
1080
+ ]);
1081
+ if (invalidIds.length > 0) {
1082
+ issues.push(
1083
+ issue3(
1084
+ "QFAI_ID_INVALID_FORMAT",
1085
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1086
+ "error",
1087
+ file,
1088
+ "id.format",
1089
+ invalidIds
1090
+ )
1091
+ );
1092
+ }
1093
+ const specIds = extractIds(text, "SPEC");
1094
+ if (specIds.length === 0) {
1095
+ issues.push(
1096
+ issue3(
1097
+ "QFAI-SPEC-001",
1098
+ "SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1099
+ "error",
1100
+ file,
1101
+ "spec.id"
1102
+ )
1103
+ );
1104
+ }
1105
+ const brIds = extractIds(text, "BR");
1106
+ if (brIds.length === 0) {
1107
+ issues.push(
1108
+ issue3(
1109
+ "QFAI-SPEC-002",
1110
+ "BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1111
+ "error",
1112
+ file,
1113
+ "spec.br"
1114
+ )
1115
+ );
1116
+ }
1117
+ const scIds = extractIds(text, "SC");
1118
+ if (scIds.length > 0) {
1119
+ issues.push(
1120
+ issue3(
1121
+ "QFAI-SPEC-003",
1122
+ "Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
1123
+ "warning",
1124
+ file,
1125
+ "spec.noSc",
1126
+ scIds
1127
+ )
1128
+ );
1129
+ }
1130
+ for (const section of requiredSections) {
1131
+ if (!text.includes(section)) {
1132
+ issues.push(
1133
+ issue3(
1134
+ "QFAI-SPEC-004",
1135
+ `\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
1136
+ "error",
1137
+ file,
1138
+ "spec.requiredSection"
1139
+ )
1140
+ );
1141
+ }
1142
+ }
1143
+ return issues;
1144
+ }
1145
+ function issue3(code, message, severity, file, rule, refs) {
1146
+ const issue5 = {
1147
+ code,
1148
+ severity,
1149
+ message
1150
+ };
1151
+ if (file) {
1152
+ issue5.file = file;
1153
+ }
1154
+ if (rule) {
1155
+ issue5.rule = rule;
1156
+ }
1157
+ if (refs && refs.length > 0) {
1158
+ issue5.refs = refs;
1159
+ }
1160
+ return issue5;
1161
+ }
1162
+
1163
+ // src/core/validators/traceability.ts
1164
+ import { readFile as readFile6 } from "fs/promises";
1165
+ async function validateTraceability(root, config) {
1166
+ const issues = [];
1167
+ const specsRoot = resolvePath(root, config, "specDir");
1168
+ const decisionsRoot = resolvePath(root, config, "decisionsDir");
1169
+ const scenariosRoot = resolvePath(root, config, "scenariosDir");
1170
+ const srcRoot = resolvePath(root, config, "srcDir");
1171
+ const testsRoot = resolvePath(root, config, "testsDir");
1172
+ const specFiles = await collectSpecFiles(specsRoot);
1173
+ const decisionFiles = await collectFiles(decisionsRoot, {
1174
+ extensions: [".md"]
1175
+ });
1176
+ const scenarioFiles = await collectFiles(scenariosRoot, {
1177
+ extensions: [".feature"]
1178
+ });
1179
+ const upstreamIds = /* @__PURE__ */ new Set();
1180
+ const brIdsInSpecs = /* @__PURE__ */ new Set();
1181
+ const brIdsInScenarios = /* @__PURE__ */ new Set();
1182
+ const scIdsInScenarios = /* @__PURE__ */ new Set();
1183
+ const scenarioContractIds = /* @__PURE__ */ new Set();
1184
+ const scWithContracts = /* @__PURE__ */ new Set();
1185
+ for (const file of [...specFiles, ...decisionFiles]) {
1186
+ const text = await readFile6(file, "utf-8");
1187
+ extractAllIds(text).forEach((id) => upstreamIds.add(id));
1188
+ extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
1189
+ }
1190
+ for (const file of scenarioFiles) {
1191
+ const text = await readFile6(file, "utf-8");
1192
+ extractAllIds(text).forEach((id) => upstreamIds.add(id));
1193
+ const brIds = extractIds(text, "BR");
1194
+ brIds.forEach((id) => brIdsInScenarios.add(id));
1195
+ const scIds = extractIds(text, "SC");
1196
+ scIds.forEach((id) => scIdsInScenarios.add(id));
1197
+ const contractIds = [
1198
+ ...extractIds(text, "UI"),
1199
+ ...extractIds(text, "API"),
1200
+ ...extractIds(text, "DATA")
1201
+ ];
1202
+ contractIds.forEach((id) => scenarioContractIds.add(id));
1203
+ if (contractIds.length > 0) {
1204
+ scIds.forEach((id) => scWithContracts.add(id));
1205
+ }
1206
+ }
1207
+ if (upstreamIds.size === 0) {
1208
+ return [
1209
+ issue4(
1210
+ "QFAI-TRACE-000",
1211
+ "\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1212
+ "info",
1213
+ specsRoot,
1214
+ "traceability.upstream"
1215
+ )
1216
+ ];
1217
+ }
1218
+ if (config.validation.traceability.brMustHaveSc && brIdsInSpecs.size > 0) {
1219
+ const orphanBrIds = Array.from(brIdsInSpecs).filter(
1220
+ (id) => !brIdsInScenarios.has(id)
1221
+ );
1222
+ if (orphanBrIds.length > 0) {
1223
+ issues.push(
1224
+ issue4(
1225
+ "QFAI_TRACE_BR_ORPHAN",
1226
+ `BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
1227
+ "error",
1228
+ specsRoot,
1229
+ "traceability.brMustHaveSc",
1230
+ orphanBrIds
1231
+ )
1232
+ );
1233
+ }
1234
+ }
1235
+ if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
1236
+ const scWithoutContracts = Array.from(scIdsInScenarios).filter(
1237
+ (id) => !scWithContracts.has(id)
1238
+ );
1239
+ if (scWithoutContracts.length > 0) {
1240
+ issues.push(
1241
+ issue4(
1242
+ "QFAI_TRACE_SC_NO_CONTRACT",
1243
+ `SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
1244
+ ", "
1245
+ )}`,
1246
+ "error",
1247
+ scenariosRoot,
1248
+ "traceability.scMustTouchContracts",
1249
+ scWithoutContracts
1250
+ )
1251
+ );
1252
+ }
1253
+ }
1254
+ if (!config.validation.traceability.allowOrphanContracts) {
1255
+ const contractIds = await collectContractIds(root, config);
1256
+ if (contractIds.size > 0) {
1257
+ const orphanContracts = Array.from(contractIds).filter(
1258
+ (id) => !scenarioContractIds.has(id)
1259
+ );
1260
+ if (orphanContracts.length > 0) {
1261
+ issues.push(
1262
+ issue4(
1263
+ "QFAI_CONTRACT_ORPHAN",
1264
+ `\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
1265
+ "error",
1266
+ scenariosRoot,
1267
+ "traceability.allowOrphanContracts",
1268
+ orphanContracts
1269
+ )
1270
+ );
1271
+ }
1272
+ }
1273
+ }
1274
+ issues.push(
1275
+ ...await validateCodeReferences(upstreamIds, srcRoot, testsRoot)
1276
+ );
1277
+ return issues;
1278
+ }
1279
+ async function collectContractIds(root, config) {
1280
+ const contractIds = /* @__PURE__ */ new Set();
1281
+ const uiRoot = resolvePath(root, config, "uiContractsDir");
1282
+ const apiRoot = resolvePath(root, config, "apiContractsDir");
1283
+ const dataRoot = resolvePath(root, config, "dataContractsDir");
1284
+ const uiFiles = await collectUiContractFiles(uiRoot);
1285
+ const apiFiles = await collectApiContractFiles(apiRoot);
1286
+ const dataFiles = await collectDataContractFiles(dataRoot);
1287
+ await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
1288
+ await collectIdsFromFiles(apiFiles, ["API"], contractIds);
1289
+ await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
1290
+ return contractIds;
1291
+ }
1292
+ async function collectIdsFromFiles(files, prefixes, out) {
1293
+ for (const file of files) {
1294
+ const text = await readFile6(file, "utf-8");
1295
+ for (const prefix of prefixes) {
1296
+ extractIds(text, prefix).forEach((id) => out.add(id));
1297
+ }
1298
+ }
1299
+ }
1300
+ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
1301
+ const issues = [];
1302
+ const codeFiles = await collectFiles(srcRoot, {
1303
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
1304
+ });
1305
+ const testFiles = await collectFiles(testsRoot, {
1306
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
1307
+ });
1308
+ const targetFiles = [...codeFiles, ...testFiles];
1309
+ if (targetFiles.length === 0) {
1310
+ issues.push(
1311
+ issue4(
1312
+ "QFAI-TRACE-001",
1313
+ "\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1314
+ "info",
1315
+ srcRoot,
1316
+ "traceability.codeReferences"
1317
+ )
1318
+ );
1319
+ return issues;
1320
+ }
1321
+ const pattern = buildIdPattern(Array.from(upstreamIds));
1322
+ let found = false;
1323
+ for (const file of targetFiles) {
1324
+ const text = await readFile6(file, "utf-8");
1325
+ if (pattern.test(text)) {
1326
+ found = true;
1327
+ break;
1328
+ }
1329
+ }
1330
+ if (!found) {
1331
+ issues.push(
1332
+ issue4(
1333
+ "QFAI-TRACE-002",
1334
+ "\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
1335
+ "warning",
1336
+ srcRoot,
1337
+ "traceability.codeReferences"
1338
+ )
1339
+ );
1340
+ }
1341
+ return issues;
1342
+ }
1343
+ function buildIdPattern(ids) {
1344
+ const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1345
+ return new RegExp(`\\b(${escaped.join("|")})\\b`);
1346
+ }
1347
+ function issue4(code, message, severity, file, rule, refs) {
1348
+ const issue5 = {
1349
+ code,
1350
+ severity,
1351
+ message
1352
+ };
1353
+ if (file) {
1354
+ issue5.file = file;
1355
+ }
1356
+ if (rule) {
1357
+ issue5.rule = rule;
1358
+ }
1359
+ if (refs && refs.length > 0) {
1360
+ issue5.refs = refs;
1361
+ }
1362
+ return issue5;
1363
+ }
1364
+
1365
+ // src/core/validate.ts
1366
+ async function validateProject(root, configResult) {
1367
+ const resolved = configResult ?? await loadConfig(root);
1368
+ const { config, issues: configIssues } = resolved;
1369
+ const issues = [
1370
+ ...configIssues,
1371
+ ...await validateSpecs(root, config),
1372
+ ...await validateScenarios(root, config),
1373
+ ...await validateContracts(root, config),
1374
+ ...await validateTraceability(root, config)
1375
+ ];
1376
+ const toolVersion = await resolveToolVersion();
1377
+ return {
1378
+ schemaVersion: VALIDATION_SCHEMA_VERSION,
1379
+ toolVersion,
1380
+ issues,
1381
+ counts: countIssues(issues)
1382
+ };
1383
+ }
1384
+ function countIssues(issues) {
1385
+ return issues.reduce(
1386
+ (acc, issue5) => {
1387
+ acc[issue5.severity] += 1;
1388
+ return acc;
1389
+ },
1390
+ { info: 0, warning: 0, error: 0 }
1391
+ );
1392
+ }
1393
+
1394
+ // src/core/report.ts
1395
+ var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
1396
+ async function createReportData(root, validation, configResult) {
1397
+ const resolved = configResult ?? await loadConfig(root);
1398
+ const config = resolved.config;
1399
+ const configPath = resolved.configPath;
1400
+ const specRoot = resolvePath(root, config, "specDir");
1401
+ const decisionsRoot = resolvePath(root, config, "decisionsDir");
1402
+ const scenariosRoot = resolvePath(root, config, "scenariosDir");
1403
+ const rulesRoot = resolvePath(root, config, "rulesDir");
1404
+ const apiRoot = resolvePath(root, config, "apiContractsDir");
1405
+ const uiRoot = resolvePath(root, config, "uiContractsDir");
1406
+ const dbRoot = resolvePath(root, config, "dataContractsDir");
1407
+ const srcRoot = resolvePath(root, config, "srcDir");
1408
+ const testsRoot = resolvePath(root, config, "testsDir");
1409
+ const specFiles = await collectSpecFiles(specRoot);
1410
+ const scenarioFiles = await collectFiles(scenariosRoot, {
1411
+ extensions: [".feature"]
1412
+ });
1413
+ const decisionFiles = await collectFiles(decisionsRoot, {
1414
+ extensions: [".md"]
1415
+ });
1416
+ const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
1417
+ const {
1418
+ api: apiFiles,
1419
+ ui: uiFiles,
1420
+ db: dbFiles
1421
+ } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
1422
+ const idsByPrefix = await collectIds([
1423
+ ...specFiles,
1424
+ ...scenarioFiles,
1425
+ ...decisionFiles,
1426
+ ...ruleFiles,
1427
+ ...apiFiles,
1428
+ ...uiFiles,
1429
+ ...dbFiles
1430
+ ]);
1431
+ const upstreamIds = await collectUpstreamIds([
1432
+ ...specFiles,
1433
+ ...scenarioFiles
1434
+ ]);
1435
+ const traceability = await evaluateTraceability(
1436
+ upstreamIds,
1437
+ srcRoot,
1438
+ testsRoot
1439
+ );
1440
+ const resolvedValidation = validation ?? await validateProject(root, resolved);
1441
+ const version = await resolveToolVersion();
1442
+ return {
1443
+ tool: "qfai",
1444
+ version,
1445
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1446
+ root,
1447
+ configPath,
1448
+ summary: {
1449
+ specs: specFiles.length,
1450
+ scenarios: scenarioFiles.length,
1451
+ decisions: decisionFiles.length,
1452
+ rules: ruleFiles.length,
1453
+ contracts: {
1454
+ api: apiFiles.length,
1455
+ ui: uiFiles.length,
1456
+ db: dbFiles.length
1457
+ },
1458
+ counts: resolvedValidation.counts
1459
+ },
1460
+ ids: {
1461
+ spec: idsByPrefix.SPEC,
1462
+ br: idsByPrefix.BR,
1463
+ sc: idsByPrefix.SC,
1464
+ ui: idsByPrefix.UI,
1465
+ api: idsByPrefix.API,
1466
+ data: idsByPrefix.DATA
1467
+ },
1468
+ traceability: {
1469
+ upstreamIdsFound: upstreamIds.size,
1470
+ referencedInCodeOrTests: traceability
1471
+ },
1472
+ issues: resolvedValidation.issues
1473
+ };
1474
+ }
1475
+ function formatReportMarkdown(data) {
1476
+ const lines = [];
1477
+ lines.push("# QFAI Report");
1478
+ lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
1479
+ lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
1480
+ lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
1481
+ lines.push(`- \u7248: ${data.version}`);
1482
+ lines.push("");
1483
+ lines.push("## \u6982\u8981");
1484
+ lines.push(`- specs: ${data.summary.specs}`);
1485
+ lines.push(`- scenarios: ${data.summary.scenarios}`);
1486
+ lines.push(`- decisions: ${data.summary.decisions}`);
1487
+ lines.push(`- rules: ${data.summary.rules}`);
1488
+ lines.push(
1489
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
1490
+ );
1491
+ lines.push(
1492
+ `- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
1493
+ );
1494
+ lines.push("");
1495
+ lines.push("## ID\u96C6\u8A08");
1496
+ lines.push(formatIdLine("SPEC", data.ids.spec));
1497
+ lines.push(formatIdLine("BR", data.ids.br));
1498
+ lines.push(formatIdLine("SC", data.ids.sc));
1499
+ lines.push(formatIdLine("UI", data.ids.ui));
1500
+ lines.push(formatIdLine("API", data.ids.api));
1501
+ lines.push(formatIdLine("DATA", data.ids.data));
1502
+ lines.push("");
1503
+ lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
1504
+ lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
1505
+ lines.push(
1506
+ `- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
1507
+ );
1508
+ lines.push("");
1509
+ lines.push("## Hotspots");
1510
+ const hotspots = buildHotspots(data.issues);
1511
+ if (hotspots.length === 0) {
1512
+ lines.push("- (none)");
1513
+ } else {
1514
+ for (const spot of hotspots) {
1515
+ lines.push(
1516
+ `- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
1517
+ );
1518
+ }
1519
+ }
1520
+ lines.push("");
1521
+ lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
1522
+ const traceIssues = data.issues.filter(
1523
+ (item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
1524
+ );
1525
+ if (traceIssues.length === 0) {
1526
+ lines.push("- (none)");
1527
+ } else {
1528
+ for (const item of traceIssues) {
1529
+ const location = item.file ? ` (${item.file})` : "";
1530
+ lines.push(
1531
+ `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}`
1532
+ );
1533
+ }
1534
+ }
1535
+ lines.push("");
1536
+ lines.push("## \u691C\u8A3C\u7D50\u679C");
1537
+ if (data.issues.length === 0) {
1538
+ lines.push("- (none)");
1539
+ } else {
1540
+ for (const item of data.issues) {
1541
+ const location = item.file ? ` (${item.file})` : "";
1542
+ const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
1543
+ lines.push(
1544
+ `- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
1545
+ );
1546
+ }
1547
+ }
1548
+ return lines.join("\n");
1549
+ }
1550
+ function formatReportJson(data) {
1551
+ return JSON.stringify(data, null, 2);
1552
+ }
1553
+ async function collectIds(files) {
1554
+ const result = {
1555
+ SPEC: /* @__PURE__ */ new Set(),
1556
+ BR: /* @__PURE__ */ new Set(),
1557
+ SC: /* @__PURE__ */ new Set(),
1558
+ UI: /* @__PURE__ */ new Set(),
1559
+ API: /* @__PURE__ */ new Set(),
1560
+ DATA: /* @__PURE__ */ new Set()
1561
+ };
1562
+ for (const file of files) {
1563
+ const text = await readFile7(file, "utf-8");
1564
+ for (const prefix of ID_PREFIXES) {
1565
+ const ids = extractIds(text, prefix);
1566
+ ids.forEach((id) => result[prefix].add(id));
1567
+ }
1568
+ }
1569
+ return {
1570
+ SPEC: toSortedArray(result.SPEC),
1571
+ BR: toSortedArray(result.BR),
1572
+ SC: toSortedArray(result.SC),
1573
+ UI: toSortedArray(result.UI),
1574
+ API: toSortedArray(result.API),
1575
+ DATA: toSortedArray(result.DATA)
1576
+ };
1577
+ }
1578
+ async function collectUpstreamIds(files) {
1579
+ const ids = /* @__PURE__ */ new Set();
1580
+ for (const file of files) {
1581
+ const text = await readFile7(file, "utf-8");
1582
+ extractAllIds(text).forEach((id) => ids.add(id));
1583
+ }
1584
+ return ids;
1585
+ }
1586
+ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
1587
+ if (upstreamIds.size === 0) {
1588
+ return false;
1589
+ }
1590
+ const codeFiles = await collectFiles(srcRoot, {
1591
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
1592
+ });
1593
+ const testFiles = await collectFiles(testsRoot, {
1594
+ extensions: [".ts", ".tsx", ".js", ".jsx"]
1595
+ });
1596
+ const targetFiles = [...codeFiles, ...testFiles];
1597
+ if (targetFiles.length === 0) {
1598
+ return false;
1599
+ }
1600
+ const pattern = buildIdPattern2(Array.from(upstreamIds));
1601
+ for (const file of targetFiles) {
1602
+ const text = await readFile7(file, "utf-8");
1603
+ if (pattern.test(text)) {
1604
+ return true;
1605
+ }
1606
+ }
1607
+ return false;
1608
+ }
1609
+ function buildIdPattern2(ids) {
1610
+ const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1611
+ return new RegExp(`\\b(${escaped.join("|")})\\b`);
1612
+ }
1613
+ function formatIdLine(label, values) {
1614
+ if (values.length === 0) {
1615
+ return `- ${label}: (none)`;
1616
+ }
1617
+ return `- ${label}: ${values.join(", ")}`;
1618
+ }
1619
+ function toSortedArray(values) {
1620
+ return Array.from(values).sort((a, b) => a.localeCompare(b));
1621
+ }
1622
+ function buildHotspots(issues) {
1623
+ const map = /* @__PURE__ */ new Map();
1624
+ for (const issue5 of issues) {
1625
+ if (!issue5.file) {
1626
+ continue;
1627
+ }
1628
+ const current = map.get(issue5.file) ?? {
1629
+ file: issue5.file,
1630
+ total: 0,
1631
+ error: 0,
1632
+ warning: 0,
1633
+ info: 0
1634
+ };
1635
+ current.total += 1;
1636
+ current[issue5.severity] += 1;
1637
+ map.set(issue5.file, current);
1638
+ }
1639
+ return Array.from(map.values()).sort(
1640
+ (a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
1641
+ );
1642
+ }
1643
+
1644
+ // src/cli/commands/report.ts
1645
+ async function runReport(options) {
1646
+ const root = path9.resolve(options.root);
1647
+ const configResult = await loadConfig(root);
1648
+ const input = options.jsonPath ?? configResult.config.output.jsonPath;
1649
+ const inputPath = path9.isAbsolute(input) ? input : path9.resolve(root, input);
1650
+ let validation;
1651
+ try {
1652
+ validation = await readValidationResult(inputPath);
1653
+ } catch (err) {
1654
+ if (isMissingFileError(err)) {
1655
+ error(
1656
+ [
1657
+ `qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
1658
+ "",
1659
+ "\u307E\u305A validate.json \u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
1660
+ ` qfai validate --json-path ${input}`,
1661
+ "",
1662
+ "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"
1663
+ ].join("\n")
1664
+ );
1665
+ process.exitCode = 2;
1666
+ return;
1667
+ }
1668
+ throw err;
1669
+ }
1670
+ const data = await createReportData(root, validation, configResult);
1671
+ const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
1672
+ const defaultOut = options.format === "json" ? ".qfai/out/report.json" : ".qfai/out/report.md";
1673
+ const out = options.outPath ?? defaultOut;
1674
+ const outPath = path9.isAbsolute(out) ? out : path9.resolve(root, out);
1675
+ await mkdir2(path9.dirname(outPath), { recursive: true });
1676
+ await writeFile(outPath, `${output}
1677
+ `, "utf-8");
1678
+ info(
1679
+ `report: info=${validation.counts.info} warning=${validation.counts.warning} error=${validation.counts.error}`
1680
+ );
1681
+ info(`wrote report: ${outPath}`);
1682
+ }
1683
+ async function readValidationResult(inputPath) {
1684
+ const raw = await readFile8(inputPath, "utf-8");
1685
+ const parsed = JSON.parse(raw);
1686
+ if (!isValidationResult(parsed)) {
1687
+ throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
1688
+ }
1689
+ if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
1690
+ throw new Error(
1691
+ `validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
1692
+ );
1693
+ }
1694
+ return parsed;
1695
+ }
1696
+ function isValidationResult(value) {
1697
+ if (!value || typeof value !== "object") {
1698
+ return false;
1699
+ }
1700
+ const record = value;
1701
+ if (typeof record.schemaVersion !== "string") {
1702
+ return false;
1703
+ }
1704
+ if (typeof record.toolVersion !== "string") {
1705
+ return false;
1706
+ }
1707
+ if (!Array.isArray(record.issues)) {
1708
+ return false;
1709
+ }
1710
+ const counts = record.counts;
1711
+ if (!counts) {
1712
+ return false;
1713
+ }
1714
+ return typeof counts.info === "number" && typeof counts.warning === "number" && typeof counts.error === "number";
1715
+ }
1716
+ function isMissingFileError(error2) {
1717
+ if (!error2 || typeof error2 !== "object") {
1718
+ return false;
1719
+ }
1720
+ const record = error2;
1721
+ return record.code === "ENOENT";
1722
+ }
1723
+
1724
+ // src/cli/commands/validate.ts
1725
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
1726
+ import path10 from "path";
1727
+
1728
+ // src/cli/lib/failOn.ts
1729
+ function shouldFail(result, failOn) {
1730
+ if (failOn === "never") {
1731
+ return false;
1732
+ }
1733
+ if (failOn === "error") {
1734
+ return result.counts.error > 0;
1735
+ }
1736
+ return result.counts.error + result.counts.warning > 0;
1737
+ }
1738
+
1739
+ // src/cli/commands/validate.ts
1740
+ async function runValidate(options) {
1741
+ const root = path10.resolve(options.root);
1742
+ const configResult = await loadConfig(root);
1743
+ const result = await validateProject(root, configResult);
1744
+ const format = options.format ?? configResult.config.output.format;
1745
+ const explicitJsonPath = options.jsonPath;
1746
+ if (format === "text") {
1747
+ emitText(result);
1748
+ }
1749
+ if (format === "github") {
1750
+ result.issues.forEach(emitGitHub);
1751
+ }
1752
+ const shouldWriteJson = format === "json" || explicitJsonPath !== void 0;
1753
+ if (shouldWriteJson) {
1754
+ const jsonPath = format === "json" ? options.jsonPath ?? configResult.config.output.jsonPath : explicitJsonPath;
1755
+ if (jsonPath) {
1756
+ await emitJson(result, root, jsonPath);
1757
+ }
1758
+ }
1759
+ const failOn = resolveFailOn(options, configResult.config.validation.failOn);
1760
+ return shouldFail(result, failOn) ? 1 : 0;
1761
+ }
1762
+ function resolveFailOn(options, fallback) {
1763
+ if (options.failOn) {
1764
+ return options.failOn;
1765
+ }
1766
+ if (options.strict) {
1767
+ return "warning";
1768
+ }
1769
+ return fallback;
1770
+ }
1771
+ function emitText(result) {
1772
+ for (const item of result.issues) {
1773
+ const location = item.file ? ` (${item.file})` : "";
1774
+ const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
1775
+ process.stdout.write(
1776
+ `[${item.severity}] ${item.code} ${item.message}${location}${refs}
1777
+ `
1778
+ );
1779
+ }
1780
+ process.stdout.write(
1781
+ `counts: info=${result.counts.info} warning=${result.counts.warning} error=${result.counts.error}
1782
+ `
1783
+ );
1784
+ }
1785
+ function emitGitHub(issue5) {
1786
+ const level = issue5.severity === "error" ? "error" : issue5.severity === "warning" ? "warning" : "notice";
1787
+ const file = issue5.file ? `file=${issue5.file}` : "";
1788
+ const line = issue5.loc?.line ? `,line=${issue5.loc.line}` : "";
1789
+ const column = issue5.loc?.column ? `,col=${issue5.loc.column}` : "";
1790
+ const location = file ? ` ${file}${line}${column}` : "";
1791
+ process.stdout.write(
1792
+ `::${level}${location}::${issue5.code}: ${issue5.message}
1793
+ `
1794
+ );
1795
+ }
1796
+ async function emitJson(result, root, jsonPath) {
1797
+ const abs = path10.isAbsolute(jsonPath) ? jsonPath : path10.resolve(root, jsonPath);
1798
+ await mkdir3(path10.dirname(abs), { recursive: true });
1799
+ await writeFile2(abs, `${JSON.stringify(result, null, 2)}
1800
+ `, "utf-8");
1801
+ }
1802
+
1803
+ // src/cli/lib/args.ts
1804
+ function parseArgs(argv, cwd) {
1805
+ const options = {
1806
+ root: cwd,
1807
+ dir: cwd,
1808
+ force: false,
1809
+ yes: false,
1810
+ dryRun: false,
1811
+ reportFormat: "md",
1812
+ validateFormat: "text",
1813
+ strict: false,
1814
+ help: false
1815
+ };
1816
+ const args = [...argv];
1817
+ let command = args.shift() ?? null;
1818
+ if (command === "--help" || command === "-h") {
1819
+ options.help = true;
1820
+ command = null;
1821
+ }
1822
+ for (let i = 0; i < args.length; i += 1) {
1823
+ const arg = args[i];
1824
+ switch (arg) {
1825
+ case "--root":
1826
+ options.root = args[i + 1] ?? options.root;
1827
+ i += 1;
1828
+ break;
1829
+ case "--dir":
1830
+ options.dir = args[i + 1] ?? options.dir;
1831
+ i += 1;
1832
+ break;
1833
+ case "--force":
1834
+ options.force = true;
1835
+ break;
1836
+ case "--yes":
1837
+ options.yes = true;
1838
+ break;
1839
+ case "--dry-run":
1840
+ options.dryRun = true;
1841
+ break;
1842
+ case "--format": {
1843
+ const next = args[i + 1];
1844
+ applyFormatOption(command, next, options);
1845
+ i += 1;
1846
+ break;
1847
+ }
1848
+ case "--strict":
1849
+ options.strict = true;
1850
+ break;
1851
+ case "--fail-on": {
1852
+ const next = args[i + 1];
1853
+ if (next === "never" || next === "warning" || next === "error") {
1854
+ options.failOn = next;
1855
+ }
1856
+ i += 1;
1857
+ break;
1858
+ }
1859
+ case "--json-path":
1860
+ {
1861
+ const next = args[i + 1];
1862
+ if (next) {
1863
+ options.jsonPath = next;
1864
+ }
1865
+ }
1866
+ i += 1;
1867
+ break;
1868
+ case "--out":
1869
+ {
1870
+ const next = args[i + 1];
1871
+ if (next) {
1872
+ options.reportOut = next;
1873
+ }
1874
+ }
1875
+ i += 1;
1876
+ break;
1877
+ case "--help":
1878
+ case "-h":
1879
+ options.help = true;
1880
+ break;
1881
+ default:
1882
+ break;
1883
+ }
1884
+ }
1885
+ return { command, options };
1886
+ }
1887
+ function applyFormatOption(command, value, options) {
1888
+ if (!value) {
1889
+ return;
1890
+ }
1891
+ if (command === "report") {
1892
+ if (value === "md" || value === "json") {
1893
+ options.reportFormat = value;
1894
+ }
1895
+ return;
1896
+ }
1897
+ if (command === "validate") {
1898
+ if (value === "text" || value === "json" || value === "github") {
1899
+ options.validateFormat = value;
1900
+ }
1901
+ return;
1902
+ }
1903
+ if (value === "md" || value === "json") {
1904
+ options.reportFormat = value;
1905
+ }
1906
+ if (value === "text" || value === "json" || value === "github") {
1907
+ options.validateFormat = value;
1908
+ }
1909
+ }
1910
+
1911
+ // src/cli/main.ts
1912
+ async function run(argv, cwd) {
1913
+ const { command, options } = parseArgs(argv, cwd);
1914
+ if (!command || options.help) {
1915
+ info(usage());
1916
+ return;
1917
+ }
1918
+ switch (command) {
1919
+ case "init":
1920
+ await runInit({
1921
+ dir: options.dir,
1922
+ force: options.force,
1923
+ dryRun: options.dryRun,
1924
+ yes: options.yes
1925
+ });
1926
+ return;
1927
+ case "validate":
1928
+ process.exitCode = await runValidate({
1929
+ root: options.root,
1930
+ strict: options.strict,
1931
+ format: options.validateFormat,
1932
+ ...options.failOn !== void 0 ? { failOn: options.failOn } : {},
1933
+ ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {}
1934
+ });
1935
+ return;
1936
+ case "report":
1937
+ await runReport({
1938
+ root: options.root,
1939
+ format: options.reportFormat,
1940
+ ...options.jsonPath !== void 0 ? { jsonPath: options.jsonPath } : {},
1941
+ ...options.reportOut !== void 0 ? { outPath: options.reportOut } : {}
1942
+ });
1943
+ return;
1944
+ default:
1945
+ error(`Unknown command: ${command}`);
1946
+ info(usage());
1947
+ return;
1948
+ }
1949
+ }
1950
+ function usage() {
1951
+ return `qfai <command> [options]
1952
+
1953
+ Commands:
1954
+ init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
1955
+ validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
1956
+ report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
1957
+
1958
+ Options:
1959
+ --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
1960
+ --dir <path> init \u306E\u51FA\u529B\u5148
1961
+ --force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
1962
+ --yes init: \u975E\u5BFE\u8A71\u3067\u30C7\u30D5\u30A9\u30EB\u30C8\u3092\u63A1\u7528\uFF08\u73FE\u5728\u306F\u975E\u5BFE\u8A71\u304C\u65E2\u5B9A\u3001\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u3082\u81EA\u52D5Yes\uFF09
1963
+ --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
1964
+ --format <text|json|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
1965
+ --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
1966
+ --strict validate: warning \u4EE5\u4E0A\u3067 exit 1
1967
+ --fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
1968
+ --json-path <path> validate: JSON \u51FA\u529B\u5148 / report: validate JSON \u5165\u529B
1969
+ --out <path> report: \u51FA\u529B\u5148
1970
+ -h, --help \u30D8\u30EB\u30D7\u8868\u793A
1971
+ `;
1972
+ }
1973
+
1974
+ // src/cli/index.ts
1975
+ run(process.argv.slice(2), process.cwd()).catch((err) => {
1976
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
1977
+ `);
1978
+ process.exitCode = 1;
1979
+ });
1980
+ //# sourceMappingURL=index.mjs.map