qfai 0.7.2 → 0.8.1
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/LICENSE +21 -0
- package/README.md +1 -1
- package/assets/init/.qfai/README.md +4 -1
- package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +34 -0
- package/assets/init/.qfai/prompts.local/README.md +5 -0
- package/dist/cli/index.cjs +598 -196
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +586 -184
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +408 -70
- 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 +408 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -2
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.8.1".length > 0) {
|
|
1000
|
+
return "0.8.1";
|
|
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,39 @@ 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");
|
|
1356
1525
|
const rootResult = await copyTemplateTree(rootAssets, destRoot, {
|
|
1357
|
-
force:
|
|
1358
|
-
dryRun: options.dryRun
|
|
1526
|
+
force: false,
|
|
1527
|
+
dryRun: options.dryRun,
|
|
1528
|
+
conflictPolicy: "skip"
|
|
1359
1529
|
});
|
|
1360
1530
|
const qfaiResult = await copyTemplateTree(qfaiAssets, destQfai, {
|
|
1361
|
-
force:
|
|
1531
|
+
force: false,
|
|
1362
1532
|
dryRun: options.dryRun,
|
|
1363
|
-
|
|
1533
|
+
conflictPolicy: "skip",
|
|
1534
|
+
protect: ["prompts.local"],
|
|
1535
|
+
exclude: ["prompts"]
|
|
1364
1536
|
});
|
|
1537
|
+
const promptsResult = await copyTemplatePaths(
|
|
1538
|
+
qfaiAssets,
|
|
1539
|
+
destQfai,
|
|
1540
|
+
["prompts"],
|
|
1541
|
+
{
|
|
1542
|
+
force: options.force,
|
|
1543
|
+
dryRun: options.dryRun,
|
|
1544
|
+
conflictPolicy: "skip",
|
|
1545
|
+
protect: ["prompts.local"]
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1365
1548
|
report(
|
|
1366
|
-
[...rootResult.copied, ...qfaiResult.copied],
|
|
1367
|
-
[...rootResult.skipped, ...qfaiResult.skipped],
|
|
1549
|
+
[...rootResult.copied, ...qfaiResult.copied, ...promptsResult.copied],
|
|
1550
|
+
[...rootResult.skipped, ...qfaiResult.skipped, ...promptsResult.skipped],
|
|
1368
1551
|
options.dryRun,
|
|
1369
1552
|
"init"
|
|
1370
1553
|
);
|
|
@@ -1380,8 +1563,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1380
1563
|
}
|
|
1381
1564
|
|
|
1382
1565
|
// src/cli/commands/report.ts
|
|
1383
|
-
import { mkdir as mkdir3, readFile as
|
|
1384
|
-
import
|
|
1566
|
+
import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
|
|
1567
|
+
import path19 from "path";
|
|
1385
1568
|
|
|
1386
1569
|
// src/core/normalize.ts
|
|
1387
1570
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1421,12 +1604,12 @@ function normalizeValidationResult(root, result) {
|
|
|
1421
1604
|
}
|
|
1422
1605
|
|
|
1423
1606
|
// src/core/report.ts
|
|
1424
|
-
import { readFile as
|
|
1425
|
-
import
|
|
1607
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1608
|
+
import path18 from "path";
|
|
1426
1609
|
|
|
1427
1610
|
// src/core/contractIndex.ts
|
|
1428
|
-
import { readFile as
|
|
1429
|
-
import
|
|
1611
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1612
|
+
import path13 from "path";
|
|
1430
1613
|
|
|
1431
1614
|
// src/core/contractsDecl.ts
|
|
1432
1615
|
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
@@ -1448,9 +1631,9 @@ function stripContractDeclarationLines(text) {
|
|
|
1448
1631
|
// src/core/contractIndex.ts
|
|
1449
1632
|
async function buildContractIndex(root, config) {
|
|
1450
1633
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1451
|
-
const uiRoot =
|
|
1452
|
-
const apiRoot =
|
|
1453
|
-
const dbRoot =
|
|
1634
|
+
const uiRoot = path13.join(contractsRoot, "ui");
|
|
1635
|
+
const apiRoot = path13.join(contractsRoot, "api");
|
|
1636
|
+
const dbRoot = path13.join(contractsRoot, "db");
|
|
1454
1637
|
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1455
1638
|
collectUiContractFiles(uiRoot),
|
|
1456
1639
|
collectApiContractFiles(apiRoot),
|
|
@@ -1468,7 +1651,7 @@ async function buildContractIndex(root, config) {
|
|
|
1468
1651
|
}
|
|
1469
1652
|
async function indexContractFiles(files, index) {
|
|
1470
1653
|
for (const file of files) {
|
|
1471
|
-
const text = await
|
|
1654
|
+
const text = await readFile5(file, "utf-8");
|
|
1472
1655
|
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1473
1656
|
}
|
|
1474
1657
|
}
|
|
@@ -1703,14 +1886,14 @@ function parseSpec(md, file) {
|
|
|
1703
1886
|
}
|
|
1704
1887
|
|
|
1705
1888
|
// src/core/validators/contracts.ts
|
|
1706
|
-
import { readFile as
|
|
1707
|
-
import
|
|
1889
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1890
|
+
import path15 from "path";
|
|
1708
1891
|
|
|
1709
1892
|
// src/core/contracts.ts
|
|
1710
|
-
import
|
|
1893
|
+
import path14 from "path";
|
|
1711
1894
|
import { parse as parseYaml2 } from "yaml";
|
|
1712
1895
|
function parseStructuredContract(file, text) {
|
|
1713
|
-
const ext =
|
|
1896
|
+
const ext = path14.extname(file).toLowerCase();
|
|
1714
1897
|
if (ext === ".json") {
|
|
1715
1898
|
return JSON.parse(text);
|
|
1716
1899
|
}
|
|
@@ -1730,9 +1913,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1730
1913
|
async function validateContracts(root, config) {
|
|
1731
1914
|
const issues = [];
|
|
1732
1915
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1733
|
-
issues.push(...await validateUiContracts(
|
|
1734
|
-
issues.push(...await validateApiContracts(
|
|
1735
|
-
issues.push(...await validateDbContracts(
|
|
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")));
|
|
1736
1919
|
const contractIndex = await buildContractIndex(root, config);
|
|
1737
1920
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1738
1921
|
return issues;
|
|
@@ -1752,7 +1935,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
1752
1935
|
}
|
|
1753
1936
|
const issues = [];
|
|
1754
1937
|
for (const file of files) {
|
|
1755
|
-
const text = await
|
|
1938
|
+
const text = await readFile6(file, "utf-8");
|
|
1756
1939
|
const invalidIds = extractInvalidIds(text, [
|
|
1757
1940
|
"SPEC",
|
|
1758
1941
|
"BR",
|
|
@@ -1807,7 +1990,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1807
1990
|
}
|
|
1808
1991
|
const issues = [];
|
|
1809
1992
|
for (const file of files) {
|
|
1810
|
-
const text = await
|
|
1993
|
+
const text = await readFile6(file, "utf-8");
|
|
1811
1994
|
const invalidIds = extractInvalidIds(text, [
|
|
1812
1995
|
"SPEC",
|
|
1813
1996
|
"BR",
|
|
@@ -1875,7 +2058,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1875
2058
|
}
|
|
1876
2059
|
const issues = [];
|
|
1877
2060
|
for (const file of files) {
|
|
1878
|
-
const text = await
|
|
2061
|
+
const text = await readFile6(file, "utf-8");
|
|
1879
2062
|
const invalidIds = extractInvalidIds(text, [
|
|
1880
2063
|
"SPEC",
|
|
1881
2064
|
"BR",
|
|
@@ -1995,12 +2178,16 @@ function formatError4(error2) {
|
|
|
1995
2178
|
}
|
|
1996
2179
|
return String(error2);
|
|
1997
2180
|
}
|
|
1998
|
-
function issue(code, message, severity, file, rule, refs) {
|
|
2181
|
+
function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
1999
2182
|
const issue7 = {
|
|
2000
2183
|
code,
|
|
2001
2184
|
severity,
|
|
2185
|
+
category,
|
|
2002
2186
|
message
|
|
2003
2187
|
};
|
|
2188
|
+
if (suggested_action) {
|
|
2189
|
+
issue7.suggested_action = suggested_action;
|
|
2190
|
+
}
|
|
2004
2191
|
if (file) {
|
|
2005
2192
|
issue7.file = file;
|
|
2006
2193
|
}
|
|
@@ -2014,8 +2201,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
2014
2201
|
}
|
|
2015
2202
|
|
|
2016
2203
|
// src/core/validators/delta.ts
|
|
2017
|
-
import { readFile as
|
|
2018
|
-
import
|
|
2204
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2205
|
+
import path16 from "path";
|
|
2019
2206
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
2020
2207
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2021
2208
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -2029,10 +2216,10 @@ async function validateDeltas(root, config) {
|
|
|
2029
2216
|
}
|
|
2030
2217
|
const issues = [];
|
|
2031
2218
|
for (const pack of packs) {
|
|
2032
|
-
const deltaPath =
|
|
2219
|
+
const deltaPath = path16.join(pack, "delta.md");
|
|
2033
2220
|
let text;
|
|
2034
2221
|
try {
|
|
2035
|
-
text = await
|
|
2222
|
+
text = await readFile7(deltaPath, "utf-8");
|
|
2036
2223
|
} catch (error2) {
|
|
2037
2224
|
if (isMissingFileError2(error2)) {
|
|
2038
2225
|
issues.push(
|
|
@@ -2055,7 +2242,7 @@ async function validateDeltas(root, config) {
|
|
|
2055
2242
|
issues.push(
|
|
2056
2243
|
issue2(
|
|
2057
2244
|
"QFAI-DELTA-002",
|
|
2058
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2245
|
+
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002`## \u5909\u66F4\u533A\u5206` \u3068\u30C1\u30A7\u30C3\u30AF\u30DC\u30C3\u30AF\u30B9\uFF08Compatibility / Change/Improvement\uFF09\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2059
2246
|
"error",
|
|
2060
2247
|
deltaPath,
|
|
2061
2248
|
"delta.section"
|
|
@@ -2085,12 +2272,16 @@ function isMissingFileError2(error2) {
|
|
|
2085
2272
|
}
|
|
2086
2273
|
return error2.code === "ENOENT";
|
|
2087
2274
|
}
|
|
2088
|
-
function issue2(code, message, severity, file, rule, refs) {
|
|
2275
|
+
function issue2(code, message, severity, file, rule, refs, category = "change", suggested_action) {
|
|
2089
2276
|
const issue7 = {
|
|
2090
2277
|
code,
|
|
2091
2278
|
severity,
|
|
2279
|
+
category,
|
|
2092
2280
|
message
|
|
2093
2281
|
};
|
|
2282
|
+
if (suggested_action) {
|
|
2283
|
+
issue7.suggested_action = suggested_action;
|
|
2284
|
+
}
|
|
2094
2285
|
if (file) {
|
|
2095
2286
|
issue7.file = file;
|
|
2096
2287
|
}
|
|
@@ -2104,8 +2295,8 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
2104
2295
|
}
|
|
2105
2296
|
|
|
2106
2297
|
// src/core/validators/ids.ts
|
|
2107
|
-
import { readFile as
|
|
2108
|
-
import
|
|
2298
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2299
|
+
import path17 from "path";
|
|
2109
2300
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2110
2301
|
async function validateDefinedIds(root, config) {
|
|
2111
2302
|
const issues = [];
|
|
@@ -2140,7 +2331,7 @@ async function validateDefinedIds(root, config) {
|
|
|
2140
2331
|
}
|
|
2141
2332
|
async function collectSpecDefinitionIds(files, out) {
|
|
2142
2333
|
for (const file of files) {
|
|
2143
|
-
const text = await
|
|
2334
|
+
const text = await readFile8(file, "utf-8");
|
|
2144
2335
|
const parsed = parseSpec(text, file);
|
|
2145
2336
|
if (parsed.specId) {
|
|
2146
2337
|
recordId(out, parsed.specId, file);
|
|
@@ -2150,7 +2341,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
2150
2341
|
}
|
|
2151
2342
|
async function collectScenarioDefinitionIds(files, out) {
|
|
2152
2343
|
for (const file of files) {
|
|
2153
|
-
const text = await
|
|
2344
|
+
const text = await readFile8(file, "utf-8");
|
|
2154
2345
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2155
2346
|
if (!document || errors.length > 0) {
|
|
2156
2347
|
continue;
|
|
@@ -2171,16 +2362,20 @@ function recordId(out, id, file) {
|
|
|
2171
2362
|
}
|
|
2172
2363
|
function formatFileList(files, root) {
|
|
2173
2364
|
return files.map((file) => {
|
|
2174
|
-
const relative =
|
|
2365
|
+
const relative = path17.relative(root, file);
|
|
2175
2366
|
return relative.length > 0 ? relative : file;
|
|
2176
2367
|
}).join(", ");
|
|
2177
2368
|
}
|
|
2178
|
-
function issue3(code, message, severity, file, rule, refs) {
|
|
2369
|
+
function issue3(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2179
2370
|
const issue7 = {
|
|
2180
2371
|
code,
|
|
2181
2372
|
severity,
|
|
2373
|
+
category,
|
|
2182
2374
|
message
|
|
2183
2375
|
};
|
|
2376
|
+
if (suggested_action) {
|
|
2377
|
+
issue7.suggested_action = suggested_action;
|
|
2378
|
+
}
|
|
2184
2379
|
if (file) {
|
|
2185
2380
|
issue7.file = file;
|
|
2186
2381
|
}
|
|
@@ -2193,8 +2388,39 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
2193
2388
|
return issue7;
|
|
2194
2389
|
}
|
|
2195
2390
|
|
|
2391
|
+
// src/core/validators/promptsIntegrity.ts
|
|
2392
|
+
async function validatePromptsIntegrity(root) {
|
|
2393
|
+
const diff = await diffProjectPromptsAgainstInitAssets(root);
|
|
2394
|
+
if (diff.status !== "modified") {
|
|
2395
|
+
return [];
|
|
2396
|
+
}
|
|
2397
|
+
const total = diff.missing.length + diff.extra.length + diff.changed.length;
|
|
2398
|
+
const hints = [
|
|
2399
|
+
diff.changed.length > 0 ? `\u5909\u66F4: ${diff.changed.length}` : null,
|
|
2400
|
+
diff.missing.length > 0 ? `\u524A\u9664: ${diff.missing.length}` : null,
|
|
2401
|
+
diff.extra.length > 0 ? `\u8FFD\u52A0: ${diff.extra.length}` : null
|
|
2402
|
+
].filter(Boolean).join(" / ");
|
|
2403
|
+
const sample = [...diff.changed, ...diff.missing, ...diff.extra].slice(0, 10);
|
|
2404
|
+
const sampleText = sample.length > 0 ? ` \u4F8B: ${sample.join(", ")}` : "";
|
|
2405
|
+
return [
|
|
2406
|
+
{
|
|
2407
|
+
code: "QFAI-PROMPTS-001",
|
|
2408
|
+
severity: "error",
|
|
2409
|
+
category: "change",
|
|
2410
|
+
message: `\u6A19\u6E96\u8CC7\u7523 '.qfai/prompts/**' \u304C\u6539\u5909\u3055\u308C\u3066\u3044\u307E\u3059\uFF08${hints || `\u5DEE\u5206=${total}`}\uFF09\u3002${sampleText}`,
|
|
2411
|
+
suggested_action: [
|
|
2412
|
+
"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",
|
|
2413
|
+
"\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u5B9F\u65BD\u3057\u3066\u304F\u3060\u3055\u3044:",
|
|
2414
|
+
"- \u5909\u66F4\u3057\u305F\u3044\u5834\u5408: \u540C\u4E00\u76F8\u5BFE\u30D1\u30B9\u3067 '.qfai/prompts.local/**' \u306B\u7F6E\u3044\u3066 overlay",
|
|
2415
|
+
"- \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"
|
|
2416
|
+
].join("\n"),
|
|
2417
|
+
rule: "prompts.integrity"
|
|
2418
|
+
}
|
|
2419
|
+
];
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2196
2422
|
// src/core/validators/scenario.ts
|
|
2197
|
-
import { readFile as
|
|
2423
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2198
2424
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2199
2425
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2200
2426
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2220,7 +2446,7 @@ async function validateScenarios(root, config) {
|
|
|
2220
2446
|
for (const entry of entries) {
|
|
2221
2447
|
let text;
|
|
2222
2448
|
try {
|
|
2223
|
-
text = await
|
|
2449
|
+
text = await readFile9(entry.scenarioPath, "utf-8");
|
|
2224
2450
|
} catch (error2) {
|
|
2225
2451
|
if (isMissingFileError3(error2)) {
|
|
2226
2452
|
issues.push(
|
|
@@ -2365,12 +2591,16 @@ function validateScenarioContent(text, file) {
|
|
|
2365
2591
|
}
|
|
2366
2592
|
return issues;
|
|
2367
2593
|
}
|
|
2368
|
-
function issue4(code, message, severity, file, rule, refs) {
|
|
2594
|
+
function issue4(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2369
2595
|
const issue7 = {
|
|
2370
2596
|
code,
|
|
2371
2597
|
severity,
|
|
2598
|
+
category,
|
|
2372
2599
|
message
|
|
2373
2600
|
};
|
|
2601
|
+
if (suggested_action) {
|
|
2602
|
+
issue7.suggested_action = suggested_action;
|
|
2603
|
+
}
|
|
2374
2604
|
if (file) {
|
|
2375
2605
|
issue7.file = file;
|
|
2376
2606
|
}
|
|
@@ -2390,7 +2620,7 @@ function isMissingFileError3(error2) {
|
|
|
2390
2620
|
}
|
|
2391
2621
|
|
|
2392
2622
|
// src/core/validators/spec.ts
|
|
2393
|
-
import { readFile as
|
|
2623
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2394
2624
|
async function validateSpecs(root, config) {
|
|
2395
2625
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2396
2626
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -2411,7 +2641,7 @@ async function validateSpecs(root, config) {
|
|
|
2411
2641
|
for (const entry of entries) {
|
|
2412
2642
|
let text;
|
|
2413
2643
|
try {
|
|
2414
|
-
text = await
|
|
2644
|
+
text = await readFile10(entry.specPath, "utf-8");
|
|
2415
2645
|
} catch (error2) {
|
|
2416
2646
|
if (isMissingFileError4(error2)) {
|
|
2417
2647
|
issues.push(
|
|
@@ -2535,12 +2765,16 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2535
2765
|
}
|
|
2536
2766
|
return issues;
|
|
2537
2767
|
}
|
|
2538
|
-
function issue5(code, message, severity, file, rule, refs) {
|
|
2768
|
+
function issue5(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
2539
2769
|
const issue7 = {
|
|
2540
2770
|
code,
|
|
2541
2771
|
severity,
|
|
2772
|
+
category,
|
|
2542
2773
|
message
|
|
2543
2774
|
};
|
|
2775
|
+
if (suggested_action) {
|
|
2776
|
+
issue7.suggested_action = suggested_action;
|
|
2777
|
+
}
|
|
2544
2778
|
if (file) {
|
|
2545
2779
|
issue7.file = file;
|
|
2546
2780
|
}
|
|
@@ -2560,7 +2794,7 @@ function isMissingFileError4(error2) {
|
|
|
2560
2794
|
}
|
|
2561
2795
|
|
|
2562
2796
|
// src/core/validators/traceability.ts
|
|
2563
|
-
import { readFile as
|
|
2797
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2564
2798
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2565
2799
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2566
2800
|
async function validateTraceability(root, config) {
|
|
@@ -2580,7 +2814,7 @@ async function validateTraceability(root, config) {
|
|
|
2580
2814
|
const contractIndex = await buildContractIndex(root, config);
|
|
2581
2815
|
const contractIds = contractIndex.ids;
|
|
2582
2816
|
for (const file of specFiles) {
|
|
2583
|
-
const text = await
|
|
2817
|
+
const text = await readFile11(file, "utf-8");
|
|
2584
2818
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2585
2819
|
const parsed = parseSpec(text, file);
|
|
2586
2820
|
if (parsed.specId) {
|
|
@@ -2598,7 +2832,7 @@ async function validateTraceability(root, config) {
|
|
|
2598
2832
|
issues.push(
|
|
2599
2833
|
issue6(
|
|
2600
2834
|
"QFAI-TRACE-020",
|
|
2601
|
-
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2835
|
+
"Spec \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002\u4F8B: `QFAI-CONTRACT-REF: none` \u307E\u305F\u306F `QFAI-CONTRACT-REF: UI-0001`",
|
|
2602
2836
|
"error",
|
|
2603
2837
|
file,
|
|
2604
2838
|
"traceability.specContractRefRequired"
|
|
@@ -2609,7 +2843,7 @@ async function validateTraceability(root, config) {
|
|
|
2609
2843
|
issues.push(
|
|
2610
2844
|
issue6(
|
|
2611
2845
|
"QFAI-TRACE-023",
|
|
2612
|
-
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2846
|
+
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002none \u304B \u5951\u7D04ID \u306E\u3069\u3061\u3089\u304B\u4E00\u65B9\u3060\u3051\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2613
2847
|
"error",
|
|
2614
2848
|
file,
|
|
2615
2849
|
"traceability.specContractRefFormat"
|
|
@@ -2622,7 +2856,7 @@ async function validateTraceability(root, config) {
|
|
|
2622
2856
|
"QFAI-TRACE-021",
|
|
2623
2857
|
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2624
2858
|
", "
|
|
2625
|
-
)}`,
|
|
2859
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
2626
2860
|
"error",
|
|
2627
2861
|
file,
|
|
2628
2862
|
"traceability.specContractRefFormat",
|
|
@@ -2653,7 +2887,7 @@ async function validateTraceability(root, config) {
|
|
|
2653
2887
|
}
|
|
2654
2888
|
}
|
|
2655
2889
|
for (const file of scenarioFiles) {
|
|
2656
|
-
const text = await
|
|
2890
|
+
const text = await readFile11(file, "utf-8");
|
|
2657
2891
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2658
2892
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2659
2893
|
allowCommentPrefix: true
|
|
@@ -2662,7 +2896,7 @@ async function validateTraceability(root, config) {
|
|
|
2662
2896
|
issues.push(
|
|
2663
2897
|
issue6(
|
|
2664
2898
|
"QFAI-TRACE-031",
|
|
2665
|
-
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2899
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002\u4F8B: `# QFAI-CONTRACT-REF: none` \u307E\u305F\u306F `# QFAI-CONTRACT-REF: UI-0001`",
|
|
2666
2900
|
"error",
|
|
2667
2901
|
file,
|
|
2668
2902
|
"traceability.scenarioContractRefRequired"
|
|
@@ -2673,7 +2907,7 @@ async function validateTraceability(root, config) {
|
|
|
2673
2907
|
issues.push(
|
|
2674
2908
|
issue6(
|
|
2675
2909
|
"QFAI-TRACE-033",
|
|
2676
|
-
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2910
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002none \u304B \u5951\u7D04ID \u306E\u3069\u3061\u3089\u304B\u4E00\u65B9\u3060\u3051\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2677
2911
|
"error",
|
|
2678
2912
|
file,
|
|
2679
2913
|
"traceability.scenarioContractRefFormat"
|
|
@@ -2686,7 +2920,7 @@ async function validateTraceability(root, config) {
|
|
|
2686
2920
|
"QFAI-TRACE-032",
|
|
2687
2921
|
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2688
2922
|
", "
|
|
2689
|
-
)}`,
|
|
2923
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
2690
2924
|
"error",
|
|
2691
2925
|
file,
|
|
2692
2926
|
"traceability.scenarioContractRefFormat",
|
|
@@ -2975,7 +3209,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2975
3209
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2976
3210
|
let found = false;
|
|
2977
3211
|
for (const file of targetFiles) {
|
|
2978
|
-
const text = await
|
|
3212
|
+
const text = await readFile11(file, "utf-8");
|
|
2979
3213
|
if (pattern.test(text)) {
|
|
2980
3214
|
found = true;
|
|
2981
3215
|
break;
|
|
@@ -2998,12 +3232,16 @@ function buildIdPattern(ids) {
|
|
|
2998
3232
|
const escaped = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2999
3233
|
return new RegExp(`\\b(${escaped.join("|")})\\b`);
|
|
3000
3234
|
}
|
|
3001
|
-
function issue6(code, message, severity, file, rule, refs) {
|
|
3235
|
+
function issue6(code, message, severity, file, rule, refs, category = "compatibility", suggested_action) {
|
|
3002
3236
|
const issue7 = {
|
|
3003
3237
|
code,
|
|
3004
3238
|
severity,
|
|
3239
|
+
category,
|
|
3005
3240
|
message
|
|
3006
3241
|
};
|
|
3242
|
+
if (suggested_action) {
|
|
3243
|
+
issue7.suggested_action = suggested_action;
|
|
3244
|
+
}
|
|
3007
3245
|
if (file) {
|
|
3008
3246
|
issue7.file = file;
|
|
3009
3247
|
}
|
|
@@ -3022,6 +3260,7 @@ async function validateProject(root, configResult) {
|
|
|
3022
3260
|
const { config, issues: configIssues } = resolved;
|
|
3023
3261
|
const issues = [
|
|
3024
3262
|
...configIssues,
|
|
3263
|
+
...await validatePromptsIntegrity(root),
|
|
3025
3264
|
...await validateSpecs(root, config),
|
|
3026
3265
|
...await validateDeltas(root, config),
|
|
3027
3266
|
...await validateScenarios(root, config),
|
|
@@ -3062,15 +3301,15 @@ function countIssues(issues) {
|
|
|
3062
3301
|
// src/core/report.ts
|
|
3063
3302
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
3064
3303
|
async function createReportData(root, validation, configResult) {
|
|
3065
|
-
const resolvedRoot =
|
|
3304
|
+
const resolvedRoot = path18.resolve(root);
|
|
3066
3305
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3067
3306
|
const config = resolved.config;
|
|
3068
3307
|
const configPath = resolved.configPath;
|
|
3069
3308
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3070
3309
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3071
|
-
const apiRoot =
|
|
3072
|
-
const uiRoot =
|
|
3073
|
-
const dbRoot =
|
|
3310
|
+
const apiRoot = path18.join(contractsRoot, "api");
|
|
3311
|
+
const uiRoot = path18.join(contractsRoot, "ui");
|
|
3312
|
+
const dbRoot = path18.join(contractsRoot, "db");
|
|
3074
3313
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3075
3314
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3076
3315
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3184,7 +3423,39 @@ function formatReportMarkdown(data) {
|
|
|
3184
3423
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
3185
3424
|
lines.push(`- \u7248: ${data.version}`);
|
|
3186
3425
|
lines.push("");
|
|
3187
|
-
|
|
3426
|
+
const severityOrder = {
|
|
3427
|
+
error: 0,
|
|
3428
|
+
warning: 1,
|
|
3429
|
+
info: 2
|
|
3430
|
+
};
|
|
3431
|
+
const categoryOrder = {
|
|
3432
|
+
compatibility: 0,
|
|
3433
|
+
change: 1
|
|
3434
|
+
};
|
|
3435
|
+
const issuesByCategory = {
|
|
3436
|
+
compatibility: [],
|
|
3437
|
+
change: []
|
|
3438
|
+
};
|
|
3439
|
+
for (const issue7 of data.issues) {
|
|
3440
|
+
const cat = issue7.category;
|
|
3441
|
+
if (cat === "change") {
|
|
3442
|
+
issuesByCategory.change.push(issue7);
|
|
3443
|
+
} else {
|
|
3444
|
+
issuesByCategory.compatibility.push(issue7);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
const countIssuesBySeverity = (issues) => issues.reduce(
|
|
3448
|
+
(acc, i) => {
|
|
3449
|
+
acc[i.severity] += 1;
|
|
3450
|
+
return acc;
|
|
3451
|
+
},
|
|
3452
|
+
{ info: 0, warning: 0, error: 0 }
|
|
3453
|
+
);
|
|
3454
|
+
const compatCounts = countIssuesBySeverity(issuesByCategory.compatibility);
|
|
3455
|
+
const changeCounts = countIssuesBySeverity(issuesByCategory.change);
|
|
3456
|
+
lines.push("## Dashboard");
|
|
3457
|
+
lines.push("");
|
|
3458
|
+
lines.push("### Summary");
|
|
3188
3459
|
lines.push("");
|
|
3189
3460
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
3190
3461
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
@@ -3192,10 +3463,141 @@ function formatReportMarkdown(data) {
|
|
|
3192
3463
|
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
3193
3464
|
);
|
|
3194
3465
|
lines.push(
|
|
3195
|
-
`- issues: info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
3466
|
+
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
3467
|
+
);
|
|
3468
|
+
lines.push(
|
|
3469
|
+
`- issues(compatibility): info ${compatCounts.info} / warning ${compatCounts.warning} / error ${compatCounts.error}`
|
|
3470
|
+
);
|
|
3471
|
+
lines.push(
|
|
3472
|
+
`- issues(change): info ${changeCounts.info} / warning ${changeCounts.warning} / error ${changeCounts.error}`
|
|
3473
|
+
);
|
|
3474
|
+
lines.push(
|
|
3475
|
+
`- fail-on=error: ${data.summary.counts.error > 0 ? "FAIL" : "PASS"}`
|
|
3476
|
+
);
|
|
3477
|
+
lines.push(
|
|
3478
|
+
`- fail-on=warning: ${data.summary.counts.error + data.summary.counts.warning > 0 ? "FAIL" : "PASS"}`
|
|
3196
3479
|
);
|
|
3197
3480
|
lines.push("");
|
|
3198
|
-
lines.push("
|
|
3481
|
+
lines.push("### Next Actions");
|
|
3482
|
+
lines.push("");
|
|
3483
|
+
if (data.summary.counts.error > 0) {
|
|
3484
|
+
lines.push(
|
|
3485
|
+
"- 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"
|
|
3486
|
+
);
|
|
3487
|
+
lines.push(
|
|
3488
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
3489
|
+
);
|
|
3490
|
+
} else if (data.summary.counts.warning > 0) {
|
|
3491
|
+
lines.push(
|
|
3492
|
+
"- 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"
|
|
3493
|
+
);
|
|
3494
|
+
lines.push(
|
|
3495
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
3496
|
+
);
|
|
3497
|
+
} else {
|
|
3498
|
+
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");
|
|
3499
|
+
lines.push(
|
|
3500
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor` \u2192 `qfai validate` \u2192 `qfai report`\uFF08\u5B9A\u671F\u7684\u306B\u5B9F\u884C\uFF09"
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
lines.push("");
|
|
3504
|
+
lines.push("### Index");
|
|
3505
|
+
lines.push("");
|
|
3506
|
+
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
3507
|
+
lines.push("- [Change Issues](#change-issues)");
|
|
3508
|
+
lines.push("- [IDs](#ids)");
|
|
3509
|
+
lines.push("- [Traceability](#traceability)");
|
|
3510
|
+
lines.push("");
|
|
3511
|
+
const formatIssueSummaryTable = (issues) => {
|
|
3512
|
+
const issueKeyToCount = /* @__PURE__ */ new Map();
|
|
3513
|
+
for (const issue7 of issues) {
|
|
3514
|
+
const key = `${issue7.category}|${issue7.severity}|${issue7.code}`;
|
|
3515
|
+
const current = issueKeyToCount.get(key);
|
|
3516
|
+
if (current) {
|
|
3517
|
+
current.count += 1;
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
issueKeyToCount.set(key, {
|
|
3521
|
+
category: issue7.category,
|
|
3522
|
+
severity: issue7.severity,
|
|
3523
|
+
code: issue7.code,
|
|
3524
|
+
count: 1
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
const rows = Array.from(issueKeyToCount.values()).sort((a, b) => {
|
|
3528
|
+
const ca = categoryOrder[a.category] ?? 999;
|
|
3529
|
+
const cb = categoryOrder[b.category] ?? 999;
|
|
3530
|
+
if (ca !== cb) return ca - cb;
|
|
3531
|
+
const sa = severityOrder[a.severity] ?? 999;
|
|
3532
|
+
const sb = severityOrder[b.severity] ?? 999;
|
|
3533
|
+
if (sa !== sb) return sa - sb;
|
|
3534
|
+
return a.code.localeCompare(b.code);
|
|
3535
|
+
}).map((x) => [x.severity, x.code, String(x.count)]);
|
|
3536
|
+
return rows.length === 0 ? ["- (none)"] : formatMarkdownTable(["Severity", "Code", "Count"], rows);
|
|
3537
|
+
};
|
|
3538
|
+
const formatIssueCards = (issues) => {
|
|
3539
|
+
const sorted = [...issues].sort((a, b) => {
|
|
3540
|
+
const sa = severityOrder[a.severity] ?? 999;
|
|
3541
|
+
const sb = severityOrder[b.severity] ?? 999;
|
|
3542
|
+
if (sa !== sb) return sa - sb;
|
|
3543
|
+
const code = a.code.localeCompare(b.code);
|
|
3544
|
+
if (code !== 0) return code;
|
|
3545
|
+
const fileA = a.file ?? "";
|
|
3546
|
+
const fileB = b.file ?? "";
|
|
3547
|
+
const file = fileA.localeCompare(fileB);
|
|
3548
|
+
if (file !== 0) return file;
|
|
3549
|
+
const lineA = a.loc?.line ?? 0;
|
|
3550
|
+
const lineB = b.loc?.line ?? 0;
|
|
3551
|
+
return lineA - lineB;
|
|
3552
|
+
});
|
|
3553
|
+
if (sorted.length === 0) {
|
|
3554
|
+
return ["- (none)"];
|
|
3555
|
+
}
|
|
3556
|
+
const out = [];
|
|
3557
|
+
for (const item of sorted) {
|
|
3558
|
+
out.push(
|
|
3559
|
+
`#### ${item.severity.toUpperCase()} [${item.code}] ${item.message}`
|
|
3560
|
+
);
|
|
3561
|
+
if (item.file) {
|
|
3562
|
+
const loc = item.loc?.line ? `:${item.loc.line}` : "";
|
|
3563
|
+
out.push(`- file: ${item.file}${loc}`);
|
|
3564
|
+
}
|
|
3565
|
+
if (item.rule) {
|
|
3566
|
+
out.push(`- rule: ${item.rule}`);
|
|
3567
|
+
}
|
|
3568
|
+
if (item.refs && item.refs.length > 0) {
|
|
3569
|
+
out.push(`- refs: ${item.refs.join(", ")}`);
|
|
3570
|
+
}
|
|
3571
|
+
if (item.suggested_action) {
|
|
3572
|
+
out.push("- suggested_action:");
|
|
3573
|
+
const actionLines = String(item.suggested_action).split("\n");
|
|
3574
|
+
for (const line of actionLines) {
|
|
3575
|
+
out.push(` ${line}`);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
out.push("");
|
|
3579
|
+
}
|
|
3580
|
+
return out;
|
|
3581
|
+
};
|
|
3582
|
+
lines.push("## Compatibility Issues");
|
|
3583
|
+
lines.push("");
|
|
3584
|
+
lines.push("### Summary");
|
|
3585
|
+
lines.push("");
|
|
3586
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.compatibility));
|
|
3587
|
+
lines.push("");
|
|
3588
|
+
lines.push("### Issues");
|
|
3589
|
+
lines.push("");
|
|
3590
|
+
lines.push(...formatIssueCards(issuesByCategory.compatibility));
|
|
3591
|
+
lines.push("## Change Issues");
|
|
3592
|
+
lines.push("");
|
|
3593
|
+
lines.push("### Summary");
|
|
3594
|
+
lines.push("");
|
|
3595
|
+
lines.push(...formatIssueSummaryTable(issuesByCategory.change));
|
|
3596
|
+
lines.push("");
|
|
3597
|
+
lines.push("### Issues");
|
|
3598
|
+
lines.push("");
|
|
3599
|
+
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
3600
|
+
lines.push("## IDs");
|
|
3199
3601
|
lines.push("");
|
|
3200
3602
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
3201
3603
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
@@ -3204,14 +3606,14 @@ function formatReportMarkdown(data) {
|
|
|
3204
3606
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3205
3607
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
3206
3608
|
lines.push("");
|
|
3207
|
-
lines.push("##
|
|
3609
|
+
lines.push("## Traceability");
|
|
3208
3610
|
lines.push("");
|
|
3209
3611
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
3210
3612
|
lines.push(
|
|
3211
3613
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
3212
3614
|
);
|
|
3213
3615
|
lines.push("");
|
|
3214
|
-
lines.push("
|
|
3616
|
+
lines.push("### Contract Coverage");
|
|
3215
3617
|
lines.push("");
|
|
3216
3618
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
3217
3619
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
@@ -3220,7 +3622,7 @@ function formatReportMarkdown(data) {
|
|
|
3220
3622
|
`- specContractRefMissing: ${data.traceability.specs.contractRefMissing}`
|
|
3221
3623
|
);
|
|
3222
3624
|
lines.push("");
|
|
3223
|
-
lines.push("
|
|
3625
|
+
lines.push("### Contract \u2192 Spec");
|
|
3224
3626
|
lines.push("");
|
|
3225
3627
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
3226
3628
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
@@ -3239,7 +3641,7 @@ function formatReportMarkdown(data) {
|
|
|
3239
3641
|
}
|
|
3240
3642
|
}
|
|
3241
3643
|
lines.push("");
|
|
3242
|
-
lines.push("
|
|
3644
|
+
lines.push("### Spec \u2192 Contracts");
|
|
3243
3645
|
lines.push("");
|
|
3244
3646
|
const specToContracts = data.traceability.specs.specToContracts;
|
|
3245
3647
|
const specIds = Object.keys(specToContracts).sort(
|
|
@@ -3257,7 +3659,7 @@ function formatReportMarkdown(data) {
|
|
|
3257
3659
|
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
3258
3660
|
}
|
|
3259
3661
|
lines.push("");
|
|
3260
|
-
lines.push("
|
|
3662
|
+
lines.push("### Specs missing contract-ref");
|
|
3261
3663
|
lines.push("");
|
|
3262
3664
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
3263
3665
|
if (missingRefSpecs.length === 0) {
|
|
@@ -3268,7 +3670,7 @@ function formatReportMarkdown(data) {
|
|
|
3268
3670
|
}
|
|
3269
3671
|
}
|
|
3270
3672
|
lines.push("");
|
|
3271
|
-
lines.push("
|
|
3673
|
+
lines.push("### SC coverage");
|
|
3272
3674
|
lines.push("");
|
|
3273
3675
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
3274
3676
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
@@ -3298,7 +3700,7 @@ function formatReportMarkdown(data) {
|
|
|
3298
3700
|
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
3299
3701
|
}
|
|
3300
3702
|
lines.push("");
|
|
3301
|
-
lines.push("
|
|
3703
|
+
lines.push("### SC \u2192 referenced tests");
|
|
3302
3704
|
lines.push("");
|
|
3303
3705
|
const scRefs = data.traceability.sc.refs;
|
|
3304
3706
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
@@ -3315,7 +3717,7 @@ function formatReportMarkdown(data) {
|
|
|
3315
3717
|
}
|
|
3316
3718
|
}
|
|
3317
3719
|
lines.push("");
|
|
3318
|
-
lines.push("
|
|
3720
|
+
lines.push("### Spec:SC=1:1 violations");
|
|
3319
3721
|
lines.push("");
|
|
3320
3722
|
const specScIssues = data.issues.filter(
|
|
3321
3723
|
(item) => item.code === "QFAI-TRACE-012"
|
|
@@ -3330,7 +3732,7 @@ function formatReportMarkdown(data) {
|
|
|
3330
3732
|
}
|
|
3331
3733
|
}
|
|
3332
3734
|
lines.push("");
|
|
3333
|
-
lines.push("
|
|
3735
|
+
lines.push("### Hotspots");
|
|
3334
3736
|
lines.push("");
|
|
3335
3737
|
const hotspots = buildHotspots(data.issues);
|
|
3336
3738
|
if (hotspots.length === 0) {
|
|
@@ -3343,35 +3745,28 @@ function formatReportMarkdown(data) {
|
|
|
3343
3745
|
}
|
|
3344
3746
|
}
|
|
3345
3747
|
lines.push("");
|
|
3346
|
-
lines.push("##
|
|
3748
|
+
lines.push("## Guidance");
|
|
3347
3749
|
lines.push("");
|
|
3348
|
-
|
|
3349
|
-
|
|
3750
|
+
lines.push(
|
|
3751
|
+
"- \u6B21\u306E\u624B\u9806: `qfai doctor --fail-on error` \u2192 `qfai validate --fail-on error` \u2192 `qfai report`"
|
|
3350
3752
|
);
|
|
3351
|
-
if (
|
|
3352
|
-
lines.push("-
|
|
3353
|
-
} else {
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
`- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}`
|
|
3358
|
-
);
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
lines.push("");
|
|
3362
|
-
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
3363
|
-
lines.push("");
|
|
3364
|
-
if (data.issues.length === 0) {
|
|
3365
|
-
lines.push("- (none)");
|
|
3753
|
+
if (data.summary.counts.error > 0) {
|
|
3754
|
+
lines.push("- error \u304C\u3042\u308B\u305F\u3081\u3001\u307E\u305A error \u304B\u3089\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
3755
|
+
} else if (data.summary.counts.warning > 0) {
|
|
3756
|
+
lines.push(
|
|
3757
|
+
"- warning \u306E\u6271\u3044\uFF08Hard Gate \u306B\u3059\u308B\u304B\uFF09\u306F\u904B\u7528\u3067\u6C7A\u3081\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3758
|
+
);
|
|
3366
3759
|
} else {
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
lines.push(
|
|
3371
|
-
`- ${item.severity.toUpperCase()} [${item.code}] ${item.message}${location}${refs}`
|
|
3372
|
-
);
|
|
3373
|
-
}
|
|
3760
|
+
lines.push(
|
|
3761
|
+
"- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3762
|
+
);
|
|
3374
3763
|
}
|
|
3764
|
+
lines.push(
|
|
3765
|
+
"- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
|
|
3766
|
+
);
|
|
3767
|
+
lines.push(
|
|
3768
|
+
"- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
|
|
3769
|
+
);
|
|
3375
3770
|
return lines.join("\n");
|
|
3376
3771
|
}
|
|
3377
3772
|
function formatReportJson(data) {
|
|
@@ -3385,7 +3780,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
3385
3780
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
3386
3781
|
}
|
|
3387
3782
|
for (const file of specFiles) {
|
|
3388
|
-
const text = await
|
|
3783
|
+
const text = await readFile12(file, "utf-8");
|
|
3389
3784
|
const parsed = parseSpec(text, file);
|
|
3390
3785
|
const specKey = parsed.specId;
|
|
3391
3786
|
if (!specKey) {
|
|
@@ -3426,7 +3821,7 @@ async function collectIds(files) {
|
|
|
3426
3821
|
DB: /* @__PURE__ */ new Set()
|
|
3427
3822
|
};
|
|
3428
3823
|
for (const file of files) {
|
|
3429
|
-
const text = await
|
|
3824
|
+
const text = await readFile12(file, "utf-8");
|
|
3430
3825
|
for (const prefix of ID_PREFIXES2) {
|
|
3431
3826
|
const ids = extractIds(text, prefix);
|
|
3432
3827
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3444,7 +3839,7 @@ async function collectIds(files) {
|
|
|
3444
3839
|
async function collectUpstreamIds(files) {
|
|
3445
3840
|
const ids = /* @__PURE__ */ new Set();
|
|
3446
3841
|
for (const file of files) {
|
|
3447
|
-
const text = await
|
|
3842
|
+
const text = await readFile12(file, "utf-8");
|
|
3448
3843
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3449
3844
|
}
|
|
3450
3845
|
return ids;
|
|
@@ -3465,7 +3860,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3465
3860
|
}
|
|
3466
3861
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3467
3862
|
for (const file of targetFiles) {
|
|
3468
|
-
const text = await
|
|
3863
|
+
const text = await readFile12(file, "utf-8");
|
|
3469
3864
|
if (pattern.test(text)) {
|
|
3470
3865
|
return true;
|
|
3471
3866
|
}
|
|
@@ -3557,7 +3952,7 @@ function buildHotspots(issues) {
|
|
|
3557
3952
|
|
|
3558
3953
|
// src/cli/commands/report.ts
|
|
3559
3954
|
async function runReport(options) {
|
|
3560
|
-
const root =
|
|
3955
|
+
const root = path19.resolve(options.root);
|
|
3561
3956
|
const configResult = await loadConfig(root);
|
|
3562
3957
|
let validation;
|
|
3563
3958
|
if (options.runValidate) {
|
|
@@ -3574,7 +3969,7 @@ async function runReport(options) {
|
|
|
3574
3969
|
validation = normalized;
|
|
3575
3970
|
} else {
|
|
3576
3971
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3577
|
-
const inputPath =
|
|
3972
|
+
const inputPath = path19.isAbsolute(input) ? input : path19.resolve(root, input);
|
|
3578
3973
|
try {
|
|
3579
3974
|
validation = await readValidationResult(inputPath);
|
|
3580
3975
|
} catch (err) {
|
|
@@ -3600,10 +3995,10 @@ async function runReport(options) {
|
|
|
3600
3995
|
const data = await createReportData(root, validation, configResult);
|
|
3601
3996
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3602
3997
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3603
|
-
const defaultOut = options.format === "json" ?
|
|
3998
|
+
const defaultOut = options.format === "json" ? path19.join(outRoot, "report.json") : path19.join(outRoot, "report.md");
|
|
3604
3999
|
const out = options.outPath ?? defaultOut;
|
|
3605
|
-
const outPath =
|
|
3606
|
-
await mkdir3(
|
|
4000
|
+
const outPath = path19.isAbsolute(out) ? out : path19.resolve(root, out);
|
|
4001
|
+
await mkdir3(path19.dirname(outPath), { recursive: true });
|
|
3607
4002
|
await writeFile2(outPath, `${output}
|
|
3608
4003
|
`, "utf-8");
|
|
3609
4004
|
info(
|
|
@@ -3612,7 +4007,7 @@ async function runReport(options) {
|
|
|
3612
4007
|
info(`wrote report: ${outPath}`);
|
|
3613
4008
|
}
|
|
3614
4009
|
async function readValidationResult(inputPath) {
|
|
3615
|
-
const raw = await
|
|
4010
|
+
const raw = await readFile13(inputPath, "utf-8");
|
|
3616
4011
|
const parsed = JSON.parse(raw);
|
|
3617
4012
|
if (!isValidationResult(parsed)) {
|
|
3618
4013
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -3668,15 +4063,15 @@ function isMissingFileError5(error2) {
|
|
|
3668
4063
|
return record2.code === "ENOENT";
|
|
3669
4064
|
}
|
|
3670
4065
|
async function writeValidationResult(root, outputPath, result) {
|
|
3671
|
-
const abs =
|
|
3672
|
-
await mkdir3(
|
|
4066
|
+
const abs = path19.isAbsolute(outputPath) ? outputPath : path19.resolve(root, outputPath);
|
|
4067
|
+
await mkdir3(path19.dirname(abs), { recursive: true });
|
|
3673
4068
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
3674
4069
|
`, "utf-8");
|
|
3675
4070
|
}
|
|
3676
4071
|
|
|
3677
4072
|
// src/cli/commands/validate.ts
|
|
3678
4073
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
3679
|
-
import
|
|
4074
|
+
import path20 from "path";
|
|
3680
4075
|
|
|
3681
4076
|
// src/cli/lib/failOn.ts
|
|
3682
4077
|
function shouldFail(result, failOn) {
|
|
@@ -3691,10 +4086,12 @@ function shouldFail(result, failOn) {
|
|
|
3691
4086
|
|
|
3692
4087
|
// src/cli/commands/validate.ts
|
|
3693
4088
|
async function runValidate(options) {
|
|
3694
|
-
const root =
|
|
4089
|
+
const root = path20.resolve(options.root);
|
|
3695
4090
|
const configResult = await loadConfig(root);
|
|
3696
4091
|
const result = await validateProject(root, configResult);
|
|
3697
4092
|
const normalized = normalizeValidationResult(root, result);
|
|
4093
|
+
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
4094
|
+
const willFail = shouldFail(normalized, failOn);
|
|
3698
4095
|
const format = options.format ?? "text";
|
|
3699
4096
|
if (format === "text") {
|
|
3700
4097
|
emitText(normalized);
|
|
@@ -3704,11 +4101,10 @@ async function runValidate(options) {
|
|
|
3704
4101
|
root,
|
|
3705
4102
|
configResult.config.output.validateJsonPath
|
|
3706
4103
|
);
|
|
3707
|
-
emitGitHubOutput(normalized, root, jsonPath);
|
|
4104
|
+
emitGitHubOutput(normalized, root, jsonPath, { failOn, willFail });
|
|
3708
4105
|
}
|
|
3709
4106
|
await emitJson(normalized, root, configResult.config.output.validateJsonPath);
|
|
3710
|
-
|
|
3711
|
-
return shouldFail(normalized, failOn) ? 1 : 0;
|
|
4107
|
+
return willFail ? 1 : 0;
|
|
3712
4108
|
}
|
|
3713
4109
|
function resolveFailOn(options, fallback) {
|
|
3714
4110
|
if (options.failOn) {
|
|
@@ -3733,7 +4129,7 @@ function emitText(result) {
|
|
|
3733
4129
|
`
|
|
3734
4130
|
);
|
|
3735
4131
|
}
|
|
3736
|
-
function emitGitHubOutput(result, root, jsonPath) {
|
|
4132
|
+
function emitGitHubOutput(result, root, jsonPath, status) {
|
|
3737
4133
|
const deduped = dedupeIssues(result.issues);
|
|
3738
4134
|
const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
|
|
3739
4135
|
const dropped = Math.max(result.issues.length - deduped.length, 0);
|
|
@@ -3742,7 +4138,8 @@ function emitGitHubOutput(result, root, jsonPath) {
|
|
|
3742
4138
|
omitted,
|
|
3743
4139
|
dropped,
|
|
3744
4140
|
jsonPath,
|
|
3745
|
-
root
|
|
4141
|
+
root,
|
|
4142
|
+
...status
|
|
3746
4143
|
});
|
|
3747
4144
|
const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
|
|
3748
4145
|
for (const issue7 of issues) {
|
|
@@ -3766,7 +4163,9 @@ function emitGitHubSummary(result, options) {
|
|
|
3766
4163
|
`error=${result.counts.error}`,
|
|
3767
4164
|
`warning=${result.counts.warning}`,
|
|
3768
4165
|
`info=${result.counts.info}`,
|
|
3769
|
-
`annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}
|
|
4166
|
+
`annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`,
|
|
4167
|
+
`failOn=${options.failOn}`,
|
|
4168
|
+
`result=${options.willFail ? "FAIL" : "PASS"}`
|
|
3770
4169
|
].join(" ");
|
|
3771
4170
|
process.stdout.write(`${summary}
|
|
3772
4171
|
`);
|
|
@@ -3784,6 +4183,9 @@ function emitGitHubSummary(result, options) {
|
|
|
3784
4183
|
`qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
3785
4184
|
`
|
|
3786
4185
|
);
|
|
4186
|
+
process.stdout.write(
|
|
4187
|
+
"qfai validate note: \u6B21\u306F qfai report \u3067 report.md \u3092\u751F\u6210\u3067\u304D\u307E\u3059\uFF08\u4F8B: qfai report\uFF09\u3002\n"
|
|
4188
|
+
);
|
|
3787
4189
|
}
|
|
3788
4190
|
function dedupeIssues(issues) {
|
|
3789
4191
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3808,12 +4210,12 @@ function issueKey(issue7) {
|
|
|
3808
4210
|
}
|
|
3809
4211
|
async function emitJson(result, root, jsonPath) {
|
|
3810
4212
|
const abs = resolveJsonPath(root, jsonPath);
|
|
3811
|
-
await mkdir4(
|
|
4213
|
+
await mkdir4(path20.dirname(abs), { recursive: true });
|
|
3812
4214
|
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
3813
4215
|
`, "utf-8");
|
|
3814
4216
|
}
|
|
3815
4217
|
function resolveJsonPath(root, jsonPath) {
|
|
3816
|
-
return
|
|
4218
|
+
return path20.isAbsolute(jsonPath) ? jsonPath : path20.resolve(root, jsonPath);
|
|
3817
4219
|
}
|
|
3818
4220
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3819
4221
|
|