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.
- package/README.md +28 -0
- package/assets/init/qfai/README.md +6 -0
- package/assets/init/qfai/contracts/api/api-0001-sample.yaml +14 -0
- package/assets/init/qfai/contracts/db/db-0001-sample.sql +5 -0
- package/assets/init/qfai/contracts/ui/ui-0001-sample.yaml +4 -0
- package/assets/init/qfai/prompts/makeBusinessFlow.md +34 -0
- package/assets/init/qfai/prompts/makeOverview.md +27 -0
- package/assets/init/qfai/spec/decisions/ADR-0001.md +7 -0
- package/assets/init/qfai/spec/scenarios.feature +6 -0
- package/assets/init/qfai/spec/spec-0001-sample.md +29 -0
- package/assets/init/root/.github/workflows/qfai.yml +22 -0
- package/assets/init/root/qfai.config.yaml +29 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +30 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/report.d.ts +8 -0
- package/dist/cli/commands/report.d.ts.map +1 -0
- package/dist/cli/commands/report.js +83 -0
- package/dist/cli/commands/report.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +10 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +66 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.cjs +2003 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +1980 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/cli/lib/args.d.ts +19 -0
- package/dist/cli/lib/args.d.ts.map +1 -0
- package/dist/cli/lib/args.js +107 -0
- package/dist/cli/lib/args.js.map +1 -0
- package/dist/cli/lib/assets.d.ts +2 -0
- package/dist/cli/lib/assets.d.ts.map +1 -0
- package/dist/cli/lib/assets.js +8 -0
- package/dist/cli/lib/assets.js.map +1 -0
- package/dist/cli/lib/failOn.d.ts +5 -0
- package/dist/cli/lib/failOn.d.ts.map +1 -0
- package/dist/cli/lib/failOn.js +10 -0
- package/dist/cli/lib/failOn.js.map +1 -0
- package/dist/cli/lib/fs.d.ts +11 -0
- package/dist/cli/lib/fs.d.ts.map +1 -0
- package/dist/cli/lib/fs.js +91 -0
- package/dist/cli/lib/fs.js.map +1 -0
- package/dist/cli/lib/logger.d.ts +4 -0
- package/dist/cli/lib/logger.d.ts.map +1 -0
- package/dist/cli/lib/logger.js +10 -0
- package/dist/cli/lib/logger.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core/config.d.ts +46 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +224 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/discovery.d.ts +11 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +31 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/fs.d.ts +6 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +55 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/ids.d.ts +5 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +49 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/report.d.ts +41 -0
- package/dist/core/report.d.ts.map +1 -0
- package/dist/core/report.js +238 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +4 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +32 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/core/validators/contracts.d.ts +5 -0
- package/dist/core/validators/contracts.d.ts.map +1 -0
- package/dist/core/validators/contracts.js +157 -0
- package/dist/core/validators/contracts.js.map +1 -0
- package/dist/core/validators/scenario.d.ts +5 -0
- package/dist/core/validators/scenario.d.ts.map +1 -0
- package/dist/core/validators/scenario.js +82 -0
- package/dist/core/validators/scenario.js.map +1 -0
- package/dist/core/validators/spec.d.ts +5 -0
- package/dist/core/validators/spec.d.ts.map +1 -0
- package/dist/core/validators/spec.js +69 -0
- package/dist/core/validators/spec.js.map +1 -0
- package/dist/core/validators/traceability.d.ts +4 -0
- package/dist/core/validators/traceability.d.ts.map +1 -0
- package/dist/core/validators/traceability.js +148 -0
- package/dist/core/validators/traceability.js.map +1 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +25 -0
- package/dist/core/version.js.map +1 -0
- package/dist/index.cjs +1579 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +132 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1523 -0
- package/dist/index.mjs.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- 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
|