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
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
var defaultConfig = {
|
|
6
|
+
paths: {
|
|
7
|
+
specDir: "qfai/spec",
|
|
8
|
+
decisionsDir: "qfai/spec/decisions",
|
|
9
|
+
scenariosDir: "qfai/spec",
|
|
10
|
+
rulesDir: "qfai/rules",
|
|
11
|
+
contractsDir: "qfai/contracts",
|
|
12
|
+
uiContractsDir: "qfai/contracts/ui",
|
|
13
|
+
apiContractsDir: "qfai/contracts/api",
|
|
14
|
+
dataContractsDir: "qfai/contracts/db",
|
|
15
|
+
srcDir: "src",
|
|
16
|
+
testsDir: "tests"
|
|
17
|
+
},
|
|
18
|
+
validation: {
|
|
19
|
+
failOn: "error",
|
|
20
|
+
require: {
|
|
21
|
+
specSections: [
|
|
22
|
+
"\u80CC\u666F",
|
|
23
|
+
"\u30B9\u30B3\u30FC\u30D7",
|
|
24
|
+
"\u975E\u30B4\u30FC\u30EB",
|
|
25
|
+
"\u7528\u8A9E",
|
|
26
|
+
"\u524D\u63D0",
|
|
27
|
+
"\u6C7A\u5B9A\u4E8B\u9805",
|
|
28
|
+
"\u696D\u52D9\u30EB\u30FC\u30EB"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
traceability: {
|
|
32
|
+
brMustHaveSc: true,
|
|
33
|
+
scMustTouchContracts: true,
|
|
34
|
+
allowOrphanContracts: false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
output: {
|
|
38
|
+
format: "text",
|
|
39
|
+
jsonPath: ".qfai/out/validate.json"
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function getConfigPath(root) {
|
|
43
|
+
return path.join(root, "qfai.config.yaml");
|
|
44
|
+
}
|
|
45
|
+
async function loadConfig(root) {
|
|
46
|
+
const configPath = getConfigPath(root);
|
|
47
|
+
const issues = [];
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(configPath, "utf-8");
|
|
51
|
+
parsed = parseYaml(raw);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (isMissingFile(error)) {
|
|
54
|
+
return { config: defaultConfig, issues, configPath };
|
|
55
|
+
}
|
|
56
|
+
issues.push(configIssue(configPath, formatError(error)));
|
|
57
|
+
return { config: defaultConfig, issues, configPath };
|
|
58
|
+
}
|
|
59
|
+
const normalized = normalizeConfig(parsed, configPath, issues);
|
|
60
|
+
return { config: normalized, issues, configPath };
|
|
61
|
+
}
|
|
62
|
+
function resolvePath(root, config, key) {
|
|
63
|
+
return path.resolve(root, config.paths[key]);
|
|
64
|
+
}
|
|
65
|
+
function normalizeConfig(raw, configPath, issues) {
|
|
66
|
+
if (!isRecord(raw)) {
|
|
67
|
+
issues.push(configIssue(configPath, "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059\u3002"));
|
|
68
|
+
return defaultConfig;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
paths: normalizePaths(raw.paths, configPath, issues),
|
|
72
|
+
validation: normalizeValidation(raw.validation, configPath, issues),
|
|
73
|
+
output: normalizeOutput(raw.output, configPath, issues)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizePaths(raw, configPath, issues) {
|
|
77
|
+
const base = defaultConfig.paths;
|
|
78
|
+
if (!raw) {
|
|
79
|
+
return base;
|
|
80
|
+
}
|
|
81
|
+
if (!isRecord(raw)) {
|
|
82
|
+
issues.push(
|
|
83
|
+
configIssue(configPath, "paths \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
84
|
+
);
|
|
85
|
+
return base;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
specDir: readString(
|
|
89
|
+
raw.specDir,
|
|
90
|
+
base.specDir,
|
|
91
|
+
"paths.specDir",
|
|
92
|
+
configPath,
|
|
93
|
+
issues
|
|
94
|
+
),
|
|
95
|
+
decisionsDir: readString(
|
|
96
|
+
raw.decisionsDir,
|
|
97
|
+
base.decisionsDir,
|
|
98
|
+
"paths.decisionsDir",
|
|
99
|
+
configPath,
|
|
100
|
+
issues
|
|
101
|
+
),
|
|
102
|
+
scenariosDir: readString(
|
|
103
|
+
raw.scenariosDir,
|
|
104
|
+
base.scenariosDir,
|
|
105
|
+
"paths.scenariosDir",
|
|
106
|
+
configPath,
|
|
107
|
+
issues
|
|
108
|
+
),
|
|
109
|
+
rulesDir: readString(
|
|
110
|
+
raw.rulesDir,
|
|
111
|
+
base.rulesDir,
|
|
112
|
+
"paths.rulesDir",
|
|
113
|
+
configPath,
|
|
114
|
+
issues
|
|
115
|
+
),
|
|
116
|
+
contractsDir: readString(
|
|
117
|
+
raw.contractsDir,
|
|
118
|
+
base.contractsDir,
|
|
119
|
+
"paths.contractsDir",
|
|
120
|
+
configPath,
|
|
121
|
+
issues
|
|
122
|
+
),
|
|
123
|
+
uiContractsDir: readString(
|
|
124
|
+
raw.uiContractsDir,
|
|
125
|
+
base.uiContractsDir,
|
|
126
|
+
"paths.uiContractsDir",
|
|
127
|
+
configPath,
|
|
128
|
+
issues
|
|
129
|
+
),
|
|
130
|
+
apiContractsDir: readString(
|
|
131
|
+
raw.apiContractsDir,
|
|
132
|
+
base.apiContractsDir,
|
|
133
|
+
"paths.apiContractsDir",
|
|
134
|
+
configPath,
|
|
135
|
+
issues
|
|
136
|
+
),
|
|
137
|
+
dataContractsDir: readString(
|
|
138
|
+
raw.dataContractsDir,
|
|
139
|
+
base.dataContractsDir,
|
|
140
|
+
"paths.dataContractsDir",
|
|
141
|
+
configPath,
|
|
142
|
+
issues
|
|
143
|
+
),
|
|
144
|
+
srcDir: readString(
|
|
145
|
+
raw.srcDir,
|
|
146
|
+
base.srcDir,
|
|
147
|
+
"paths.srcDir",
|
|
148
|
+
configPath,
|
|
149
|
+
issues
|
|
150
|
+
),
|
|
151
|
+
testsDir: readString(
|
|
152
|
+
raw.testsDir,
|
|
153
|
+
base.testsDir,
|
|
154
|
+
"paths.testsDir",
|
|
155
|
+
configPath,
|
|
156
|
+
issues
|
|
157
|
+
)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function normalizeValidation(raw, configPath, issues) {
|
|
161
|
+
const base = defaultConfig.validation;
|
|
162
|
+
if (!raw) {
|
|
163
|
+
return base;
|
|
164
|
+
}
|
|
165
|
+
if (!isRecord(raw)) {
|
|
166
|
+
issues.push(
|
|
167
|
+
configIssue(
|
|
168
|
+
configPath,
|
|
169
|
+
"validation \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
return base;
|
|
173
|
+
}
|
|
174
|
+
let requireRaw;
|
|
175
|
+
if (raw.require === void 0) {
|
|
176
|
+
requireRaw = void 0;
|
|
177
|
+
} else if (isRecord(raw.require)) {
|
|
178
|
+
requireRaw = raw.require;
|
|
179
|
+
} else {
|
|
180
|
+
issues.push(
|
|
181
|
+
configIssue(
|
|
182
|
+
configPath,
|
|
183
|
+
"validation.require \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
requireRaw = void 0;
|
|
187
|
+
}
|
|
188
|
+
let traceabilityRaw;
|
|
189
|
+
if (raw.traceability === void 0) {
|
|
190
|
+
traceabilityRaw = void 0;
|
|
191
|
+
} else if (isRecord(raw.traceability)) {
|
|
192
|
+
traceabilityRaw = raw.traceability;
|
|
193
|
+
} else {
|
|
194
|
+
issues.push(
|
|
195
|
+
configIssue(
|
|
196
|
+
configPath,
|
|
197
|
+
"validation.traceability \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
traceabilityRaw = void 0;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
failOn: readFailOn(
|
|
204
|
+
raw.failOn,
|
|
205
|
+
base.failOn,
|
|
206
|
+
"validation.failOn",
|
|
207
|
+
configPath,
|
|
208
|
+
issues
|
|
209
|
+
),
|
|
210
|
+
require: {
|
|
211
|
+
specSections: readStringArray(
|
|
212
|
+
requireRaw?.specSections,
|
|
213
|
+
base.require.specSections,
|
|
214
|
+
"validation.require.specSections",
|
|
215
|
+
configPath,
|
|
216
|
+
issues
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
traceability: {
|
|
220
|
+
brMustHaveSc: readBoolean(
|
|
221
|
+
traceabilityRaw?.brMustHaveSc,
|
|
222
|
+
base.traceability.brMustHaveSc,
|
|
223
|
+
"validation.traceability.brMustHaveSc",
|
|
224
|
+
configPath,
|
|
225
|
+
issues
|
|
226
|
+
),
|
|
227
|
+
scMustTouchContracts: readBoolean(
|
|
228
|
+
traceabilityRaw?.scMustTouchContracts,
|
|
229
|
+
base.traceability.scMustTouchContracts,
|
|
230
|
+
"validation.traceability.scMustTouchContracts",
|
|
231
|
+
configPath,
|
|
232
|
+
issues
|
|
233
|
+
),
|
|
234
|
+
allowOrphanContracts: readBoolean(
|
|
235
|
+
traceabilityRaw?.allowOrphanContracts,
|
|
236
|
+
base.traceability.allowOrphanContracts,
|
|
237
|
+
"validation.traceability.allowOrphanContracts",
|
|
238
|
+
configPath,
|
|
239
|
+
issues
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function normalizeOutput(raw, configPath, issues) {
|
|
245
|
+
const base = defaultConfig.output;
|
|
246
|
+
if (!raw) {
|
|
247
|
+
return base;
|
|
248
|
+
}
|
|
249
|
+
if (!isRecord(raw)) {
|
|
250
|
+
issues.push(
|
|
251
|
+
configIssue(configPath, "output \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
252
|
+
);
|
|
253
|
+
return base;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
format: readOutputFormat(
|
|
257
|
+
raw.format,
|
|
258
|
+
base.format,
|
|
259
|
+
"output.format",
|
|
260
|
+
configPath,
|
|
261
|
+
issues
|
|
262
|
+
),
|
|
263
|
+
jsonPath: readString(
|
|
264
|
+
raw.jsonPath,
|
|
265
|
+
base.jsonPath,
|
|
266
|
+
"output.jsonPath",
|
|
267
|
+
configPath,
|
|
268
|
+
issues
|
|
269
|
+
)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function readString(value, fallback, label, configPath, issues) {
|
|
273
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
if (value !== void 0) {
|
|
277
|
+
issues.push(
|
|
278
|
+
configIssue(configPath, `${label} \u306F\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return fallback;
|
|
282
|
+
}
|
|
283
|
+
function readStringArray(value, fallback, label, configPath, issues) {
|
|
284
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
if (value !== void 0) {
|
|
288
|
+
issues.push(
|
|
289
|
+
configIssue(configPath, `${label} \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return fallback;
|
|
293
|
+
}
|
|
294
|
+
function readBoolean(value, fallback, label, configPath, issues) {
|
|
295
|
+
if (typeof value === "boolean") {
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
if (value !== void 0) {
|
|
299
|
+
issues.push(
|
|
300
|
+
configIssue(configPath, `${label} \u306F\u771F\u507D\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return fallback;
|
|
304
|
+
}
|
|
305
|
+
function readFailOn(value, fallback, label, configPath, issues) {
|
|
306
|
+
if (value === "never" || value === "warning" || value === "error") {
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
if (value !== void 0) {
|
|
310
|
+
issues.push(
|
|
311
|
+
configIssue(
|
|
312
|
+
configPath,
|
|
313
|
+
`${label} \u306F never|warning|error \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return fallback;
|
|
318
|
+
}
|
|
319
|
+
function readOutputFormat(value, fallback, label, configPath, issues) {
|
|
320
|
+
if (value === "text" || value === "json" || value === "github") {
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
if (value !== void 0) {
|
|
324
|
+
issues.push(
|
|
325
|
+
configIssue(
|
|
326
|
+
configPath,
|
|
327
|
+
`${label} \u306F text|json|github \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return fallback;
|
|
332
|
+
}
|
|
333
|
+
function configIssue(file, message) {
|
|
334
|
+
return {
|
|
335
|
+
code: "QFAI_CONFIG_INVALID",
|
|
336
|
+
severity: "error",
|
|
337
|
+
message,
|
|
338
|
+
file,
|
|
339
|
+
rule: "config.invalid"
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function isMissingFile(error) {
|
|
343
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
344
|
+
return error.code === "ENOENT";
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
function formatError(error) {
|
|
349
|
+
if (error instanceof Error) {
|
|
350
|
+
return error.message;
|
|
351
|
+
}
|
|
352
|
+
return String(error);
|
|
353
|
+
}
|
|
354
|
+
function isRecord(value) {
|
|
355
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/core/ids.ts
|
|
359
|
+
var ID_PATTERNS = {
|
|
360
|
+
SPEC: /\bSPEC-[A-Z0-9-]+\b/g,
|
|
361
|
+
BR: /\bBR-[A-Z0-9-]+\b/g,
|
|
362
|
+
SC: /\bSC-[A-Z0-9-]+\b/g,
|
|
363
|
+
UI: /\bUI-[A-Z0-9-]+\b/g,
|
|
364
|
+
API: /\bAPI-[A-Z0-9-]+\b/g,
|
|
365
|
+
DATA: /\bDATA-[A-Z0-9-]+\b/g
|
|
366
|
+
};
|
|
367
|
+
var LOOSE_ID_PATTERNS = {
|
|
368
|
+
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
369
|
+
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
370
|
+
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
371
|
+
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
372
|
+
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
373
|
+
DATA: /\bDATA-[A-Za-z0-9_-]+\b/gi
|
|
374
|
+
};
|
|
375
|
+
function extractIds(text, prefix) {
|
|
376
|
+
const pattern = ID_PATTERNS[prefix];
|
|
377
|
+
const matches = text.match(pattern);
|
|
378
|
+
return unique(matches ?? []);
|
|
379
|
+
}
|
|
380
|
+
function extractAllIds(text) {
|
|
381
|
+
const all = [];
|
|
382
|
+
Object.keys(ID_PATTERNS).forEach((prefix) => {
|
|
383
|
+
all.push(...extractIds(text, prefix));
|
|
384
|
+
});
|
|
385
|
+
return unique(all);
|
|
386
|
+
}
|
|
387
|
+
function extractInvalidIds(text, prefixes) {
|
|
388
|
+
const invalid = [];
|
|
389
|
+
for (const prefix of prefixes) {
|
|
390
|
+
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
391
|
+
for (const candidate of candidates) {
|
|
392
|
+
if (!isValidId(candidate, prefix)) {
|
|
393
|
+
invalid.push(candidate);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return unique(invalid);
|
|
398
|
+
}
|
|
399
|
+
function unique(values) {
|
|
400
|
+
return Array.from(new Set(values));
|
|
401
|
+
}
|
|
402
|
+
function isValidId(value, prefix) {
|
|
403
|
+
const pattern = ID_PATTERNS[prefix];
|
|
404
|
+
const strict = new RegExp(pattern.source);
|
|
405
|
+
return strict.test(value);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/core/report.ts
|
|
409
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
410
|
+
|
|
411
|
+
// src/core/discovery.ts
|
|
412
|
+
import path3 from "path";
|
|
413
|
+
|
|
414
|
+
// src/core/fs.ts
|
|
415
|
+
import { access, readdir } from "fs/promises";
|
|
416
|
+
import path2 from "path";
|
|
417
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
418
|
+
"node_modules",
|
|
419
|
+
".git",
|
|
420
|
+
"dist",
|
|
421
|
+
".pnpm",
|
|
422
|
+
"tmp",
|
|
423
|
+
".mcp-tools"
|
|
424
|
+
]);
|
|
425
|
+
async function collectFiles(root, options = {}) {
|
|
426
|
+
const entries = [];
|
|
427
|
+
if (!await exists(root)) {
|
|
428
|
+
return entries;
|
|
429
|
+
}
|
|
430
|
+
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
431
|
+
...DEFAULT_IGNORE_DIRS,
|
|
432
|
+
...options.ignoreDirs ?? []
|
|
433
|
+
]);
|
|
434
|
+
const extensions = options.extensions?.map((ext) => ext.toLowerCase()) ?? [];
|
|
435
|
+
await walk(root, root, ignoreDirs, extensions, entries);
|
|
436
|
+
return entries;
|
|
437
|
+
}
|
|
438
|
+
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
439
|
+
const items = await readdir(current, { withFileTypes: true });
|
|
440
|
+
for (const item of items) {
|
|
441
|
+
const fullPath = path2.join(current, item.name);
|
|
442
|
+
if (item.isDirectory()) {
|
|
443
|
+
if (ignoreDirs.has(item.name)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
await walk(base, fullPath, ignoreDirs, extensions, out);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (item.isFile()) {
|
|
450
|
+
if (extensions.length > 0) {
|
|
451
|
+
const ext = path2.extname(item.name).toLowerCase();
|
|
452
|
+
if (!extensions.includes(ext)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
out.push(fullPath);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function exists(target) {
|
|
461
|
+
try {
|
|
462
|
+
await access(target);
|
|
463
|
+
return true;
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/core/discovery.ts
|
|
470
|
+
var LEGACY_SPEC_NAME = "spec.md";
|
|
471
|
+
var SPEC_NAMED_PATTERN = /^spec-\d{4}-[^/\\]+\.md$/i;
|
|
472
|
+
async function collectSpecFiles(specRoot) {
|
|
473
|
+
const files = await collectFiles(specRoot, { extensions: [".md"] });
|
|
474
|
+
return files.filter((file) => isSpecFile(file));
|
|
475
|
+
}
|
|
476
|
+
async function collectUiContractFiles(uiRoot) {
|
|
477
|
+
return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
478
|
+
}
|
|
479
|
+
async function collectApiContractFiles(apiRoot) {
|
|
480
|
+
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
481
|
+
}
|
|
482
|
+
async function collectDataContractFiles(dataRoot) {
|
|
483
|
+
return collectFiles(dataRoot, { extensions: [".sql"] });
|
|
484
|
+
}
|
|
485
|
+
async function collectContractFiles(uiRoot, apiRoot, dataRoot) {
|
|
486
|
+
const [ui, api, db] = await Promise.all([
|
|
487
|
+
collectUiContractFiles(uiRoot),
|
|
488
|
+
collectApiContractFiles(apiRoot),
|
|
489
|
+
collectDataContractFiles(dataRoot)
|
|
490
|
+
]);
|
|
491
|
+
return { ui, api, db };
|
|
492
|
+
}
|
|
493
|
+
function isSpecFile(filePath) {
|
|
494
|
+
const name = path3.basename(filePath).toLowerCase();
|
|
495
|
+
return name === LEGACY_SPEC_NAME || SPEC_NAMED_PATTERN.test(name);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/core/types.ts
|
|
499
|
+
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
500
|
+
|
|
501
|
+
// src/core/version.ts
|
|
502
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
503
|
+
import path4 from "path";
|
|
504
|
+
import { fileURLToPath } from "url";
|
|
505
|
+
async function resolveToolVersion() {
|
|
506
|
+
if ("0.2.5".length > 0) {
|
|
507
|
+
return "0.2.5";
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const packagePath = resolvePackageJsonPath();
|
|
511
|
+
const raw = await readFile2(packagePath, "utf-8");
|
|
512
|
+
const parsed = JSON.parse(raw);
|
|
513
|
+
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
514
|
+
return version.length > 0 ? version : "unknown";
|
|
515
|
+
} catch {
|
|
516
|
+
return "unknown";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function resolvePackageJsonPath() {
|
|
520
|
+
const base = import.meta.url;
|
|
521
|
+
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
522
|
+
return path4.resolve(path4.dirname(basePath), "../../package.json");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/core/validators/contracts.ts
|
|
526
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
527
|
+
import path5 from "path";
|
|
528
|
+
import { parse as parseYaml2 } from "yaml";
|
|
529
|
+
var SQL_DANGEROUS_PATTERNS = [
|
|
530
|
+
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
531
|
+
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
532
|
+
{ pattern: /\bTRUNCATE\b/i, label: "TRUNCATE" },
|
|
533
|
+
{
|
|
534
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
|
|
535
|
+
label: "ALTER TABLE ... DROP"
|
|
536
|
+
}
|
|
537
|
+
];
|
|
538
|
+
async function validateContracts(root, config) {
|
|
539
|
+
const issues = [];
|
|
540
|
+
issues.push(
|
|
541
|
+
...await validateUiContracts(resolvePath(root, config, "uiContractsDir"))
|
|
542
|
+
);
|
|
543
|
+
issues.push(
|
|
544
|
+
...await validateApiContracts(
|
|
545
|
+
resolvePath(root, config, "apiContractsDir")
|
|
546
|
+
)
|
|
547
|
+
);
|
|
548
|
+
issues.push(
|
|
549
|
+
...await validateDataContracts(
|
|
550
|
+
resolvePath(root, config, "dataContractsDir")
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
return issues;
|
|
554
|
+
}
|
|
555
|
+
async function validateUiContracts(uiRoot) {
|
|
556
|
+
const files = await collectUiContractFiles(uiRoot);
|
|
557
|
+
if (files.length === 0) {
|
|
558
|
+
return [
|
|
559
|
+
issue(
|
|
560
|
+
"QFAI-UI-000",
|
|
561
|
+
"UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
562
|
+
"info",
|
|
563
|
+
uiRoot,
|
|
564
|
+
"contracts.ui.files"
|
|
565
|
+
)
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
const issues = [];
|
|
569
|
+
for (const file of files) {
|
|
570
|
+
const text = await readFile3(file, "utf-8");
|
|
571
|
+
const invalidIds = extractInvalidIds(text, [
|
|
572
|
+
"SPEC",
|
|
573
|
+
"BR",
|
|
574
|
+
"SC",
|
|
575
|
+
"UI",
|
|
576
|
+
"API",
|
|
577
|
+
"DATA"
|
|
578
|
+
]);
|
|
579
|
+
if (invalidIds.length > 0) {
|
|
580
|
+
issues.push(
|
|
581
|
+
issue(
|
|
582
|
+
"QFAI_ID_INVALID_FORMAT",
|
|
583
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
584
|
+
"error",
|
|
585
|
+
file,
|
|
586
|
+
"id.format",
|
|
587
|
+
invalidIds
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
const doc = parseYaml2(text);
|
|
593
|
+
const id = typeof doc.id === "string" ? doc.id : "";
|
|
594
|
+
if (!id.startsWith("UI-")) {
|
|
595
|
+
issues.push(
|
|
596
|
+
issue(
|
|
597
|
+
"QFAI-UI-001",
|
|
598
|
+
"UI \u5951\u7D04\u306E id \u306F UI- \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
599
|
+
"error",
|
|
600
|
+
file,
|
|
601
|
+
"contracts.ui.id"
|
|
602
|
+
)
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
} catch (error) {
|
|
606
|
+
issues.push(
|
|
607
|
+
issue(
|
|
608
|
+
"QFAI-UI-002",
|
|
609
|
+
`UI YAML \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error)}`,
|
|
610
|
+
"error",
|
|
611
|
+
file,
|
|
612
|
+
"contracts.ui.parse"
|
|
613
|
+
)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return issues;
|
|
618
|
+
}
|
|
619
|
+
async function validateApiContracts(apiRoot) {
|
|
620
|
+
const files = await collectApiContractFiles(apiRoot);
|
|
621
|
+
if (files.length === 0) {
|
|
622
|
+
return [
|
|
623
|
+
issue(
|
|
624
|
+
"QFAI-API-000",
|
|
625
|
+
"API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
626
|
+
"info",
|
|
627
|
+
apiRoot,
|
|
628
|
+
"contracts.api.files"
|
|
629
|
+
)
|
|
630
|
+
];
|
|
631
|
+
}
|
|
632
|
+
const issues = [];
|
|
633
|
+
for (const file of files) {
|
|
634
|
+
const text = await readFile3(file, "utf-8");
|
|
635
|
+
const invalidIds = extractInvalidIds(text, [
|
|
636
|
+
"SPEC",
|
|
637
|
+
"BR",
|
|
638
|
+
"SC",
|
|
639
|
+
"UI",
|
|
640
|
+
"API",
|
|
641
|
+
"DATA"
|
|
642
|
+
]);
|
|
643
|
+
if (invalidIds.length > 0) {
|
|
644
|
+
issues.push(
|
|
645
|
+
issue(
|
|
646
|
+
"QFAI_ID_INVALID_FORMAT",
|
|
647
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
648
|
+
"error",
|
|
649
|
+
file,
|
|
650
|
+
"id.format",
|
|
651
|
+
invalidIds
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const doc = parseStructured(file, text);
|
|
657
|
+
if (!doc || !hasOpenApi(doc)) {
|
|
658
|
+
issues.push(
|
|
659
|
+
issue(
|
|
660
|
+
"QFAI-API-001",
|
|
661
|
+
"OpenAPI \u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
662
|
+
"error",
|
|
663
|
+
file,
|
|
664
|
+
"contracts.api.openapi"
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
} catch (error) {
|
|
669
|
+
issues.push(
|
|
670
|
+
issue(
|
|
671
|
+
"QFAI-API-002",
|
|
672
|
+
`API \u5B9A\u7FA9\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError2(error)}`,
|
|
673
|
+
"error",
|
|
674
|
+
file,
|
|
675
|
+
"contracts.api.parse"
|
|
676
|
+
)
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return issues;
|
|
681
|
+
}
|
|
682
|
+
async function validateDataContracts(dataRoot) {
|
|
683
|
+
const files = await collectDataContractFiles(dataRoot);
|
|
684
|
+
if (files.length === 0) {
|
|
685
|
+
return [
|
|
686
|
+
issue(
|
|
687
|
+
"QFAI-DATA-000",
|
|
688
|
+
"DATA \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
689
|
+
"info",
|
|
690
|
+
dataRoot,
|
|
691
|
+
"contracts.data.files"
|
|
692
|
+
)
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
const issues = [];
|
|
696
|
+
for (const file of files) {
|
|
697
|
+
const text = await readFile3(file, "utf-8");
|
|
698
|
+
const invalidIds = extractInvalidIds(text, [
|
|
699
|
+
"SPEC",
|
|
700
|
+
"BR",
|
|
701
|
+
"SC",
|
|
702
|
+
"UI",
|
|
703
|
+
"API",
|
|
704
|
+
"DATA"
|
|
705
|
+
]);
|
|
706
|
+
if (invalidIds.length > 0) {
|
|
707
|
+
issues.push(
|
|
708
|
+
issue(
|
|
709
|
+
"QFAI_ID_INVALID_FORMAT",
|
|
710
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
711
|
+
"error",
|
|
712
|
+
file,
|
|
713
|
+
"id.format",
|
|
714
|
+
invalidIds
|
|
715
|
+
)
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
issues.push(...lintSql(text, file));
|
|
719
|
+
}
|
|
720
|
+
return issues;
|
|
721
|
+
}
|
|
722
|
+
function lintSql(text, file) {
|
|
723
|
+
const issues = [];
|
|
724
|
+
for (const { pattern, label } of SQL_DANGEROUS_PATTERNS) {
|
|
725
|
+
if (pattern.test(text)) {
|
|
726
|
+
issues.push(
|
|
727
|
+
issue(
|
|
728
|
+
"QFAI-DATA-001",
|
|
729
|
+
`\u5371\u967A\u306A SQL \u64CD\u4F5C\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059: ${label}`,
|
|
730
|
+
"warning",
|
|
731
|
+
file,
|
|
732
|
+
"contracts.data.sql"
|
|
733
|
+
)
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return issues;
|
|
738
|
+
}
|
|
739
|
+
function parseStructured(file, text) {
|
|
740
|
+
const ext = path5.extname(file).toLowerCase();
|
|
741
|
+
if (ext === ".json") {
|
|
742
|
+
return JSON.parse(text);
|
|
743
|
+
}
|
|
744
|
+
return parseYaml2(text);
|
|
745
|
+
}
|
|
746
|
+
function hasOpenApi(doc) {
|
|
747
|
+
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
748
|
+
}
|
|
749
|
+
function formatError2(error) {
|
|
750
|
+
if (error instanceof Error) {
|
|
751
|
+
return error.message;
|
|
752
|
+
}
|
|
753
|
+
return String(error);
|
|
754
|
+
}
|
|
755
|
+
function issue(code, message, severity, file, rule, refs) {
|
|
756
|
+
const issue5 = {
|
|
757
|
+
code,
|
|
758
|
+
severity,
|
|
759
|
+
message
|
|
760
|
+
};
|
|
761
|
+
if (file) {
|
|
762
|
+
issue5.file = file;
|
|
763
|
+
}
|
|
764
|
+
if (rule) {
|
|
765
|
+
issue5.rule = rule;
|
|
766
|
+
}
|
|
767
|
+
if (refs && refs.length > 0) {
|
|
768
|
+
issue5.refs = refs;
|
|
769
|
+
}
|
|
770
|
+
return issue5;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/core/validators/scenario.ts
|
|
774
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
775
|
+
var GIVEN_PATTERN = /\bGiven\b/;
|
|
776
|
+
var WHEN_PATTERN = /\bWhen\b/;
|
|
777
|
+
var THEN_PATTERN = /\bThen\b/;
|
|
778
|
+
async function validateScenarios(root, config) {
|
|
779
|
+
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
780
|
+
const files = await collectFiles(scenariosRoot, {
|
|
781
|
+
extensions: [".feature"]
|
|
782
|
+
});
|
|
783
|
+
if (files.length === 0) {
|
|
784
|
+
return [
|
|
785
|
+
issue2(
|
|
786
|
+
"QFAI-SC-000",
|
|
787
|
+
"Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
788
|
+
"info",
|
|
789
|
+
scenariosRoot,
|
|
790
|
+
"scenario.files"
|
|
791
|
+
)
|
|
792
|
+
];
|
|
793
|
+
}
|
|
794
|
+
const issues = [];
|
|
795
|
+
for (const file of files) {
|
|
796
|
+
const text = await readFile4(file, "utf-8");
|
|
797
|
+
issues.push(...validateScenarioContent(text, file));
|
|
798
|
+
}
|
|
799
|
+
return issues;
|
|
800
|
+
}
|
|
801
|
+
function validateScenarioContent(text, file) {
|
|
802
|
+
const issues = [];
|
|
803
|
+
const invalidIds = extractInvalidIds(text, [
|
|
804
|
+
"SPEC",
|
|
805
|
+
"BR",
|
|
806
|
+
"SC",
|
|
807
|
+
"UI",
|
|
808
|
+
"API",
|
|
809
|
+
"DATA"
|
|
810
|
+
]);
|
|
811
|
+
if (invalidIds.length > 0) {
|
|
812
|
+
issues.push(
|
|
813
|
+
issue2(
|
|
814
|
+
"QFAI_ID_INVALID_FORMAT",
|
|
815
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
816
|
+
"error",
|
|
817
|
+
file,
|
|
818
|
+
"id.format",
|
|
819
|
+
invalidIds
|
|
820
|
+
)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const scIds = extractIds(text, "SC");
|
|
824
|
+
if (scIds.length === 0) {
|
|
825
|
+
issues.push(
|
|
826
|
+
issue2(
|
|
827
|
+
"QFAI-SC-001",
|
|
828
|
+
"SC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
829
|
+
"error",
|
|
830
|
+
file,
|
|
831
|
+
"scenario.id"
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const specIds = extractIds(text, "SPEC");
|
|
836
|
+
if (specIds.length === 0) {
|
|
837
|
+
issues.push(
|
|
838
|
+
issue2(
|
|
839
|
+
"QFAI-SC-002",
|
|
840
|
+
"SC \u306F SPEC \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
841
|
+
"error",
|
|
842
|
+
file,
|
|
843
|
+
"scenario.spec"
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const brIds = extractIds(text, "BR");
|
|
848
|
+
if (brIds.length === 0) {
|
|
849
|
+
issues.push(
|
|
850
|
+
issue2(
|
|
851
|
+
"QFAI-SC-003",
|
|
852
|
+
"SC \u306F BR \u3092\u53C2\u7167\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002",
|
|
853
|
+
"error",
|
|
854
|
+
file,
|
|
855
|
+
"scenario.br"
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const missingSteps = [];
|
|
860
|
+
if (!GIVEN_PATTERN.test(text)) {
|
|
861
|
+
missingSteps.push("Given");
|
|
862
|
+
}
|
|
863
|
+
if (!WHEN_PATTERN.test(text)) {
|
|
864
|
+
missingSteps.push("When");
|
|
865
|
+
}
|
|
866
|
+
if (!THEN_PATTERN.test(text)) {
|
|
867
|
+
missingSteps.push("Then");
|
|
868
|
+
}
|
|
869
|
+
if (missingSteps.length > 0) {
|
|
870
|
+
issues.push(
|
|
871
|
+
issue2(
|
|
872
|
+
"QFAI-SC-005",
|
|
873
|
+
`Given/When/Then \u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${missingSteps.join(", ")}`,
|
|
874
|
+
"warning",
|
|
875
|
+
file,
|
|
876
|
+
"scenario.steps"
|
|
877
|
+
)
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return issues;
|
|
881
|
+
}
|
|
882
|
+
function issue2(code, message, severity, file, rule, refs) {
|
|
883
|
+
const issue5 = {
|
|
884
|
+
code,
|
|
885
|
+
severity,
|
|
886
|
+
message
|
|
887
|
+
};
|
|
888
|
+
if (file) {
|
|
889
|
+
issue5.file = file;
|
|
890
|
+
}
|
|
891
|
+
if (rule) {
|
|
892
|
+
issue5.rule = rule;
|
|
893
|
+
}
|
|
894
|
+
if (refs && refs.length > 0) {
|
|
895
|
+
issue5.refs = refs;
|
|
896
|
+
}
|
|
897
|
+
return issue5;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/core/validators/spec.ts
|
|
901
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
902
|
+
async function validateSpecs(root, config) {
|
|
903
|
+
const specsRoot = resolvePath(root, config, "specDir");
|
|
904
|
+
const files = await collectSpecFiles(specsRoot);
|
|
905
|
+
if (files.length === 0) {
|
|
906
|
+
return [
|
|
907
|
+
issue3(
|
|
908
|
+
"QFAI-SPEC-000",
|
|
909
|
+
"Spec \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
910
|
+
"info",
|
|
911
|
+
specsRoot,
|
|
912
|
+
"spec.files"
|
|
913
|
+
)
|
|
914
|
+
];
|
|
915
|
+
}
|
|
916
|
+
const issues = [];
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
const text = await readFile5(file, "utf-8");
|
|
919
|
+
issues.push(
|
|
920
|
+
...validateSpecContent(
|
|
921
|
+
text,
|
|
922
|
+
file,
|
|
923
|
+
config.validation.require.specSections
|
|
924
|
+
)
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
return issues;
|
|
928
|
+
}
|
|
929
|
+
function validateSpecContent(text, file, requiredSections) {
|
|
930
|
+
const issues = [];
|
|
931
|
+
const invalidIds = extractInvalidIds(text, [
|
|
932
|
+
"SPEC",
|
|
933
|
+
"BR",
|
|
934
|
+
"SC",
|
|
935
|
+
"UI",
|
|
936
|
+
"API",
|
|
937
|
+
"DATA"
|
|
938
|
+
]);
|
|
939
|
+
if (invalidIds.length > 0) {
|
|
940
|
+
issues.push(
|
|
941
|
+
issue3(
|
|
942
|
+
"QFAI_ID_INVALID_FORMAT",
|
|
943
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
944
|
+
"error",
|
|
945
|
+
file,
|
|
946
|
+
"id.format",
|
|
947
|
+
invalidIds
|
|
948
|
+
)
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
const specIds = extractIds(text, "SPEC");
|
|
952
|
+
if (specIds.length === 0) {
|
|
953
|
+
issues.push(
|
|
954
|
+
issue3(
|
|
955
|
+
"QFAI-SPEC-001",
|
|
956
|
+
"SPEC ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
957
|
+
"error",
|
|
958
|
+
file,
|
|
959
|
+
"spec.id"
|
|
960
|
+
)
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const brIds = extractIds(text, "BR");
|
|
964
|
+
if (brIds.length === 0) {
|
|
965
|
+
issues.push(
|
|
966
|
+
issue3(
|
|
967
|
+
"QFAI-SPEC-002",
|
|
968
|
+
"BR ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
969
|
+
"error",
|
|
970
|
+
file,
|
|
971
|
+
"spec.br"
|
|
972
|
+
)
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const scIds = extractIds(text, "SC");
|
|
976
|
+
if (scIds.length > 0) {
|
|
977
|
+
issues.push(
|
|
978
|
+
issue3(
|
|
979
|
+
"QFAI-SPEC-003",
|
|
980
|
+
"Spec \u306F SC \u3092\u53C2\u7167\u3057\u306A\u3044\u30EB\u30FC\u30EB\u3067\u3059\u3002",
|
|
981
|
+
"warning",
|
|
982
|
+
file,
|
|
983
|
+
"spec.noSc",
|
|
984
|
+
scIds
|
|
985
|
+
)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
for (const section of requiredSections) {
|
|
989
|
+
if (!text.includes(section)) {
|
|
990
|
+
issues.push(
|
|
991
|
+
issue3(
|
|
992
|
+
"QFAI-SPEC-004",
|
|
993
|
+
`\u5FC5\u9808\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: ${section}`,
|
|
994
|
+
"error",
|
|
995
|
+
file,
|
|
996
|
+
"spec.requiredSection"
|
|
997
|
+
)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return issues;
|
|
1002
|
+
}
|
|
1003
|
+
function issue3(code, message, severity, file, rule, refs) {
|
|
1004
|
+
const issue5 = {
|
|
1005
|
+
code,
|
|
1006
|
+
severity,
|
|
1007
|
+
message
|
|
1008
|
+
};
|
|
1009
|
+
if (file) {
|
|
1010
|
+
issue5.file = file;
|
|
1011
|
+
}
|
|
1012
|
+
if (rule) {
|
|
1013
|
+
issue5.rule = rule;
|
|
1014
|
+
}
|
|
1015
|
+
if (refs && refs.length > 0) {
|
|
1016
|
+
issue5.refs = refs;
|
|
1017
|
+
}
|
|
1018
|
+
return issue5;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/core/validators/traceability.ts
|
|
1022
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1023
|
+
async function validateTraceability(root, config) {
|
|
1024
|
+
const issues = [];
|
|
1025
|
+
const specsRoot = resolvePath(root, config, "specDir");
|
|
1026
|
+
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1027
|
+
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1028
|
+
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1029
|
+
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1030
|
+
const specFiles = await collectSpecFiles(specsRoot);
|
|
1031
|
+
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1032
|
+
extensions: [".md"]
|
|
1033
|
+
});
|
|
1034
|
+
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1035
|
+
extensions: [".feature"]
|
|
1036
|
+
});
|
|
1037
|
+
const upstreamIds = /* @__PURE__ */ new Set();
|
|
1038
|
+
const brIdsInSpecs = /* @__PURE__ */ new Set();
|
|
1039
|
+
const brIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1040
|
+
const scIdsInScenarios = /* @__PURE__ */ new Set();
|
|
1041
|
+
const scenarioContractIds = /* @__PURE__ */ new Set();
|
|
1042
|
+
const scWithContracts = /* @__PURE__ */ new Set();
|
|
1043
|
+
for (const file of [...specFiles, ...decisionFiles]) {
|
|
1044
|
+
const text = await readFile6(file, "utf-8");
|
|
1045
|
+
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1046
|
+
extractIds(text, "BR").forEach((id) => brIdsInSpecs.add(id));
|
|
1047
|
+
}
|
|
1048
|
+
for (const file of scenarioFiles) {
|
|
1049
|
+
const text = await readFile6(file, "utf-8");
|
|
1050
|
+
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1051
|
+
const brIds = extractIds(text, "BR");
|
|
1052
|
+
brIds.forEach((id) => brIdsInScenarios.add(id));
|
|
1053
|
+
const scIds = extractIds(text, "SC");
|
|
1054
|
+
scIds.forEach((id) => scIdsInScenarios.add(id));
|
|
1055
|
+
const contractIds = [
|
|
1056
|
+
...extractIds(text, "UI"),
|
|
1057
|
+
...extractIds(text, "API"),
|
|
1058
|
+
...extractIds(text, "DATA")
|
|
1059
|
+
];
|
|
1060
|
+
contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1061
|
+
if (contractIds.length > 0) {
|
|
1062
|
+
scIds.forEach((id) => scWithContracts.add(id));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (upstreamIds.size === 0) {
|
|
1066
|
+
return [
|
|
1067
|
+
issue4(
|
|
1068
|
+
"QFAI-TRACE-000",
|
|
1069
|
+
"\u4E0A\u6D41 ID \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1070
|
+
"info",
|
|
1071
|
+
specsRoot,
|
|
1072
|
+
"traceability.upstream"
|
|
1073
|
+
)
|
|
1074
|
+
];
|
|
1075
|
+
}
|
|
1076
|
+
if (config.validation.traceability.brMustHaveSc && brIdsInSpecs.size > 0) {
|
|
1077
|
+
const orphanBrIds = Array.from(brIdsInSpecs).filter(
|
|
1078
|
+
(id) => !brIdsInScenarios.has(id)
|
|
1079
|
+
);
|
|
1080
|
+
if (orphanBrIds.length > 0) {
|
|
1081
|
+
issues.push(
|
|
1082
|
+
issue4(
|
|
1083
|
+
"QFAI_TRACE_BR_ORPHAN",
|
|
1084
|
+
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
1085
|
+
"error",
|
|
1086
|
+
specsRoot,
|
|
1087
|
+
"traceability.brMustHaveSc",
|
|
1088
|
+
orphanBrIds
|
|
1089
|
+
)
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (config.validation.traceability.scMustTouchContracts && scIdsInScenarios.size > 0) {
|
|
1094
|
+
const scWithoutContracts = Array.from(scIdsInScenarios).filter(
|
|
1095
|
+
(id) => !scWithContracts.has(id)
|
|
1096
|
+
);
|
|
1097
|
+
if (scWithoutContracts.length > 0) {
|
|
1098
|
+
issues.push(
|
|
1099
|
+
issue4(
|
|
1100
|
+
"QFAI_TRACE_SC_NO_CONTRACT",
|
|
1101
|
+
`SC \u304C\u5951\u7D04(UI/API/DATA)\u306B\u63A5\u7D9A\u3057\u3066\u3044\u307E\u305B\u3093: ${scWithoutContracts.join(
|
|
1102
|
+
", "
|
|
1103
|
+
)}`,
|
|
1104
|
+
"error",
|
|
1105
|
+
scenariosRoot,
|
|
1106
|
+
"traceability.scMustTouchContracts",
|
|
1107
|
+
scWithoutContracts
|
|
1108
|
+
)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (!config.validation.traceability.allowOrphanContracts) {
|
|
1113
|
+
const contractIds = await collectContractIds(root, config);
|
|
1114
|
+
if (contractIds.size > 0) {
|
|
1115
|
+
const orphanContracts = Array.from(contractIds).filter(
|
|
1116
|
+
(id) => !scenarioContractIds.has(id)
|
|
1117
|
+
);
|
|
1118
|
+
if (orphanContracts.length > 0) {
|
|
1119
|
+
issues.push(
|
|
1120
|
+
issue4(
|
|
1121
|
+
"QFAI_CONTRACT_ORPHAN",
|
|
1122
|
+
`\u5951\u7D04\u304C SC \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
1123
|
+
"error",
|
|
1124
|
+
scenariosRoot,
|
|
1125
|
+
"traceability.allowOrphanContracts",
|
|
1126
|
+
orphanContracts
|
|
1127
|
+
)
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
issues.push(
|
|
1133
|
+
...await validateCodeReferences(upstreamIds, srcRoot, testsRoot)
|
|
1134
|
+
);
|
|
1135
|
+
return issues;
|
|
1136
|
+
}
|
|
1137
|
+
async function collectContractIds(root, config) {
|
|
1138
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
1139
|
+
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1140
|
+
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1141
|
+
const dataRoot = resolvePath(root, config, "dataContractsDir");
|
|
1142
|
+
const uiFiles = await collectUiContractFiles(uiRoot);
|
|
1143
|
+
const apiFiles = await collectApiContractFiles(apiRoot);
|
|
1144
|
+
const dataFiles = await collectDataContractFiles(dataRoot);
|
|
1145
|
+
await collectIdsFromFiles(uiFiles, ["UI"], contractIds);
|
|
1146
|
+
await collectIdsFromFiles(apiFiles, ["API"], contractIds);
|
|
1147
|
+
await collectIdsFromFiles(dataFiles, ["DATA"], contractIds);
|
|
1148
|
+
return contractIds;
|
|
1149
|
+
}
|
|
1150
|
+
async function collectIdsFromFiles(files, prefixes, out) {
|
|
1151
|
+
for (const file of files) {
|
|
1152
|
+
const text = await readFile6(file, "utf-8");
|
|
1153
|
+
for (const prefix of prefixes) {
|
|
1154
|
+
extractIds(text, prefix).forEach((id) => out.add(id));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
1159
|
+
const issues = [];
|
|
1160
|
+
const codeFiles = await collectFiles(srcRoot, {
|
|
1161
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
1162
|
+
});
|
|
1163
|
+
const testFiles = await collectFiles(testsRoot, {
|
|
1164
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
1165
|
+
});
|
|
1166
|
+
const targetFiles = [...codeFiles, ...testFiles];
|
|
1167
|
+
if (targetFiles.length === 0) {
|
|
1168
|
+
issues.push(
|
|
1169
|
+
issue4(
|
|
1170
|
+
"QFAI-TRACE-001",
|
|
1171
|
+
"\u53C2\u7167\u5BFE\u8C61\u306E\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1172
|
+
"info",
|
|
1173
|
+
srcRoot,
|
|
1174
|
+
"traceability.codeReferences"
|
|
1175
|
+
)
|
|
1176
|
+
);
|
|
1177
|
+
return issues;
|
|
1178
|
+
}
|
|
1179
|
+
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
1180
|
+
let found = false;
|
|
1181
|
+
for (const file of targetFiles) {
|
|
1182
|
+
const text = await readFile6(file, "utf-8");
|
|
1183
|
+
if (pattern.test(text)) {
|
|
1184
|
+
found = true;
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (!found) {
|
|
1189
|
+
issues.push(
|
|
1190
|
+
issue4(
|
|
1191
|
+
"QFAI-TRACE-002",
|
|
1192
|
+
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
1193
|
+
"warning",
|
|
1194
|
+
srcRoot,
|
|
1195
|
+
"traceability.codeReferences"
|
|
1196
|
+
)
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
return issues;
|
|
1200
|
+
}
|
|
1201
|
+
function buildIdPattern(ids) {
|
|
1202
|
+
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1203
|
+
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1204
|
+
}
|
|
1205
|
+
function issue4(code, message, severity, file, rule, refs) {
|
|
1206
|
+
const issue5 = {
|
|
1207
|
+
code,
|
|
1208
|
+
severity,
|
|
1209
|
+
message
|
|
1210
|
+
};
|
|
1211
|
+
if (file) {
|
|
1212
|
+
issue5.file = file;
|
|
1213
|
+
}
|
|
1214
|
+
if (rule) {
|
|
1215
|
+
issue5.rule = rule;
|
|
1216
|
+
}
|
|
1217
|
+
if (refs && refs.length > 0) {
|
|
1218
|
+
issue5.refs = refs;
|
|
1219
|
+
}
|
|
1220
|
+
return issue5;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/core/validate.ts
|
|
1224
|
+
async function validateProject(root, configResult) {
|
|
1225
|
+
const resolved = configResult ?? await loadConfig(root);
|
|
1226
|
+
const { config, issues: configIssues } = resolved;
|
|
1227
|
+
const issues = [
|
|
1228
|
+
...configIssues,
|
|
1229
|
+
...await validateSpecs(root, config),
|
|
1230
|
+
...await validateScenarios(root, config),
|
|
1231
|
+
...await validateContracts(root, config),
|
|
1232
|
+
...await validateTraceability(root, config)
|
|
1233
|
+
];
|
|
1234
|
+
const toolVersion = await resolveToolVersion();
|
|
1235
|
+
return {
|
|
1236
|
+
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
1237
|
+
toolVersion,
|
|
1238
|
+
issues,
|
|
1239
|
+
counts: countIssues(issues)
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function countIssues(issues) {
|
|
1243
|
+
return issues.reduce(
|
|
1244
|
+
(acc, issue5) => {
|
|
1245
|
+
acc[issue5.severity] += 1;
|
|
1246
|
+
return acc;
|
|
1247
|
+
},
|
|
1248
|
+
{ info: 0, warning: 0, error: 0 }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/core/report.ts
|
|
1253
|
+
var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DATA"];
|
|
1254
|
+
async function createReportData(root, validation, configResult) {
|
|
1255
|
+
const resolved = configResult ?? await loadConfig(root);
|
|
1256
|
+
const config = resolved.config;
|
|
1257
|
+
const configPath = resolved.configPath;
|
|
1258
|
+
const specRoot = resolvePath(root, config, "specDir");
|
|
1259
|
+
const decisionsRoot = resolvePath(root, config, "decisionsDir");
|
|
1260
|
+
const scenariosRoot = resolvePath(root, config, "scenariosDir");
|
|
1261
|
+
const rulesRoot = resolvePath(root, config, "rulesDir");
|
|
1262
|
+
const apiRoot = resolvePath(root, config, "apiContractsDir");
|
|
1263
|
+
const uiRoot = resolvePath(root, config, "uiContractsDir");
|
|
1264
|
+
const dbRoot = resolvePath(root, config, "dataContractsDir");
|
|
1265
|
+
const srcRoot = resolvePath(root, config, "srcDir");
|
|
1266
|
+
const testsRoot = resolvePath(root, config, "testsDir");
|
|
1267
|
+
const specFiles = await collectSpecFiles(specRoot);
|
|
1268
|
+
const scenarioFiles = await collectFiles(scenariosRoot, {
|
|
1269
|
+
extensions: [".feature"]
|
|
1270
|
+
});
|
|
1271
|
+
const decisionFiles = await collectFiles(decisionsRoot, {
|
|
1272
|
+
extensions: [".md"]
|
|
1273
|
+
});
|
|
1274
|
+
const ruleFiles = await collectFiles(rulesRoot, { extensions: [".md"] });
|
|
1275
|
+
const {
|
|
1276
|
+
api: apiFiles,
|
|
1277
|
+
ui: uiFiles,
|
|
1278
|
+
db: dbFiles
|
|
1279
|
+
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
1280
|
+
const idsByPrefix = await collectIds([
|
|
1281
|
+
...specFiles,
|
|
1282
|
+
...scenarioFiles,
|
|
1283
|
+
...decisionFiles,
|
|
1284
|
+
...ruleFiles,
|
|
1285
|
+
...apiFiles,
|
|
1286
|
+
...uiFiles,
|
|
1287
|
+
...dbFiles
|
|
1288
|
+
]);
|
|
1289
|
+
const upstreamIds = await collectUpstreamIds([
|
|
1290
|
+
...specFiles,
|
|
1291
|
+
...scenarioFiles
|
|
1292
|
+
]);
|
|
1293
|
+
const traceability = await evaluateTraceability(
|
|
1294
|
+
upstreamIds,
|
|
1295
|
+
srcRoot,
|
|
1296
|
+
testsRoot
|
|
1297
|
+
);
|
|
1298
|
+
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
1299
|
+
const version = await resolveToolVersion();
|
|
1300
|
+
return {
|
|
1301
|
+
tool: "qfai",
|
|
1302
|
+
version,
|
|
1303
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1304
|
+
root,
|
|
1305
|
+
configPath,
|
|
1306
|
+
summary: {
|
|
1307
|
+
specs: specFiles.length,
|
|
1308
|
+
scenarios: scenarioFiles.length,
|
|
1309
|
+
decisions: decisionFiles.length,
|
|
1310
|
+
rules: ruleFiles.length,
|
|
1311
|
+
contracts: {
|
|
1312
|
+
api: apiFiles.length,
|
|
1313
|
+
ui: uiFiles.length,
|
|
1314
|
+
db: dbFiles.length
|
|
1315
|
+
},
|
|
1316
|
+
counts: resolvedValidation.counts
|
|
1317
|
+
},
|
|
1318
|
+
ids: {
|
|
1319
|
+
spec: idsByPrefix.SPEC,
|
|
1320
|
+
br: idsByPrefix.BR,
|
|
1321
|
+
sc: idsByPrefix.SC,
|
|
1322
|
+
ui: idsByPrefix.UI,
|
|
1323
|
+
api: idsByPrefix.API,
|
|
1324
|
+
data: idsByPrefix.DATA
|
|
1325
|
+
},
|
|
1326
|
+
traceability: {
|
|
1327
|
+
upstreamIdsFound: upstreamIds.size,
|
|
1328
|
+
referencedInCodeOrTests: traceability
|
|
1329
|
+
},
|
|
1330
|
+
issues: resolvedValidation.issues
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function formatReportMarkdown(data) {
|
|
1334
|
+
const lines = [];
|
|
1335
|
+
lines.push("# QFAI Report");
|
|
1336
|
+
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
1337
|
+
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
1338
|
+
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
1339
|
+
lines.push(`- \u7248: ${data.version}`);
|
|
1340
|
+
lines.push("");
|
|
1341
|
+
lines.push("## \u6982\u8981");
|
|
1342
|
+
lines.push(`- specs: ${data.summary.specs}`);
|
|
1343
|
+
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
1344
|
+
lines.push(`- decisions: ${data.summary.decisions}`);
|
|
1345
|
+
lines.push(`- rules: ${data.summary.rules}`);
|
|
1346
|
+
lines.push(
|
|
1347
|
+
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
1348
|
+
);
|
|
1349
|
+
lines.push(
|
|
1350
|
+
`- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
1351
|
+
);
|
|
1352
|
+
lines.push("");
|
|
1353
|
+
lines.push("## ID\u96C6\u8A08");
|
|
1354
|
+
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
1355
|
+
lines.push(formatIdLine("BR", data.ids.br));
|
|
1356
|
+
lines.push(formatIdLine("SC", data.ids.sc));
|
|
1357
|
+
lines.push(formatIdLine("UI", data.ids.ui));
|
|
1358
|
+
lines.push(formatIdLine("API", data.ids.api));
|
|
1359
|
+
lines.push(formatIdLine("DATA", data.ids.data));
|
|
1360
|
+
lines.push("");
|
|
1361
|
+
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
1362
|
+
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
1363
|
+
lines.push(
|
|
1364
|
+
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
1365
|
+
);
|
|
1366
|
+
lines.push("");
|
|
1367
|
+
lines.push("## Hotspots");
|
|
1368
|
+
const hotspots = buildHotspots(data.issues);
|
|
1369
|
+
if (hotspots.length === 0) {
|
|
1370
|
+
lines.push("- (none)");
|
|
1371
|
+
} else {
|
|
1372
|
+
for (const spot of hotspots) {
|
|
1373
|
+
lines.push(
|
|
1374
|
+
`- ${spot.file}: total ${spot.total} (error ${spot.error} / warning ${spot.warning} / info ${spot.info})`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
lines.push("");
|
|
1379
|
+
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
1380
|
+
const traceIssues = data.issues.filter(
|
|
1381
|
+
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code === "QFAI_CONTRACT_ORPHAN"
|
|
1382
|
+
);
|
|
1383
|
+
if (traceIssues.length === 0) {
|
|
1384
|
+
lines.push("- (none)");
|
|
1385
|
+
} else {
|
|
1386
|
+
for (const item of traceIssues) {
|
|
1387
|
+
const location = item.file ? ` (${item.file})` : "";
|
|
1388
|
+
lines.push(
|
|
1389
|
+
`- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
lines.push("");
|
|
1394
|
+
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
1395
|
+
if (data.issues.length === 0) {
|
|
1396
|
+
lines.push("- (none)");
|
|
1397
|
+
} else {
|
|
1398
|
+
for (const item of data.issues) {
|
|
1399
|
+
const location = item.file ? ` (${item.file})` : "";
|
|
1400
|
+
const refs = item.refs && item.refs.length > 0 ? ` refs=${item.refs.join(",")}` : "";
|
|
1401
|
+
lines.push(
|
|
1402
|
+
`- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return lines.join("\n");
|
|
1407
|
+
}
|
|
1408
|
+
function formatReportJson(data) {
|
|
1409
|
+
return JSON.stringify(data, null, 2);
|
|
1410
|
+
}
|
|
1411
|
+
async function collectIds(files) {
|
|
1412
|
+
const result = {
|
|
1413
|
+
SPEC: /* @__PURE__ */ new Set(),
|
|
1414
|
+
BR: /* @__PURE__ */ new Set(),
|
|
1415
|
+
SC: /* @__PURE__ */ new Set(),
|
|
1416
|
+
UI: /* @__PURE__ */ new Set(),
|
|
1417
|
+
API: /* @__PURE__ */ new Set(),
|
|
1418
|
+
DATA: /* @__PURE__ */ new Set()
|
|
1419
|
+
};
|
|
1420
|
+
for (const file of files) {
|
|
1421
|
+
const text = await readFile7(file, "utf-8");
|
|
1422
|
+
for (const prefix of ID_PREFIXES) {
|
|
1423
|
+
const ids = extractIds(text, prefix);
|
|
1424
|
+
ids.forEach((id) => result[prefix].add(id));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
SPEC: toSortedArray(result.SPEC),
|
|
1429
|
+
BR: toSortedArray(result.BR),
|
|
1430
|
+
SC: toSortedArray(result.SC),
|
|
1431
|
+
UI: toSortedArray(result.UI),
|
|
1432
|
+
API: toSortedArray(result.API),
|
|
1433
|
+
DATA: toSortedArray(result.DATA)
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
async function collectUpstreamIds(files) {
|
|
1437
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1438
|
+
for (const file of files) {
|
|
1439
|
+
const text = await readFile7(file, "utf-8");
|
|
1440
|
+
extractAllIds(text).forEach((id) => ids.add(id));
|
|
1441
|
+
}
|
|
1442
|
+
return ids;
|
|
1443
|
+
}
|
|
1444
|
+
async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
1445
|
+
if (upstreamIds.size === 0) {
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
const codeFiles = await collectFiles(srcRoot, {
|
|
1449
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
1450
|
+
});
|
|
1451
|
+
const testFiles = await collectFiles(testsRoot, {
|
|
1452
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
1453
|
+
});
|
|
1454
|
+
const targetFiles = [...codeFiles, ...testFiles];
|
|
1455
|
+
if (targetFiles.length === 0) {
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
1459
|
+
for (const file of targetFiles) {
|
|
1460
|
+
const text = await readFile7(file, "utf-8");
|
|
1461
|
+
if (pattern.test(text)) {
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
function buildIdPattern2(ids) {
|
|
1468
|
+
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1469
|
+
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
1470
|
+
}
|
|
1471
|
+
function formatIdLine(label, values) {
|
|
1472
|
+
if (values.length === 0) {
|
|
1473
|
+
return `- ${label}: (none)`;
|
|
1474
|
+
}
|
|
1475
|
+
return `- ${label}: ${values.join(", ")}`;
|
|
1476
|
+
}
|
|
1477
|
+
function toSortedArray(values) {
|
|
1478
|
+
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
1479
|
+
}
|
|
1480
|
+
function buildHotspots(issues) {
|
|
1481
|
+
const map = /* @__PURE__ */ new Map();
|
|
1482
|
+
for (const issue5 of issues) {
|
|
1483
|
+
if (!issue5.file) {
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const current = map.get(issue5.file) ?? {
|
|
1487
|
+
file: issue5.file,
|
|
1488
|
+
total: 0,
|
|
1489
|
+
error: 0,
|
|
1490
|
+
warning: 0,
|
|
1491
|
+
info: 0
|
|
1492
|
+
};
|
|
1493
|
+
current.total += 1;
|
|
1494
|
+
current[issue5.severity] += 1;
|
|
1495
|
+
map.set(issue5.file, current);
|
|
1496
|
+
}
|
|
1497
|
+
return Array.from(map.values()).sort(
|
|
1498
|
+
(a, b) => b.total !== a.total ? b.total - a.total : a.file.localeCompare(b.file)
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
export {
|
|
1502
|
+
VALIDATION_SCHEMA_VERSION,
|
|
1503
|
+
createReportData,
|
|
1504
|
+
defaultConfig,
|
|
1505
|
+
extractAllIds,
|
|
1506
|
+
extractIds,
|
|
1507
|
+
extractInvalidIds,
|
|
1508
|
+
formatReportJson,
|
|
1509
|
+
formatReportMarkdown,
|
|
1510
|
+
getConfigPath,
|
|
1511
|
+
lintSql,
|
|
1512
|
+
loadConfig,
|
|
1513
|
+
resolvePath,
|
|
1514
|
+
resolveToolVersion,
|
|
1515
|
+
validateContracts,
|
|
1516
|
+
validateProject,
|
|
1517
|
+
validateScenarioContent,
|
|
1518
|
+
validateScenarios,
|
|
1519
|
+
validateSpecContent,
|
|
1520
|
+
validateSpecs,
|
|
1521
|
+
validateTraceability
|
|
1522
|
+
};
|
|
1523
|
+
//# sourceMappingURL=index.mjs.map
|