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