qfai 0.8.1 → 0.9.2

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.
@@ -1,16 +1,189 @@
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
+ async function collectFiles(root, options = {}) {
20
+ const entries = [];
21
+ if (!await exists(root)) {
22
+ return entries;
23
+ }
24
+ const ignoreDirs = /* @__PURE__ */ new Set([
25
+ ...DEFAULT_IGNORE_DIRS,
26
+ ...options.ignoreDirs ?? []
27
+ ]);
28
+ const extensions = options.extensions?.map((ext) => ext.toLowerCase()) ?? [];
29
+ await walk(root, root, ignoreDirs, extensions, entries);
30
+ return entries;
31
+ }
32
+ async function collectFilesByGlobs(root, options) {
33
+ if (options.globs.length === 0) {
34
+ return [];
35
+ }
36
+ return fg(options.globs, {
37
+ cwd: root,
38
+ ignore: options.ignore ?? [],
39
+ onlyFiles: true,
40
+ absolute: true,
41
+ unique: true
42
+ });
43
+ }
44
+ async function walk(base, current, ignoreDirs, extensions, out) {
45
+ const items = await readdir(current, { withFileTypes: true });
46
+ for (const item of items) {
47
+ const fullPath = path.join(current, item.name);
48
+ if (item.isDirectory()) {
49
+ if (ignoreDirs.has(item.name)) {
50
+ continue;
51
+ }
52
+ await walk(base, fullPath, ignoreDirs, extensions, out);
53
+ continue;
54
+ }
55
+ if (item.isFile()) {
56
+ if (extensions.length > 0) {
57
+ const ext = path.extname(item.name).toLowerCase();
58
+ if (!extensions.includes(ext)) {
59
+ continue;
60
+ }
61
+ }
62
+ out.push(fullPath);
63
+ }
64
+ }
65
+ }
66
+ async function exists(target) {
67
+ try {
68
+ await access(target);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // src/cli/commands/analyze.ts
76
+ async function runAnalyze(options) {
77
+ const root = path2.resolve(options.root);
78
+ const localDir = path2.join(root, ".qfai", "prompts.local", "analyze");
79
+ const standardDir = path2.join(root, ".qfai", "prompts", "analyze");
80
+ const available = await listPromptNames([localDir, standardDir]);
81
+ const promptName = normalizePromptName(options.prompt);
82
+ if (!promptName || options.list) {
83
+ emitList(available);
84
+ return 0;
85
+ }
86
+ const resolved = await resolvePromptPath(promptName, [localDir, standardDir]);
87
+ if (!resolved) {
88
+ emitPromptNotFound(promptName, available);
89
+ return 1;
90
+ }
91
+ const content = await readFile(resolved, "utf-8");
92
+ process.stdout.write(content);
93
+ if (!content.endsWith("\n")) {
94
+ process.stdout.write("\n");
95
+ }
96
+ return 0;
97
+ }
98
+ function normalizePromptName(value) {
99
+ const trimmed = (value ?? "").trim();
100
+ if (!trimmed) {
101
+ return null;
102
+ }
103
+ return trimmed.endsWith(".md") ? trimmed.slice(0, -3) : trimmed;
104
+ }
105
+ async function listPromptNames(dirs) {
106
+ const byName = /* @__PURE__ */ new Map();
107
+ for (const dir of dirs) {
108
+ const files = await collectFiles(dir, { extensions: [".md"] });
109
+ for (const abs of files) {
110
+ const base = path2.basename(abs);
111
+ if (base.toLowerCase() === "readme.md") {
112
+ continue;
113
+ }
114
+ const name = base.slice(0, -3);
115
+ if (byName.has(name)) {
116
+ continue;
117
+ }
118
+ if (await isDeprecatedPrompt(abs)) {
119
+ continue;
120
+ }
121
+ byName.set(name, abs);
122
+ }
123
+ }
124
+ return [...byName.keys()].sort((a, b) => a.localeCompare(b));
125
+ }
126
+ function emitList(names) {
127
+ process.stdout.write("# qfai analyze: prompts\n\n");
128
+ if (names.length === 0) {
129
+ process.stdout.write(
130
+ "\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"
131
+ );
132
+ return;
133
+ }
134
+ process.stdout.write("\u5229\u7528\u53EF\u80FD\u306A\u30D7\u30ED\u30F3\u30D7\u30C8\u4E00\u89A7:\n\n");
135
+ for (const name of names) {
136
+ process.stdout.write(`- ${name}
137
+ `);
138
+ }
139
+ }
140
+ async function resolvePromptPath(promptName, dirs) {
141
+ const filename = `${promptName}.md`;
142
+ for (const dir of dirs) {
143
+ const full = path2.join(dir, filename);
144
+ try {
145
+ await readFile(full, "utf-8");
146
+ return full;
147
+ } catch {
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+ async function isDeprecatedPrompt(filePath) {
153
+ try {
154
+ const content = await readFile(filePath, "utf-8");
155
+ const firstLine = firstLineOf(content);
156
+ return firstLine.trim() === "# Deprecated";
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+ function firstLineOf(content) {
162
+ return content.match(/^[^\r\n]*/)?.[0] ?? "";
163
+ }
164
+ function emitPromptNotFound(promptName, candidates) {
165
+ process.stderr.write(`qfai analyze: prompt not found: ${promptName}
166
+ `);
167
+ if (candidates.length > 0) {
168
+ process.stderr.write("candidates:\n");
169
+ for (const c of candidates) {
170
+ process.stderr.write(`- ${c}
171
+ `);
172
+ }
173
+ }
174
+ }
175
+
3
176
  // src/cli/commands/doctor.ts
4
177
  import { mkdir, writeFile } from "fs/promises";
5
- import path10 from "path";
178
+ import path11 from "path";
6
179
 
7
180
  // src/core/doctor.ts
8
181
  import { access as access4 } from "fs/promises";
9
- import path9 from "path";
182
+ import path10 from "path";
10
183
 
11
184
  // src/core/config.ts
12
- import { access, readFile } from "fs/promises";
13
- import path from "path";
185
+ import { access as access2, readFile as readFile2 } from "fs/promises";
186
+ import path3 from "path";
14
187
  import { parse as parseYaml } from "yaml";
15
188
  var defaultConfig = {
16
189
  paths: {
@@ -50,17 +223,17 @@ var defaultConfig = {
50
223
  }
51
224
  };
52
225
  function getConfigPath(root) {
53
- return path.join(root, "qfai.config.yaml");
226
+ return path3.join(root, "qfai.config.yaml");
54
227
  }
55
228
  async function findConfigRoot(startDir) {
56
- const resolvedStart = path.resolve(startDir);
229
+ const resolvedStart = path3.resolve(startDir);
57
230
  let current = resolvedStart;
58
231
  while (true) {
59
232
  const configPath = getConfigPath(current);
60
- if (await exists(configPath)) {
233
+ if (await exists2(configPath)) {
61
234
  return { root: current, configPath, found: true };
62
235
  }
63
- const parent = path.dirname(current);
236
+ const parent = path3.dirname(current);
64
237
  if (parent === current) {
65
238
  break;
66
239
  }
@@ -77,7 +250,7 @@ async function loadConfig(root) {
77
250
  const issues = [];
78
251
  let parsed;
79
252
  try {
80
- const raw = await readFile(configPath, "utf-8");
253
+ const raw = await readFile2(configPath, "utf-8");
81
254
  parsed = parseYaml(raw);
82
255
  } catch (error2) {
83
256
  if (isMissingFile(error2)) {
@@ -90,7 +263,7 @@ async function loadConfig(root) {
90
263
  return { config: normalized, issues, configPath };
91
264
  }
92
265
  function resolvePath(root, config, key) {
93
- return path.resolve(root, config.paths[key]);
266
+ return path3.resolve(root, config.paths[key]);
94
267
  }
95
268
  function normalizeConfig(raw, configPath, issues) {
96
269
  if (!isRecord(raw)) {
@@ -390,9 +563,9 @@ function isMissingFile(error2) {
390
563
  }
391
564
  return false;
392
565
  }
393
- async function exists(target) {
566
+ async function exists2(target) {
394
567
  try {
395
- await access(target);
568
+ await access2(target);
396
569
  return true;
397
570
  } catch {
398
571
  return false;
@@ -411,92 +584,24 @@ function isRecord(value) {
411
584
  // src/core/discovery.ts
412
585
  import { access as access3 } from "fs/promises";
413
586
 
414
- // src/core/fs.ts
415
- import { access as access2, readdir } from "fs/promises";
416
- import path2 from "path";
417
- import fg from "fast-glob";
418
- var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
419
- "node_modules",
420
- ".git",
421
- "dist",
422
- ".pnpm",
423
- "tmp",
424
- ".mcp-tools"
425
- ]);
426
- async function collectFiles(root, options = {}) {
427
- const entries = [];
428
- if (!await exists2(root)) {
429
- return entries;
430
- }
431
- const ignoreDirs = /* @__PURE__ */ new Set([
432
- ...DEFAULT_IGNORE_DIRS,
433
- ...options.ignoreDirs ?? []
434
- ]);
435
- const extensions = options.extensions?.map((ext) => ext.toLowerCase()) ?? [];
436
- await walk(root, root, ignoreDirs, extensions, entries);
437
- return entries;
438
- }
439
- async function collectFilesByGlobs(root, options) {
440
- if (options.globs.length === 0) {
441
- return [];
442
- }
443
- return fg(options.globs, {
444
- cwd: root,
445
- ignore: options.ignore ?? [],
446
- onlyFiles: true,
447
- absolute: true,
448
- unique: true
449
- });
450
- }
451
- async function walk(base, current, ignoreDirs, extensions, out) {
452
- const items = await readdir(current, { withFileTypes: true });
453
- for (const item of items) {
454
- const fullPath = path2.join(current, item.name);
455
- if (item.isDirectory()) {
456
- if (ignoreDirs.has(item.name)) {
457
- continue;
458
- }
459
- await walk(base, fullPath, ignoreDirs, extensions, out);
460
- continue;
461
- }
462
- if (item.isFile()) {
463
- if (extensions.length > 0) {
464
- const ext = path2.extname(item.name).toLowerCase();
465
- if (!extensions.includes(ext)) {
466
- continue;
467
- }
468
- }
469
- out.push(fullPath);
470
- }
471
- }
472
- }
473
- async function exists2(target) {
474
- try {
475
- await access2(target);
476
- return true;
477
- } catch {
478
- return false;
479
- }
480
- }
481
-
482
587
  // src/core/specLayout.ts
483
588
  import { readdir as readdir2 } from "fs/promises";
484
- import path3 from "path";
589
+ import path4 from "path";
485
590
  var SPEC_DIR_RE = /^spec-\d{4}$/;
486
591
  async function collectSpecEntries(specsRoot) {
487
592
  const dirs = await listSpecDirs(specsRoot);
488
593
  const entries = dirs.map((dir) => ({
489
594
  dir,
490
- specPath: path3.join(dir, "spec.md"),
491
- deltaPath: path3.join(dir, "delta.md"),
492
- scenarioPath: path3.join(dir, "scenario.md")
595
+ specPath: path4.join(dir, "spec.md"),
596
+ deltaPath: path4.join(dir, "delta.md"),
597
+ scenarioPath: path4.join(dir, "scenario.md")
493
598
  }));
494
599
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
495
600
  }
496
601
  async function listSpecDirs(specsRoot) {
497
602
  try {
498
603
  const items = await readdir2(specsRoot, { withFileTypes: true });
499
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
604
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path4.join(specsRoot, name));
500
605
  } catch (error2) {
501
606
  if (isMissingFileError(error2)) {
502
607
  return [];
@@ -560,15 +665,15 @@ async function exists3(target) {
560
665
  }
561
666
 
562
667
  // src/core/paths.ts
563
- import path4 from "path";
668
+ import path5 from "path";
564
669
  function toRelativePath(root, target) {
565
670
  if (!target) {
566
671
  return target;
567
672
  }
568
- if (!path4.isAbsolute(target)) {
673
+ if (!path5.isAbsolute(target)) {
569
674
  return toPosixPath(target);
570
675
  }
571
- const relative = path4.relative(root, target);
676
+ const relative = path5.relative(root, target);
572
677
  if (!relative) {
573
678
  return ".";
574
679
  }
@@ -579,8 +684,8 @@ function toPosixPath(value) {
579
684
  }
580
685
 
581
686
  // src/core/traceability.ts
582
- import { readFile as readFile2 } from "fs/promises";
583
- import path5 from "path";
687
+ import { readFile as readFile3 } from "fs/promises";
688
+ import path6 from "path";
584
689
 
585
690
  // src/core/gherkin/parse.ts
586
691
  import {
@@ -736,7 +841,7 @@ function extractAnnotatedScIds(text) {
736
841
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
737
842
  const scIds = /* @__PURE__ */ new Set();
738
843
  for (const file of scenarioFiles) {
739
- const text = await readFile2(file, "utf-8");
844
+ const text = await readFile3(file, "utf-8");
740
845
  const { document, errors } = parseScenarioDocument(text, file);
741
846
  if (!document || errors.length > 0) {
742
847
  continue;
@@ -754,7 +859,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
754
859
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
755
860
  const sources = /* @__PURE__ */ new Map();
756
861
  for (const file of scenarioFiles) {
757
- const text = await readFile2(file, "utf-8");
862
+ const text = await readFile3(file, "utf-8");
758
863
  const { document, errors } = parseScenarioDocument(text, file);
759
864
  if (!document || errors.length > 0) {
760
865
  continue;
@@ -807,10 +912,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
807
912
  };
808
913
  }
809
914
  const normalizedFiles = Array.from(
810
- new Set(files.map((file) => path5.normalize(file)))
915
+ new Set(files.map((file) => path6.normalize(file)))
811
916
  );
812
917
  for (const file of normalizedFiles) {
813
- const text = await readFile2(file, "utf-8");
918
+ const text = await readFile3(file, "utf-8");
814
919
  const scIds = extractAnnotatedScIds(text);
815
920
  if (scIds.length === 0) {
816
921
  continue;
@@ -867,20 +972,20 @@ function formatError3(error2) {
867
972
  }
868
973
 
869
974
  // src/core/promptsIntegrity.ts
870
- import { readFile as readFile3 } from "fs/promises";
871
- import path7 from "path";
975
+ import { readFile as readFile4 } from "fs/promises";
976
+ import path8 from "path";
872
977
 
873
978
  // src/shared/assets.ts
874
979
  import { existsSync } from "fs";
875
- import path6 from "path";
980
+ import path7 from "path";
876
981
  import { fileURLToPath } from "url";
877
982
  function getInitAssetsDir() {
878
983
  const base = import.meta.url;
879
984
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
880
- const baseDir = path6.dirname(basePath);
985
+ const baseDir = path7.dirname(basePath);
881
986
  const candidates = [
882
- path6.resolve(baseDir, "../../../assets/init"),
883
- path6.resolve(baseDir, "../../assets/init")
987
+ path7.resolve(baseDir, "../../../assets/init"),
988
+ path7.resolve(baseDir, "../../assets/init")
884
989
  ];
885
990
  for (const candidate of candidates) {
886
991
  if (existsSync(candidate)) {
@@ -898,10 +1003,10 @@ function getInitAssetsDir() {
898
1003
 
899
1004
  // src/core/promptsIntegrity.ts
900
1005
  async function diffProjectPromptsAgainstInitAssets(root) {
901
- const promptsDir = path7.resolve(root, ".qfai", "prompts");
1006
+ const promptsDir = path8.resolve(root, ".qfai", "prompts");
902
1007
  let templateDir;
903
1008
  try {
904
- templateDir = path7.join(getInitAssetsDir(), ".qfai", "prompts");
1009
+ templateDir = path8.join(getInitAssetsDir(), ".qfai", "prompts");
905
1010
  } catch {
906
1011
  return {
907
1012
  status: "skipped_missing_assets",
@@ -954,8 +1059,8 @@ async function diffProjectPromptsAgainstInitAssets(root) {
954
1059
  }
955
1060
  try {
956
1061
  const [a, b] = await Promise.all([
957
- readFile3(templateAbs, "utf-8"),
958
- readFile3(projectAbs, "utf-8")
1062
+ readFile4(templateAbs, "utf-8"),
1063
+ readFile4(projectAbs, "utf-8")
959
1064
  ]);
960
1065
  if (normalizeNewlines(a) !== normalizeNewlines(b)) {
961
1066
  changed.push(rel);
@@ -978,7 +1083,7 @@ function normalizeNewlines(text) {
978
1083
  return text.replace(/\r\n/g, "\n");
979
1084
  }
980
1085
  function toRel(base, abs) {
981
- const rel = path7.relative(base, abs);
1086
+ const rel = path8.relative(base, abs);
982
1087
  return rel.replace(/[\\/]+/g, "/");
983
1088
  }
984
1089
  function intersectKeys(a, b) {
@@ -992,16 +1097,16 @@ function intersectKeys(a, b) {
992
1097
  }
993
1098
 
994
1099
  // src/core/version.ts
995
- import { readFile as readFile4 } from "fs/promises";
996
- import path8 from "path";
1100
+ import { readFile as readFile5 } from "fs/promises";
1101
+ import path9 from "path";
997
1102
  import { fileURLToPath as fileURLToPath2 } from "url";
998
1103
  async function resolveToolVersion() {
999
- if ("0.8.1".length > 0) {
1000
- return "0.8.1";
1104
+ if ("0.9.2".length > 0) {
1105
+ return "0.9.2";
1001
1106
  }
1002
1107
  try {
1003
1108
  const packagePath = resolvePackageJsonPath();
1004
- const raw = await readFile4(packagePath, "utf-8");
1109
+ const raw = await readFile5(packagePath, "utf-8");
1005
1110
  const parsed = JSON.parse(raw);
1006
1111
  const version = typeof parsed.version === "string" ? parsed.version : "";
1007
1112
  return version.length > 0 ? version : "unknown";
@@ -1012,7 +1117,7 @@ async function resolveToolVersion() {
1012
1117
  function resolvePackageJsonPath() {
1013
1118
  const base = import.meta.url;
1014
1119
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1015
- return path8.resolve(path8.dirname(basePath), "../../package.json");
1120
+ return path9.resolve(path9.dirname(basePath), "../../package.json");
1016
1121
  }
1017
1122
 
1018
1123
  // src/core/doctor.ts
@@ -1038,7 +1143,7 @@ function normalizeGlobs2(values) {
1038
1143
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1039
1144
  }
1040
1145
  async function createDoctorData(options) {
1041
- const startDir = path9.resolve(options.startDir);
1146
+ const startDir = path10.resolve(options.startDir);
1042
1147
  const checks = [];
1043
1148
  const configPath = getConfigPath(startDir);
1044
1149
  const search = options.rootExplicit ? {
@@ -1101,9 +1206,9 @@ async function createDoctorData(options) {
1101
1206
  details: { path: toRelativePath(root, resolved) }
1102
1207
  });
1103
1208
  if (key === "promptsDir") {
1104
- const promptsLocalDir = path9.join(
1105
- path9.dirname(resolved),
1106
- `${path9.basename(resolved)}.local`
1209
+ const promptsLocalDir = path10.join(
1210
+ path10.dirname(resolved),
1211
+ `${path10.basename(resolved)}.local`
1107
1212
  );
1108
1213
  const found = await exists4(promptsLocalDir);
1109
1214
  addCheck(checks, {
@@ -1176,7 +1281,7 @@ async function createDoctorData(options) {
1176
1281
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1177
1282
  details: { specPacks: entries.length, missingFiles }
1178
1283
  });
1179
- const validateJsonAbs = path9.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path9.resolve(root, config.output.validateJsonPath);
1284
+ const validateJsonAbs = path10.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path10.resolve(root, config.output.validateJsonPath);
1180
1285
  const validateJsonExists = await exists4(validateJsonAbs);
1181
1286
  addCheck(checks, {
1182
1287
  id: "output.validateJson",
@@ -1186,8 +1291,8 @@ async function createDoctorData(options) {
1186
1291
  details: { path: toRelativePath(root, validateJsonAbs) }
1187
1292
  });
1188
1293
  const outDirAbs = resolvePath(root, config, "outDir");
1189
- const rel = path9.relative(outDirAbs, validateJsonAbs);
1190
- const inside = rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
1294
+ const rel = path10.relative(outDirAbs, validateJsonAbs);
1295
+ const inside = rel !== "" && !rel.startsWith("..") && !path10.isAbsolute(rel);
1191
1296
  addCheck(checks, {
1192
1297
  id: "output.pathAlignment",
1193
1298
  severity: inside ? "ok" : "warning",
@@ -1293,12 +1398,12 @@ async function detectOutDirCollisions(root) {
1293
1398
  ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
1294
1399
  });
1295
1400
  const configRoots = Array.from(
1296
- new Set(configPaths.map((configPath) => path9.dirname(configPath)))
1401
+ new Set(configPaths.map((configPath) => path10.dirname(configPath)))
1297
1402
  ).sort((a, b) => a.localeCompare(b));
1298
1403
  const outDirToRoots = /* @__PURE__ */ new Map();
1299
1404
  for (const configRoot of configRoots) {
1300
1405
  const { config } = await loadConfig(configRoot);
1301
- const outDir = path9.normalize(resolvePath(configRoot, config, "outDir"));
1406
+ const outDir = path10.normalize(resolvePath(configRoot, config, "outDir"));
1302
1407
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1303
1408
  roots.add(configRoot);
1304
1409
  outDirToRoots.set(outDir, roots);
@@ -1315,20 +1420,20 @@ async function detectOutDirCollisions(root) {
1315
1420
  return { monorepoRoot, configRoots, collisions };
1316
1421
  }
1317
1422
  async function findMonorepoRoot(startDir) {
1318
- let current = path9.resolve(startDir);
1423
+ let current = path10.resolve(startDir);
1319
1424
  while (true) {
1320
- const gitPath = path9.join(current, ".git");
1321
- const workspacePath = path9.join(current, "pnpm-workspace.yaml");
1425
+ const gitPath = path10.join(current, ".git");
1426
+ const workspacePath = path10.join(current, "pnpm-workspace.yaml");
1322
1427
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1323
1428
  return current;
1324
1429
  }
1325
- const parent = path9.dirname(current);
1430
+ const parent = path10.dirname(current);
1326
1431
  if (parent === current) {
1327
1432
  break;
1328
1433
  }
1329
1434
  current = parent;
1330
1435
  }
1331
- return path9.resolve(startDir);
1436
+ return path10.resolve(startDir);
1332
1437
  }
1333
1438
 
1334
1439
  // src/cli/lib/logger.ts
@@ -1370,8 +1475,8 @@ async function runDoctor(options) {
1370
1475
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1371
1476
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1372
1477
  if (options.outPath) {
1373
- const outAbs = path10.isAbsolute(options.outPath) ? options.outPath : path10.resolve(process.cwd(), options.outPath);
1374
- await mkdir(path10.dirname(outAbs), { recursive: true });
1478
+ const outAbs = path11.isAbsolute(options.outPath) ? options.outPath : path11.resolve(process.cwd(), options.outPath);
1479
+ await mkdir(path11.dirname(outAbs), { recursive: true });
1375
1480
  await writeFile(outAbs, `${output}
1376
1481
  `, "utf-8");
1377
1482
  info(`doctor: wrote ${outAbs}`);
@@ -1391,11 +1496,11 @@ function shouldFailDoctor(summary, failOn) {
1391
1496
  }
1392
1497
 
1393
1498
  // src/cli/commands/init.ts
1394
- import path12 from "path";
1499
+ import path13 from "path";
1395
1500
 
1396
1501
  // src/cli/lib/fs.ts
1397
1502
  import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1398
- import path11 from "path";
1503
+ import path12 from "path";
1399
1504
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1400
1505
  const files = await collectTemplateFiles(sourceRoot);
1401
1506
  return copyFiles(files, sourceRoot, destRoot, options);
@@ -1403,7 +1508,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
1403
1508
  async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1404
1509
  const allFiles = [];
1405
1510
  for (const relPath of relativePaths) {
1406
- const fullPath = path11.join(sourceRoot, relPath);
1511
+ const fullPath = path12.join(sourceRoot, relPath);
1407
1512
  const files = await collectTemplateFiles(fullPath);
1408
1513
  allFiles.push(...files);
1409
1514
  }
@@ -1413,13 +1518,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1413
1518
  const copied = [];
1414
1519
  const skipped = [];
1415
1520
  const conflicts = [];
1416
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path11.sep);
1417
- const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path11.sep);
1521
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1522
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1418
1523
  const isProtectedRelative = (relative) => {
1419
1524
  if (protectPrefixes.length === 0) {
1420
1525
  return false;
1421
1526
  }
1422
- const normalized = relative.replace(/[\\/]+/g, path11.sep);
1527
+ const normalized = relative.replace(/[\\/]+/g, path12.sep);
1423
1528
  return protectPrefixes.some(
1424
1529
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1425
1530
  );
@@ -1428,7 +1533,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1428
1533
  if (excludePrefixes.length === 0) {
1429
1534
  return false;
1430
1535
  }
1431
- const normalized = relative.replace(/[\\/]+/g, path11.sep);
1536
+ const normalized = relative.replace(/[\\/]+/g, path12.sep);
1432
1537
  return excludePrefixes.some(
1433
1538
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1434
1539
  );
@@ -1436,14 +1541,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1436
1541
  const conflictPolicy = options.conflictPolicy ?? "error";
1437
1542
  if (!options.force && conflictPolicy === "error") {
1438
1543
  for (const file of files) {
1439
- const relative = path11.relative(sourceRoot, file);
1544
+ const relative = path12.relative(sourceRoot, file);
1440
1545
  if (isExcludedRelative(relative)) {
1441
1546
  continue;
1442
1547
  }
1443
1548
  if (isProtectedRelative(relative)) {
1444
1549
  continue;
1445
1550
  }
1446
- const dest = path11.join(destRoot, relative);
1551
+ const dest = path12.join(destRoot, relative);
1447
1552
  if (!await shouldWrite(dest, options.force)) {
1448
1553
  conflicts.push(dest);
1449
1554
  }
@@ -1453,18 +1558,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1453
1558
  }
1454
1559
  }
1455
1560
  for (const file of files) {
1456
- const relative = path11.relative(sourceRoot, file);
1561
+ const relative = path12.relative(sourceRoot, file);
1457
1562
  if (isExcludedRelative(relative)) {
1458
1563
  continue;
1459
1564
  }
1460
- const dest = path11.join(destRoot, relative);
1565
+ const dest = path12.join(destRoot, relative);
1461
1566
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1462
1567
  if (!await shouldWrite(dest, forceForThisFile)) {
1463
1568
  skipped.push(dest);
1464
1569
  continue;
1465
1570
  }
1466
1571
  if (!options.dryRun) {
1467
- await mkdir2(path11.dirname(dest), { recursive: true });
1572
+ await mkdir2(path12.dirname(dest), { recursive: true });
1468
1573
  await copyFile(file, dest);
1469
1574
  }
1470
1575
  copied.push(dest);
@@ -1488,7 +1593,7 @@ async function collectTemplateFiles(root) {
1488
1593
  }
1489
1594
  const items = await readdir3(root, { withFileTypes: true });
1490
1595
  for (const item of items) {
1491
- const fullPath = path11.join(root, item.name);
1596
+ const fullPath = path12.join(root, item.name);
1492
1597
  if (item.isDirectory()) {
1493
1598
  const nested = await collectTemplateFiles(fullPath);
1494
1599
  entries.push(...nested);
@@ -1518,10 +1623,15 @@ async function exists5(target) {
1518
1623
  // src/cli/commands/init.ts
1519
1624
  async function runInit(options) {
1520
1625
  const assetsRoot = getInitAssetsDir();
1521
- const rootAssets = path12.join(assetsRoot, "root");
1522
- const qfaiAssets = path12.join(assetsRoot, ".qfai");
1523
- const destRoot = path12.resolve(options.dir);
1524
- const destQfai = path12.join(destRoot, ".qfai");
1626
+ const rootAssets = path13.join(assetsRoot, "root");
1627
+ const qfaiAssets = path13.join(assetsRoot, ".qfai");
1628
+ const destRoot = path13.resolve(options.dir);
1629
+ const destQfai = path13.join(destRoot, ".qfai");
1630
+ if (options.force) {
1631
+ info(
1632
+ "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"
1633
+ );
1634
+ }
1525
1635
  const rootResult = await copyTemplateTree(rootAssets, destRoot, {
1526
1636
  force: false,
1527
1637
  dryRun: options.dryRun,
@@ -1563,8 +1673,8 @@ function report(copied, skipped, dryRun, label) {
1563
1673
  }
1564
1674
 
1565
1675
  // src/cli/commands/report.ts
1566
- import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
1567
- import path19 from "path";
1676
+ import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
1677
+ import path20 from "path";
1568
1678
 
1569
1679
  // src/core/normalize.ts
1570
1680
  function normalizeIssuePaths(root, issues) {
@@ -1604,12 +1714,12 @@ function normalizeValidationResult(root, result) {
1604
1714
  }
1605
1715
 
1606
1716
  // src/core/report.ts
1607
- import { readFile as readFile12 } from "fs/promises";
1608
- import path18 from "path";
1717
+ import { readFile as readFile13 } from "fs/promises";
1718
+ import path19 from "path";
1609
1719
 
1610
1720
  // src/core/contractIndex.ts
1611
- import { readFile as readFile5 } from "fs/promises";
1612
- import path13 from "path";
1721
+ import { readFile as readFile6 } from "fs/promises";
1722
+ import path14 from "path";
1613
1723
 
1614
1724
  // src/core/contractsDecl.ts
1615
1725
  var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
@@ -1631,9 +1741,9 @@ function stripContractDeclarationLines(text) {
1631
1741
  // src/core/contractIndex.ts
1632
1742
  async function buildContractIndex(root, config) {
1633
1743
  const contractsRoot = resolvePath(root, config, "contractsDir");
1634
- const uiRoot = path13.join(contractsRoot, "ui");
1635
- const apiRoot = path13.join(contractsRoot, "api");
1636
- const dbRoot = path13.join(contractsRoot, "db");
1744
+ const uiRoot = path14.join(contractsRoot, "ui");
1745
+ const apiRoot = path14.join(contractsRoot, "api");
1746
+ const dbRoot = path14.join(contractsRoot, "db");
1637
1747
  const [uiFiles, apiFiles, dbFiles] = await Promise.all([
1638
1748
  collectUiContractFiles(uiRoot),
1639
1749
  collectApiContractFiles(apiRoot),
@@ -1651,7 +1761,7 @@ async function buildContractIndex(root, config) {
1651
1761
  }
1652
1762
  async function indexContractFiles(files, index) {
1653
1763
  for (const file of files) {
1654
- const text = await readFile5(file, "utf-8");
1764
+ const text = await readFile6(file, "utf-8");
1655
1765
  extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1656
1766
  }
1657
1767
  }
@@ -1886,14 +1996,14 @@ function parseSpec(md, file) {
1886
1996
  }
1887
1997
 
1888
1998
  // src/core/validators/contracts.ts
1889
- import { readFile as readFile6 } from "fs/promises";
1890
- import path15 from "path";
1999
+ import { readFile as readFile7 } from "fs/promises";
2000
+ import path16 from "path";
1891
2001
 
1892
2002
  // src/core/contracts.ts
1893
- import path14 from "path";
2003
+ import path15 from "path";
1894
2004
  import { parse as parseYaml2 } from "yaml";
1895
2005
  function parseStructuredContract(file, text) {
1896
- const ext = path14.extname(file).toLowerCase();
2006
+ const ext = path15.extname(file).toLowerCase();
1897
2007
  if (ext === ".json") {
1898
2008
  return JSON.parse(text);
1899
2009
  }
@@ -1913,9 +2023,9 @@ var SQL_DANGEROUS_PATTERNS = [
1913
2023
  async function validateContracts(root, config) {
1914
2024
  const issues = [];
1915
2025
  const contractsRoot = resolvePath(root, config, "contractsDir");
1916
- issues.push(...await validateUiContracts(path15.join(contractsRoot, "ui")));
1917
- issues.push(...await validateApiContracts(path15.join(contractsRoot, "api")));
1918
- issues.push(...await validateDbContracts(path15.join(contractsRoot, "db")));
2026
+ issues.push(...await validateUiContracts(path16.join(contractsRoot, "ui")));
2027
+ issues.push(...await validateApiContracts(path16.join(contractsRoot, "api")));
2028
+ issues.push(...await validateDbContracts(path16.join(contractsRoot, "db")));
1919
2029
  const contractIndex = await buildContractIndex(root, config);
1920
2030
  issues.push(...validateDuplicateContractIds(contractIndex));
1921
2031
  return issues;
@@ -1935,7 +2045,7 @@ async function validateUiContracts(uiRoot) {
1935
2045
  }
1936
2046
  const issues = [];
1937
2047
  for (const file of files) {
1938
- const text = await readFile6(file, "utf-8");
2048
+ const text = await readFile7(file, "utf-8");
1939
2049
  const invalidIds = extractInvalidIds(text, [
1940
2050
  "SPEC",
1941
2051
  "BR",
@@ -1990,7 +2100,7 @@ async function validateApiContracts(apiRoot) {
1990
2100
  }
1991
2101
  const issues = [];
1992
2102
  for (const file of files) {
1993
- const text = await readFile6(file, "utf-8");
2103
+ const text = await readFile7(file, "utf-8");
1994
2104
  const invalidIds = extractInvalidIds(text, [
1995
2105
  "SPEC",
1996
2106
  "BR",
@@ -2058,7 +2168,7 @@ async function validateDbContracts(dbRoot) {
2058
2168
  }
2059
2169
  const issues = [];
2060
2170
  for (const file of files) {
2061
- const text = await readFile6(file, "utf-8");
2171
+ const text = await readFile7(file, "utf-8");
2062
2172
  const invalidIds = extractInvalidIds(text, [
2063
2173
  "SPEC",
2064
2174
  "BR",
@@ -2201,8 +2311,8 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
2201
2311
  }
2202
2312
 
2203
2313
  // src/core/validators/delta.ts
2204
- import { readFile as readFile7 } from "fs/promises";
2205
- import path16 from "path";
2314
+ import { readFile as readFile8 } from "fs/promises";
2315
+ import path17 from "path";
2206
2316
  var SECTION_RE = /^##\s+変更区分/m;
2207
2317
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
2208
2318
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -2216,10 +2326,10 @@ async function validateDeltas(root, config) {
2216
2326
  }
2217
2327
  const issues = [];
2218
2328
  for (const pack of packs) {
2219
- const deltaPath = path16.join(pack, "delta.md");
2329
+ const deltaPath = path17.join(pack, "delta.md");
2220
2330
  let text;
2221
2331
  try {
2222
- text = await readFile7(deltaPath, "utf-8");
2332
+ text = await readFile8(deltaPath, "utf-8");
2223
2333
  } catch (error2) {
2224
2334
  if (isMissingFileError2(error2)) {
2225
2335
  issues.push(
@@ -2295,8 +2405,8 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2295
2405
  }
2296
2406
 
2297
2407
  // src/core/validators/ids.ts
2298
- import { readFile as readFile8 } from "fs/promises";
2299
- import path17 from "path";
2408
+ import { readFile as readFile9 } from "fs/promises";
2409
+ import path18 from "path";
2300
2410
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2301
2411
  async function validateDefinedIds(root, config) {
2302
2412
  const issues = [];
@@ -2331,7 +2441,7 @@ async function validateDefinedIds(root, config) {
2331
2441
  }
2332
2442
  async function collectSpecDefinitionIds(files, out) {
2333
2443
  for (const file of files) {
2334
- const text = await readFile8(file, "utf-8");
2444
+ const text = await readFile9(file, "utf-8");
2335
2445
  const parsed = parseSpec(text, file);
2336
2446
  if (parsed.specId) {
2337
2447
  recordId(out, parsed.specId, file);
@@ -2341,7 +2451,7 @@ async function collectSpecDefinitionIds(files, out) {
2341
2451
  }
2342
2452
  async function collectScenarioDefinitionIds(files, out) {
2343
2453
  for (const file of files) {
2344
- const text = await readFile8(file, "utf-8");
2454
+ const text = await readFile9(file, "utf-8");
2345
2455
  const { document, errors } = parseScenarioDocument(text, file);
2346
2456
  if (!document || errors.length > 0) {
2347
2457
  continue;
@@ -2362,7 +2472,7 @@ function recordId(out, id, file) {
2362
2472
  }
2363
2473
  function formatFileList(files, root) {
2364
2474
  return files.map((file) => {
2365
- const relative = path17.relative(root, file);
2475
+ const relative = path18.relative(root, file);
2366
2476
  return relative.length > 0 ? relative : file;
2367
2477
  }).join(", ");
2368
2478
  }
@@ -2420,7 +2530,7 @@ async function validatePromptsIntegrity(root) {
2420
2530
  }
2421
2531
 
2422
2532
  // src/core/validators/scenario.ts
2423
- import { readFile as readFile9 } from "fs/promises";
2533
+ import { readFile as readFile10 } from "fs/promises";
2424
2534
  var GIVEN_PATTERN = /\bGiven\b/;
2425
2535
  var WHEN_PATTERN = /\bWhen\b/;
2426
2536
  var THEN_PATTERN = /\bThen\b/;
@@ -2446,7 +2556,7 @@ async function validateScenarios(root, config) {
2446
2556
  for (const entry of entries) {
2447
2557
  let text;
2448
2558
  try {
2449
- text = await readFile9(entry.scenarioPath, "utf-8");
2559
+ text = await readFile10(entry.scenarioPath, "utf-8");
2450
2560
  } catch (error2) {
2451
2561
  if (isMissingFileError3(error2)) {
2452
2562
  issues.push(
@@ -2620,7 +2730,7 @@ function isMissingFileError3(error2) {
2620
2730
  }
2621
2731
 
2622
2732
  // src/core/validators/spec.ts
2623
- import { readFile as readFile10 } from "fs/promises";
2733
+ import { readFile as readFile11 } from "fs/promises";
2624
2734
  async function validateSpecs(root, config) {
2625
2735
  const specsRoot = resolvePath(root, config, "specsDir");
2626
2736
  const entries = await collectSpecEntries(specsRoot);
@@ -2641,7 +2751,7 @@ async function validateSpecs(root, config) {
2641
2751
  for (const entry of entries) {
2642
2752
  let text;
2643
2753
  try {
2644
- text = await readFile10(entry.specPath, "utf-8");
2754
+ text = await readFile11(entry.specPath, "utf-8");
2645
2755
  } catch (error2) {
2646
2756
  if (isMissingFileError4(error2)) {
2647
2757
  issues.push(
@@ -2794,7 +2904,7 @@ function isMissingFileError4(error2) {
2794
2904
  }
2795
2905
 
2796
2906
  // src/core/validators/traceability.ts
2797
- import { readFile as readFile11 } from "fs/promises";
2907
+ import { readFile as readFile12 } from "fs/promises";
2798
2908
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2799
2909
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2800
2910
  async function validateTraceability(root, config) {
@@ -2814,7 +2924,7 @@ async function validateTraceability(root, config) {
2814
2924
  const contractIndex = await buildContractIndex(root, config);
2815
2925
  const contractIds = contractIndex.ids;
2816
2926
  for (const file of specFiles) {
2817
- const text = await readFile11(file, "utf-8");
2927
+ const text = await readFile12(file, "utf-8");
2818
2928
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2819
2929
  const parsed = parseSpec(text, file);
2820
2930
  if (parsed.specId) {
@@ -2887,7 +2997,7 @@ async function validateTraceability(root, config) {
2887
2997
  }
2888
2998
  }
2889
2999
  for (const file of scenarioFiles) {
2890
- const text = await readFile11(file, "utf-8");
3000
+ const text = await readFile12(file, "utf-8");
2891
3001
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2892
3002
  const scenarioContractRefs = parseContractRefs(text, {
2893
3003
  allowCommentPrefix: true
@@ -3209,7 +3319,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
3209
3319
  const pattern = buildIdPattern(Array.from(upstreamIds));
3210
3320
  let found = false;
3211
3321
  for (const file of targetFiles) {
3212
- const text = await readFile11(file, "utf-8");
3322
+ const text = await readFile12(file, "utf-8");
3213
3323
  if (pattern.test(text)) {
3214
3324
  found = true;
3215
3325
  break;
@@ -3301,15 +3411,15 @@ function countIssues(issues) {
3301
3411
  // src/core/report.ts
3302
3412
  var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3303
3413
  async function createReportData(root, validation, configResult) {
3304
- const resolvedRoot = path18.resolve(root);
3414
+ const resolvedRoot = path19.resolve(root);
3305
3415
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3306
3416
  const config = resolved.config;
3307
3417
  const configPath = resolved.configPath;
3308
3418
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3309
3419
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3310
- const apiRoot = path18.join(contractsRoot, "api");
3311
- const uiRoot = path18.join(contractsRoot, "ui");
3312
- const dbRoot = path18.join(contractsRoot, "db");
3420
+ const apiRoot = path19.join(contractsRoot, "api");
3421
+ const uiRoot = path19.join(contractsRoot, "ui");
3422
+ const dbRoot = path19.join(contractsRoot, "db");
3313
3423
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3314
3424
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3315
3425
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3780,7 +3890,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3780
3890
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3781
3891
  }
3782
3892
  for (const file of specFiles) {
3783
- const text = await readFile12(file, "utf-8");
3893
+ const text = await readFile13(file, "utf-8");
3784
3894
  const parsed = parseSpec(text, file);
3785
3895
  const specKey = parsed.specId;
3786
3896
  if (!specKey) {
@@ -3821,7 +3931,7 @@ async function collectIds(files) {
3821
3931
  DB: /* @__PURE__ */ new Set()
3822
3932
  };
3823
3933
  for (const file of files) {
3824
- const text = await readFile12(file, "utf-8");
3934
+ const text = await readFile13(file, "utf-8");
3825
3935
  for (const prefix of ID_PREFIXES2) {
3826
3936
  const ids = extractIds(text, prefix);
3827
3937
  ids.forEach((id) => result[prefix].add(id));
@@ -3839,7 +3949,7 @@ async function collectIds(files) {
3839
3949
  async function collectUpstreamIds(files) {
3840
3950
  const ids = /* @__PURE__ */ new Set();
3841
3951
  for (const file of files) {
3842
- const text = await readFile12(file, "utf-8");
3952
+ const text = await readFile13(file, "utf-8");
3843
3953
  extractAllIds(text).forEach((id) => ids.add(id));
3844
3954
  }
3845
3955
  return ids;
@@ -3860,7 +3970,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3860
3970
  }
3861
3971
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3862
3972
  for (const file of targetFiles) {
3863
- const text = await readFile12(file, "utf-8");
3973
+ const text = await readFile13(file, "utf-8");
3864
3974
  if (pattern.test(text)) {
3865
3975
  return true;
3866
3976
  }
@@ -3952,7 +4062,7 @@ function buildHotspots(issues) {
3952
4062
 
3953
4063
  // src/cli/commands/report.ts
3954
4064
  async function runReport(options) {
3955
- const root = path19.resolve(options.root);
4065
+ const root = path20.resolve(options.root);
3956
4066
  const configResult = await loadConfig(root);
3957
4067
  let validation;
3958
4068
  if (options.runValidate) {
@@ -3969,7 +4079,7 @@ async function runReport(options) {
3969
4079
  validation = normalized;
3970
4080
  } else {
3971
4081
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
3972
- const inputPath = path19.isAbsolute(input) ? input : path19.resolve(root, input);
4082
+ const inputPath = path20.isAbsolute(input) ? input : path20.resolve(root, input);
3973
4083
  try {
3974
4084
  validation = await readValidationResult(inputPath);
3975
4085
  } catch (err) {
@@ -3995,10 +4105,10 @@ async function runReport(options) {
3995
4105
  const data = await createReportData(root, validation, configResult);
3996
4106
  const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
3997
4107
  const outRoot = resolvePath(root, configResult.config, "outDir");
3998
- const defaultOut = options.format === "json" ? path19.join(outRoot, "report.json") : path19.join(outRoot, "report.md");
4108
+ const defaultOut = options.format === "json" ? path20.join(outRoot, "report.json") : path20.join(outRoot, "report.md");
3999
4109
  const out = options.outPath ?? defaultOut;
4000
- const outPath = path19.isAbsolute(out) ? out : path19.resolve(root, out);
4001
- await mkdir3(path19.dirname(outPath), { recursive: true });
4110
+ const outPath = path20.isAbsolute(out) ? out : path20.resolve(root, out);
4111
+ await mkdir3(path20.dirname(outPath), { recursive: true });
4002
4112
  await writeFile2(outPath, `${output}
4003
4113
  `, "utf-8");
4004
4114
  info(
@@ -4007,7 +4117,7 @@ async function runReport(options) {
4007
4117
  info(`wrote report: ${outPath}`);
4008
4118
  }
4009
4119
  async function readValidationResult(inputPath) {
4010
- const raw = await readFile13(inputPath, "utf-8");
4120
+ const raw = await readFile14(inputPath, "utf-8");
4011
4121
  const parsed = JSON.parse(raw);
4012
4122
  if (!isValidationResult(parsed)) {
4013
4123
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -4063,15 +4173,15 @@ function isMissingFileError5(error2) {
4063
4173
  return record2.code === "ENOENT";
4064
4174
  }
4065
4175
  async function writeValidationResult(root, outputPath, result) {
4066
- const abs = path19.isAbsolute(outputPath) ? outputPath : path19.resolve(root, outputPath);
4067
- await mkdir3(path19.dirname(abs), { recursive: true });
4176
+ const abs = path20.isAbsolute(outputPath) ? outputPath : path20.resolve(root, outputPath);
4177
+ await mkdir3(path20.dirname(abs), { recursive: true });
4068
4178
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
4069
4179
  `, "utf-8");
4070
4180
  }
4071
4181
 
4072
4182
  // src/cli/commands/validate.ts
4073
4183
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
4074
- import path20 from "path";
4184
+ import path21 from "path";
4075
4185
 
4076
4186
  // src/cli/lib/failOn.ts
4077
4187
  function shouldFail(result, failOn) {
@@ -4086,7 +4196,7 @@ function shouldFail(result, failOn) {
4086
4196
 
4087
4197
  // src/cli/commands/validate.ts
4088
4198
  async function runValidate(options) {
4089
- const root = path20.resolve(options.root);
4199
+ const root = path21.resolve(options.root);
4090
4200
  const configResult = await loadConfig(root);
4091
4201
  const result = await validateProject(root, configResult);
4092
4202
  const normalized = normalizeValidationResult(root, result);
@@ -4210,12 +4320,12 @@ function issueKey(issue7) {
4210
4320
  }
4211
4321
  async function emitJson(result, root, jsonPath) {
4212
4322
  const abs = resolveJsonPath(root, jsonPath);
4213
- await mkdir4(path20.dirname(abs), { recursive: true });
4323
+ await mkdir4(path21.dirname(abs), { recursive: true });
4214
4324
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
4215
4325
  `, "utf-8");
4216
4326
  }
4217
4327
  function resolveJsonPath(root, jsonPath) {
4218
- return path20.isAbsolute(jsonPath) ? jsonPath : path20.resolve(root, jsonPath);
4328
+ return path21.isAbsolute(jsonPath) ? jsonPath : path21.resolve(root, jsonPath);
4219
4329
  }
4220
4330
  var GITHUB_ANNOTATION_LIMIT = 100;
4221
4331
 
@@ -4228,6 +4338,7 @@ function parseArgs(argv, cwd) {
4228
4338
  force: false,
4229
4339
  yes: false,
4230
4340
  dryRun: false,
4341
+ analyzeList: false,
4231
4342
  reportFormat: "md",
4232
4343
  reportRunValidate: false,
4233
4344
  doctorFormat: "text",
@@ -4237,6 +4348,7 @@ function parseArgs(argv, cwd) {
4237
4348
  };
4238
4349
  const args = [...argv];
4239
4350
  let command = args.shift() ?? null;
4351
+ let invalid = false;
4240
4352
  if (command === "--help" || command === "-h") {
4241
4353
  options.help = true;
4242
4354
  command = null;
@@ -4245,13 +4357,29 @@ function parseArgs(argv, cwd) {
4245
4357
  const arg = args[i];
4246
4358
  switch (arg) {
4247
4359
  case "--root":
4248
- options.root = args[i + 1] ?? options.root;
4249
- options.rootExplicit = true;
4250
- i += 1;
4360
+ {
4361
+ const next = readOptionValue(args, i);
4362
+ if (next === null) {
4363
+ invalid = true;
4364
+ options.help = true;
4365
+ break;
4366
+ }
4367
+ options.root = next;
4368
+ options.rootExplicit = true;
4369
+ i += 1;
4370
+ }
4251
4371
  break;
4252
4372
  case "--dir":
4253
- options.dir = args[i + 1] ?? options.dir;
4254
- i += 1;
4373
+ {
4374
+ const next = readOptionValue(args, i);
4375
+ if (next === null) {
4376
+ invalid = true;
4377
+ options.help = true;
4378
+ break;
4379
+ }
4380
+ options.dir = next;
4381
+ i += 1;
4382
+ }
4255
4383
  break;
4256
4384
  case "--force":
4257
4385
  options.force = true;
@@ -4262,8 +4390,25 @@ function parseArgs(argv, cwd) {
4262
4390
  case "--dry-run":
4263
4391
  options.dryRun = true;
4264
4392
  break;
4393
+ case "--list":
4394
+ options.analyzeList = true;
4395
+ break;
4396
+ case "--prompt":
4397
+ {
4398
+ const next = readOptionValue(args, i);
4399
+ if (next) {
4400
+ options.analyzePrompt = next;
4401
+ i += 1;
4402
+ }
4403
+ }
4404
+ break;
4265
4405
  case "--format": {
4266
- const next = args[i + 1];
4406
+ const next = readOptionValue(args, i);
4407
+ if (next === null) {
4408
+ invalid = true;
4409
+ options.help = true;
4410
+ break;
4411
+ }
4267
4412
  applyFormatOption(command, next, options);
4268
4413
  i += 1;
4269
4414
  break;
@@ -4272,35 +4417,44 @@ function parseArgs(argv, cwd) {
4272
4417
  options.strict = true;
4273
4418
  break;
4274
4419
  case "--fail-on": {
4275
- const next = args[i + 1];
4420
+ const next = readOptionValue(args, i);
4421
+ if (next === null) {
4422
+ invalid = true;
4423
+ options.help = true;
4424
+ break;
4425
+ }
4276
4426
  if (next === "never" || next === "warning" || next === "error") {
4277
4427
  options.failOn = next;
4278
4428
  }
4279
4429
  i += 1;
4280
4430
  break;
4281
4431
  }
4282
- case "--out":
4283
- {
4284
- const next = args[i + 1];
4285
- if (next) {
4286
- if (command === "doctor") {
4287
- options.doctorOut = next;
4288
- } else {
4289
- options.reportOut = next;
4290
- }
4291
- }
4432
+ case "--out": {
4433
+ const next = readOptionValue(args, i);
4434
+ if (next === null) {
4435
+ invalid = true;
4436
+ options.help = true;
4437
+ break;
4438
+ }
4439
+ if (command === "doctor") {
4440
+ options.doctorOut = next;
4441
+ } else {
4442
+ options.reportOut = next;
4292
4443
  }
4293
4444
  i += 1;
4294
4445
  break;
4295
- case "--in":
4296
- {
4297
- const next = args[i + 1];
4298
- if (next) {
4299
- options.reportIn = next;
4300
- }
4446
+ }
4447
+ case "--in": {
4448
+ const next = readOptionValue(args, i);
4449
+ if (next === null) {
4450
+ invalid = true;
4451
+ options.help = true;
4452
+ break;
4301
4453
  }
4454
+ options.reportIn = next;
4302
4455
  i += 1;
4303
4456
  break;
4457
+ }
4304
4458
  case "--run-validate":
4305
4459
  options.reportRunValidate = true;
4306
4460
  break;
@@ -4312,7 +4466,14 @@ function parseArgs(argv, cwd) {
4312
4466
  break;
4313
4467
  }
4314
4468
  }
4315
- return { command, options };
4469
+ return { command, invalid, options };
4470
+ }
4471
+ function readOptionValue(args, index) {
4472
+ const next = args[index + 1];
4473
+ if (!next || next.startsWith("--")) {
4474
+ return null;
4475
+ }
4476
+ return next;
4316
4477
  }
4317
4478
  function applyFormatOption(command, value, options) {
4318
4479
  if (!value) {
@@ -4346,9 +4507,12 @@ function applyFormatOption(command, value, options) {
4346
4507
 
4347
4508
  // src/cli/main.ts
4348
4509
  async function run(argv, cwd) {
4349
- const { command, options } = parseArgs(argv, cwd);
4510
+ const { command, invalid, options } = parseArgs(argv, cwd);
4350
4511
  if (!command || options.help) {
4351
4512
  info(usage());
4513
+ if (invalid) {
4514
+ process.exitCode = 1;
4515
+ }
4352
4516
  return;
4353
4517
  }
4354
4518
  switch (command) {
@@ -4360,6 +4524,17 @@ async function run(argv, cwd) {
4360
4524
  yes: options.yes
4361
4525
  });
4362
4526
  return;
4527
+ case "analyze":
4528
+ {
4529
+ const resolvedRoot = await resolveRoot(options);
4530
+ const exitCode = await runAnalyze({
4531
+ root: resolvedRoot,
4532
+ list: options.analyzeList,
4533
+ ...options.analyzePrompt !== void 0 ? { prompt: options.analyzePrompt } : {}
4534
+ });
4535
+ process.exitCode = exitCode;
4536
+ }
4537
+ return;
4363
4538
  case "validate":
4364
4539
  {
4365
4540
  const resolvedRoot = await resolveRoot(options);
@@ -4406,6 +4581,7 @@ function usage() {
4406
4581
 
4407
4582
  Commands:
4408
4583
  init \u30C6\u30F3\u30D7\u30EC\u3092\u751F\u6210
4584
+ analyze \u610F\u5473\u30EC\u30D9\u30EB\u306E\u30EC\u30D3\u30E5\u30FC\u88DC\u52A9\uFF08\u30D7\u30ED\u30F3\u30D7\u30C8\u51FA\u529B\uFF09
4409
4585
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
4410
4586
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
4411
4587
  doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
@@ -4413,9 +4589,11 @@ Commands:
4413
4589
  Options:
4414
4590
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
4415
4591
  --dir <path> init \u306E\u51FA\u529B\u5148
4416
- --force \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u3092\u4E0A\u66F8\u304D
4592
+ --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
4417
4593
  --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
4418
4594
  --dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
4595
+ --list analyze: \u5229\u7528\u53EF\u80FD\u306A\u30D7\u30ED\u30F3\u30D7\u30C8\u4E00\u89A7\u3092\u8868\u793A
4596
+ --prompt <name> analyze: \u6307\u5B9A\u30D7\u30ED\u30F3\u30D7\u30C8\uFF08.md\u7701\u7565\u53EF\uFF09\u3092\u51FA\u529B
4419
4597
  --format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
4420
4598
  --format <md|json> report \u306E\u51FA\u529B\u5F62\u5F0F
4421
4599
  --format <text|json> doctor \u306E\u51FA\u529B\u5F62\u5F0F