qfai 0.8.0 → 0.9.0
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 +29 -2
- package/assets/init/.qfai/README.md +8 -1
- package/assets/init/.qfai/prompts/README.md +6 -1
- package/assets/init/.qfai/prompts/analyze/README.md +38 -0
- package/assets/init/.qfai/prompts/analyze/scenario_test_consistency.md +35 -0
- package/assets/init/.qfai/prompts/analyze/spec_contract_consistency.md +36 -0
- package/assets/init/.qfai/prompts/analyze/spec_scenario_consistency.md +35 -0
- package/assets/init/.qfai/prompts.local/README.md +2 -1
- package/assets/init/.qfai/samples/analyze/analysis.md +38 -0
- package/dist/cli/index.cjs +529 -193
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +517 -181
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +346 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +346 -72
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/commands/doctor.ts
|
|
4
4
|
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path10 from "path";
|
|
6
6
|
|
|
7
7
|
// src/core/doctor.ts
|
|
8
8
|
import { access as access4 } from "fs/promises";
|
|
9
|
-
import
|
|
9
|
+
import path9 from "path";
|
|
10
10
|
|
|
11
11
|
// src/core/config.ts
|
|
12
12
|
import { access, readFile } from "fs/promises";
|
|
@@ -378,6 +378,7 @@ function configIssue(file, message) {
|
|
|
378
378
|
return {
|
|
379
379
|
code: "QFAI_CONFIG_INVALID",
|
|
380
380
|
severity: "error",
|
|
381
|
+
category: "compatibility",
|
|
381
382
|
message,
|
|
382
383
|
file,
|
|
383
384
|
rule: "config.invalid"
|
|
@@ -865,17 +866,142 @@ function formatError3(error2) {
|
|
|
865
866
|
return String(error2);
|
|
866
867
|
}
|
|
867
868
|
|
|
868
|
-
// src/core/
|
|
869
|
+
// src/core/promptsIntegrity.ts
|
|
869
870
|
import { readFile as readFile3 } from "fs/promises";
|
|
871
|
+
import path7 from "path";
|
|
872
|
+
|
|
873
|
+
// src/shared/assets.ts
|
|
874
|
+
import { existsSync } from "fs";
|
|
870
875
|
import path6 from "path";
|
|
871
876
|
import { fileURLToPath } from "url";
|
|
877
|
+
function getInitAssetsDir() {
|
|
878
|
+
const base = import.meta.url;
|
|
879
|
+
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
880
|
+
const baseDir = path6.dirname(basePath);
|
|
881
|
+
const candidates = [
|
|
882
|
+
path6.resolve(baseDir, "../../../assets/init"),
|
|
883
|
+
path6.resolve(baseDir, "../../assets/init")
|
|
884
|
+
];
|
|
885
|
+
for (const candidate of candidates) {
|
|
886
|
+
if (existsSync(candidate)) {
|
|
887
|
+
return candidate;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
throw new Error(
|
|
891
|
+
[
|
|
892
|
+
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
893
|
+
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
894
|
+
...candidates.map((candidate) => `- ${candidate}`)
|
|
895
|
+
].join("\n")
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/core/promptsIntegrity.ts
|
|
900
|
+
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
901
|
+
const promptsDir = path7.resolve(root, ".qfai", "prompts");
|
|
902
|
+
let templateDir;
|
|
903
|
+
try {
|
|
904
|
+
templateDir = path7.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
905
|
+
} catch {
|
|
906
|
+
return {
|
|
907
|
+
status: "skipped_missing_assets",
|
|
908
|
+
promptsDir,
|
|
909
|
+
templateDir: "",
|
|
910
|
+
missing: [],
|
|
911
|
+
extra: [],
|
|
912
|
+
changed: []
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const projectFiles = await collectFiles(promptsDir);
|
|
916
|
+
if (projectFiles.length === 0) {
|
|
917
|
+
return {
|
|
918
|
+
status: "skipped_missing_prompts",
|
|
919
|
+
promptsDir,
|
|
920
|
+
templateDir,
|
|
921
|
+
missing: [],
|
|
922
|
+
extra: [],
|
|
923
|
+
changed: []
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const templateFiles = await collectFiles(templateDir);
|
|
927
|
+
const templateByRel = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const abs of templateFiles) {
|
|
929
|
+
templateByRel.set(toRel(templateDir, abs), abs);
|
|
930
|
+
}
|
|
931
|
+
const projectByRel = /* @__PURE__ */ new Map();
|
|
932
|
+
for (const abs of projectFiles) {
|
|
933
|
+
projectByRel.set(toRel(promptsDir, abs), abs);
|
|
934
|
+
}
|
|
935
|
+
const missing = [];
|
|
936
|
+
const extra = [];
|
|
937
|
+
const changed = [];
|
|
938
|
+
for (const rel of templateByRel.keys()) {
|
|
939
|
+
if (!projectByRel.has(rel)) {
|
|
940
|
+
missing.push(rel);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
for (const rel of projectByRel.keys()) {
|
|
944
|
+
if (!templateByRel.has(rel)) {
|
|
945
|
+
extra.push(rel);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const common = intersectKeys(templateByRel, projectByRel);
|
|
949
|
+
for (const rel of common) {
|
|
950
|
+
const templateAbs = templateByRel.get(rel);
|
|
951
|
+
const projectAbs = projectByRel.get(rel);
|
|
952
|
+
if (!templateAbs || !projectAbs) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
const [a, b] = await Promise.all([
|
|
957
|
+
readFile3(templateAbs, "utf-8"),
|
|
958
|
+
readFile3(projectAbs, "utf-8")
|
|
959
|
+
]);
|
|
960
|
+
if (normalizeNewlines(a) !== normalizeNewlines(b)) {
|
|
961
|
+
changed.push(rel);
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
changed.push(rel);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
|
|
968
|
+
return {
|
|
969
|
+
status,
|
|
970
|
+
promptsDir,
|
|
971
|
+
templateDir,
|
|
972
|
+
missing: missing.sort(),
|
|
973
|
+
extra: extra.sort(),
|
|
974
|
+
changed: changed.sort()
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function normalizeNewlines(text) {
|
|
978
|
+
return text.replace(/\r\n/g, "\n");
|
|
979
|
+
}
|
|
980
|
+
function toRel(base, abs) {
|
|
981
|
+
const rel = path7.relative(base, abs);
|
|
982
|
+
return rel.replace(/[\\/]+/g, "/");
|
|
983
|
+
}
|
|
984
|
+
function intersectKeys(a, b) {
|
|
985
|
+
const out = [];
|
|
986
|
+
for (const key of a.keys()) {
|
|
987
|
+
if (b.has(key)) {
|
|
988
|
+
out.push(key);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/core/version.ts
|
|
995
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
996
|
+
import path8 from "path";
|
|
997
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
872
998
|
async function resolveToolVersion() {
|
|
873
|
-
if ("0.
|
|
874
|
-
return "0.
|
|
999
|
+
if ("0.9.0".length > 0) {
|
|
1000
|
+
return "0.9.0";
|
|
875
1001
|
}
|
|
876
1002
|
try {
|
|
877
1003
|
const packagePath = resolvePackageJsonPath();
|
|
878
|
-
const raw = await
|
|
1004
|
+
const raw = await readFile4(packagePath, "utf-8");
|
|
879
1005
|
const parsed = JSON.parse(raw);
|
|
880
1006
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
881
1007
|
return version.length > 0 ? version : "unknown";
|
|
@@ -885,8 +1011,8 @@ async function resolveToolVersion() {
|
|
|
885
1011
|
}
|
|
886
1012
|
function resolvePackageJsonPath() {
|
|
887
1013
|
const base = import.meta.url;
|
|
888
|
-
const basePath = base.startsWith("file:") ?
|
|
889
|
-
return
|
|
1014
|
+
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1015
|
+
return path8.resolve(path8.dirname(basePath), "../../package.json");
|
|
890
1016
|
}
|
|
891
1017
|
|
|
892
1018
|
// src/core/doctor.ts
|
|
@@ -912,7 +1038,7 @@ function normalizeGlobs2(values) {
|
|
|
912
1038
|
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
913
1039
|
}
|
|
914
1040
|
async function createDoctorData(options) {
|
|
915
|
-
const startDir =
|
|
1041
|
+
const startDir = path9.resolve(options.startDir);
|
|
916
1042
|
const checks = [];
|
|
917
1043
|
const configPath = getConfigPath(startDir);
|
|
918
1044
|
const search = options.rootExplicit ? {
|
|
@@ -975,9 +1101,9 @@ async function createDoctorData(options) {
|
|
|
975
1101
|
details: { path: toRelativePath(root, resolved) }
|
|
976
1102
|
});
|
|
977
1103
|
if (key === "promptsDir") {
|
|
978
|
-
const promptsLocalDir =
|
|
979
|
-
|
|
980
|
-
`${
|
|
1104
|
+
const promptsLocalDir = path9.join(
|
|
1105
|
+
path9.dirname(resolved),
|
|
1106
|
+
`${path9.basename(resolved)}.local`
|
|
981
1107
|
);
|
|
982
1108
|
const found = await exists4(promptsLocalDir);
|
|
983
1109
|
addCheck(checks, {
|
|
@@ -987,6 +1113,49 @@ async function createDoctorData(options) {
|
|
|
987
1113
|
message: found ? "prompts.local exists (overlay can be used)" : "prompts.local is optional (create it to override prompts)",
|
|
988
1114
|
details: { path: toRelativePath(root, promptsLocalDir) }
|
|
989
1115
|
});
|
|
1116
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
1117
|
+
if (diff.status === "skipped_missing_prompts") {
|
|
1118
|
+
addCheck(checks, {
|
|
1119
|
+
id: "prompts.integrity",
|
|
1120
|
+
severity: "info",
|
|
1121
|
+
title: "Prompts integrity (.qfai/prompts)",
|
|
1122
|
+
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",
|
|
1123
|
+
details: { promptsDir: toRelativePath(root, diff.promptsDir) }
|
|
1124
|
+
});
|
|
1125
|
+
} else if (diff.status === "skipped_missing_assets") {
|
|
1126
|
+
addCheck(checks, {
|
|
1127
|
+
id: "prompts.integrity",
|
|
1128
|
+
severity: "info",
|
|
1129
|
+
title: "Prompts integrity (.qfai/prompts)",
|
|
1130
|
+
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",
|
|
1131
|
+
details: { promptsDir: toRelativePath(root, diff.promptsDir) }
|
|
1132
|
+
});
|
|
1133
|
+
} else if (diff.status === "ok") {
|
|
1134
|
+
addCheck(checks, {
|
|
1135
|
+
id: "prompts.integrity",
|
|
1136
|
+
severity: "ok",
|
|
1137
|
+
title: "Prompts integrity (.qfai/prompts)",
|
|
1138
|
+
message: "\u6A19\u6E96 assets \u3068\u4E00\u81F4\u3057\u3066\u3044\u307E\u3059",
|
|
1139
|
+
details: { promptsDir: toRelativePath(root, diff.promptsDir) }
|
|
1140
|
+
});
|
|
1141
|
+
} else {
|
|
1142
|
+
addCheck(checks, {
|
|
1143
|
+
id: "prompts.integrity",
|
|
1144
|
+
severity: "error",
|
|
1145
|
+
title: "Prompts integrity (.qfai/prompts)",
|
|
1146
|
+
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",
|
|
1147
|
+
details: {
|
|
1148
|
+
promptsDir: toRelativePath(root, diff.promptsDir),
|
|
1149
|
+
missing: diff.missing,
|
|
1150
|
+
extra: diff.extra,
|
|
1151
|
+
changed: diff.changed,
|
|
1152
|
+
nextActions: [
|
|
1153
|
+
"\u5909\u66F4\u5185\u5BB9\u3092 .qfai/prompts.local/** \u306B\u79FB\u3059\uFF08\u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067\u914D\u7F6E\uFF09",
|
|
1154
|
+
"\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"
|
|
1155
|
+
]
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
990
1159
|
}
|
|
991
1160
|
}
|
|
992
1161
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1007,7 +1176,7 @@ async function createDoctorData(options) {
|
|
|
1007
1176
|
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1008
1177
|
details: { specPacks: entries.length, missingFiles }
|
|
1009
1178
|
});
|
|
1010
|
-
const validateJsonAbs =
|
|
1179
|
+
const validateJsonAbs = path9.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path9.resolve(root, config.output.validateJsonPath);
|
|
1011
1180
|
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1012
1181
|
addCheck(checks, {
|
|
1013
1182
|
id: "output.validateJson",
|
|
@@ -1017,8 +1186,8 @@ async function createDoctorData(options) {
|
|
|
1017
1186
|
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1018
1187
|
});
|
|
1019
1188
|
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1020
|
-
const rel =
|
|
1021
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
1189
|
+
const rel = path9.relative(outDirAbs, validateJsonAbs);
|
|
1190
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
1022
1191
|
addCheck(checks, {
|
|
1023
1192
|
id: "output.pathAlignment",
|
|
1024
1193
|
severity: inside ? "ok" : "warning",
|
|
@@ -1124,12 +1293,12 @@ async function detectOutDirCollisions(root) {
|
|
|
1124
1293
|
ignore: DEFAULT_CONFIG_SEARCH_IGNORE_GLOBS
|
|
1125
1294
|
});
|
|
1126
1295
|
const configRoots = Array.from(
|
|
1127
|
-
new Set(configPaths.map((configPath) =>
|
|
1296
|
+
new Set(configPaths.map((configPath) => path9.dirname(configPath)))
|
|
1128
1297
|
).sort((a, b) => a.localeCompare(b));
|
|
1129
1298
|
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1130
1299
|
for (const configRoot of configRoots) {
|
|
1131
1300
|
const { config } = await loadConfig(configRoot);
|
|
1132
|
-
const outDir =
|
|
1301
|
+
const outDir = path9.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1133
1302
|
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1134
1303
|
roots.add(configRoot);
|
|
1135
1304
|
outDirToRoots.set(outDir, roots);
|
|
@@ -1146,20 +1315,20 @@ async function detectOutDirCollisions(root) {
|
|
|
1146
1315
|
return { monorepoRoot, configRoots, collisions };
|
|
1147
1316
|
}
|
|
1148
1317
|
async function findMonorepoRoot(startDir) {
|
|
1149
|
-
let current =
|
|
1318
|
+
let current = path9.resolve(startDir);
|
|
1150
1319
|
while (true) {
|
|
1151
|
-
const gitPath =
|
|
1152
|
-
const workspacePath =
|
|
1320
|
+
const gitPath = path9.join(current, ".git");
|
|
1321
|
+
const workspacePath = path9.join(current, "pnpm-workspace.yaml");
|
|
1153
1322
|
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1154
1323
|
return current;
|
|
1155
1324
|
}
|
|
1156
|
-
const parent =
|
|
1325
|
+
const parent = path9.dirname(current);
|
|
1157
1326
|
if (parent === current) {
|
|
1158
1327
|
break;
|
|
1159
1328
|
}
|
|
1160
1329
|
current = parent;
|
|
1161
1330
|
}
|
|
1162
|
-
return
|
|
1331
|
+
return path9.resolve(startDir);
|
|
1163
1332
|
}
|
|
1164
1333
|
|
|
1165
1334
|
// src/cli/lib/logger.ts
|
|
@@ -1201,8 +1370,8 @@ async function runDoctor(options) {
|
|
|
1201
1370
|
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1202
1371
|
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1203
1372
|
if (options.outPath) {
|
|
1204
|
-
const outAbs =
|
|
1205
|
-
await mkdir(
|
|
1373
|
+
const outAbs = path10.isAbsolute(options.outPath) ? options.outPath : path10.resolve(process.cwd(), options.outPath);
|
|
1374
|
+
await mkdir(path10.dirname(outAbs), { recursive: true });
|
|
1206
1375
|
await writeFile(outAbs, `${output}
|
|
1207
1376
|
`, "utf-8");
|
|
1208
1377
|
info(`doctor: wrote ${outAbs}`);
|
|
@@ -1222,36 +1391,59 @@ function shouldFailDoctor(summary, failOn) {
|
|
|
1222
1391
|
}
|
|
1223
1392
|
|
|
1224
1393
|
// src/cli/commands/init.ts
|
|
1225
|
-
import
|
|
1394
|
+
import path12 from "path";
|
|
1226
1395
|
|
|
1227
1396
|
// src/cli/lib/fs.ts
|
|
1228
1397
|
import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
|
|
1229
|
-
import
|
|
1398
|
+
import path11 from "path";
|
|
1230
1399
|
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1231
1400
|
const files = await collectTemplateFiles(sourceRoot);
|
|
1232
1401
|
return copyFiles(files, sourceRoot, destRoot, options);
|
|
1233
1402
|
}
|
|
1403
|
+
async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
|
|
1404
|
+
const allFiles = [];
|
|
1405
|
+
for (const relPath of relativePaths) {
|
|
1406
|
+
const fullPath = path11.join(sourceRoot, relPath);
|
|
1407
|
+
const files = await collectTemplateFiles(fullPath);
|
|
1408
|
+
allFiles.push(...files);
|
|
1409
|
+
}
|
|
1410
|
+
return copyFiles(allFiles, sourceRoot, destRoot, options);
|
|
1411
|
+
}
|
|
1234
1412
|
async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
1235
1413
|
const copied = [];
|
|
1236
1414
|
const skipped = [];
|
|
1237
1415
|
const conflicts = [];
|
|
1238
|
-
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
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);
|
|
1239
1418
|
const isProtectedRelative = (relative) => {
|
|
1240
1419
|
if (protectPrefixes.length === 0) {
|
|
1241
1420
|
return false;
|
|
1242
1421
|
}
|
|
1243
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1422
|
+
const normalized = relative.replace(/[\\/]+/g, path11.sep);
|
|
1244
1423
|
return protectPrefixes.some(
|
|
1245
1424
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1246
1425
|
);
|
|
1247
1426
|
};
|
|
1248
|
-
|
|
1427
|
+
const isExcludedRelative = (relative) => {
|
|
1428
|
+
if (excludePrefixes.length === 0) {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
const normalized = relative.replace(/[\\/]+/g, path11.sep);
|
|
1432
|
+
return excludePrefixes.some(
|
|
1433
|
+
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1434
|
+
);
|
|
1435
|
+
};
|
|
1436
|
+
const conflictPolicy = options.conflictPolicy ?? "error";
|
|
1437
|
+
if (!options.force && conflictPolicy === "error") {
|
|
1249
1438
|
for (const file of files) {
|
|
1250
|
-
const relative =
|
|
1439
|
+
const relative = path11.relative(sourceRoot, file);
|
|
1440
|
+
if (isExcludedRelative(relative)) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1251
1443
|
if (isProtectedRelative(relative)) {
|
|
1252
1444
|
continue;
|
|
1253
1445
|
}
|
|
1254
|
-
const dest =
|
|
1446
|
+
const dest = path11.join(destRoot, relative);
|
|
1255
1447
|
if (!await shouldWrite(dest, options.force)) {
|
|
1256
1448
|
conflicts.push(dest);
|
|
1257
1449
|
}
|
|
@@ -1261,15 +1453,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1261
1453
|
}
|
|
1262
1454
|
}
|
|
1263
1455
|
for (const file of files) {
|
|
1264
|
-
const relative =
|
|
1265
|
-
|
|
1456
|
+
const relative = path11.relative(sourceRoot, file);
|
|
1457
|
+
if (isExcludedRelative(relative)) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const dest = path11.join(destRoot, relative);
|
|
1266
1461
|
const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
|
|
1267
1462
|
if (!await shouldWrite(dest, forceForThisFile)) {
|
|
1268
1463
|
skipped.push(dest);
|
|
1269
1464
|
continue;
|
|
1270
1465
|
}
|
|
1271
1466
|
if (!options.dryRun) {
|
|
1272
|
-
await mkdir2(
|
|
1467
|
+
await mkdir2(path11.dirname(dest), { recursive: true });
|
|
1273
1468
|
await copyFile(file, dest);
|
|
1274
1469
|
}
|
|
1275
1470
|
copied.push(dest);
|
|
@@ -1293,7 +1488,7 @@ async function collectTemplateFiles(root) {
|
|
|
1293
1488
|
}
|
|
1294
1489
|
const items = await readdir3(root, { withFileTypes: true });
|
|
1295
1490
|
for (const item of items) {
|
|
1296
|
-
const fullPath =
|
|
1491
|
+
const fullPath = path11.join(root, item.name);
|
|
1297
1492
|
if (item.isDirectory()) {
|
|
1298
1493
|
const nested = await collectTemplateFiles(fullPath);
|
|
1299
1494
|
entries.push(...nested);
|
|
@@ -1320,51 +1515,44 @@ async function exists5(target) {
|
|
|
1320
1515
|
}
|
|
1321
1516
|
}
|
|
1322
1517
|
|
|
1323
|
-
// src/cli/lib/assets.ts
|
|
1324
|
-
import { existsSync } from "fs";
|
|
1325
|
-
import path10 from "path";
|
|
1326
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1327
|
-
function getInitAssetsDir() {
|
|
1328
|
-
const base = import.meta.url;
|
|
1329
|
-
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1330
|
-
const baseDir = path10.dirname(basePath);
|
|
1331
|
-
const candidates = [
|
|
1332
|
-
path10.resolve(baseDir, "../../../assets/init"),
|
|
1333
|
-
path10.resolve(baseDir, "../../assets/init")
|
|
1334
|
-
];
|
|
1335
|
-
for (const candidate of candidates) {
|
|
1336
|
-
if (existsSync(candidate)) {
|
|
1337
|
-
return candidate;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
throw new Error(
|
|
1341
|
-
[
|
|
1342
|
-
"init \u7528\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Template assets not found.",
|
|
1343
|
-
"\u78BA\u8A8D\u3057\u305F\u30D1\u30B9 / Checked paths:",
|
|
1344
|
-
...candidates.map((candidate) => `- ${candidate}`)
|
|
1345
|
-
].join("\n")
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
1518
|
// src/cli/commands/init.ts
|
|
1350
1519
|
async function runInit(options) {
|
|
1351
1520
|
const assetsRoot = getInitAssetsDir();
|
|
1352
|
-
const rootAssets =
|
|
1353
|
-
const qfaiAssets =
|
|
1354
|
-
const destRoot =
|
|
1355
|
-
const destQfai =
|
|
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");
|
|
1525
|
+
if (options.force) {
|
|
1526
|
+
info(
|
|
1527
|
+
"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"
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1356
1530
|
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
1357
|
-
force:
|
|
1358
|
-
dryRun: options.dryRun
|
|
1531
|
+
force: false,
|
|
1532
|
+
dryRun: options.dryRun,
|
|
1533
|
+
conflictPolicy: "skip"
|
|
1359
1534
|
});
|
|
1360
1535
|
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
1361
|
-
force:
|
|
1536
|
+
force: false,
|
|
1362
1537
|
dryRun: options.dryRun,
|
|
1363
|
-
|
|
1538
|
+
conflictPolicy: "skip",
|
|
1539
|
+
protect: ["prompts.local"],
|
|
1540
|
+
exclude: ["prompts"]
|
|
1364
1541
|
});
|
|
1542
|
+
const promptsResult = await copyTemplatePaths(
|
|
1543
|
+
qfaiAssets,
|
|
1544
|
+
destQfai,
|
|
1545
|
+
["prompts"],
|
|
1546
|
+
{
|
|
1547
|
+
force: options.force,
|
|
1548
|
+
dryRun: options.dryRun,
|
|
1549
|
+
conflictPolicy: "skip",
|
|
1550
|
+
protect: ["prompts.local"]
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1365
1553
|
report(
|
|
1366
|
-
[...rootResult.copied, ...qfaiResult.copied],
|
|
1367
|
-
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
1554
|
+
[...rootResult.copied, ...qfaiResult.copied, ...promptsResult.copied],
|
|
1555
|
+
[...rootResult.skipped, ...qfaiResult.skipped, ...promptsResult.skipped],
|
|
1368
1556
|
options.dryRun,
|
|
1369
1557
|
"init"
|
|
1370
1558
|
);
|
|
@@ -1380,8 +1568,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1380
1568
|
}
|
|
1381
1569
|
|
|
1382
1570
|
// src/cli/commands/report.ts
|
|
1383
|
-
import { mkdir as mkdir3, readFile as
|
|
1384
|
-
import
|
|
1571
|
+
import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
|
|
1572
|
+
import path19 from "path";
|
|
1385
1573
|
|
|
1386
1574
|
// src/core/normalize.ts
|
|
1387
1575
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1421,12 +1609,12 @@ function normalizeValidationResult(root, result) {
|
|
|
1421
1609
|
}
|
|
1422
1610
|
|
|
1423
1611
|
// src/core/report.ts
|
|
1424
|
-
import { readFile as
|
|
1425
|
-
import
|
|
1612
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1613
|
+
import path18 from "path";
|
|
1426
1614
|
|
|
1427
1615
|
// src/core/contractIndex.ts
|
|
1428
|
-
import { readFile as
|
|
1429
|
-
import
|
|
1616
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1617
|
+
import path13 from "path";
|
|
1430
1618
|
|
|
1431
1619
|
// src/core/contractsDecl.ts
|
|
1432
1620
|
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
@@ -1448,9 +1636,9 @@ function stripContractDeclarationLines(text) {
|
|
|
1448
1636
|
// src/core/contractIndex.ts
|
|
1449
1637
|
async function buildContractIndex(root, config) {
|
|
1450
1638
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1451
|
-
const uiRoot =
|
|
1452
|
-
const apiRoot =
|
|
1453
|
-
const dbRoot =
|
|
1639
|
+
const uiRoot = path13.join(contractsRoot, "ui");
|
|
1640
|
+
const apiRoot = path13.join(contractsRoot, "api");
|
|
1641
|
+
const dbRoot = path13.join(contractsRoot, "db");
|
|
1454
1642
|
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1455
1643
|
collectUiContractFiles(uiRoot),
|
|
1456
1644
|
collectApiContractFiles(apiRoot),
|
|
@@ -1468,7 +1656,7 @@ async function buildContractIndex(root, config) {
|
|
|
1468
1656
|
}
|
|
1469
1657
|
async function indexContractFiles(files, index) {
|
|
1470
1658
|
for (const file of files) {
|
|
1471
|
-
const text = await
|
|
1659
|
+
const text = await readFile5(file, "utf-8");
|
|
1472
1660
|
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1473
1661
|
}
|
|
1474
1662
|
}
|
|
@@ -1703,14 +1891,14 @@ function parseSpec(md, file) {
|
|
|
1703
1891
|
}
|
|
1704
1892
|
|
|
1705
1893
|
// src/core/validators/contracts.ts
|
|
1706
|
-
import { readFile as
|
|
1707
|
-
import
|
|
1894
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1895
|
+
import path15 from "path";
|
|
1708
1896
|
|
|
1709
1897
|
// src/core/contracts.ts
|
|
1710
|
-
import
|
|
1898
|
+
import path14 from "path";
|
|
1711
1899
|
import { parse as parseYaml2 } from "yaml";
|
|
1712
1900
|
function parseStructuredContract(file, text) {
|
|
1713
|
-
const ext =
|
|
1901
|
+
const ext = path14.extname(file).toLowerCase();
|
|
1714
1902
|
if (ext === ".json") {
|
|
1715
1903
|
return JSON.parse(text);
|
|
1716
1904
|
}
|
|
@@ -1730,9 +1918,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1730
1918
|
async function validateContracts(root, config) {
|
|
1731
1919
|
const issues = [];
|
|
1732
1920
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1733
|
-
issues.push(...await validateUiContracts(
|
|
1734
|
-
issues.push(...await validateApiContracts(
|
|
1735
|
-
issues.push(...await validateDbContracts(
|
|
1921
|
+
issues.push(...await validateUiContracts(path15.join(contractsRoot, "ui")));
|
|
1922
|
+
issues.push(...await validateApiContracts(path15.join(contractsRoot, "api")));
|
|
1923
|
+
issues.push(...await validateDbContracts(path15.join(contractsRoot, "db")));
|
|
1736
1924
|
const contractIndex = await buildContractIndex(root, config);
|
|
1737
1925
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1738
1926
|
return issues;
|
|
@@ -1752,7 +1940,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
1752
1940
|
}
|
|
1753
1941
|
const issues = [];
|
|
1754
1942
|
for (const file of files) {
|
|
1755
|
-
const text = await
|
|
1943
|
+
const text = await readFile6(file, "utf-8");
|
|
1756
1944
|
const invalidIds = extractInvalidIds(text, [
|
|
1757
1945
|
"SPEC",
|
|
1758
1946
|
"BR",
|
|
@@ -1807,7 +1995,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1807
1995
|
}
|
|
1808
1996
|
const issues = [];
|
|
1809
1997
|
for (const file of files) {
|
|
1810
|
-
const text = await
|
|
1998
|
+
const text = await readFile6(file, "utf-8");
|
|
1811
1999
|
const invalidIds = extractInvalidIds(text, [
|
|
1812
2000
|
"SPEC",
|
|
1813
2001
|
"BR",
|
|
@@ -1875,7 +2063,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1875
2063
|
}
|
|
1876
2064
|
const issues = [];
|
|
1877
2065
|
for (const file of files) {
|
|
1878
|
-
const text = await
|
|
2066
|
+
const text = await readFile6(file, "utf-8");
|
|
1879
2067
|
const invalidIds = extractInvalidIds(text, [
|
|
1880
2068
|
"SPEC",
|
|
1881
2069
|
"BR",
|
|
@@ -1995,12 +2183,16 @@ function formatError4(error2) {
|
|
|
1995
2183
|
}
|
|
1996
2184
|
return String(error2);
|
|
1997
2185
|
}
|
|
1998
|
-
function issue(code, message, severity, file, rule, refs) {
|
|
2186
|
+
function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1999
2187
|
const issue7 = {
|
|
2000
2188
|
code,
|
|
2001
2189
|
severity,
|
|
2190
|
+
category,
|
|
2002
2191
|
message
|
|
2003
2192
|
};
|
|
2193
|
+
if (suggested_action) {
|
|
2194
|
+
issue7.suggested_action = suggested_action;
|
|
2195
|
+
}
|
|
2004
2196
|
if (file) {
|
|
2005
2197
|
issue7.file = file;
|
|
2006
2198
|
}
|
|
@@ -2014,8 +2206,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
2014
2206
|
}
|
|
2015
2207
|
|
|
2016
2208
|
// src/core/validators/delta.ts
|
|
2017
|
-
import { readFile as
|
|
2018
|
-
import
|
|
2209
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2210
|
+
import path16 from "path";
|
|
2019
2211
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
2020
2212
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2021
2213
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -2029,10 +2221,10 @@ async function validateDeltas(root, config) {
|
|
|
2029
2221
|
}
|
|
2030
2222
|
const issues = [];
|
|
2031
2223
|
for (const pack of packs) {
|
|
2032
|
-
const deltaPath =
|
|
2224
|
+
const deltaPath = path16.join(pack, "delta.md");
|
|
2033
2225
|
let text;
|
|
2034
2226
|
try {
|
|
2035
|
-
text = await
|
|
2227
|
+
text = await readFile7(deltaPath, "utf-8");
|
|
2036
2228
|
} catch (error2) {
|
|
2037
2229
|
if (isMissingFileError2(error2)) {
|
|
2038
2230
|
issues.push(
|
|
@@ -2085,12 +2277,16 @@ function isMissingFileError2(error2) {
|
|
|
2085
2277
|
}
|
|
2086
2278
|
return error2.code === "ENOENT";
|
|
2087
2279
|
}
|
|
2088
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
2280
|
+
function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
|
|
2089
2281
|
const issue7 = {
|
|
2090
2282
|
code,
|
|
2091
2283
|
severity,
|
|
2284
|
+
category,
|
|
2092
2285
|
message
|
|
2093
2286
|
};
|
|
2287
|
+
if (suggested_action) {
|
|
2288
|
+
issue7.suggested_action = suggested_action;
|
|
2289
|
+
}
|
|
2094
2290
|
if (file) {
|
|
2095
2291
|
issue7.file = file;
|
|
2096
2292
|
}
|
|
@@ -2104,8 +2300,8 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
2104
2300
|
}
|
|
2105
2301
|
|
|
2106
2302
|
// src/core/validators/ids.ts
|
|
2107
|
-
import { readFile as
|
|
2108
|
-
import
|
|
2303
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2304
|
+
import path17 from "path";
|
|
2109
2305
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2110
2306
|
async function validateDefinedIds(root, config) {
|
|
2111
2307
|
const issues = [];
|
|
@@ -2140,7 +2336,7 @@ async function validateDefinedIds(root, config) {
|
|
|
2140
2336
|
}
|
|
2141
2337
|
async function collectSpecDefinitionIds(files, out) {
|
|
2142
2338
|
for (const file of files) {
|
|
2143
|
-
const text = await
|
|
2339
|
+
const text = await readFile8(file, "utf-8");
|
|
2144
2340
|
const parsed = parseSpec(text, file);
|
|
2145
2341
|
if (parsed.specId) {
|
|
2146
2342
|
recordId(out, parsed.specId, file);
|
|
@@ -2150,7 +2346,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
2150
2346
|
}
|
|
2151
2347
|
async function collectScenarioDefinitionIds(files, out) {
|
|
2152
2348
|
for (const file of files) {
|
|
2153
|
-
const text = await
|
|
2349
|
+
const text = await readFile8(file, "utf-8");
|
|
2154
2350
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2155
2351
|
if (!document || errors.length > 0) {
|
|
2156
2352
|
continue;
|
|
@@ -2171,16 +2367,20 @@ function recordId(out, id, file) {
|
|
|
2171
2367
|
}
|
|
2172
2368
|
function formatFileList(files, root) {
|
|
2173
2369
|
return files.map((file) => {
|
|
2174
|
-
const relative =
|
|
2370
|
+
const relative = path17.relative(root, file);
|
|
2175
2371
|
return relative.length > 0 ? relative : file;
|
|
2176
2372
|
}).join(", ");
|
|
2177
2373
|
}
|
|
2178
|
-
function issue3(code, message, severity, file, rule, refs) {
|
|
2374
|
+
function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2179
2375
|
const issue7 = {
|
|
2180
2376
|
code,
|
|
2181
2377
|
severity,
|
|
2378
|
+
category,
|
|
2182
2379
|
message
|
|
2183
2380
|
};
|
|
2381
|
+
if (suggested_action) {
|
|
2382
|
+
issue7.suggested_action = suggested_action;
|
|
2383
|
+
}
|
|
2184
2384
|
if (file) {
|
|
2185
2385
|
issue7.file = file;
|
|
2186
2386
|
}
|
|
@@ -2193,8 +2393,39 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
2193
2393
|
return issue7;
|
|
2194
2394
|
}
|
|
2195
2395
|
|
|
2396
|
+
// src/core/validators/promptsIntegrity.ts
|
|
2397
|
+
async function validatePromptsIntegrity(root) {
|
|
2398
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
2399
|
+
if (diff.status !== "modified") {
|
|
2400
|
+
return [];
|
|
2401
|
+
}
|
|
2402
|
+
const total = diff.missing.length + diff.extra.length + diff.changed.length;
|
|
2403
|
+
const hints = [
|
|
2404
|
+
diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
|
|
2405
|
+
diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
|
|
2406
|
+
diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
|
|
2407
|
+
].filter(Boolean).join(" / ");
|
|
2408
|
+
const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
|
|
2409
|
+
const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
|
|
2410
|
+
return [
|
|
2411
|
+
{
|
|
2412
|
+
code: "QFAI-PROMPTS-001",
|
|
2413
|
+
severity: "error",
|
|
2414
|
+
category: "change",
|
|
2415
|
+
message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
|
|
2416
|
+
suggested_action: [
|
|
2417
|
+
"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",
|
|
2418
|
+
"\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
|
|
2419
|
+
"- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
|
|
2420
|
+
"- \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"
|
|
2421
|
+
].join("\n"),
|
|
2422
|
+
rule: "prompts.integrity"
|
|
2423
|
+
}
|
|
2424
|
+
];
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2196
2427
|
// src/core/validators/scenario.ts
|
|
2197
|
-
import { readFile as
|
|
2428
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2198
2429
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2199
2430
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2200
2431
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2220,7 +2451,7 @@ async function validateScenarios(root, config) {
|
|
|
2220
2451
|
for (const entry of entries) {
|
|
2221
2452
|
let text;
|
|
2222
2453
|
try {
|
|
2223
|
-
text = await
|
|
2454
|
+
text = await readFile9(entry.scenarioPath, "utf-8");
|
|
2224
2455
|
} catch (error2) {
|
|
2225
2456
|
if (isMissingFileError3(error2)) {
|
|
2226
2457
|
issues.push(
|
|
@@ -2365,12 +2596,16 @@ function validateScenarioContent(text, file) {
|
|
|
2365
2596
|
}
|
|
2366
2597
|
return issues;
|
|
2367
2598
|
}
|
|
2368
|
-
function issue4(code, message, severity, file, rule, refs) {
|
|
2599
|
+
function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2369
2600
|
const issue7 = {
|
|
2370
2601
|
code,
|
|
2371
2602
|
severity,
|
|
2603
|
+
category,
|
|
2372
2604
|
message
|
|
2373
2605
|
};
|
|
2606
|
+
if (suggested_action) {
|
|
2607
|
+
issue7.suggested_action = suggested_action;
|
|
2608
|
+
}
|
|
2374
2609
|
if (file) {
|
|
2375
2610
|
issue7.file = file;
|
|
2376
2611
|
}
|
|
@@ -2390,7 +2625,7 @@ function isMissingFileError3(error2) {
|
|
|
2390
2625
|
}
|
|
2391
2626
|
|
|
2392
2627
|
// src/core/validators/spec.ts
|
|
2393
|
-
import { readFile as
|
|
2628
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2394
2629
|
async function validateSpecs(root, config) {
|
|
2395
2630
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2396
2631
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -2411,7 +2646,7 @@ async function validateSpecs(root, config) {
|
|
|
2411
2646
|
for (const entry of entries) {
|
|
2412
2647
|
let text;
|
|
2413
2648
|
try {
|
|
2414
|
-
text = await
|
|
2649
|
+
text = await readFile10(entry.specPath, "utf-8");
|
|
2415
2650
|
} catch (error2) {
|
|
2416
2651
|
if (isMissingFileError4(error2)) {
|
|
2417
2652
|
issues.push(
|
|
@@ -2535,12 +2770,16 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2535
2770
|
}
|
|
2536
2771
|
return issues;
|
|
2537
2772
|
}
|
|
2538
|
-
function issue5(code, message, severity, file, rule, refs) {
|
|
2773
|
+
function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2539
2774
|
const issue7 = {
|
|
2540
2775
|
code,
|
|
2541
2776
|
severity,
|
|
2777
|
+
category,
|
|
2542
2778
|
message
|
|
2543
2779
|
};
|
|
2780
|
+
if (suggested_action) {
|
|
2781
|
+
issue7.suggested_action = suggested_action;
|
|
2782
|
+
}
|
|
2544
2783
|
if (file) {
|
|
2545
2784
|
issue7.file = file;
|
|
2546
2785
|
}
|
|
@@ -2560,7 +2799,7 @@ function isMissingFileError4(error2) {
|
|
|
2560
2799
|
}
|
|
2561
2800
|
|
|
2562
2801
|
// src/core/validators/traceability.ts
|
|
2563
|
-
import { readFile as
|
|
2802
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2564
2803
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2565
2804
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2566
2805
|
async function validateTraceability(root, config) {
|
|
@@ -2580,7 +2819,7 @@ async function validateTraceability(root, config) {
|
|
|
2580
2819
|
const contractIndex = await buildContractIndex(root, config);
|
|
2581
2820
|
const contractIds = contractIndex.ids;
|
|
2582
2821
|
for (const file of specFiles) {
|
|
2583
|
-
const text = await
|
|
2822
|
+
const text = await readFile11(file, "utf-8");
|
|
2584
2823
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2585
2824
|
const parsed = parseSpec(text, file);
|
|
2586
2825
|
if (parsed.specId) {
|
|
@@ -2653,7 +2892,7 @@ async function validateTraceability(root, config) {
|
|
|
2653
2892
|
}
|
|
2654
2893
|
}
|
|
2655
2894
|
for (const file of scenarioFiles) {
|
|
2656
|
-
const text = await
|
|
2895
|
+
const text = await readFile11(file, "utf-8");
|
|
2657
2896
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2658
2897
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2659
2898
|
allowCommentPrefix: true
|
|
@@ -2975,7 +3214,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2975
3214
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2976
3215
|
let found = false;
|
|
2977
3216
|
for (const file of targetFiles) {
|
|
2978
|
-
const text = await
|
|
3217
|
+
const text = await readFile11(file, "utf-8");
|
|
2979
3218
|
if (pattern.test(text)) {
|
|
2980
3219
|
found = true;
|
|
2981
3220
|
break;
|
|
@@ -2998,12 +3237,16 @@ function buildIdPattern(ids) {
|
|
|
2998
3237
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2999
3238
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
3000
3239
|
}
|
|
3001
|
-
function issue6(code, message, severity, file, rule, refs) {
|
|
3240
|
+
function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
3002
3241
|
const issue7 = {
|
|
3003
3242
|
code,
|
|
3004
3243
|
severity,
|
|
3244
|
+
category,
|
|
3005
3245
|
message
|
|
3006
3246
|
};
|
|
3247
|
+
if (suggested_action) {
|
|
3248
|
+
issue7.suggested_action = suggested_action;
|
|
3249
|
+
}
|
|
3007
3250
|
if (file) {
|
|
3008
3251
|
issue7.file = file;
|
|
3009
3252
|
}
|
|
@@ -3022,6 +3265,7 @@ async function validateProject(root, configResult) {
|
|
|
3022
3265
|
const { config, issues: configIssues } = resolved;
|
|
3023
3266
|
const issues = [
|
|
3024
3267
|
...configIssues,
|
|
3268
|
+
...await validatePromptsIntegrity(root),
|
|
3025
3269
|
...await validateSpecs(root, config),
|
|
3026
3270
|
...await validateDeltas(root, config),
|
|
3027
3271
|
...await validateScenarios(root, config),
|
|
@@ -3062,15 +3306,15 @@ function countIssues(issues) {
|
|
|
3062
3306
|
// src/core/report.ts
|
|
3063
3307
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
3064
3308
|
async function createReportData(root, validation, configResult) {
|
|
3065
|
-
const resolvedRoot =
|
|
3309
|
+
const resolvedRoot = path18.resolve(root);
|
|
3066
3310
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3067
3311
|
const config = resolved.config;
|
|
3068
3312
|
const configPath = resolved.configPath;
|
|
3069
3313
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3070
3314
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3071
|
-
const apiRoot =
|
|
3072
|
-
const uiRoot =
|
|
3073
|
-
const dbRoot =
|
|
3315
|
+
const apiRoot = path18.join(contractsRoot, "api");
|
|
3316
|
+
const uiRoot = path18.join(contractsRoot, "ui");
|
|
3317
|
+
const dbRoot = path18.join(contractsRoot, "db");
|
|
3074
3318
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3075
3319
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3076
3320
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3184,7 +3428,39 @@ function formatReportMarkdown(data) {
|
|
|
3184
3428
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
3185
3429
|
lines.push(`- \u7248: ${data.version}`);
|
|
3186
3430
|
lines.push("");
|
|
3187
|
-
|
|
3431
|
+
const severityOrder = {
|
|
3432
|
+
error: 0,
|
|
3433
|
+
warning: 1,
|
|
3434
|
+
info: 2
|
|
3435
|
+
};
|
|
3436
|
+
const categoryOrder = {
|
|
3437
|
+
compatibility: 0,
|
|
3438
|
+
change: 1
|
|
3439
|
+
};
|
|
3440
|
+
const issuesByCategory = {
|
|
3441
|
+
compatibility: [],
|
|
3442
|
+
change: []
|
|
3443
|
+
};
|
|
3444
|
+
for (const issue7 of data.issues) {
|
|
3445
|
+
const cat = issue7.category;
|
|
3446
|
+
if (cat === "change") {
|
|
3447
|
+
issuesByCategory.change.push(issue7);
|
|
3448
|
+
} else {
|
|
3449
|
+
issuesByCategory.compatibility.push(issue7);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
const countIssuesBySeverity = (issues) => issues.reduce(
|
|
3453
|
+
(acc, i) => {
|
|
3454
|
+
acc[i.severity] += 1;
|
|
3455
|
+
return acc;
|
|
3456
|
+
},
|
|
3457
|
+
{ info: 0, warning: 0, error: 0 }
|
|
3458
|
+
);
|
|
3459
|
+
const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
|
|
3460
|
+
const changeCounts = countIssuesBySeverity(issuesByCategory.change);
|
|
3461
|
+
lines.push("## Dashboard");
|
|
3462
|
+
lines.push("");
|
|
3463
|
+
lines.push("### Summary");
|
|
3188
3464
|
lines.push("");
|
|
3189
3465
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
3190
3466
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
@@ -3192,7 +3468,13 @@ function formatReportMarkdown(data) {
|
|
|
3192
3468
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
3193
3469
|
);
|
|
3194
3470
|
lines.push(
|
|
3195
|
-
`- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
3471
|
+
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
3472
|
+
);
|
|
3473
|
+
lines.push(
|
|
3474
|
+
`- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
|
|
3475
|
+
);
|
|
3476
|
+
lines.push(
|
|
3477
|
+
`- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
|
|
3196
3478
|
);
|
|
3197
3479
|
lines.push(
|
|
3198
3480
|
`- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
|
|
@@ -3201,49 +3483,65 @@ function formatReportMarkdown(data) {
|
|
|
3201
3483
|
`- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
|
|
3202
3484
|
);
|
|
3203
3485
|
lines.push("");
|
|
3204
|
-
lines.push("
|
|
3205
|
-
lines.push("");
|
|
3206
|
-
lines.push("### Issues (by code)");
|
|
3486
|
+
lines.push("### Next Actions");
|
|
3207
3487
|
lines.push("");
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
severity: issue7.severity,
|
|
3223
|
-
code: issue7.code,
|
|
3224
|
-
count: 1
|
|
3225
|
-
});
|
|
3226
|
-
}
|
|
3227
|
-
const issueSummaryRows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
3228
|
-
const sa = severityOrder[a.severity] ?? 999;
|
|
3229
|
-
const sb = severityOrder[b.severity] ?? 999;
|
|
3230
|
-
if (sa !== sb) return sa - sb;
|
|
3231
|
-
return a.code.localeCompare(b.code);
|
|
3232
|
-
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
3233
|
-
if (issueSummaryRows.length === 0) {
|
|
3234
|
-
lines.push("- (none)");
|
|
3488
|
+
if (data.summary.counts.error > 0) {
|
|
3489
|
+
lines.push(
|
|
3490
|
+
"- error \u304C\u3042\u308B\u305F\u3081\u3001\u307E\u305A `qfai validate --fail-on error` \u3092\u901A\u308B\u307E\u3067\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3491
|
+
);
|
|
3492
|
+
lines.push(
|
|
3493
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
3494
|
+
);
|
|
3495
|
+
} else if (data.summary.counts.warning > 0) {
|
|
3496
|
+
lines.push(
|
|
3497
|
+
"- warning \u306E\u6271\u3044\u306F\u30C1\u30FC\u30E0\u5224\u65AD\u3067\u3059\u3002`--fail-on warning` \u904B\u7528\u306A\u3089\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3498
|
+
);
|
|
3499
|
+
lines.push(
|
|
3500
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
3501
|
+
);
|
|
3235
3502
|
} else {
|
|
3503
|
+
lines.push("- issue \u306F\u3042\u308A\u307E\u305B\u3093\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
3236
3504
|
lines.push(
|
|
3237
|
-
|
|
3505
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
|
|
3238
3506
|
);
|
|
3239
3507
|
}
|
|
3240
3508
|
lines.push("");
|
|
3241
|
-
lines.push("###
|
|
3509
|
+
lines.push("### Index");
|
|
3242
3510
|
lines.push("");
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3511
|
+
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
3512
|
+
lines.push("- [Change Issues](#change-issues)");
|
|
3513
|
+
lines.push("- [IDs](#ids)");
|
|
3514
|
+
lines.push("- [Traceability](#traceability)");
|
|
3515
|
+
lines.push("");
|
|
3516
|
+
const formatIssueSummaryTable = (issues) => {
|
|
3517
|
+
const issueKeyToCount = /* @__PURE__ */ new Map();
|
|
3518
|
+
for (const issue7 of issues) {
|
|
3519
|
+
const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
|
|
3520
|
+
const current = issueKeyToCount.get(key);
|
|
3521
|
+
if (current) {
|
|
3522
|
+
current.count += 1;
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3525
|
+
issueKeyToCount.set(key, {
|
|
3526
|
+
category: issue7.category,
|
|
3527
|
+
severity: issue7.severity,
|
|
3528
|
+
code: issue7.code,
|
|
3529
|
+
count: 1
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
3533
|
+
const ca = categoryOrder[a.category] ?? 999;
|
|
3534
|
+
const cb = categoryOrder[b.category] ?? 999;
|
|
3535
|
+
if (ca !== cb) return ca - cb;
|
|
3536
|
+
const sa = severityOrder[a.severity] ?? 999;
|
|
3537
|
+
const sb = severityOrder[b.severity] ?? 999;
|
|
3538
|
+
if (sa !== sb) return sa - sb;
|
|
3539
|
+
return a.code.localeCompare(b.code);
|
|
3540
|
+
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
3541
|
+
return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
|
|
3542
|
+
};
|
|
3543
|
+
const formatIssueCards = (issues) => {
|
|
3544
|
+
const sorted = [...issues].sort((a, b) => {
|
|
3247
3545
|
const sa = severityOrder[a.severity] ?? 999;
|
|
3248
3546
|
const sb = severityOrder[b.severity] ?? 999;
|
|
3249
3547
|
if (sa !== sb) return sa - sb;
|
|
@@ -3257,16 +3555,54 @@ function formatReportMarkdown(data) {
|
|
|
3257
3555
|
const lineB = b.loc?.line ?? 0;
|
|
3258
3556
|
return lineA - lineB;
|
|
3259
3557
|
});
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3558
|
+
if (sorted.length === 0) {
|
|
3559
|
+
return ["- (none)"];
|
|
3560
|
+
}
|
|
3561
|
+
const out = [];
|
|
3562
|
+
for (const item of sorted) {
|
|
3563
|
+
out.push(
|
|
3564
|
+
`#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
|
|
3265
3565
|
);
|
|
3566
|
+
if (item.file) {
|
|
3567
|
+
const loc = item.loc?.line ? `:${item.loc.line}` : "";
|
|
3568
|
+
out.push(`- file: ${item.file}${loc}`);
|
|
3569
|
+
}
|
|
3570
|
+
if (item.rule) {
|
|
3571
|
+
out.push(`- rule: ${item.rule}`);
|
|
3572
|
+
}
|
|
3573
|
+
if (item.refs && item.refs.length > 0) {
|
|
3574
|
+
out.push(`- refs: ${item.refs.join(", ")}`);
|
|
3575
|
+
}
|
|
3576
|
+
if (item.suggested_action) {
|
|
3577
|
+
out.push("- suggested_action:");
|
|
3578
|
+
const actionLines = String(item.suggested_action).split("\n");
|
|
3579
|
+
for (const line of actionLines) {
|
|
3580
|
+
out.push(` ${line}`);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
out.push("");
|
|
3266
3584
|
}
|
|
3267
|
-
|
|
3585
|
+
return out;
|
|
3586
|
+
};
|
|
3587
|
+
lines.push("## Compatibility Issues");
|
|
3268
3588
|
lines.push("");
|
|
3269
|
-
lines.push("###
|
|
3589
|
+
lines.push("### Summary");
|
|
3590
|
+
lines.push("");
|
|
3591
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
|
|
3592
|
+
lines.push("");
|
|
3593
|
+
lines.push("### Issues");
|
|
3594
|
+
lines.push("");
|
|
3595
|
+
lines.push(...formatIssueCards(issuesByCategory.compatibility));
|
|
3596
|
+
lines.push("## Change Issues");
|
|
3597
|
+
lines.push("");
|
|
3598
|
+
lines.push("### Summary");
|
|
3599
|
+
lines.push("");
|
|
3600
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.change));
|
|
3601
|
+
lines.push("");
|
|
3602
|
+
lines.push("### Issues");
|
|
3603
|
+
lines.push("");
|
|
3604
|
+
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
3605
|
+
lines.push("## IDs");
|
|
3270
3606
|
lines.push("");
|
|
3271
3607
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
3272
3608
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
@@ -3275,7 +3611,7 @@ function formatReportMarkdown(data) {
|
|
|
3275
3611
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3276
3612
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
3277
3613
|
lines.push("");
|
|
3278
|
-
lines.push("
|
|
3614
|
+
lines.push("## Traceability");
|
|
3279
3615
|
lines.push("");
|
|
3280
3616
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
3281
3617
|
lines.push(
|
|
@@ -3449,7 +3785,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
3449
3785
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
3450
3786
|
}
|
|
3451
3787
|
for (const file of specFiles) {
|
|
3452
|
-
const text = await
|
|
3788
|
+
const text = await readFile12(file, "utf-8");
|
|
3453
3789
|
const parsed = parseSpec(text, file);
|
|
3454
3790
|
const specKey = parsed.specId;
|
|
3455
3791
|
if (!specKey) {
|
|
@@ -3490,7 +3826,7 @@ async function collectIds(files) {
|
|
|
3490
3826
|
DB: /* @__PURE__ */ new Set()
|
|
3491
3827
|
};
|
|
3492
3828
|
for (const file of files) {
|
|
3493
|
-
const text = await
|
|
3829
|
+
const text = await readFile12(file, "utf-8");
|
|
3494
3830
|
for (const prefix of ID_PREFIXES2) {
|
|
3495
3831
|
const ids = extractIds(text, prefix);
|
|
3496
3832
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3508,7 +3844,7 @@ async function collectIds(files) {
|
|
|
3508
3844
|
async function collectUpstreamIds(files) {
|
|
3509
3845
|
const ids = /* @__PURE__ */ new Set();
|
|
3510
3846
|
for (const file of files) {
|
|
3511
|
-
const text = await
|
|
3847
|
+
const text = await readFile12(file, "utf-8");
|
|
3512
3848
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3513
3849
|
}
|
|
3514
3850
|
return ids;
|
|
@@ -3529,7 +3865,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3529
3865
|
}
|
|
3530
3866
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3531
3867
|
for (const file of targetFiles) {
|
|
3532
|
-
const text = await
|
|
3868
|
+
const text = await readFile12(file, "utf-8");
|
|
3533
3869
|
if (pattern.test(text)) {
|
|
3534
3870
|
return true;
|
|
3535
3871
|
}
|
|
@@ -3621,7 +3957,7 @@ function buildHotspots(issues) {
|
|
|
3621
3957
|
|
|
3622
3958
|
// src/cli/commands/report.ts
|
|
3623
3959
|
async function runReport(options) {
|
|
3624
|
-
const root =
|
|
3960
|
+
const root = path19.resolve(options.root);
|
|
3625
3961
|
const configResult = await loadConfig(root);
|
|
3626
3962
|
let validation;
|
|
3627
3963
|
if (options.runValidate) {
|
|
@@ -3638,7 +3974,7 @@ async function runReport(options) {
|
|
|
3638
3974
|
validation = normalized;
|
|
3639
3975
|
} else {
|
|
3640
3976
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3641
|
-
const inputPath =
|
|
3977
|
+
const inputPath = path19.isAbsolute(input) ? input : path19.resolve(root, input);
|
|
3642
3978
|
try {
|
|
3643
3979
|
validation = await readValidationResult(inputPath);
|
|
3644
3980
|
} catch (err) {
|
|
@@ -3664,10 +4000,10 @@ async function runReport(options) {
|
|
|
3664
4000
|
const data = await createReportData(root, validation, configResult);
|
|
3665
4001
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3666
4002
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3667
|
-
const defaultOut = options.format === "json" ?
|
|
4003
|
+
const defaultOut = options.format === "json" ? path19.join(outRoot, "report.json") : path19.join(outRoot, "report.md");
|
|
3668
4004
|
const out = options.outPath ?? defaultOut;
|
|
3669
|
-
const outPath =
|
|
3670
|
-
await mkdir3(
|
|
4005
|
+
const outPath = path19.isAbsolute(out) ? out : path19.resolve(root, out);
|
|
4006
|
+
await mkdir3(path19.dirname(outPath), { recursive: true });
|
|
3671
4007
|
await writeFile2(outPath, `${output}
|
|
3672
4008
|
`, "utf-8");
|
|
3673
4009
|
info(
|
|
@@ -3676,7 +4012,7 @@ async function runReport(options) {
|
|
|
3676
4012
|
info(`wrote report: ${outPath}`);
|
|
3677
4013
|
}
|
|
3678
4014
|
async function readValidationResult(inputPath) {
|
|
3679
|
-
const raw = await
|
|
4015
|
+
const raw = await readFile13(inputPath, "utf-8");
|
|
3680
4016
|
const parsed = JSON.parse(raw);
|
|
3681
4017
|
if (!isValidationResult(parsed)) {
|
|
3682
4018
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -3732,15 +4068,15 @@ function isMissingFileError5(error2) {
|
|
|
3732
4068
|
return record2.code === "ENOENT";
|
|
3733
4069
|
}
|
|
3734
4070
|
async function writeValidationResult(root, outputPath, result) {
|
|
3735
|
-
const abs =
|
|
3736
|
-
await mkdir3(
|
|
4071
|
+
const abs = path19.isAbsolute(outputPath) ? outputPath : path19.resolve(root, outputPath);
|
|
4072
|
+
await mkdir3(path19.dirname(abs), { recursive: true });
|
|
3737
4073
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
3738
4074
|
`, "utf-8");
|
|
3739
4075
|
}
|
|
3740
4076
|
|
|
3741
4077
|
// src/cli/commands/validate.ts
|
|
3742
4078
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
3743
|
-
import
|
|
4079
|
+
import path20 from "path";
|
|
3744
4080
|
|
|
3745
4081
|
// src/cli/lib/failOn.ts
|
|
3746
4082
|
function shouldFail(result, failOn) {
|
|
@@ -3755,7 +4091,7 @@ function shouldFail(result, failOn) {
|
|
|
3755
4091
|
|
|
3756
4092
|
// src/cli/commands/validate.ts
|
|
3757
4093
|
async function runValidate(options) {
|
|
3758
|
-
const root =
|
|
4094
|
+
const root = path20.resolve(options.root);
|
|
3759
4095
|
const configResult = await loadConfig(root);
|
|
3760
4096
|
const result = await validateProject(root, configResult);
|
|
3761
4097
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -3879,12 +4215,12 @@ function issueKey(issue7) {
|
|
|
3879
4215
|
}
|
|
3880
4216
|
async function emitJson(result, root, jsonPath) {
|
|
3881
4217
|
const abs = resolveJsonPath(root, jsonPath);
|
|
3882
|
-
await mkdir4(
|
|
4218
|
+
await mkdir4(path20.dirname(abs), { recursive: true });
|
|
3883
4219
|
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
3884
4220
|
`, "utf-8");
|
|
3885
4221
|
}
|
|
3886
4222
|
function resolveJsonPath(root, jsonPath) {
|
|
3887
|
-
return
|
|
4223
|
+
return path20.isAbsolute(jsonPath) ? jsonPath : path20.resolve(root, jsonPath);
|
|
3888
4224
|
}
|
|
3889
4225
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3890
4226
|
|
|
@@ -4082,7 +4418,7 @@ Commands:
|
|
|
4082
4418
|
Options:
|
|
4083
4419
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
4084
4420
|
--dir <path> init \u306E\u51FA\u529B\u5148
|
|
4085
|
-
--force \u65E2\u5B58\
|
|
4421
|
+
--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
|
|
4086
4422
|
--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
|
|
4087
4423
|
--dry-run \u5909\u66F4\u3092\u884C\u308F\u305A\u8868\u793A\u306E\u307F
|
|
4088
4424
|
--format <text|github> validate \u306E\u51FA\u529B\u5F62\u5F0F
|