qfai 1.0.3 → 1.0.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 +53 -74
- package/assets/init/.qfai/README.md +17 -82
- package/assets/init/.qfai/assistant/README.md +9 -0
- package/assets/init/.qfai/assistant/agents/README.md +34 -0
- package/assets/init/.qfai/assistant/agents/architect.md +73 -0
- package/assets/init/.qfai/assistant/agents/backend-engineer.md +73 -0
- package/assets/init/.qfai/assistant/agents/code-reviewer.md +73 -0
- package/assets/init/.qfai/assistant/agents/contract-designer.md +73 -0
- package/assets/init/.qfai/assistant/agents/devops-ci-engineer.md +73 -0
- package/assets/init/.qfai/assistant/agents/facilitator.md +74 -0
- package/assets/init/.qfai/assistant/agents/frontend-engineer.md +73 -0
- package/assets/init/.qfai/assistant/agents/interviewer.md +72 -0
- package/assets/init/.qfai/assistant/agents/planner.md +73 -0
- package/assets/init/.qfai/assistant/agents/qa-engineer.md +73 -0
- package/assets/init/.qfai/assistant/agents/requirements-analyst.md +73 -0
- package/assets/init/.qfai/assistant/agents/test-engineer.md +73 -0
- package/assets/init/.qfai/assistant/instructions/README.md +6 -0
- package/assets/init/.qfai/assistant/instructions/constitution.md +131 -0
- package/assets/init/.qfai/assistant/instructions/workflow.md +75 -0
- package/assets/init/.qfai/assistant/prompts/README.md +19 -0
- package/assets/init/.qfai/assistant/prompts/qfai-discuss.md +173 -0
- package/assets/init/.qfai/assistant/prompts/qfai-implement.md +239 -0
- package/assets/init/.qfai/assistant/prompts/qfai-pr.md +218 -0
- package/assets/init/.qfai/assistant/prompts/qfai-require.md +273 -0
- package/assets/init/.qfai/assistant/prompts/qfai-scenario-test.md +229 -0
- package/assets/init/.qfai/assistant/prompts/qfai-spec.md +287 -0
- package/assets/init/.qfai/assistant/prompts/qfai-unit-test.md +202 -0
- package/assets/init/.qfai/assistant/prompts/qfai-verify.md +231 -0
- package/assets/init/.qfai/assistant/prompts.local/README.md +6 -0
- package/assets/init/.qfai/assistant/steering/README.md +33 -0
- package/assets/init/.qfai/assistant/steering/product.md +32 -0
- package/assets/init/.qfai/assistant/steering/structure.md +34 -0
- package/assets/init/.qfai/assistant/steering/tech.md +37 -0
- package/assets/init/.qfai/contracts/README.md +7 -87
- package/assets/init/.qfai/contracts/api/README.md +8 -0
- package/assets/init/.qfai/contracts/db/README.md +8 -0
- package/assets/init/.qfai/contracts/ui/README.md +8 -0
- package/assets/init/.qfai/report/README.md +13 -0
- package/assets/init/.qfai/require/README.md +4 -26
- package/assets/init/.qfai/require/require.md +74 -0
- package/assets/init/.qfai/specs/README.md +6 -57
- package/assets/init/root/.github/workflows/qfai.yml +1 -1
- package/assets/init/root/qfai.config.yaml +3 -4
- package/dist/cli/index.cjs +313 -472
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +295 -454
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +37 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +37 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/contracts/api/api-0001-sample.yaml +0 -15
- package/assets/init/.qfai/contracts/db/db-0001-sample.sql +0 -7
- package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/assets.yaml +0 -6
- package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/palette.png +0 -0
- package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/assets.yaml +0 -6
- package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/snapshots/login__desktop__light__default.png +0 -0
- package/assets/init/.qfai/contracts/ui/thema-001-facebook-like.yml +0 -13
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +0 -17
- package/assets/init/.qfai/out/README.md +0 -17
- package/assets/init/.qfai/promptpack/commands/implement.md +0 -8
- package/assets/init/.qfai/promptpack/commands/plan.md +0 -11
- package/assets/init/.qfai/promptpack/commands/release.md +0 -6
- package/assets/init/.qfai/promptpack/commands/review.md +0 -7
- package/assets/init/.qfai/promptpack/constitution.md +0 -15
- package/assets/init/.qfai/promptpack/modes/change.md +0 -5
- package/assets/init/.qfai/promptpack/modes/compatibility.md +0 -6
- package/assets/init/.qfai/promptpack/roles/qa.md +0 -4
- package/assets/init/.qfai/promptpack/roles/spec.md +0 -4
- package/assets/init/.qfai/promptpack/roles/test.md +0 -4
- package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +0 -42
- package/assets/init/.qfai/promptpack/steering/naming.md +0 -7
- package/assets/init/.qfai/promptpack/steering/traceability.md +0 -25
- package/assets/init/.qfai/prompts/README.md +0 -70
- package/assets/init/.qfai/prompts/analyze/README.md +0 -21
- package/assets/init/.qfai/prompts/analyze/scenario_test_consistency.md +0 -8
- package/assets/init/.qfai/prompts/analyze/scenario_to_test.md +0 -56
- package/assets/init/.qfai/prompts/analyze/spec_contract_consistency.md +0 -8
- package/assets/init/.qfai/prompts/analyze/spec_scenario_consistency.md +0 -8
- package/assets/init/.qfai/prompts/analyze/spec_to_contract.md +0 -54
- package/assets/init/.qfai/prompts/analyze/spec_to_scenario.md +0 -56
- package/assets/init/.qfai/prompts/makeBusinessFlow.md +0 -34
- package/assets/init/.qfai/prompts/makeOverview.md +0 -27
- package/assets/init/.qfai/prompts/qfai-classify-change.md +0 -33
- package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +0 -29
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +0 -35
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +0 -36
- package/assets/init/.qfai/prompts/require-to-spec.md +0 -41
- package/assets/init/.qfai/prompts.local/README.md +0 -31
- package/assets/init/.qfai/rules/conventions.md +0 -27
- package/assets/init/.qfai/rules/pnpm.md +0 -29
- package/assets/init/.qfai/samples/analyze/analysis.md +0 -38
- package/assets/init/.qfai/samples/analyze/input_bundle.md +0 -54
- package/assets/init/.qfai/specs/spec-0001/delta.md +0 -30
- package/assets/init/.qfai/specs/spec-0001/scenario.feature +0 -11
- package/assets/init/.qfai/specs/spec-0001/spec.md +0 -40
package/dist/cli/index.mjs
CHANGED
|
@@ -1,230 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli/commands/analyze.ts
|
|
4
|
-
import { readFile } from "fs/promises";
|
|
5
|
-
import path2 from "path";
|
|
6
|
-
|
|
7
|
-
// src/core/fs.ts
|
|
8
|
-
import { access, readdir } from "fs/promises";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import fg from "fast-glob";
|
|
11
|
-
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
12
|
-
"node_modules",
|
|
13
|
-
".git",
|
|
14
|
-
"dist",
|
|
15
|
-
".pnpm",
|
|
16
|
-
"tmp",
|
|
17
|
-
".mcp-tools"
|
|
18
|
-
]);
|
|
19
|
-
var DEFAULT_GLOB_FILE_LIMIT = 2e4;
|
|
20
|
-
async function collectFiles(root, options = {}) {
|
|
21
|
-
const entries = [];
|
|
22
|
-
if (!await exists(root)) {
|
|
23
|
-
return entries;
|
|
24
|
-
}
|
|
25
|
-
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
26
|
-
...DEFAULT_IGNORE_DIRS,
|
|
27
|
-
...options.ignoreDirs ?? []
|
|
28
|
-
]);
|
|
29
|
-
const extensions = options.extensions?.map((ext) => ext.toLowerCase()) ?? [];
|
|
30
|
-
await walk(root, root, ignoreDirs, extensions, entries);
|
|
31
|
-
return entries;
|
|
32
|
-
}
|
|
33
|
-
async function collectFilesByGlobs(root, options) {
|
|
34
|
-
const limit = normalizeLimit(options.limit);
|
|
35
|
-
if (options.globs.length === 0) {
|
|
36
|
-
return { files: [], truncated: false, matchedFileCount: 0, limit };
|
|
37
|
-
}
|
|
38
|
-
const stream = fg.stream(options.globs, {
|
|
39
|
-
cwd: root,
|
|
40
|
-
ignore: options.ignore ?? [],
|
|
41
|
-
onlyFiles: true,
|
|
42
|
-
absolute: true,
|
|
43
|
-
unique: true
|
|
44
|
-
});
|
|
45
|
-
const files = [];
|
|
46
|
-
let truncated = false;
|
|
47
|
-
for await (const entry of stream) {
|
|
48
|
-
if (files.length >= limit) {
|
|
49
|
-
truncated = true;
|
|
50
|
-
destroyStream(stream);
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
files.push(String(entry));
|
|
54
|
-
}
|
|
55
|
-
const matchedFileCount = files.length;
|
|
56
|
-
return { files, truncated, matchedFileCount, limit };
|
|
57
|
-
}
|
|
58
|
-
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
59
|
-
const items = await readdir(current, { withFileTypes: true });
|
|
60
|
-
for (const item of items) {
|
|
61
|
-
const fullPath = path.join(current, item.name);
|
|
62
|
-
if (item.isDirectory()) {
|
|
63
|
-
if (ignoreDirs.has(item.name)) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
await walk(base, fullPath, ignoreDirs, extensions, out);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (item.isFile()) {
|
|
70
|
-
if (extensions.length > 0) {
|
|
71
|
-
const ext = path.extname(item.name).toLowerCase();
|
|
72
|
-
if (!extensions.includes(ext)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
out.push(fullPath);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function exists(target) {
|
|
81
|
-
try {
|
|
82
|
-
await access(target);
|
|
83
|
-
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function normalizeLimit(value) {
|
|
89
|
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
90
|
-
return DEFAULT_GLOB_FILE_LIMIT;
|
|
91
|
-
}
|
|
92
|
-
const flooredValue = Math.floor(value);
|
|
93
|
-
if (flooredValue <= 0) {
|
|
94
|
-
return DEFAULT_GLOB_FILE_LIMIT;
|
|
95
|
-
}
|
|
96
|
-
return flooredValue;
|
|
97
|
-
}
|
|
98
|
-
function destroyStream(stream) {
|
|
99
|
-
if (!stream || typeof stream !== "object") {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const record2 = stream;
|
|
103
|
-
if (typeof record2.destroy === "function") {
|
|
104
|
-
record2.destroy();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// src/cli/commands/analyze.ts
|
|
109
|
-
async function runAnalyze(options) {
|
|
110
|
-
const root = path2.resolve(options.root);
|
|
111
|
-
const localDir = path2.join(root, ".qfai", "prompts.local", "analyze");
|
|
112
|
-
const standardDir = path2.join(root, ".qfai", "prompts", "analyze");
|
|
113
|
-
const available = await listPromptNames([localDir, standardDir]);
|
|
114
|
-
const promptName = normalizePromptName(options.prompt);
|
|
115
|
-
if (!promptName || options.list) {
|
|
116
|
-
emitList(available);
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
const resolved = await resolvePromptPath(promptName, [localDir, standardDir]);
|
|
120
|
-
if (!resolved) {
|
|
121
|
-
emitPromptNotFound(promptName, available);
|
|
122
|
-
return 1;
|
|
123
|
-
}
|
|
124
|
-
const content = await readFile(resolved, "utf-8");
|
|
125
|
-
process.stdout.write(content);
|
|
126
|
-
if (!content.endsWith("\n")) {
|
|
127
|
-
process.stdout.write("\n");
|
|
128
|
-
}
|
|
129
|
-
return 0;
|
|
130
|
-
}
|
|
131
|
-
function normalizePromptName(value) {
|
|
132
|
-
const trimmed = (value ?? "").trim();
|
|
133
|
-
if (!trimmed) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
return trimmed.endsWith(".md") ? trimmed.slice(0, -3) : trimmed;
|
|
137
|
-
}
|
|
138
|
-
async function listPromptNames(dirs) {
|
|
139
|
-
const byName = /* @__PURE__ */ new Map();
|
|
140
|
-
for (const dir of dirs) {
|
|
141
|
-
const files = await collectFiles(dir, { extensions: [".md"] });
|
|
142
|
-
for (const abs of files) {
|
|
143
|
-
const base = path2.basename(abs);
|
|
144
|
-
if (base.toLowerCase() === "readme.md") {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const name = base.slice(0, -3);
|
|
148
|
-
if (byName.has(name)) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (await isDeprecatedPrompt(abs)) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
byName.set(name, abs);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return [...byName.keys()].sort((a, b) => a.localeCompare(b));
|
|
158
|
-
}
|
|
159
|
-
function emitList(names) {
|
|
160
|
-
process.stdout.write("# qfai analyze: prompts\n\n");
|
|
161
|
-
if (names.length === 0) {
|
|
162
|
-
process.stdout.write(
|
|
163
|
-
"\u5229\u7528\u53EF\u80FD\u306A\u30D7\u30ED\u30F3\u30D7\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u307E\u305A `qfai init` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\n"
|
|
164
|
-
);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
process.stdout.write("\u5229\u7528\u53EF\u80FD\u306A\u30D7\u30ED\u30F3\u30D7\u30C8\u4E00\u89A7:\n\n");
|
|
168
|
-
for (const name of names) {
|
|
169
|
-
process.stdout.write(`- ${name}
|
|
170
|
-
`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async function resolvePromptPath(promptName, dirs) {
|
|
174
|
-
const filename = `${promptName}.md`;
|
|
175
|
-
for (const dir of dirs) {
|
|
176
|
-
const full = path2.join(dir, filename);
|
|
177
|
-
try {
|
|
178
|
-
await readFile(full, "utf-8");
|
|
179
|
-
return full;
|
|
180
|
-
} catch {
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
async function isDeprecatedPrompt(filePath) {
|
|
186
|
-
try {
|
|
187
|
-
const content = await readFile(filePath, "utf-8");
|
|
188
|
-
const firstLine = firstLineOf(content);
|
|
189
|
-
return firstLine.trim() === "# Deprecated";
|
|
190
|
-
} catch {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function firstLineOf(content) {
|
|
195
|
-
return content.match(/^[^\r\n]*/)?.[0] ?? "";
|
|
196
|
-
}
|
|
197
|
-
function emitPromptNotFound(promptName, candidates) {
|
|
198
|
-
process.stderr.write(`qfai analyze: prompt not found: ${promptName}
|
|
199
|
-
`);
|
|
200
|
-
if (candidates.length > 0) {
|
|
201
|
-
process.stderr.write("candidates:\n");
|
|
202
|
-
for (const c of candidates) {
|
|
203
|
-
process.stderr.write(`- ${c}
|
|
204
|
-
`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
3
|
// src/cli/commands/doctor.ts
|
|
210
4
|
import { mkdir, writeFile } from "fs/promises";
|
|
211
|
-
import
|
|
5
|
+
import path11 from "path";
|
|
212
6
|
|
|
213
7
|
// src/core/doctor.ts
|
|
214
8
|
import { access as access4 } from "fs/promises";
|
|
215
|
-
import
|
|
9
|
+
import path10 from "path";
|
|
216
10
|
|
|
217
11
|
// src/core/config.ts
|
|
218
|
-
import { access
|
|
219
|
-
import
|
|
12
|
+
import { access, readFile } from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
220
14
|
import { parse as parseYaml } from "yaml";
|
|
221
15
|
var defaultConfig = {
|
|
222
16
|
paths: {
|
|
223
17
|
contractsDir: ".qfai/contracts",
|
|
224
18
|
specsDir: ".qfai/specs",
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
promptsDir: ".qfai/prompts",
|
|
19
|
+
outDir: ".qfai/report",
|
|
20
|
+
promptsDir: ".qfai/assistant/prompts",
|
|
228
21
|
srcDir: "src",
|
|
229
22
|
testsDir: "tests"
|
|
230
23
|
},
|
|
@@ -252,21 +45,21 @@ var defaultConfig = {
|
|
|
252
45
|
}
|
|
253
46
|
},
|
|
254
47
|
output: {
|
|
255
|
-
validateJsonPath: ".qfai/
|
|
48
|
+
validateJsonPath: ".qfai/report/validate.json"
|
|
256
49
|
}
|
|
257
50
|
};
|
|
258
51
|
function getConfigPath(root) {
|
|
259
|
-
return
|
|
52
|
+
return path.join(root, "qfai.config.yaml");
|
|
260
53
|
}
|
|
261
54
|
async function findConfigRoot(startDir) {
|
|
262
|
-
const resolvedStart =
|
|
55
|
+
const resolvedStart = path.resolve(startDir);
|
|
263
56
|
let current = resolvedStart;
|
|
264
57
|
while (true) {
|
|
265
58
|
const configPath = getConfigPath(current);
|
|
266
|
-
if (await
|
|
59
|
+
if (await exists(configPath)) {
|
|
267
60
|
return { root: current, configPath, found: true };
|
|
268
61
|
}
|
|
269
|
-
const parent =
|
|
62
|
+
const parent = path.dirname(current);
|
|
270
63
|
if (parent === current) {
|
|
271
64
|
break;
|
|
272
65
|
}
|
|
@@ -283,7 +76,7 @@ async function loadConfig(root) {
|
|
|
283
76
|
const issues = [];
|
|
284
77
|
let parsed;
|
|
285
78
|
try {
|
|
286
|
-
const raw = await
|
|
79
|
+
const raw = await readFile(configPath, "utf-8");
|
|
287
80
|
parsed = parseYaml(raw);
|
|
288
81
|
} catch (error2) {
|
|
289
82
|
if (isMissingFile(error2)) {
|
|
@@ -296,7 +89,7 @@ async function loadConfig(root) {
|
|
|
296
89
|
return { config: normalized, issues, configPath };
|
|
297
90
|
}
|
|
298
91
|
function resolvePath(root, config, key) {
|
|
299
|
-
return
|
|
92
|
+
return path.resolve(root, config.paths[key]);
|
|
300
93
|
}
|
|
301
94
|
function normalizeConfig(raw, configPath, issues) {
|
|
302
95
|
if (!isRecord(raw)) {
|
|
@@ -335,13 +128,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
335
128
|
configPath,
|
|
336
129
|
issues
|
|
337
130
|
),
|
|
338
|
-
rulesDir: readString(
|
|
339
|
-
raw.rulesDir,
|
|
340
|
-
base.rulesDir,
|
|
341
|
-
"paths.rulesDir",
|
|
342
|
-
configPath,
|
|
343
|
-
issues
|
|
344
|
-
),
|
|
345
131
|
outDir: readString(
|
|
346
132
|
raw.outDir,
|
|
347
133
|
base.outDir,
|
|
@@ -596,9 +382,9 @@ function isMissingFile(error2) {
|
|
|
596
382
|
}
|
|
597
383
|
return false;
|
|
598
384
|
}
|
|
599
|
-
async function
|
|
385
|
+
async function exists(target) {
|
|
600
386
|
try {
|
|
601
|
-
await
|
|
387
|
+
await access(target);
|
|
602
388
|
return true;
|
|
603
389
|
} catch {
|
|
604
390
|
return false;
|
|
@@ -616,26 +402,127 @@ function isRecord(value) {
|
|
|
616
402
|
|
|
617
403
|
// src/core/discovery.ts
|
|
618
404
|
import { access as access3 } from "fs/promises";
|
|
619
|
-
import
|
|
405
|
+
import path4 from "path";
|
|
406
|
+
|
|
407
|
+
// src/core/fs.ts
|
|
408
|
+
import { access as access2, readdir } from "fs/promises";
|
|
409
|
+
import path2 from "path";
|
|
410
|
+
import fg from "fast-glob";
|
|
411
|
+
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
412
|
+
"node_modules",
|
|
413
|
+
".git",
|
|
414
|
+
"dist",
|
|
415
|
+
".pnpm",
|
|
416
|
+
"tmp",
|
|
417
|
+
".mcp-tools"
|
|
418
|
+
]);
|
|
419
|
+
var DEFAULT_GLOB_FILE_LIMIT = 2e4;
|
|
420
|
+
async function collectFiles(root, options = {}) {
|
|
421
|
+
const entries = [];
|
|
422
|
+
if (!await exists2(root)) {
|
|
423
|
+
return entries;
|
|
424
|
+
}
|
|
425
|
+
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
426
|
+
...DEFAULT_IGNORE_DIRS,
|
|
427
|
+
...options.ignoreDirs ?? []
|
|
428
|
+
]);
|
|
429
|
+
const extensions = options.extensions?.map((ext) => ext.toLowerCase()) ?? [];
|
|
430
|
+
await walk(root, root, ignoreDirs, extensions, entries);
|
|
431
|
+
return entries;
|
|
432
|
+
}
|
|
433
|
+
async function collectFilesByGlobs(root, options) {
|
|
434
|
+
const limit = normalizeLimit(options.limit);
|
|
435
|
+
if (options.globs.length === 0) {
|
|
436
|
+
return { files: [], truncated: false, matchedFileCount: 0, limit };
|
|
437
|
+
}
|
|
438
|
+
const stream = fg.stream(options.globs, {
|
|
439
|
+
cwd: root,
|
|
440
|
+
ignore: options.ignore ?? [],
|
|
441
|
+
onlyFiles: true,
|
|
442
|
+
absolute: true,
|
|
443
|
+
unique: true
|
|
444
|
+
});
|
|
445
|
+
const files = [];
|
|
446
|
+
let truncated = false;
|
|
447
|
+
for await (const entry of stream) {
|
|
448
|
+
if (files.length >= limit) {
|
|
449
|
+
truncated = true;
|
|
450
|
+
destroyStream(stream);
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
files.push(String(entry));
|
|
454
|
+
}
|
|
455
|
+
const matchedFileCount = files.length;
|
|
456
|
+
return { files, truncated, matchedFileCount, limit };
|
|
457
|
+
}
|
|
458
|
+
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
459
|
+
const items = await readdir(current, { withFileTypes: true });
|
|
460
|
+
for (const item of items) {
|
|
461
|
+
const fullPath = path2.join(current, item.name);
|
|
462
|
+
if (item.isDirectory()) {
|
|
463
|
+
if (ignoreDirs.has(item.name)) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
await walk(base, fullPath, ignoreDirs, extensions, out);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (item.isFile()) {
|
|
470
|
+
if (extensions.length > 0) {
|
|
471
|
+
const ext = path2.extname(item.name).toLowerCase();
|
|
472
|
+
if (!extensions.includes(ext)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
out.push(fullPath);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function exists2(target) {
|
|
481
|
+
try {
|
|
482
|
+
await access2(target);
|
|
483
|
+
return true;
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function normalizeLimit(value) {
|
|
489
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
490
|
+
return DEFAULT_GLOB_FILE_LIMIT;
|
|
491
|
+
}
|
|
492
|
+
const flooredValue = Math.floor(value);
|
|
493
|
+
if (flooredValue <= 0) {
|
|
494
|
+
return DEFAULT_GLOB_FILE_LIMIT;
|
|
495
|
+
}
|
|
496
|
+
return flooredValue;
|
|
497
|
+
}
|
|
498
|
+
function destroyStream(stream) {
|
|
499
|
+
if (!stream || typeof stream !== "object") {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const record2 = stream;
|
|
503
|
+
if (typeof record2.destroy === "function") {
|
|
504
|
+
record2.destroy();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
620
507
|
|
|
621
508
|
// src/core/specLayout.ts
|
|
622
509
|
import { readdir as readdir2 } from "fs/promises";
|
|
623
|
-
import
|
|
510
|
+
import path3 from "path";
|
|
624
511
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
625
512
|
async function collectSpecEntries(specsRoot) {
|
|
626
513
|
const dirs = await listSpecDirs(specsRoot);
|
|
627
514
|
const entries = dirs.map((dir) => ({
|
|
628
515
|
dir,
|
|
629
|
-
specPath:
|
|
630
|
-
deltaPath:
|
|
631
|
-
scenarioPath:
|
|
516
|
+
specPath: path3.join(dir, "spec.md"),
|
|
517
|
+
deltaPath: path3.join(dir, "delta.md"),
|
|
518
|
+
scenarioPath: path3.join(dir, "scenario.feature")
|
|
632
519
|
}));
|
|
633
520
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
634
521
|
}
|
|
635
522
|
async function listSpecDirs(specsRoot) {
|
|
636
523
|
try {
|
|
637
524
|
const items = await readdir2(specsRoot, { withFileTypes: true });
|
|
638
|
-
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) =>
|
|
525
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
|
|
639
526
|
} catch (error2) {
|
|
640
527
|
if (isMissingFileError(error2)) {
|
|
641
528
|
return [];
|
|
@@ -706,20 +593,20 @@ async function exists3(target) {
|
|
|
706
593
|
function filterByBasenamePrefix(files, prefix) {
|
|
707
594
|
const lowerPrefix = prefix.toLowerCase();
|
|
708
595
|
return files.filter(
|
|
709
|
-
(file) =>
|
|
596
|
+
(file) => path4.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
710
597
|
);
|
|
711
598
|
}
|
|
712
599
|
|
|
713
600
|
// src/core/paths.ts
|
|
714
|
-
import
|
|
601
|
+
import path5 from "path";
|
|
715
602
|
function toRelativePath(root, target) {
|
|
716
603
|
if (!target) {
|
|
717
604
|
return target;
|
|
718
605
|
}
|
|
719
|
-
if (!
|
|
606
|
+
if (!path5.isAbsolute(target)) {
|
|
720
607
|
return toPosixPath(target);
|
|
721
608
|
}
|
|
722
|
-
const relative =
|
|
609
|
+
const relative = path5.relative(root, target);
|
|
723
610
|
if (!relative) {
|
|
724
611
|
return ".";
|
|
725
612
|
}
|
|
@@ -730,8 +617,8 @@ function toPosixPath(value) {
|
|
|
730
617
|
}
|
|
731
618
|
|
|
732
619
|
// src/core/traceability.ts
|
|
733
|
-
import { readFile as
|
|
734
|
-
import
|
|
620
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
621
|
+
import path6 from "path";
|
|
735
622
|
|
|
736
623
|
// src/core/gherkin/parse.ts
|
|
737
624
|
import {
|
|
@@ -887,7 +774,7 @@ function extractAnnotatedScIds(text) {
|
|
|
887
774
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
888
775
|
const scIds = /* @__PURE__ */ new Set();
|
|
889
776
|
for (const file of scenarioFiles) {
|
|
890
|
-
const text = await
|
|
777
|
+
const text = await readFile2(file, "utf-8");
|
|
891
778
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
892
779
|
if (!document || errors.length > 0) {
|
|
893
780
|
continue;
|
|
@@ -905,7 +792,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
905
792
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
906
793
|
const sources = /* @__PURE__ */ new Map();
|
|
907
794
|
for (const file of scenarioFiles) {
|
|
908
|
-
const text = await
|
|
795
|
+
const text = await readFile2(file, "utf-8");
|
|
909
796
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
910
797
|
if (!document || errors.length > 0) {
|
|
911
798
|
continue;
|
|
@@ -963,10 +850,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
963
850
|
};
|
|
964
851
|
}
|
|
965
852
|
const normalizedFiles = Array.from(
|
|
966
|
-
new Set(scanResult.files.map((file) =>
|
|
853
|
+
new Set(scanResult.files.map((file) => path6.normalize(file)))
|
|
967
854
|
);
|
|
968
855
|
for (const file of normalizedFiles) {
|
|
969
|
-
const text = await
|
|
856
|
+
const text = await readFile2(file, "utf-8");
|
|
970
857
|
const scIds = extractAnnotatedScIds(text);
|
|
971
858
|
if (scIds.length === 0) {
|
|
972
859
|
continue;
|
|
@@ -1025,20 +912,20 @@ function formatError3(error2) {
|
|
|
1025
912
|
}
|
|
1026
913
|
|
|
1027
914
|
// src/core/promptsIntegrity.ts
|
|
1028
|
-
import { readFile as
|
|
1029
|
-
import
|
|
915
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
916
|
+
import path8 from "path";
|
|
1030
917
|
|
|
1031
918
|
// src/shared/assets.ts
|
|
1032
919
|
import { existsSync } from "fs";
|
|
1033
|
-
import
|
|
920
|
+
import path7 from "path";
|
|
1034
921
|
import { fileURLToPath } from "url";
|
|
1035
922
|
function getInitAssetsDir() {
|
|
1036
923
|
const base = import.meta.url;
|
|
1037
924
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
1038
|
-
const baseDir =
|
|
925
|
+
const baseDir = path7.dirname(basePath);
|
|
1039
926
|
const candidates = [
|
|
1040
|
-
|
|
1041
|
-
|
|
927
|
+
path7.resolve(baseDir, "../../../assets/init"),
|
|
928
|
+
path7.resolve(baseDir, "../../assets/init")
|
|
1042
929
|
];
|
|
1043
930
|
for (const candidate of candidates) {
|
|
1044
931
|
if (existsSync(candidate)) {
|
|
@@ -1055,11 +942,25 @@ function getInitAssetsDir() {
|
|
|
1055
942
|
}
|
|
1056
943
|
|
|
1057
944
|
// src/core/promptsIntegrity.ts
|
|
1058
|
-
|
|
1059
|
-
|
|
945
|
+
var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
|
|
946
|
+
async function diffProjectPromptsAgainstInitAssets(root, config) {
|
|
947
|
+
const promptsDirConfig = config.paths.promptsDir;
|
|
948
|
+
const promptsDir = path8.isAbsolute(promptsDirConfig) ? promptsDirConfig : path8.resolve(root, promptsDirConfig);
|
|
1060
949
|
let templateDir;
|
|
1061
950
|
try {
|
|
1062
|
-
|
|
951
|
+
const rel = path8.isAbsolute(promptsDirConfig) ? path8.relative(root, promptsDirConfig) : promptsDirConfig;
|
|
952
|
+
const normalized = rel.replace(/^[\\/]+/, "");
|
|
953
|
+
if (normalized.length === 0 || normalized.startsWith("..")) {
|
|
954
|
+
return {
|
|
955
|
+
status: "skipped_missing_assets",
|
|
956
|
+
promptsDir,
|
|
957
|
+
templateDir: "",
|
|
958
|
+
missing: [],
|
|
959
|
+
extra: [],
|
|
960
|
+
changed: []
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
templateDir = path8.join(getInitAssetsDir(), normalized);
|
|
1063
964
|
} catch {
|
|
1064
965
|
return {
|
|
1065
966
|
status: "skipped_missing_assets",
|
|
@@ -1103,6 +1004,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1103
1004
|
extra.push(rel);
|
|
1104
1005
|
}
|
|
1105
1006
|
}
|
|
1007
|
+
const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
|
|
1106
1008
|
const common = intersectKeys(templateByRel, projectByRel);
|
|
1107
1009
|
for (const rel of common) {
|
|
1108
1010
|
const templateAbs = templateByRel.get(rel);
|
|
@@ -1112,8 +1014,8 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1112
1014
|
}
|
|
1113
1015
|
try {
|
|
1114
1016
|
const [a, b] = await Promise.all([
|
|
1115
|
-
|
|
1116
|
-
|
|
1017
|
+
readFile3(templateAbs, "utf-8"),
|
|
1018
|
+
readFile3(projectAbs, "utf-8")
|
|
1117
1019
|
]);
|
|
1118
1020
|
if (normalizeNewlines(a) !== normalizeNewlines(b)) {
|
|
1119
1021
|
changed.push(rel);
|
|
@@ -1122,13 +1024,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1122
1024
|
changed.push(rel);
|
|
1123
1025
|
}
|
|
1124
1026
|
}
|
|
1125
|
-
const status = missing.length > 0 ||
|
|
1027
|
+
const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
|
|
1126
1028
|
return {
|
|
1127
1029
|
status,
|
|
1128
1030
|
promptsDir,
|
|
1129
1031
|
templateDir,
|
|
1130
1032
|
missing: missing.sort(),
|
|
1131
|
-
extra:
|
|
1033
|
+
extra: filteredExtra.sort(),
|
|
1132
1034
|
changed: changed.sort()
|
|
1133
1035
|
};
|
|
1134
1036
|
}
|
|
@@ -1136,7 +1038,7 @@ function normalizeNewlines(text) {
|
|
|
1136
1038
|
return text.replace(/\r\n/g, "\n");
|
|
1137
1039
|
}
|
|
1138
1040
|
function toRel(base, abs) {
|
|
1139
|
-
const rel =
|
|
1041
|
+
const rel = path8.relative(base, abs);
|
|
1140
1042
|
return rel.replace(/[\\/]+/g, "/");
|
|
1141
1043
|
}
|
|
1142
1044
|
function intersectKeys(a, b) {
|
|
@@ -1150,16 +1052,16 @@ function intersectKeys(a, b) {
|
|
|
1150
1052
|
}
|
|
1151
1053
|
|
|
1152
1054
|
// src/core/version.ts
|
|
1153
|
-
import { readFile as
|
|
1154
|
-
import
|
|
1055
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1056
|
+
import path9 from "path";
|
|
1155
1057
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1156
1058
|
async function resolveToolVersion() {
|
|
1157
|
-
if ("1.0.
|
|
1158
|
-
return "1.0.
|
|
1059
|
+
if ("1.0.5".length > 0) {
|
|
1060
|
+
return "1.0.5";
|
|
1159
1061
|
}
|
|
1160
1062
|
try {
|
|
1161
1063
|
const packagePath = resolvePackageJsonPath();
|
|
1162
|
-
const raw = await
|
|
1064
|
+
const raw = await readFile4(packagePath, "utf-8");
|
|
1163
1065
|
const parsed = JSON.parse(raw);
|
|
1164
1066
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
1165
1067
|
return version.length > 0 ? version : "unknown";
|
|
@@ -1170,7 +1072,7 @@ async function resolveToolVersion() {
|
|
|
1170
1072
|
function resolvePackageJsonPath() {
|
|
1171
1073
|
const base = import.meta.url;
|
|
1172
1074
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1173
|
-
return
|
|
1075
|
+
return path9.resolve(path9.dirname(basePath), "../../package.json");
|
|
1174
1076
|
}
|
|
1175
1077
|
|
|
1176
1078
|
// src/core/doctor.ts
|
|
@@ -1196,7 +1098,7 @@ function normalizeGlobs2(values) {
|
|
|
1196
1098
|
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1197
1099
|
}
|
|
1198
1100
|
async function createDoctorData(options) {
|
|
1199
|
-
const startDir =
|
|
1101
|
+
const startDir = path10.resolve(options.startDir);
|
|
1200
1102
|
const checks = [];
|
|
1201
1103
|
const configPath = getConfigPath(startDir);
|
|
1202
1104
|
const search = options.rootExplicit ? {
|
|
@@ -1245,7 +1147,6 @@ async function createDoctorData(options) {
|
|
|
1245
1147
|
"outDir",
|
|
1246
1148
|
"srcDir",
|
|
1247
1149
|
"testsDir",
|
|
1248
|
-
"rulesDir",
|
|
1249
1150
|
"promptsDir"
|
|
1250
1151
|
];
|
|
1251
1152
|
for (const key of pathKeys) {
|
|
@@ -1259,9 +1160,9 @@ async function createDoctorData(options) {
|
|
|
1259
1160
|
details: { path: toRelativePath(root, resolved) }
|
|
1260
1161
|
});
|
|
1261
1162
|
if (key === "promptsDir") {
|
|
1262
|
-
const promptsLocalDir =
|
|
1263
|
-
|
|
1264
|
-
`${
|
|
1163
|
+
const promptsLocalDir = path10.join(
|
|
1164
|
+
path10.dirname(resolved),
|
|
1165
|
+
`${path10.basename(resolved)}.local`
|
|
1265
1166
|
);
|
|
1266
1167
|
const found = await exists4(promptsLocalDir);
|
|
1267
1168
|
addCheck(checks, {
|
|
@@ -1271,12 +1172,12 @@ async function createDoctorData(options) {
|
|
|
1271
1172
|
message: found ? "prompts.local exists (overlay can be used)" : "prompts.local is optional (create it to override prompts)",
|
|
1272
1173
|
details: { path: toRelativePath(root, promptsLocalDir) }
|
|
1273
1174
|
});
|
|
1274
|
-
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
1175
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root, config);
|
|
1275
1176
|
if (diff.status === "skipped_missing_prompts") {
|
|
1276
1177
|
addCheck(checks, {
|
|
1277
1178
|
id: "prompts.integrity",
|
|
1278
1179
|
severity: "info",
|
|
1279
|
-
title: "Prompts integrity (.qfai/prompts)",
|
|
1180
|
+
title: "Prompts integrity (.qfai/assistant/prompts)",
|
|
1280
1181
|
message: "prompts \u304C\u672A\u4F5C\u6210\u306E\u305F\u3081\u691C\u67FB\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F\uFF08'qfai init' \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
|
|
1281
1182
|
details: { promptsDir: toRelativePath(root, diff.promptsDir) }
|
|
1282
1183
|
});
|
|
@@ -1284,7 +1185,7 @@ async function createDoctorData(options) {
|
|
|
1284
1185
|
addCheck(checks, {
|
|
1285
1186
|
id: "prompts.integrity",
|
|
1286
1187
|
severity: "info",
|
|
1287
|
-
title: "Prompts integrity (.qfai/prompts)",
|
|
1188
|
+
title: "Prompts integrity (.qfai/assistant/prompts)",
|
|
1288
1189
|
message: "init assets \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u691C\u67FB\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F\uFF08\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
|
|
1289
1190
|
details: { promptsDir: toRelativePath(root, diff.promptsDir) }
|
|
1290
1191
|
});
|
|
@@ -1292,7 +1193,7 @@ async function createDoctorData(options) {
|
|
|
1292
1193
|
addCheck(checks, {
|
|
1293
1194
|
id: "prompts.integrity",
|
|
1294
1195
|
severity: "ok",
|
|
1295
|
-
title: "Prompts integrity (.qfai/prompts)",
|
|
1196
|
+
title: "Prompts integrity (.qfai/assistant/prompts)",
|
|
1296
1197
|
message: "\u6A19\u6E96 assets \u3068\u4E00\u81F4\u3057\u3066\u3044\u307E\u3059",
|
|
1297
1198
|
details: { promptsDir: toRelativePath(root, diff.promptsDir) }
|
|
1298
1199
|
});
|
|
@@ -1300,15 +1201,15 @@ async function createDoctorData(options) {
|
|
|
1300
1201
|
addCheck(checks, {
|
|
1301
1202
|
id: "prompts.integrity",
|
|
1302
1203
|
severity: "error",
|
|
1303
|
-
title: "Prompts integrity (.qfai/prompts)",
|
|
1304
|
-
message: "\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\u3002prompts \u306E\u76F4\u7DE8\u96C6\u306F\u975E\u63A8\u5968\u3067\u3059\uFF08\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8/\u518D init \u3067\u4E0A\u66F8\u304D\u3055\u308C\u5F97\u307E\u3059\uFF09\u3002",
|
|
1204
|
+
title: "Prompts integrity (.qfai/assistant/prompts)",
|
|
1205
|
+
message: "\u6A19\u6E96\u8CC7\u7523 '.qfai/assistant/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\u3002prompts \u306E\u76F4\u7DE8\u96C6\u306F\u975E\u63A8\u5968\u3067\u3059\uFF08\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8/\u518D init \u3067\u4E0A\u66F8\u304D\u3055\u308C\u5F97\u307E\u3059\uFF09\u3002",
|
|
1305
1206
|
details: {
|
|
1306
1207
|
promptsDir: toRelativePath(root, diff.promptsDir),
|
|
1307
1208
|
missing: diff.missing,
|
|
1308
1209
|
extra: diff.extra,
|
|
1309
1210
|
changed: diff.changed,
|
|
1310
1211
|
nextActions: [
|
|
1311
|
-
"\u5909\u66F4\u5185\u5BB9\u3092 .qfai/prompts.local/** \u306B\u79FB\u3059\uFF08\u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067\u914D\u7F6E\uFF09",
|
|
1212
|
+
"\u5909\u66F4\u5185\u5BB9\u3092 .qfai/assistant/prompts.local/** \u306B\u79FB\u3059\uFF08\u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067\u914D\u7F6E\uFF09",
|
|
1312
1213
|
"\u5FC5\u8981\u306A\u3089 qfai init --force \u3067 prompts \u3092\u6A19\u6E96\u72B6\u614B\u3078\u623B\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u307E\u3059\uFF09"
|
|
1313
1214
|
]
|
|
1314
1215
|
}
|
|
@@ -1334,7 +1235,7 @@ async function createDoctorData(options) {
|
|
|
1334
1235
|
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1335
1236
|
details: { specPacks: entries.length, missingFiles }
|
|
1336
1237
|
});
|
|
1337
|
-
const validateJsonAbs =
|
|
1238
|
+
const validateJsonAbs = path10.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path10.resolve(root, config.output.validateJsonPath);
|
|
1338
1239
|
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1339
1240
|
addCheck(checks, {
|
|
1340
1241
|
id: "output.validateJson",
|
|
@@ -1344,8 +1245,8 @@ async function createDoctorData(options) {
|
|
|
1344
1245
|
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1345
1246
|
});
|
|
1346
1247
|
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1347
|
-
const rel =
|
|
1348
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
1248
|
+
const rel = path10.relative(outDirAbs, validateJsonAbs);
|
|
1249
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path10.isAbsolute(rel);
|
|
1349
1250
|
addCheck(checks, {
|
|
1350
1251
|
id: "output.pathAlignment",
|
|
1351
1252
|
severity: inside ? "ok" : "warning",
|
|
@@ -1468,12 +1369,12 @@ async function detectOutDirCollisions(root) {
|
|
|
1468
1369
|
});
|
|
1469
1370
|
const configPaths = configScan.files;
|
|
1470
1371
|
const configRoots = Array.from(
|
|
1471
|
-
new Set(configPaths.map((configPath) =>
|
|
1372
|
+
new Set(configPaths.map((configPath) => path10.dirname(configPath)))
|
|
1472
1373
|
).sort((a, b) => a.localeCompare(b));
|
|
1473
1374
|
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1474
1375
|
for (const configRoot of configRoots) {
|
|
1475
1376
|
const { config } = await loadConfig(configRoot);
|
|
1476
|
-
const outDir =
|
|
1377
|
+
const outDir = path10.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1477
1378
|
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1478
1379
|
roots.add(configRoot);
|
|
1479
1380
|
outDirToRoots.set(outDir, roots);
|
|
@@ -1499,20 +1400,20 @@ async function detectOutDirCollisions(root) {
|
|
|
1499
1400
|
};
|
|
1500
1401
|
}
|
|
1501
1402
|
async function findMonorepoRoot(startDir) {
|
|
1502
|
-
let current =
|
|
1403
|
+
let current = path10.resolve(startDir);
|
|
1503
1404
|
while (true) {
|
|
1504
|
-
const gitPath =
|
|
1505
|
-
const workspacePath =
|
|
1405
|
+
const gitPath = path10.join(current, ".git");
|
|
1406
|
+
const workspacePath = path10.join(current, "pnpm-workspace.yaml");
|
|
1506
1407
|
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1507
1408
|
return current;
|
|
1508
1409
|
}
|
|
1509
|
-
const parent =
|
|
1410
|
+
const parent = path10.dirname(current);
|
|
1510
1411
|
if (parent === current) {
|
|
1511
1412
|
break;
|
|
1512
1413
|
}
|
|
1513
1414
|
current = parent;
|
|
1514
1415
|
}
|
|
1515
|
-
return
|
|
1416
|
+
return path10.resolve(startDir);
|
|
1516
1417
|
}
|
|
1517
1418
|
|
|
1518
1419
|
// src/cli/lib/logger.ts
|
|
@@ -1554,8 +1455,8 @@ async function runDoctor(options) {
|
|
|
1554
1455
|
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1555
1456
|
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1556
1457
|
if (options.outPath) {
|
|
1557
|
-
const outAbs =
|
|
1558
|
-
await mkdir(
|
|
1458
|
+
const outAbs = path11.isAbsolute(options.outPath) ? options.outPath : path11.resolve(process.cwd(), options.outPath);
|
|
1459
|
+
await mkdir(path11.dirname(outAbs), { recursive: true });
|
|
1559
1460
|
await writeFile(outAbs, `${output}
|
|
1560
1461
|
`, "utf-8");
|
|
1561
1462
|
info(`doctor: wrote ${outAbs}`);
|
|
@@ -1575,11 +1476,11 @@ function shouldFailDoctor(summary, failOn) {
|
|
|
1575
1476
|
}
|
|
1576
1477
|
|
|
1577
1478
|
// src/cli/commands/init.ts
|
|
1578
|
-
import
|
|
1479
|
+
import path13 from "path";
|
|
1579
1480
|
|
|
1580
1481
|
// src/cli/lib/fs.ts
|
|
1581
1482
|
import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
|
|
1582
|
-
import
|
|
1483
|
+
import path12 from "path";
|
|
1583
1484
|
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1584
1485
|
const files = await collectTemplateFiles(sourceRoot);
|
|
1585
1486
|
return copyFiles(files, sourceRoot, destRoot, options);
|
|
@@ -1587,7 +1488,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
|
1587
1488
|
async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
|
|
1588
1489
|
const allFiles = [];
|
|
1589
1490
|
for (const relPath of relativePaths) {
|
|
1590
|
-
const fullPath =
|
|
1491
|
+
const fullPath = path12.join(sourceRoot, relPath);
|
|
1591
1492
|
const files = await collectTemplateFiles(fullPath);
|
|
1592
1493
|
allFiles.push(...files);
|
|
1593
1494
|
}
|
|
@@ -1597,13 +1498,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1597
1498
|
const copied = [];
|
|
1598
1499
|
const skipped = [];
|
|
1599
1500
|
const conflicts = [];
|
|
1600
|
-
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
1601
|
-
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
1501
|
+
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
|
|
1502
|
+
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
|
|
1602
1503
|
const isProtectedRelative = (relative) => {
|
|
1603
1504
|
if (protectPrefixes.length === 0) {
|
|
1604
1505
|
return false;
|
|
1605
1506
|
}
|
|
1606
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1507
|
+
const normalized = relative.replace(/[\\/]+/g, path12.sep);
|
|
1607
1508
|
return protectPrefixes.some(
|
|
1608
1509
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1609
1510
|
);
|
|
@@ -1612,7 +1513,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1612
1513
|
if (excludePrefixes.length === 0) {
|
|
1613
1514
|
return false;
|
|
1614
1515
|
}
|
|
1615
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1516
|
+
const normalized = relative.replace(/[\\/]+/g, path12.sep);
|
|
1616
1517
|
return excludePrefixes.some(
|
|
1617
1518
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1618
1519
|
);
|
|
@@ -1620,14 +1521,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1620
1521
|
const conflictPolicy = options.conflictPolicy ?? "error";
|
|
1621
1522
|
if (!options.force && conflictPolicy === "error") {
|
|
1622
1523
|
for (const file of files) {
|
|
1623
|
-
const relative =
|
|
1524
|
+
const relative = path12.relative(sourceRoot, file);
|
|
1624
1525
|
if (isExcludedRelative(relative)) {
|
|
1625
1526
|
continue;
|
|
1626
1527
|
}
|
|
1627
1528
|
if (isProtectedRelative(relative)) {
|
|
1628
1529
|
continue;
|
|
1629
1530
|
}
|
|
1630
|
-
const dest =
|
|
1531
|
+
const dest = path12.join(destRoot, relative);
|
|
1631
1532
|
if (!await shouldWrite(dest, options.force)) {
|
|
1632
1533
|
conflicts.push(dest);
|
|
1633
1534
|
}
|
|
@@ -1637,18 +1538,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1637
1538
|
}
|
|
1638
1539
|
}
|
|
1639
1540
|
for (const file of files) {
|
|
1640
|
-
const relative =
|
|
1541
|
+
const relative = path12.relative(sourceRoot, file);
|
|
1641
1542
|
if (isExcludedRelative(relative)) {
|
|
1642
1543
|
continue;
|
|
1643
1544
|
}
|
|
1644
|
-
const dest =
|
|
1545
|
+
const dest = path12.join(destRoot, relative);
|
|
1645
1546
|
const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
|
|
1646
1547
|
if (!await shouldWrite(dest, forceForThisFile)) {
|
|
1647
1548
|
skipped.push(dest);
|
|
1648
1549
|
continue;
|
|
1649
1550
|
}
|
|
1650
1551
|
if (!options.dryRun) {
|
|
1651
|
-
await mkdir2(
|
|
1552
|
+
await mkdir2(path12.dirname(dest), { recursive: true });
|
|
1652
1553
|
await copyFile(file, dest);
|
|
1653
1554
|
}
|
|
1654
1555
|
copied.push(dest);
|
|
@@ -1672,7 +1573,7 @@ async function collectTemplateFiles(root) {
|
|
|
1672
1573
|
}
|
|
1673
1574
|
const items = await readdir3(root, { withFileTypes: true });
|
|
1674
1575
|
for (const item of items) {
|
|
1675
|
-
const fullPath =
|
|
1576
|
+
const fullPath = path12.join(root, item.name);
|
|
1676
1577
|
if (item.isDirectory()) {
|
|
1677
1578
|
const nested = await collectTemplateFiles(fullPath);
|
|
1678
1579
|
entries.push(...nested);
|
|
@@ -1702,13 +1603,13 @@ async function exists5(target) {
|
|
|
1702
1603
|
// src/cli/commands/init.ts
|
|
1703
1604
|
async function runInit(options) {
|
|
1704
1605
|
const assetsRoot = getInitAssetsDir();
|
|
1705
|
-
const rootAssets =
|
|
1706
|
-
const qfaiAssets =
|
|
1707
|
-
const destRoot =
|
|
1708
|
-
const destQfai =
|
|
1606
|
+
const rootAssets = path13.join(assetsRoot, "root");
|
|
1607
|
+
const qfaiAssets = path13.join(assetsRoot, ".qfai");
|
|
1608
|
+
const destRoot = path13.resolve(options.dir);
|
|
1609
|
+
const destQfai = path13.join(destRoot, ".qfai");
|
|
1709
1610
|
if (options.force) {
|
|
1710
1611
|
info(
|
|
1711
|
-
"NOTE: --force \u306F .qfai/prompts/** \u306E\u307F\u4E0A\u66F8\u304D\u3057\u307E\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u3001specs/contracts \u7B49\u306F\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\uFF09\u3002"
|
|
1612
|
+
"NOTE: --force \u306F .qfai/assistant/prompts/** \u306E\u307F\u4E0A\u66F8\u304D\u3057\u307E\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u3001specs/contracts \u7B49\u306F\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\uFF09\u3002"
|
|
1712
1613
|
);
|
|
1713
1614
|
}
|
|
1714
1615
|
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
@@ -1720,18 +1621,18 @@ async function runInit(options) {
|
|
|
1720
1621
|
force: false,
|
|
1721
1622
|
dryRun: options.dryRun,
|
|
1722
1623
|
conflictPolicy: "skip",
|
|
1723
|
-
protect: ["prompts.local"],
|
|
1724
|
-
exclude: ["prompts"]
|
|
1624
|
+
protect: ["assistant/prompts.local"],
|
|
1625
|
+
exclude: ["assistant/prompts"]
|
|
1725
1626
|
});
|
|
1726
1627
|
const promptsResult = await copyTemplatePaths(
|
|
1727
1628
|
qfaiAssets,
|
|
1728
1629
|
destQfai,
|
|
1729
|
-
["prompts"],
|
|
1630
|
+
["assistant/prompts"],
|
|
1730
1631
|
{
|
|
1731
1632
|
force: options.force,
|
|
1732
1633
|
dryRun: options.dryRun,
|
|
1733
1634
|
conflictPolicy: "skip",
|
|
1734
|
-
protect: ["prompts.local"]
|
|
1635
|
+
protect: ["assistant/prompts.local"]
|
|
1735
1636
|
}
|
|
1736
1637
|
);
|
|
1737
1638
|
report(
|
|
@@ -1752,8 +1653,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1752
1653
|
}
|
|
1753
1654
|
|
|
1754
1655
|
// src/cli/commands/report.ts
|
|
1755
|
-
import { mkdir as mkdir3, readFile as
|
|
1756
|
-
import
|
|
1656
|
+
import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
|
|
1657
|
+
import path21 from "path";
|
|
1757
1658
|
|
|
1758
1659
|
// src/core/normalize.ts
|
|
1759
1660
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1793,12 +1694,12 @@ function normalizeValidationResult(root, result) {
|
|
|
1793
1694
|
}
|
|
1794
1695
|
|
|
1795
1696
|
// src/core/report.ts
|
|
1796
|
-
import { readFile as
|
|
1797
|
-
import
|
|
1697
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1698
|
+
import path20 from "path";
|
|
1798
1699
|
|
|
1799
1700
|
// src/core/contractIndex.ts
|
|
1800
|
-
import { readFile as
|
|
1801
|
-
import
|
|
1701
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1702
|
+
import path14 from "path";
|
|
1802
1703
|
|
|
1803
1704
|
// src/core/contractsDecl.ts
|
|
1804
1705
|
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
@@ -1820,9 +1721,9 @@ function stripContractDeclarationLines(text) {
|
|
|
1820
1721
|
// src/core/contractIndex.ts
|
|
1821
1722
|
async function buildContractIndex(root, config) {
|
|
1822
1723
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1823
|
-
const uiRoot =
|
|
1824
|
-
const apiRoot =
|
|
1825
|
-
const dbRoot =
|
|
1724
|
+
const uiRoot = path14.join(contractsRoot, "ui");
|
|
1725
|
+
const apiRoot = path14.join(contractsRoot, "api");
|
|
1726
|
+
const dbRoot = path14.join(contractsRoot, "db");
|
|
1826
1727
|
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1827
1728
|
collectUiContractFiles(uiRoot),
|
|
1828
1729
|
collectThemaContractFiles(uiRoot),
|
|
@@ -1842,7 +1743,7 @@ async function buildContractIndex(root, config) {
|
|
|
1842
1743
|
}
|
|
1843
1744
|
async function indexContractFiles(files, index) {
|
|
1844
1745
|
for (const file of files) {
|
|
1845
|
-
const text = await
|
|
1746
|
+
const text = await readFile5(file, "utf-8");
|
|
1846
1747
|
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1847
1748
|
}
|
|
1848
1749
|
}
|
|
@@ -2087,14 +1988,14 @@ function parseSpec(md, file) {
|
|
|
2087
1988
|
}
|
|
2088
1989
|
|
|
2089
1990
|
// src/core/validators/contracts.ts
|
|
2090
|
-
import { access as access6, readFile as
|
|
2091
|
-
import
|
|
1991
|
+
import { access as access6, readFile as readFile6 } from "fs/promises";
|
|
1992
|
+
import path16 from "path";
|
|
2092
1993
|
|
|
2093
1994
|
// src/core/contracts.ts
|
|
2094
|
-
import
|
|
1995
|
+
import path15 from "path";
|
|
2095
1996
|
import { parse as parseYaml2 } from "yaml";
|
|
2096
1997
|
function parseStructuredContract(file, text) {
|
|
2097
|
-
const ext =
|
|
1998
|
+
const ext = path15.extname(file).toLowerCase();
|
|
2098
1999
|
if (ext === ".json") {
|
|
2099
2000
|
return JSON.parse(text);
|
|
2100
2001
|
}
|
|
@@ -2116,14 +2017,14 @@ async function validateContracts(root, config) {
|
|
|
2116
2017
|
const issues = [];
|
|
2117
2018
|
const contractIndex = await buildContractIndex(root, config);
|
|
2118
2019
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2119
|
-
const uiRoot =
|
|
2020
|
+
const uiRoot = path16.join(contractsRoot, "ui");
|
|
2120
2021
|
const themaIds = new Set(
|
|
2121
2022
|
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
2122
2023
|
);
|
|
2123
2024
|
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
2124
2025
|
issues.push(...await validateThemaContracts(uiRoot));
|
|
2125
|
-
issues.push(...await validateApiContracts(
|
|
2126
|
-
issues.push(...await validateDbContracts(
|
|
2026
|
+
issues.push(...await validateApiContracts(path16.join(contractsRoot, "api")));
|
|
2027
|
+
issues.push(...await validateDbContracts(path16.join(contractsRoot, "db")));
|
|
2127
2028
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
2128
2029
|
return issues;
|
|
2129
2030
|
}
|
|
@@ -2142,7 +2043,7 @@ async function validateUiContracts(uiRoot, themaIds) {
|
|
|
2142
2043
|
}
|
|
2143
2044
|
const issues = [];
|
|
2144
2045
|
for (const file of files) {
|
|
2145
|
-
const text = await
|
|
2046
|
+
const text = await readFile6(file, "utf-8");
|
|
2146
2047
|
const declaredIds = extractDeclaredContractIds(text);
|
|
2147
2048
|
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
2148
2049
|
let doc = null;
|
|
@@ -2196,7 +2097,7 @@ async function validateThemaContracts(uiRoot) {
|
|
|
2196
2097
|
}
|
|
2197
2098
|
const issues = [];
|
|
2198
2099
|
for (const file of files) {
|
|
2199
|
-
const text = await
|
|
2100
|
+
const text = await readFile6(file, "utf-8");
|
|
2200
2101
|
const invalidIds = extractInvalidIds(text, [
|
|
2201
2102
|
"SPEC",
|
|
2202
2103
|
"BR",
|
|
@@ -2330,7 +2231,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
2330
2231
|
}
|
|
2331
2232
|
const issues = [];
|
|
2332
2233
|
for (const file of files) {
|
|
2333
|
-
const text = await
|
|
2234
|
+
const text = await readFile6(file, "utf-8");
|
|
2334
2235
|
const invalidIds = extractInvalidIds(text, [
|
|
2335
2236
|
"SPEC",
|
|
2336
2237
|
"BR",
|
|
@@ -2399,7 +2300,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
2399
2300
|
}
|
|
2400
2301
|
const issues = [];
|
|
2401
2302
|
for (const file of files) {
|
|
2402
|
-
const text = await
|
|
2303
|
+
const text = await readFile6(file, "utf-8");
|
|
2403
2304
|
const invalidIds = extractInvalidIds(text, [
|
|
2404
2305
|
"SPEC",
|
|
2405
2306
|
"BR",
|
|
@@ -2596,9 +2497,9 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2596
2497
|
);
|
|
2597
2498
|
return issues;
|
|
2598
2499
|
}
|
|
2599
|
-
const packDir =
|
|
2600
|
-
const packRelative =
|
|
2601
|
-
if (packRelative.startsWith("..") ||
|
|
2500
|
+
const packDir = path16.resolve(uiRoot, packValue);
|
|
2501
|
+
const packRelative = path16.relative(uiRoot, packDir);
|
|
2502
|
+
if (packRelative.startsWith("..") || path16.isAbsolute(packRelative)) {
|
|
2602
2503
|
issues.push(
|
|
2603
2504
|
issue(
|
|
2604
2505
|
"QFAI-ASSET-001",
|
|
@@ -2624,7 +2525,7 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2624
2525
|
);
|
|
2625
2526
|
return issues;
|
|
2626
2527
|
}
|
|
2627
|
-
const assetsYamlPath =
|
|
2528
|
+
const assetsYamlPath = path16.join(packDir, "assets.yaml");
|
|
2628
2529
|
if (!await exists6(assetsYamlPath)) {
|
|
2629
2530
|
issues.push(
|
|
2630
2531
|
issue(
|
|
@@ -2639,7 +2540,7 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2639
2540
|
}
|
|
2640
2541
|
let manifest;
|
|
2641
2542
|
try {
|
|
2642
|
-
const manifestText = await
|
|
2543
|
+
const manifestText = await readFile6(assetsYamlPath, "utf-8");
|
|
2643
2544
|
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
2644
2545
|
} catch (error2) {
|
|
2645
2546
|
issues.push(
|
|
@@ -2712,9 +2613,9 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2712
2613
|
);
|
|
2713
2614
|
continue;
|
|
2714
2615
|
}
|
|
2715
|
-
const assetPath =
|
|
2716
|
-
const assetRelative =
|
|
2717
|
-
if (assetRelative.startsWith("..") ||
|
|
2616
|
+
const assetPath = path16.resolve(packDir, entry.path);
|
|
2617
|
+
const assetRelative = path16.relative(packDir, assetPath);
|
|
2618
|
+
if (assetRelative.startsWith("..") || path16.isAbsolute(assetRelative)) {
|
|
2718
2619
|
issues.push(
|
|
2719
2620
|
issue(
|
|
2720
2621
|
"QFAI-ASSET-004",
|
|
@@ -2755,7 +2656,7 @@ function shouldIgnoreInvalidId(value, doc) {
|
|
|
2755
2656
|
return false;
|
|
2756
2657
|
}
|
|
2757
2658
|
const normalized = packValue.replace(/\\/g, "/");
|
|
2758
|
-
const basename =
|
|
2659
|
+
const basename = path16.posix.basename(normalized);
|
|
2759
2660
|
if (!basename) {
|
|
2760
2661
|
return false;
|
|
2761
2662
|
}
|
|
@@ -2765,7 +2666,7 @@ function isSafeRelativePath(value) {
|
|
|
2765
2666
|
if (!value) {
|
|
2766
2667
|
return false;
|
|
2767
2668
|
}
|
|
2768
|
-
if (
|
|
2669
|
+
if (path16.isAbsolute(value)) {
|
|
2769
2670
|
return false;
|
|
2770
2671
|
}
|
|
2771
2672
|
const normalized = value.replace(/\\/g, "/");
|
|
@@ -2815,13 +2716,8 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2815
2716
|
}
|
|
2816
2717
|
|
|
2817
2718
|
// src/core/validators/delta.ts
|
|
2818
|
-
import { readFile as
|
|
2819
|
-
import
|
|
2820
|
-
var SECTION_RE = /^##\s+変更区分/m;
|
|
2821
|
-
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2822
|
-
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
2823
|
-
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
2824
|
-
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
2719
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2720
|
+
import path17 from "path";
|
|
2825
2721
|
async function validateDeltas(root, config) {
|
|
2826
2722
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2827
2723
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
@@ -2830,10 +2726,9 @@ async function validateDeltas(root, config) {
|
|
|
2830
2726
|
}
|
|
2831
2727
|
const issues = [];
|
|
2832
2728
|
for (const pack of packs) {
|
|
2833
|
-
const deltaPath =
|
|
2834
|
-
let text;
|
|
2729
|
+
const deltaPath = path17.join(pack, "delta.md");
|
|
2835
2730
|
try {
|
|
2836
|
-
|
|
2731
|
+
await readFile7(deltaPath, "utf-8");
|
|
2837
2732
|
} catch (error2) {
|
|
2838
2733
|
if (isMissingFileError2(error2)) {
|
|
2839
2734
|
issues.push(
|
|
@@ -2842,41 +2737,16 @@ async function validateDeltas(root, config) {
|
|
|
2842
2737
|
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2843
2738
|
"error",
|
|
2844
2739
|
deltaPath,
|
|
2845
|
-
"delta.exists"
|
|
2740
|
+
"delta.exists",
|
|
2741
|
+
void 0,
|
|
2742
|
+
"change",
|
|
2743
|
+
"spec-xxxx/delta.md \u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30C6\u30F3\u30D7\u30EC\u306F init \u751F\u6210\u7269\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\uFF09\u3002"
|
|
2846
2744
|
)
|
|
2847
2745
|
);
|
|
2848
2746
|
continue;
|
|
2849
2747
|
}
|
|
2850
2748
|
throw error2;
|
|
2851
2749
|
}
|
|
2852
|
-
const hasSection = SECTION_RE.test(text);
|
|
2853
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
2854
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
2855
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
2856
|
-
issues.push(
|
|
2857
|
-
issue2(
|
|
2858
|
-
"QFAI-DELTA-002",
|
|
2859
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002`## \u5909\u66F4\u533A\u5206` \u3068\u30C1\u30A7\u30C3\u30AF\u30DC\u30C3\u30AF\u30B9\uFF08Compatibility / Change/Improvement\uFF09\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2860
|
-
"error",
|
|
2861
|
-
deltaPath,
|
|
2862
|
-
"delta.section"
|
|
2863
|
-
)
|
|
2864
|
-
);
|
|
2865
|
-
continue;
|
|
2866
|
-
}
|
|
2867
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
2868
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
2869
|
-
if (compatibilityChecked === changeChecked) {
|
|
2870
|
-
issues.push(
|
|
2871
|
-
issue2(
|
|
2872
|
-
"QFAI-DELTA-003",
|
|
2873
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
2874
|
-
"error",
|
|
2875
|
-
deltaPath,
|
|
2876
|
-
"delta.classification"
|
|
2877
|
-
)
|
|
2878
|
-
);
|
|
2879
|
-
}
|
|
2880
2750
|
}
|
|
2881
2751
|
return issues;
|
|
2882
2752
|
}
|
|
@@ -2909,8 +2779,8 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2909
2779
|
}
|
|
2910
2780
|
|
|
2911
2781
|
// src/core/validators/ids.ts
|
|
2912
|
-
import { readFile as
|
|
2913
|
-
import
|
|
2782
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2783
|
+
import path18 from "path";
|
|
2914
2784
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2915
2785
|
async function validateDefinedIds(root, config) {
|
|
2916
2786
|
const issues = [];
|
|
@@ -2945,7 +2815,7 @@ async function validateDefinedIds(root, config) {
|
|
|
2945
2815
|
}
|
|
2946
2816
|
async function collectSpecDefinitionIds(files, out) {
|
|
2947
2817
|
for (const file of files) {
|
|
2948
|
-
const text = await
|
|
2818
|
+
const text = await readFile8(file, "utf-8");
|
|
2949
2819
|
const parsed = parseSpec(text, file);
|
|
2950
2820
|
if (parsed.specId) {
|
|
2951
2821
|
recordId(out, parsed.specId, file);
|
|
@@ -2955,7 +2825,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
2955
2825
|
}
|
|
2956
2826
|
async function collectScenarioDefinitionIds(files, out) {
|
|
2957
2827
|
for (const file of files) {
|
|
2958
|
-
const text = await
|
|
2828
|
+
const text = await readFile8(file, "utf-8");
|
|
2959
2829
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2960
2830
|
if (!document || errors.length > 0) {
|
|
2961
2831
|
continue;
|
|
@@ -2976,7 +2846,7 @@ function recordId(out, id, file) {
|
|
|
2976
2846
|
}
|
|
2977
2847
|
function formatFileList(files, root) {
|
|
2978
2848
|
return files.map((file) => {
|
|
2979
|
-
const relative =
|
|
2849
|
+
const relative = path18.relative(root, file);
|
|
2980
2850
|
return relative.length > 0 ? relative : file;
|
|
2981
2851
|
}).join(", ");
|
|
2982
2852
|
}
|
|
@@ -3003,8 +2873,8 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
|
|
|
3003
2873
|
}
|
|
3004
2874
|
|
|
3005
2875
|
// src/core/validators/promptsIntegrity.ts
|
|
3006
|
-
async function validatePromptsIntegrity(root) {
|
|
3007
|
-
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
2876
|
+
async function validatePromptsIntegrity(root, config) {
|
|
2877
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root, config);
|
|
3008
2878
|
if (diff.status !== "modified") {
|
|
3009
2879
|
return [];
|
|
3010
2880
|
}
|
|
@@ -3021,11 +2891,11 @@ async function validatePromptsIntegrity(root) {
|
|
|
3021
2891
|
code: "QFAI-PROMPTS-001",
|
|
3022
2892
|
severity: "error",
|
|
3023
2893
|
category: "change",
|
|
3024
|
-
message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
|
|
2894
|
+
message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/assistant/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
|
|
3025
2895
|
suggested_action: [
|
|
3026
2896
|
"prompts \u306E\u76F4\u7DE8\u96C6\u306F\u975E\u63A8\u5968\u3067\u3059\uFF08\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8/\u518D init \u3067\u4E0A\u66F8\u304D\u3055\u308C\u5F97\u307E\u3059\uFF09\u3002",
|
|
3027
2897
|
"\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
|
|
3028
|
-
"- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
|
|
2898
|
+
"- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/assistant/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
|
|
3029
2899
|
"- \u6A19\u6E96\u72B6\u614B\u3078\u623B\u3059\u5834\u5408: 'qfai init --force' \u3092\u5B9F\u884C\uFF08prompts \u306E\u307F\u4E0A\u66F8\u304D\u3001prompts.local \u306F\u4FDD\u8B77\uFF09"
|
|
3030
2900
|
].join("\n"),
|
|
3031
2901
|
rule: "prompts.integrity"
|
|
@@ -3034,8 +2904,8 @@ async function validatePromptsIntegrity(root) {
|
|
|
3034
2904
|
}
|
|
3035
2905
|
|
|
3036
2906
|
// src/core/validators/scenario.ts
|
|
3037
|
-
import { access as access7, readFile as
|
|
3038
|
-
import
|
|
2907
|
+
import { access as access7, readFile as readFile9 } from "fs/promises";
|
|
2908
|
+
import path19 from "path";
|
|
3039
2909
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
3040
2910
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
3041
2911
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -3058,7 +2928,7 @@ async function validateScenarios(root, config) {
|
|
|
3058
2928
|
}
|
|
3059
2929
|
const issues = [];
|
|
3060
2930
|
for (const entry of entries) {
|
|
3061
|
-
const legacyScenarioPath =
|
|
2931
|
+
const legacyScenarioPath = path19.join(entry.dir, "scenario.md");
|
|
3062
2932
|
if (await fileExists(legacyScenarioPath)) {
|
|
3063
2933
|
issues.push(
|
|
3064
2934
|
issue4(
|
|
@@ -3072,7 +2942,7 @@ async function validateScenarios(root, config) {
|
|
|
3072
2942
|
}
|
|
3073
2943
|
let text;
|
|
3074
2944
|
try {
|
|
3075
|
-
text = await
|
|
2945
|
+
text = await readFile9(entry.scenarioPath, "utf-8");
|
|
3076
2946
|
} catch (error2) {
|
|
3077
2947
|
if (isMissingFileError3(error2)) {
|
|
3078
2948
|
issues.push(
|
|
@@ -3255,7 +3125,7 @@ async function fileExists(target) {
|
|
|
3255
3125
|
}
|
|
3256
3126
|
|
|
3257
3127
|
// src/core/validators/spec.ts
|
|
3258
|
-
import { readFile as
|
|
3128
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3259
3129
|
async function validateSpecs(root, config) {
|
|
3260
3130
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
3261
3131
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -3276,7 +3146,7 @@ async function validateSpecs(root, config) {
|
|
|
3276
3146
|
for (const entry of entries) {
|
|
3277
3147
|
let text;
|
|
3278
3148
|
try {
|
|
3279
|
-
text = await
|
|
3149
|
+
text = await readFile10(entry.specPath, "utf-8");
|
|
3280
3150
|
} catch (error2) {
|
|
3281
3151
|
if (isMissingFileError4(error2)) {
|
|
3282
3152
|
issues.push(
|
|
@@ -3430,7 +3300,7 @@ function isMissingFileError4(error2) {
|
|
|
3430
3300
|
}
|
|
3431
3301
|
|
|
3432
3302
|
// src/core/validators/traceability.ts
|
|
3433
|
-
import { readFile as
|
|
3303
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3434
3304
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
3435
3305
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
3436
3306
|
async function validateTraceability(root, config) {
|
|
@@ -3450,7 +3320,7 @@ async function validateTraceability(root, config) {
|
|
|
3450
3320
|
const contractIndex = await buildContractIndex(root, config);
|
|
3451
3321
|
const contractIds = contractIndex.ids;
|
|
3452
3322
|
for (const file of specFiles) {
|
|
3453
|
-
const text = await
|
|
3323
|
+
const text = await readFile11(file, "utf-8");
|
|
3454
3324
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
3455
3325
|
const parsed = parseSpec(text, file);
|
|
3456
3326
|
if (parsed.specId) {
|
|
@@ -3523,7 +3393,7 @@ async function validateTraceability(root, config) {
|
|
|
3523
3393
|
}
|
|
3524
3394
|
}
|
|
3525
3395
|
for (const file of scenarioFiles) {
|
|
3526
|
-
const text = await
|
|
3396
|
+
const text = await readFile11(file, "utf-8");
|
|
3527
3397
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
3528
3398
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
3529
3399
|
allowCommentPrefix: true
|
|
@@ -3845,7 +3715,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
3845
3715
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
3846
3716
|
let found = false;
|
|
3847
3717
|
for (const file of targetFiles) {
|
|
3848
|
-
const text = await
|
|
3718
|
+
const text = await readFile11(file, "utf-8");
|
|
3849
3719
|
if (pattern.test(text)) {
|
|
3850
3720
|
found = true;
|
|
3851
3721
|
break;
|
|
@@ -3896,7 +3766,7 @@ async function validateProject(root, configResult) {
|
|
|
3896
3766
|
const { config, issues: configIssues } = resolved;
|
|
3897
3767
|
const issues = [
|
|
3898
3768
|
...configIssues,
|
|
3899
|
-
...await validatePromptsIntegrity(root),
|
|
3769
|
+
...await validatePromptsIntegrity(root, config),
|
|
3900
3770
|
...await validateSpecs(root, config),
|
|
3901
3771
|
...await validateDeltas(root, config),
|
|
3902
3772
|
...await validateScenarios(root, config),
|
|
@@ -3945,15 +3815,15 @@ var ID_PREFIXES2 = [
|
|
|
3945
3815
|
"THEMA"
|
|
3946
3816
|
];
|
|
3947
3817
|
async function createReportData(root, validation, configResult) {
|
|
3948
|
-
const resolvedRoot =
|
|
3818
|
+
const resolvedRoot = path20.resolve(root);
|
|
3949
3819
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3950
3820
|
const config = resolved.config;
|
|
3951
3821
|
const configPath = resolved.configPath;
|
|
3952
3822
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3953
3823
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3954
|
-
const apiRoot =
|
|
3955
|
-
const uiRoot =
|
|
3956
|
-
const dbRoot =
|
|
3824
|
+
const apiRoot = path20.join(contractsRoot, "api");
|
|
3825
|
+
const uiRoot = path20.join(contractsRoot, "ui");
|
|
3826
|
+
const dbRoot = path20.join(contractsRoot, "db");
|
|
3957
3827
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3958
3828
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3959
3829
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -4418,11 +4288,9 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
4418
4288
|
"- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4419
4289
|
);
|
|
4420
4290
|
}
|
|
4291
|
+
lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
|
|
4421
4292
|
lines.push(
|
|
4422
|
-
"- \
|
|
4423
|
-
);
|
|
4424
|
-
lines.push(
|
|
4425
|
-
"- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
|
|
4293
|
+
"- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/assistant/instructions/constitution.md`"
|
|
4426
4294
|
);
|
|
4427
4295
|
return lines.join("\n");
|
|
4428
4296
|
}
|
|
@@ -4437,7 +4305,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
4437
4305
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
4438
4306
|
}
|
|
4439
4307
|
for (const file of specFiles) {
|
|
4440
|
-
const text = await
|
|
4308
|
+
const text = await readFile12(file, "utf-8");
|
|
4441
4309
|
const parsed = parseSpec(text, file);
|
|
4442
4310
|
const specKey = parsed.specId;
|
|
4443
4311
|
if (!specKey) {
|
|
@@ -4479,7 +4347,7 @@ async function collectIds(files) {
|
|
|
4479
4347
|
THEMA: /* @__PURE__ */ new Set()
|
|
4480
4348
|
};
|
|
4481
4349
|
for (const file of files) {
|
|
4482
|
-
const text = await
|
|
4350
|
+
const text = await readFile12(file, "utf-8");
|
|
4483
4351
|
for (const prefix of ID_PREFIXES2) {
|
|
4484
4352
|
const ids = extractIds(text, prefix);
|
|
4485
4353
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -4498,7 +4366,7 @@ async function collectIds(files) {
|
|
|
4498
4366
|
async function collectUpstreamIds(files) {
|
|
4499
4367
|
const ids = /* @__PURE__ */ new Set();
|
|
4500
4368
|
for (const file of files) {
|
|
4501
|
-
const text = await
|
|
4369
|
+
const text = await readFile12(file, "utf-8");
|
|
4502
4370
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
4503
4371
|
}
|
|
4504
4372
|
return ids;
|
|
@@ -4519,7 +4387,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
4519
4387
|
}
|
|
4520
4388
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
4521
4389
|
for (const file of targetFiles) {
|
|
4522
|
-
const text = await
|
|
4390
|
+
const text = await readFile12(file, "utf-8");
|
|
4523
4391
|
if (pattern.test(text)) {
|
|
4524
4392
|
return true;
|
|
4525
4393
|
}
|
|
@@ -4656,7 +4524,7 @@ function warnIfTruncated(scan, context) {
|
|
|
4656
4524
|
|
|
4657
4525
|
// src/cli/commands/report.ts
|
|
4658
4526
|
async function runReport(options) {
|
|
4659
|
-
const root =
|
|
4527
|
+
const root = path21.resolve(options.root);
|
|
4660
4528
|
const configResult = await loadConfig(root);
|
|
4661
4529
|
let validation;
|
|
4662
4530
|
if (options.runValidate) {
|
|
@@ -4673,7 +4541,7 @@ async function runReport(options) {
|
|
|
4673
4541
|
validation = normalized;
|
|
4674
4542
|
} else {
|
|
4675
4543
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
4676
|
-
const inputPath =
|
|
4544
|
+
const inputPath = path21.isAbsolute(input) ? input : path21.resolve(root, input);
|
|
4677
4545
|
try {
|
|
4678
4546
|
validation = await readValidationResult(inputPath);
|
|
4679
4547
|
} catch (err) {
|
|
@@ -4684,7 +4552,7 @@ async function runReport(options) {
|
|
|
4684
4552
|
"",
|
|
4685
4553
|
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
4686
4554
|
" qfai validate",
|
|
4687
|
-
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/
|
|
4555
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/report/validate.json\uFF09",
|
|
4688
4556
|
"",
|
|
4689
4557
|
"\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
4690
4558
|
"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"
|
|
@@ -4700,10 +4568,10 @@ async function runReport(options) {
|
|
|
4700
4568
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4701
4569
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4702
4570
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4703
|
-
const defaultOut = options.format === "json" ?
|
|
4571
|
+
const defaultOut = options.format === "json" ? path21.join(outRoot, "report.json") : path21.join(outRoot, "report.md");
|
|
4704
4572
|
const out = options.outPath ?? defaultOut;
|
|
4705
|
-
const outPath =
|
|
4706
|
-
await mkdir3(
|
|
4573
|
+
const outPath = path21.isAbsolute(out) ? out : path21.resolve(root, out);
|
|
4574
|
+
await mkdir3(path21.dirname(outPath), { recursive: true });
|
|
4707
4575
|
await writeFile2(outPath, `${output}
|
|
4708
4576
|
`, "utf-8");
|
|
4709
4577
|
info(
|
|
@@ -4712,7 +4580,7 @@ async function runReport(options) {
|
|
|
4712
4580
|
info(`wrote report: ${outPath}`);
|
|
4713
4581
|
}
|
|
4714
4582
|
async function readValidationResult(inputPath) {
|
|
4715
|
-
const raw = await
|
|
4583
|
+
const raw = await readFile13(inputPath, "utf-8");
|
|
4716
4584
|
const parsed = JSON.parse(raw);
|
|
4717
4585
|
if (!isValidationResult(parsed)) {
|
|
4718
4586
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -4768,15 +4636,15 @@ function isMissingFileError5(error2) {
|
|
|
4768
4636
|
return record2.code === "ENOENT";
|
|
4769
4637
|
}
|
|
4770
4638
|
async function writeValidationResult(root, outputPath, result) {
|
|
4771
|
-
const abs =
|
|
4772
|
-
await mkdir3(
|
|
4639
|
+
const abs = path21.isAbsolute(outputPath) ? outputPath : path21.resolve(root, outputPath);
|
|
4640
|
+
await mkdir3(path21.dirname(abs), { recursive: true });
|
|
4773
4641
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
4774
4642
|
`, "utf-8");
|
|
4775
4643
|
}
|
|
4776
4644
|
|
|
4777
4645
|
// src/cli/commands/validate.ts
|
|
4778
4646
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
4779
|
-
import
|
|
4647
|
+
import path22 from "path";
|
|
4780
4648
|
|
|
4781
4649
|
// src/cli/lib/failOn.ts
|
|
4782
4650
|
function shouldFail(result, failOn) {
|
|
@@ -4791,7 +4659,7 @@ function shouldFail(result, failOn) {
|
|
|
4791
4659
|
|
|
4792
4660
|
// src/cli/commands/validate.ts
|
|
4793
4661
|
async function runValidate(options) {
|
|
4794
|
-
const root =
|
|
4662
|
+
const root = path22.resolve(options.root);
|
|
4795
4663
|
const configResult = await loadConfig(root);
|
|
4796
4664
|
const result = await validateProject(root, configResult);
|
|
4797
4665
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -4916,12 +4784,12 @@ function issueKey(issue7) {
|
|
|
4916
4784
|
}
|
|
4917
4785
|
async function emitJson(result, root, jsonPath) {
|
|
4918
4786
|
const abs = resolveJsonPath(root, jsonPath);
|
|
4919
|
-
await mkdir4(
|
|
4787
|
+
await mkdir4(path22.dirname(abs), { recursive: true });
|
|
4920
4788
|
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
4921
4789
|
`, "utf-8");
|
|
4922
4790
|
}
|
|
4923
4791
|
function resolveJsonPath(root, jsonPath) {
|
|
4924
|
-
return
|
|
4792
|
+
return path22.isAbsolute(jsonPath) ? jsonPath : path22.resolve(root, jsonPath);
|
|
4925
4793
|
}
|
|
4926
4794
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
4927
4795
|
|
|
@@ -4934,7 +4802,6 @@ function parseArgs(argv, cwd) {
|
|
|
4934
4802
|
force: false,
|
|
4935
4803
|
yes: false,
|
|
4936
4804
|
dryRun: false,
|
|
4937
|
-
analyzeList: false,
|
|
4938
4805
|
reportFormat: "md",
|
|
4939
4806
|
reportRunValidate: false,
|
|
4940
4807
|
doctorFormat: "text",
|
|
@@ -4986,18 +4853,6 @@ function parseArgs(argv, cwd) {
|
|
|
4986
4853
|
case "--dry-run":
|
|
4987
4854
|
options.dryRun = true;
|
|
4988
4855
|
break;
|
|
4989
|
-
case "--list":
|
|
4990
|
-
options.analyzeList = true;
|
|
4991
|
-
break;
|
|
4992
|
-
case "--prompt":
|
|
4993
|
-
{
|
|
4994
|
-
const next = readOptionValue(args, i);
|
|
4995
|
-
if (next) {
|
|
4996
|
-
options.analyzePrompt = next;
|
|
4997
|
-
i += 1;
|
|
4998
|
-
}
|
|
4999
|
-
}
|
|
5000
|
-
break;
|
|
5001
4856
|
case "--format": {
|
|
5002
4857
|
const next = readOptionValue(args, i);
|
|
5003
4858
|
if (next === null) {
|
|
@@ -5131,17 +4986,6 @@ async function run(argv, cwd) {
|
|
|
5131
4986
|
yes: options.yes
|
|
5132
4987
|
});
|
|
5133
4988
|
return;
|
|
5134
|
-
case "analyze":
|
|
5135
|
-
{
|
|
5136
|
-
const resolvedRoot = await resolveRoot(options);
|
|
5137
|
-
const exitCode = await runAnalyze({
|
|
5138
|
-
root: resolvedRoot,
|
|
5139
|
-
list: options.analyzeList,
|
|
5140
|
-
...options.analyzePrompt !== void 0 ? { prompt: options.analyzePrompt } : {}
|
|
5141
|
-
});
|
|
5142
|
-
process.exitCode = exitCode;
|
|
5143
|
-
}
|
|
5144
|
-
return;
|
|
5145
4989
|
case "validate":
|
|
5146
4990
|
{
|
|
5147
4991
|
const resolvedRoot = await resolveRoot(options);
|
|
@@ -5189,7 +5033,6 @@ function usage() {
|
|
|
5189
5033
|
|
|
5190
5034
|
Commands:
|
|
5191
5035
|
init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
|
|
5192
|
-
analyze \u610F\u5473\u30EC\u30D9\u30EB\u306E\u30EC\u30D3\u30E5\u30FC\u88DC\u52A9\uFF08\u30D7\u30ED\u30F3\u30D7\u30C8\u51FA\u529B\uFF09
|
|
5193
5036
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
5194
5037
|
report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
|
|
5195
5038
|
doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
|
|
@@ -5197,11 +5040,9 @@ Commands:
|
|
|
5197
5040
|
Options:
|
|
5198
5041
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
5199
5042
|
--dir <path> init \u306E\u51FA\u529B\u5148
|
|
5200
|
-
--force init: .qfai/prompts \u306E\u307F\u4E0A\u66F8\u304D\uFF08\u305D\u308C\u4EE5\u5916\u306F\u65E2\u5B58\u304C\u3042\u308C\u3070\u30B9\u30AD\u30C3\u30D7\uFF09
|
|
5043
|
+
--force init: .qfai/assistant/prompts \u306E\u307F\u4E0A\u66F8\u304D\uFF08\u305D\u308C\u4EE5\u5916\u306F\u65E2\u5B58\u304C\u3042\u308C\u3070\u30B9\u30AD\u30C3\u30D7\uFF09
|
|
5201
5044
|
--yes init: \u4E88\u7D04\u30D5\u30E9\u30B0\uFF08\u73FE\u72B6\u306F\u975E\u5BFE\u8A71\u306E\u305F\u3081\u6319\u52D5\u5DEE\u306A\u3057\u3002\u5C06\u6765\u306E\u5BFE\u8A71\u5C0E\u5165\u6642\u306B\u81EA\u52D5Yes\uFF09
|
|
5202
5045
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
5203
|
-
--list analyze: \u5229\u7528\u53EF\u80FD\u306A\u30D7\u30ED\u30F3\u30D7\u30C8\u4E00\u89A7\u3092\u8868\u793A
|
|
5204
|
-
--prompt <name> analyze: \u6307\u5B9A\u30D7\u30ED\u30F3\u30D7\u30C8\uFF08.md\u7701\u7565\u53EF\uFF09\u3092\u51FA\u529B
|
|
5205
5046
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|
|
5206
5047
|
--format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
|
|
5207
5048
|
--format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F
|