qfai 1.0.2 → 1.0.4
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 +13 -12
- package/assets/init/.qfai/README.md +2 -7
- package/assets/init/.qfai/contracts/README.md +20 -0
- package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/assets.yaml +6 -0
- package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/palette.png +0 -0
- package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/assets.yaml +6 -0
- package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/snapshots/login__desktop__light__default.png +0 -0
- package/assets/init/.qfai/contracts/ui/thema-001-facebook-like.yml +13 -0
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +9 -0
- package/assets/init/.qfai/promptpack/commands/plan.md +1 -1
- package/assets/init/.qfai/promptpack/commands/review.md +1 -2
- package/assets/init/.qfai/promptpack/constitution.md +1 -1
- package/assets/init/.qfai/prompts/README.md +1 -3
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +3 -3
- package/assets/init/.qfai/prompts/require-to-spec.md +1 -2
- package/assets/init/.qfai/specs/README.md +3 -4
- package/assets/init/.qfai/specs/spec-0001/delta.md +0 -5
- package/assets/init/.qfai/specs/spec-0001/scenario.feature +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
- package/assets/init/root/qfai.config.yaml +0 -1
- package/dist/cli/index.cjs +596 -162
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +598 -164
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +549 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +551 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/promptpack/modes/change.md +0 -5
- package/assets/init/.qfai/promptpack/modes/compatibility.md +0 -6
- package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +0 -42
- package/assets/init/.qfai/prompts/qfai-classify-change.md +0 -33
- package/assets/init/.qfai/rules/conventions.md +0 -27
- package/assets/init/.qfai/rules/pnpm.md +0 -29
- package/assets/init/.qfai/samples/analyze/analysis.md +0 -38
- package/assets/init/.qfai/samples/analyze/input_bundle.md +0 -54
package/dist/cli/index.mjs
CHANGED
|
@@ -208,11 +208,11 @@ function emitPromptNotFound(promptName, candidates) {
|
|
|
208
208
|
|
|
209
209
|
// src/cli/commands/doctor.ts
|
|
210
210
|
import { mkdir, writeFile } from "fs/promises";
|
|
211
|
-
import
|
|
211
|
+
import path12 from "path";
|
|
212
212
|
|
|
213
213
|
// src/core/doctor.ts
|
|
214
214
|
import { access as access4 } from "fs/promises";
|
|
215
|
-
import
|
|
215
|
+
import path11 from "path";
|
|
216
216
|
|
|
217
217
|
// src/core/config.ts
|
|
218
218
|
import { access as access2, readFile as readFile2 } from "fs/promises";
|
|
@@ -222,7 +222,6 @@ var defaultConfig = {
|
|
|
222
222
|
paths: {
|
|
223
223
|
contractsDir: ".qfai/contracts",
|
|
224
224
|
specsDir: ".qfai/specs",
|
|
225
|
-
rulesDir: ".qfai/rules",
|
|
226
225
|
outDir: ".qfai/out",
|
|
227
226
|
promptsDir: ".qfai/prompts",
|
|
228
227
|
srcDir: "src",
|
|
@@ -335,13 +334,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
335
334
|
configPath,
|
|
336
335
|
issues
|
|
337
336
|
),
|
|
338
|
-
rulesDir: readString(
|
|
339
|
-
raw.rulesDir,
|
|
340
|
-
base.rulesDir,
|
|
341
|
-
"paths.rulesDir",
|
|
342
|
-
configPath,
|
|
343
|
-
issues
|
|
344
|
-
),
|
|
345
337
|
outDir: readString(
|
|
346
338
|
raw.outDir,
|
|
347
339
|
base.outDir,
|
|
@@ -616,6 +608,7 @@ function isRecord(value) {
|
|
|
616
608
|
|
|
617
609
|
// src/core/discovery.ts
|
|
618
610
|
import { access as access3 } from "fs/promises";
|
|
611
|
+
import path5 from "path";
|
|
619
612
|
|
|
620
613
|
// src/core/specLayout.ts
|
|
621
614
|
import { readdir as readdir2 } from "fs/promises";
|
|
@@ -663,7 +656,12 @@ async function collectScenarioFiles(specsRoot) {
|
|
|
663
656
|
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
664
657
|
}
|
|
665
658
|
async function collectUiContractFiles(uiRoot) {
|
|
666
|
-
|
|
659
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
660
|
+
return filterByBasenamePrefix(files, "ui-");
|
|
661
|
+
}
|
|
662
|
+
async function collectThemaContractFiles(uiRoot) {
|
|
663
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
664
|
+
return filterByBasenamePrefix(files, "thema-");
|
|
667
665
|
}
|
|
668
666
|
async function collectApiContractFiles(apiRoot) {
|
|
669
667
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
@@ -672,12 +670,13 @@ async function collectDbContractFiles(dbRoot) {
|
|
|
672
670
|
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
673
671
|
}
|
|
674
672
|
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
675
|
-
const [ui, api, db] = await Promise.all([
|
|
673
|
+
const [ui, thema, api, db] = await Promise.all([
|
|
676
674
|
collectUiContractFiles(uiRoot),
|
|
675
|
+
collectThemaContractFiles(uiRoot),
|
|
677
676
|
collectApiContractFiles(apiRoot),
|
|
678
677
|
collectDbContractFiles(dbRoot)
|
|
679
678
|
]);
|
|
680
|
-
return { ui, api, db };
|
|
679
|
+
return { ui, thema, api, db };
|
|
681
680
|
}
|
|
682
681
|
async function filterExisting(files) {
|
|
683
682
|
const existing = [];
|
|
@@ -696,17 +695,23 @@ async function exists3(target) {
|
|
|
696
695
|
return false;
|
|
697
696
|
}
|
|
698
697
|
}
|
|
698
|
+
function filterByBasenamePrefix(files, prefix) {
|
|
699
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
700
|
+
return files.filter(
|
|
701
|
+
(file) => path5.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
702
|
+
);
|
|
703
|
+
}
|
|
699
704
|
|
|
700
705
|
// src/core/paths.ts
|
|
701
|
-
import
|
|
706
|
+
import path6 from "path";
|
|
702
707
|
function toRelativePath(root, target) {
|
|
703
708
|
if (!target) {
|
|
704
709
|
return target;
|
|
705
710
|
}
|
|
706
|
-
if (!
|
|
711
|
+
if (!path6.isAbsolute(target)) {
|
|
707
712
|
return toPosixPath(target);
|
|
708
713
|
}
|
|
709
|
-
const relative =
|
|
714
|
+
const relative = path6.relative(root, target);
|
|
710
715
|
if (!relative) {
|
|
711
716
|
return ".";
|
|
712
717
|
}
|
|
@@ -718,7 +723,7 @@ function toPosixPath(value) {
|
|
|
718
723
|
|
|
719
724
|
// src/core/traceability.ts
|
|
720
725
|
import { readFile as readFile3 } from "fs/promises";
|
|
721
|
-
import
|
|
726
|
+
import path7 from "path";
|
|
722
727
|
|
|
723
728
|
// src/core/gherkin/parse.ts
|
|
724
729
|
import {
|
|
@@ -950,7 +955,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
950
955
|
};
|
|
951
956
|
}
|
|
952
957
|
const normalizedFiles = Array.from(
|
|
953
|
-
new Set(scanResult.files.map((file) =>
|
|
958
|
+
new Set(scanResult.files.map((file) => path7.normalize(file)))
|
|
954
959
|
);
|
|
955
960
|
for (const file of normalizedFiles) {
|
|
956
961
|
const text = await readFile3(file, "utf-8");
|
|
@@ -1013,19 +1018,19 @@ function formatError3(error2) {
|
|
|
1013
1018
|
|
|
1014
1019
|
// src/core/promptsIntegrity.ts
|
|
1015
1020
|
import { readFile as readFile4 } from "fs/promises";
|
|
1016
|
-
import
|
|
1021
|
+
import path9 from "path";
|
|
1017
1022
|
|
|
1018
1023
|
// src/shared/assets.ts
|
|
1019
1024
|
import { existsSync } from "fs";
|
|
1020
|
-
import
|
|
1025
|
+
import path8 from "path";
|
|
1021
1026
|
import { fileURLToPath } from "url";
|
|
1022
1027
|
function getInitAssetsDir() {
|
|
1023
1028
|
const base = import.meta.url;
|
|
1024
1029
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
1025
|
-
const baseDir =
|
|
1030
|
+
const baseDir = path8.dirname(basePath);
|
|
1026
1031
|
const candidates = [
|
|
1027
|
-
|
|
1028
|
-
|
|
1032
|
+
path8.resolve(baseDir, "../../../assets/init"),
|
|
1033
|
+
path8.resolve(baseDir, "../../assets/init")
|
|
1029
1034
|
];
|
|
1030
1035
|
for (const candidate of candidates) {
|
|
1031
1036
|
if (existsSync(candidate)) {
|
|
@@ -1042,11 +1047,12 @@ function getInitAssetsDir() {
|
|
|
1042
1047
|
}
|
|
1043
1048
|
|
|
1044
1049
|
// src/core/promptsIntegrity.ts
|
|
1050
|
+
var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
|
|
1045
1051
|
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1046
|
-
const promptsDir =
|
|
1052
|
+
const promptsDir = path9.resolve(root, ".qfai", "prompts");
|
|
1047
1053
|
let templateDir;
|
|
1048
1054
|
try {
|
|
1049
|
-
templateDir =
|
|
1055
|
+
templateDir = path9.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1050
1056
|
} catch {
|
|
1051
1057
|
return {
|
|
1052
1058
|
status: "skipped_missing_assets",
|
|
@@ -1090,6 +1096,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1090
1096
|
extra.push(rel);
|
|
1091
1097
|
}
|
|
1092
1098
|
}
|
|
1099
|
+
const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
|
|
1093
1100
|
const common = intersectKeys(templateByRel, projectByRel);
|
|
1094
1101
|
for (const rel of common) {
|
|
1095
1102
|
const templateAbs = templateByRel.get(rel);
|
|
@@ -1109,13 +1116,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1109
1116
|
changed.push(rel);
|
|
1110
1117
|
}
|
|
1111
1118
|
}
|
|
1112
|
-
const status = missing.length > 0 ||
|
|
1119
|
+
const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
|
|
1113
1120
|
return {
|
|
1114
1121
|
status,
|
|
1115
1122
|
promptsDir,
|
|
1116
1123
|
templateDir,
|
|
1117
1124
|
missing: missing.sort(),
|
|
1118
|
-
extra:
|
|
1125
|
+
extra: filteredExtra.sort(),
|
|
1119
1126
|
changed: changed.sort()
|
|
1120
1127
|
};
|
|
1121
1128
|
}
|
|
@@ -1123,7 +1130,7 @@ function normalizeNewlines(text) {
|
|
|
1123
1130
|
return text.replace(/\r\n/g, "\n");
|
|
1124
1131
|
}
|
|
1125
1132
|
function toRel(base, abs) {
|
|
1126
|
-
const rel =
|
|
1133
|
+
const rel = path9.relative(base, abs);
|
|
1127
1134
|
return rel.replace(/[\\/]+/g, "/");
|
|
1128
1135
|
}
|
|
1129
1136
|
function intersectKeys(a, b) {
|
|
@@ -1138,11 +1145,11 @@ function intersectKeys(a, b) {
|
|
|
1138
1145
|
|
|
1139
1146
|
// src/core/version.ts
|
|
1140
1147
|
import { readFile as readFile5 } from "fs/promises";
|
|
1141
|
-
import
|
|
1148
|
+
import path10 from "path";
|
|
1142
1149
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1143
1150
|
async function resolveToolVersion() {
|
|
1144
|
-
if ("1.0.
|
|
1145
|
-
return "1.0.
|
|
1151
|
+
if ("1.0.4".length > 0) {
|
|
1152
|
+
return "1.0.4";
|
|
1146
1153
|
}
|
|
1147
1154
|
try {
|
|
1148
1155
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1157,7 +1164,7 @@ async function resolveToolVersion() {
|
|
|
1157
1164
|
function resolvePackageJsonPath() {
|
|
1158
1165
|
const base = import.meta.url;
|
|
1159
1166
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1160
|
-
return
|
|
1167
|
+
return path10.resolve(path10.dirname(basePath), "../../package.json");
|
|
1161
1168
|
}
|
|
1162
1169
|
|
|
1163
1170
|
// src/core/doctor.ts
|
|
@@ -1183,7 +1190,7 @@ function normalizeGlobs2(values) {
|
|
|
1183
1190
|
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1184
1191
|
}
|
|
1185
1192
|
async function createDoctorData(options) {
|
|
1186
|
-
const startDir =
|
|
1193
|
+
const startDir = path11.resolve(options.startDir);
|
|
1187
1194
|
const checks = [];
|
|
1188
1195
|
const configPath = getConfigPath(startDir);
|
|
1189
1196
|
const search = options.rootExplicit ? {
|
|
@@ -1232,7 +1239,6 @@ async function createDoctorData(options) {
|
|
|
1232
1239
|
"outDir",
|
|
1233
1240
|
"srcDir",
|
|
1234
1241
|
"testsDir",
|
|
1235
|
-
"rulesDir",
|
|
1236
1242
|
"promptsDir"
|
|
1237
1243
|
];
|
|
1238
1244
|
for (const key of pathKeys) {
|
|
@@ -1246,9 +1252,9 @@ async function createDoctorData(options) {
|
|
|
1246
1252
|
details: { path: toRelativePath(root, resolved) }
|
|
1247
1253
|
});
|
|
1248
1254
|
if (key === "promptsDir") {
|
|
1249
|
-
const promptsLocalDir =
|
|
1250
|
-
|
|
1251
|
-
`${
|
|
1255
|
+
const promptsLocalDir = path11.join(
|
|
1256
|
+
path11.dirname(resolved),
|
|
1257
|
+
`${path11.basename(resolved)}.local`
|
|
1252
1258
|
);
|
|
1253
1259
|
const found = await exists4(promptsLocalDir);
|
|
1254
1260
|
addCheck(checks, {
|
|
@@ -1321,7 +1327,7 @@ async function createDoctorData(options) {
|
|
|
1321
1327
|
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1322
1328
|
details: { specPacks: entries.length, missingFiles }
|
|
1323
1329
|
});
|
|
1324
|
-
const validateJsonAbs =
|
|
1330
|
+
const validateJsonAbs = path11.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path11.resolve(root, config.output.validateJsonPath);
|
|
1325
1331
|
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1326
1332
|
addCheck(checks, {
|
|
1327
1333
|
id: "output.validateJson",
|
|
@@ -1331,8 +1337,8 @@ async function createDoctorData(options) {
|
|
|
1331
1337
|
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1332
1338
|
});
|
|
1333
1339
|
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1334
|
-
const rel =
|
|
1335
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
1340
|
+
const rel = path11.relative(outDirAbs, validateJsonAbs);
|
|
1341
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
1336
1342
|
addCheck(checks, {
|
|
1337
1343
|
id: "output.pathAlignment",
|
|
1338
1344
|
severity: inside ? "ok" : "warning",
|
|
@@ -1455,12 +1461,12 @@ async function detectOutDirCollisions(root) {
|
|
|
1455
1461
|
});
|
|
1456
1462
|
const configPaths = configScan.files;
|
|
1457
1463
|
const configRoots = Array.from(
|
|
1458
|
-
new Set(configPaths.map((configPath) =>
|
|
1464
|
+
new Set(configPaths.map((configPath) => path11.dirname(configPath)))
|
|
1459
1465
|
).sort((a, b) => a.localeCompare(b));
|
|
1460
1466
|
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1461
1467
|
for (const configRoot of configRoots) {
|
|
1462
1468
|
const { config } = await loadConfig(configRoot);
|
|
1463
|
-
const outDir =
|
|
1469
|
+
const outDir = path11.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1464
1470
|
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1465
1471
|
roots.add(configRoot);
|
|
1466
1472
|
outDirToRoots.set(outDir, roots);
|
|
@@ -1486,20 +1492,20 @@ async function detectOutDirCollisions(root) {
|
|
|
1486
1492
|
};
|
|
1487
1493
|
}
|
|
1488
1494
|
async function findMonorepoRoot(startDir) {
|
|
1489
|
-
let current =
|
|
1495
|
+
let current = path11.resolve(startDir);
|
|
1490
1496
|
while (true) {
|
|
1491
|
-
const gitPath =
|
|
1492
|
-
const workspacePath =
|
|
1497
|
+
const gitPath = path11.join(current, ".git");
|
|
1498
|
+
const workspacePath = path11.join(current, "pnpm-workspace.yaml");
|
|
1493
1499
|
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1494
1500
|
return current;
|
|
1495
1501
|
}
|
|
1496
|
-
const parent =
|
|
1502
|
+
const parent = path11.dirname(current);
|
|
1497
1503
|
if (parent === current) {
|
|
1498
1504
|
break;
|
|
1499
1505
|
}
|
|
1500
1506
|
current = parent;
|
|
1501
1507
|
}
|
|
1502
|
-
return
|
|
1508
|
+
return path11.resolve(startDir);
|
|
1503
1509
|
}
|
|
1504
1510
|
|
|
1505
1511
|
// src/cli/lib/logger.ts
|
|
@@ -1541,8 +1547,8 @@ async function runDoctor(options) {
|
|
|
1541
1547
|
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1542
1548
|
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1543
1549
|
if (options.outPath) {
|
|
1544
|
-
const outAbs =
|
|
1545
|
-
await mkdir(
|
|
1550
|
+
const outAbs = path12.isAbsolute(options.outPath) ? options.outPath : path12.resolve(process.cwd(), options.outPath);
|
|
1551
|
+
await mkdir(path12.dirname(outAbs), { recursive: true });
|
|
1546
1552
|
await writeFile(outAbs, `${output}
|
|
1547
1553
|
`, "utf-8");
|
|
1548
1554
|
info(`doctor: wrote ${outAbs}`);
|
|
@@ -1562,11 +1568,11 @@ function shouldFailDoctor(summary, failOn) {
|
|
|
1562
1568
|
}
|
|
1563
1569
|
|
|
1564
1570
|
// src/cli/commands/init.ts
|
|
1565
|
-
import
|
|
1571
|
+
import path14 from "path";
|
|
1566
1572
|
|
|
1567
1573
|
// src/cli/lib/fs.ts
|
|
1568
1574
|
import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
|
|
1569
|
-
import
|
|
1575
|
+
import path13 from "path";
|
|
1570
1576
|
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1571
1577
|
const files = await collectTemplateFiles(sourceRoot);
|
|
1572
1578
|
return copyFiles(files, sourceRoot, destRoot, options);
|
|
@@ -1574,7 +1580,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
|
1574
1580
|
async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
|
|
1575
1581
|
const allFiles = [];
|
|
1576
1582
|
for (const relPath of relativePaths) {
|
|
1577
|
-
const fullPath =
|
|
1583
|
+
const fullPath = path13.join(sourceRoot, relPath);
|
|
1578
1584
|
const files = await collectTemplateFiles(fullPath);
|
|
1579
1585
|
allFiles.push(...files);
|
|
1580
1586
|
}
|
|
@@ -1584,13 +1590,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1584
1590
|
const copied = [];
|
|
1585
1591
|
const skipped = [];
|
|
1586
1592
|
const conflicts = [];
|
|
1587
|
-
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
1588
|
-
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
1593
|
+
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
|
|
1594
|
+
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
|
|
1589
1595
|
const isProtectedRelative = (relative) => {
|
|
1590
1596
|
if (protectPrefixes.length === 0) {
|
|
1591
1597
|
return false;
|
|
1592
1598
|
}
|
|
1593
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1599
|
+
const normalized = relative.replace(/[\\/]+/g, path13.sep);
|
|
1594
1600
|
return protectPrefixes.some(
|
|
1595
1601
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1596
1602
|
);
|
|
@@ -1599,7 +1605,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1599
1605
|
if (excludePrefixes.length === 0) {
|
|
1600
1606
|
return false;
|
|
1601
1607
|
}
|
|
1602
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1608
|
+
const normalized = relative.replace(/[\\/]+/g, path13.sep);
|
|
1603
1609
|
return excludePrefixes.some(
|
|
1604
1610
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1605
1611
|
);
|
|
@@ -1607,14 +1613,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1607
1613
|
const conflictPolicy = options.conflictPolicy ?? "error";
|
|
1608
1614
|
if (!options.force && conflictPolicy === "error") {
|
|
1609
1615
|
for (const file of files) {
|
|
1610
|
-
const relative =
|
|
1616
|
+
const relative = path13.relative(sourceRoot, file);
|
|
1611
1617
|
if (isExcludedRelative(relative)) {
|
|
1612
1618
|
continue;
|
|
1613
1619
|
}
|
|
1614
1620
|
if (isProtectedRelative(relative)) {
|
|
1615
1621
|
continue;
|
|
1616
1622
|
}
|
|
1617
|
-
const dest =
|
|
1623
|
+
const dest = path13.join(destRoot, relative);
|
|
1618
1624
|
if (!await shouldWrite(dest, options.force)) {
|
|
1619
1625
|
conflicts.push(dest);
|
|
1620
1626
|
}
|
|
@@ -1624,18 +1630,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1624
1630
|
}
|
|
1625
1631
|
}
|
|
1626
1632
|
for (const file of files) {
|
|
1627
|
-
const relative =
|
|
1633
|
+
const relative = path13.relative(sourceRoot, file);
|
|
1628
1634
|
if (isExcludedRelative(relative)) {
|
|
1629
1635
|
continue;
|
|
1630
1636
|
}
|
|
1631
|
-
const dest =
|
|
1637
|
+
const dest = path13.join(destRoot, relative);
|
|
1632
1638
|
const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
|
|
1633
1639
|
if (!await shouldWrite(dest, forceForThisFile)) {
|
|
1634
1640
|
skipped.push(dest);
|
|
1635
1641
|
continue;
|
|
1636
1642
|
}
|
|
1637
1643
|
if (!options.dryRun) {
|
|
1638
|
-
await mkdir2(
|
|
1644
|
+
await mkdir2(path13.dirname(dest), { recursive: true });
|
|
1639
1645
|
await copyFile(file, dest);
|
|
1640
1646
|
}
|
|
1641
1647
|
copied.push(dest);
|
|
@@ -1659,7 +1665,7 @@ async function collectTemplateFiles(root) {
|
|
|
1659
1665
|
}
|
|
1660
1666
|
const items = await readdir3(root, { withFileTypes: true });
|
|
1661
1667
|
for (const item of items) {
|
|
1662
|
-
const fullPath =
|
|
1668
|
+
const fullPath = path13.join(root, item.name);
|
|
1663
1669
|
if (item.isDirectory()) {
|
|
1664
1670
|
const nested = await collectTemplateFiles(fullPath);
|
|
1665
1671
|
entries.push(...nested);
|
|
@@ -1689,10 +1695,10 @@ async function exists5(target) {
|
|
|
1689
1695
|
// src/cli/commands/init.ts
|
|
1690
1696
|
async function runInit(options) {
|
|
1691
1697
|
const assetsRoot = getInitAssetsDir();
|
|
1692
|
-
const rootAssets =
|
|
1693
|
-
const qfaiAssets =
|
|
1694
|
-
const destRoot =
|
|
1695
|
-
const destQfai =
|
|
1698
|
+
const rootAssets = path14.join(assetsRoot, "root");
|
|
1699
|
+
const qfaiAssets = path14.join(assetsRoot, ".qfai");
|
|
1700
|
+
const destRoot = path14.resolve(options.dir);
|
|
1701
|
+
const destQfai = path14.join(destRoot, ".qfai");
|
|
1696
1702
|
if (options.force) {
|
|
1697
1703
|
info(
|
|
1698
1704
|
"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"
|
|
@@ -1740,7 +1746,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1740
1746
|
|
|
1741
1747
|
// src/cli/commands/report.ts
|
|
1742
1748
|
import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
|
|
1743
|
-
import
|
|
1749
|
+
import path22 from "path";
|
|
1744
1750
|
|
|
1745
1751
|
// src/core/normalize.ts
|
|
1746
1752
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1781,15 +1787,15 @@ function normalizeValidationResult(root, result) {
|
|
|
1781
1787
|
|
|
1782
1788
|
// src/core/report.ts
|
|
1783
1789
|
import { readFile as readFile13 } from "fs/promises";
|
|
1784
|
-
import
|
|
1790
|
+
import path21 from "path";
|
|
1785
1791
|
|
|
1786
1792
|
// src/core/contractIndex.ts
|
|
1787
1793
|
import { readFile as readFile6 } from "fs/promises";
|
|
1788
|
-
import
|
|
1794
|
+
import path15 from "path";
|
|
1789
1795
|
|
|
1790
1796
|
// src/core/contractsDecl.ts
|
|
1791
|
-
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
1792
|
-
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
1797
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
1798
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
|
|
1793
1799
|
function extractDeclaredContractIds(text) {
|
|
1794
1800
|
const ids = [];
|
|
1795
1801
|
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
@@ -1807,20 +1813,22 @@ function stripContractDeclarationLines(text) {
|
|
|
1807
1813
|
// src/core/contractIndex.ts
|
|
1808
1814
|
async function buildContractIndex(root, config) {
|
|
1809
1815
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1810
|
-
const uiRoot =
|
|
1811
|
-
const apiRoot =
|
|
1812
|
-
const dbRoot =
|
|
1813
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1816
|
+
const uiRoot = path15.join(contractsRoot, "ui");
|
|
1817
|
+
const apiRoot = path15.join(contractsRoot, "api");
|
|
1818
|
+
const dbRoot = path15.join(contractsRoot, "db");
|
|
1819
|
+
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1814
1820
|
collectUiContractFiles(uiRoot),
|
|
1821
|
+
collectThemaContractFiles(uiRoot),
|
|
1815
1822
|
collectApiContractFiles(apiRoot),
|
|
1816
1823
|
collectDbContractFiles(dbRoot)
|
|
1817
1824
|
]);
|
|
1818
1825
|
const index = {
|
|
1819
1826
|
ids: /* @__PURE__ */ new Set(),
|
|
1820
1827
|
idToFiles: /* @__PURE__ */ new Map(),
|
|
1821
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1828
|
+
files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
|
|
1822
1829
|
};
|
|
1823
1830
|
await indexContractFiles(uiFiles, index);
|
|
1831
|
+
await indexContractFiles(themaFiles, index);
|
|
1824
1832
|
await indexContractFiles(apiFiles, index);
|
|
1825
1833
|
await indexContractFiles(dbFiles, index);
|
|
1826
1834
|
return index;
|
|
@@ -1839,7 +1847,15 @@ function record(index, id, file) {
|
|
|
1839
1847
|
}
|
|
1840
1848
|
|
|
1841
1849
|
// src/core/ids.ts
|
|
1842
|
-
var ID_PREFIXES = [
|
|
1850
|
+
var ID_PREFIXES = [
|
|
1851
|
+
"SPEC",
|
|
1852
|
+
"BR",
|
|
1853
|
+
"SC",
|
|
1854
|
+
"UI",
|
|
1855
|
+
"API",
|
|
1856
|
+
"DB",
|
|
1857
|
+
"THEMA"
|
|
1858
|
+
];
|
|
1843
1859
|
var STRICT_ID_PATTERNS = {
|
|
1844
1860
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1845
1861
|
BR: /\bBR-\d{4}\b/g,
|
|
@@ -1847,6 +1863,7 @@ var STRICT_ID_PATTERNS = {
|
|
|
1847
1863
|
UI: /\bUI-\d{4}\b/g,
|
|
1848
1864
|
API: /\bAPI-\d{4}\b/g,
|
|
1849
1865
|
DB: /\bDB-\d{4}\b/g,
|
|
1866
|
+
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
1850
1867
|
ADR: /\bADR-\d{4}\b/g
|
|
1851
1868
|
};
|
|
1852
1869
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -1856,6 +1873,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
1856
1873
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1857
1874
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1858
1875
|
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1876
|
+
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
1859
1877
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1860
1878
|
};
|
|
1861
1879
|
function extractIds(text, prefix) {
|
|
@@ -1892,7 +1910,7 @@ function isValidId(value, prefix) {
|
|
|
1892
1910
|
}
|
|
1893
1911
|
|
|
1894
1912
|
// src/core/parse/contractRefs.ts
|
|
1895
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1913
|
+
var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
|
|
1896
1914
|
function parseContractRefs(text, options = {}) {
|
|
1897
1915
|
const linePattern = buildLinePattern(options);
|
|
1898
1916
|
const lines = [];
|
|
@@ -2062,14 +2080,14 @@ function parseSpec(md, file) {
|
|
|
2062
2080
|
}
|
|
2063
2081
|
|
|
2064
2082
|
// src/core/validators/contracts.ts
|
|
2065
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
2066
|
-
import
|
|
2083
|
+
import { access as access6, readFile as readFile7 } from "fs/promises";
|
|
2084
|
+
import path17 from "path";
|
|
2067
2085
|
|
|
2068
2086
|
// src/core/contracts.ts
|
|
2069
|
-
import
|
|
2087
|
+
import path16 from "path";
|
|
2070
2088
|
import { parse as parseYaml2 } from "yaml";
|
|
2071
2089
|
function parseStructuredContract(file, text) {
|
|
2072
|
-
const ext =
|
|
2090
|
+
const ext = path16.extname(file).toLowerCase();
|
|
2073
2091
|
if (ext === ".json") {
|
|
2074
2092
|
return JSON.parse(text);
|
|
2075
2093
|
}
|
|
@@ -2086,17 +2104,23 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
2086
2104
|
label: "ALTER TABLE ... DROP"
|
|
2087
2105
|
}
|
|
2088
2106
|
];
|
|
2107
|
+
var THEMA_ID_RE = /^THEMA-\d{3}$/;
|
|
2089
2108
|
async function validateContracts(root, config) {
|
|
2090
2109
|
const issues = [];
|
|
2091
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2092
|
-
issues.push(...await validateUiContracts(path16.join(contractsRoot, "ui")));
|
|
2093
|
-
issues.push(...await validateApiContracts(path16.join(contractsRoot, "api")));
|
|
2094
|
-
issues.push(...await validateDbContracts(path16.join(contractsRoot, "db")));
|
|
2095
2110
|
const contractIndex = await buildContractIndex(root, config);
|
|
2111
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2112
|
+
const uiRoot = path17.join(contractsRoot, "ui");
|
|
2113
|
+
const themaIds = new Set(
|
|
2114
|
+
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
2115
|
+
);
|
|
2116
|
+
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
2117
|
+
issues.push(...await validateThemaContracts(uiRoot));
|
|
2118
|
+
issues.push(...await validateApiContracts(path17.join(contractsRoot, "api")));
|
|
2119
|
+
issues.push(...await validateDbContracts(path17.join(contractsRoot, "db")));
|
|
2096
2120
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
2097
2121
|
return issues;
|
|
2098
2122
|
}
|
|
2099
|
-
async function validateUiContracts(uiRoot) {
|
|
2123
|
+
async function validateUiContracts(uiRoot, themaIds) {
|
|
2100
2124
|
const files = await collectUiContractFiles(uiRoot);
|
|
2101
2125
|
if (files.length === 0) {
|
|
2102
2126
|
return [
|
|
@@ -2112,6 +2136,22 @@ async function validateUiContracts(uiRoot) {
|
|
|
2112
2136
|
const issues = [];
|
|
2113
2137
|
for (const file of files) {
|
|
2114
2138
|
const text = await readFile7(file, "utf-8");
|
|
2139
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
2140
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
2141
|
+
let doc = null;
|
|
2142
|
+
try {
|
|
2143
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2144
|
+
} catch (error2) {
|
|
2145
|
+
issues.push(
|
|
2146
|
+
issue(
|
|
2147
|
+
"QFAI-CONTRACT-001",
|
|
2148
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2149
|
+
"error",
|
|
2150
|
+
file,
|
|
2151
|
+
"contracts.ui.parse"
|
|
2152
|
+
)
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2115
2155
|
const invalidIds = extractInvalidIds(text, [
|
|
2116
2156
|
"SPEC",
|
|
2117
2157
|
"BR",
|
|
@@ -2119,6 +2159,45 @@ async function validateUiContracts(uiRoot) {
|
|
|
2119
2159
|
"UI",
|
|
2120
2160
|
"API",
|
|
2121
2161
|
"DB",
|
|
2162
|
+
"THEMA",
|
|
2163
|
+
"ADR"
|
|
2164
|
+
]).filter((id) => !shouldIgnoreInvalidId(id, doc));
|
|
2165
|
+
if (invalidIds.length > 0) {
|
|
2166
|
+
issues.push(
|
|
2167
|
+
issue(
|
|
2168
|
+
"QFAI-ID-002",
|
|
2169
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
2170
|
+
"error",
|
|
2171
|
+
file,
|
|
2172
|
+
"id.format",
|
|
2173
|
+
invalidIds
|
|
2174
|
+
)
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
if (doc) {
|
|
2178
|
+
issues.push(
|
|
2179
|
+
...await validateUiContractDoc(doc, file, uiRoot, themaIds)
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return issues;
|
|
2184
|
+
}
|
|
2185
|
+
async function validateThemaContracts(uiRoot) {
|
|
2186
|
+
const files = await collectThemaContractFiles(uiRoot);
|
|
2187
|
+
if (files.length === 0) {
|
|
2188
|
+
return [];
|
|
2189
|
+
}
|
|
2190
|
+
const issues = [];
|
|
2191
|
+
for (const file of files) {
|
|
2192
|
+
const text = await readFile7(file, "utf-8");
|
|
2193
|
+
const invalidIds = extractInvalidIds(text, [
|
|
2194
|
+
"SPEC",
|
|
2195
|
+
"BR",
|
|
2196
|
+
"SC",
|
|
2197
|
+
"UI",
|
|
2198
|
+
"API",
|
|
2199
|
+
"DB",
|
|
2200
|
+
"THEMA",
|
|
2122
2201
|
"ADR"
|
|
2123
2202
|
]);
|
|
2124
2203
|
if (invalidIds.length > 0) {
|
|
@@ -2134,17 +2213,95 @@ async function validateUiContracts(uiRoot) {
|
|
|
2134
2213
|
);
|
|
2135
2214
|
}
|
|
2136
2215
|
const declaredIds = extractDeclaredContractIds(text);
|
|
2137
|
-
|
|
2216
|
+
if (declaredIds.length === 0) {
|
|
2217
|
+
issues.push(
|
|
2218
|
+
issue(
|
|
2219
|
+
"QFAI-THEMA-010",
|
|
2220
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
2221
|
+
"error",
|
|
2222
|
+
file,
|
|
2223
|
+
"contracts.thema.declaration"
|
|
2224
|
+
)
|
|
2225
|
+
);
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (declaredIds.length > 1) {
|
|
2229
|
+
issues.push(
|
|
2230
|
+
issue(
|
|
2231
|
+
"QFAI-THEMA-011",
|
|
2232
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B\u8907\u6570\u306E QFAI-CONTRACT-ID \u304C\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059: ${declaredIds.join(
|
|
2233
|
+
", "
|
|
2234
|
+
)}`,
|
|
2235
|
+
"error",
|
|
2236
|
+
file,
|
|
2237
|
+
"contracts.thema.declaration",
|
|
2238
|
+
declaredIds
|
|
2239
|
+
)
|
|
2240
|
+
);
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
const declaredId = declaredIds[0] ?? "";
|
|
2244
|
+
if (!THEMA_ID_RE.test(declaredId)) {
|
|
2245
|
+
issues.push(
|
|
2246
|
+
issue(
|
|
2247
|
+
"QFAI-THEMA-012",
|
|
2248
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
|
|
2249
|
+
"error",
|
|
2250
|
+
file,
|
|
2251
|
+
"contracts.thema.idFormat",
|
|
2252
|
+
[declaredId]
|
|
2253
|
+
)
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
let doc;
|
|
2138
2257
|
try {
|
|
2139
|
-
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2258
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2140
2259
|
} catch (error2) {
|
|
2141
2260
|
issues.push(
|
|
2142
2261
|
issue(
|
|
2143
|
-
"QFAI-
|
|
2144
|
-
`
|
|
2262
|
+
"QFAI-THEMA-001",
|
|
2263
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2145
2264
|
"error",
|
|
2146
2265
|
file,
|
|
2147
|
-
"contracts.
|
|
2266
|
+
"contracts.thema.parse"
|
|
2267
|
+
)
|
|
2268
|
+
);
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const docId = typeof doc.id === "string" ? doc.id : "";
|
|
2272
|
+
if (!THEMA_ID_RE.test(docId)) {
|
|
2273
|
+
issues.push(
|
|
2274
|
+
issue(
|
|
2275
|
+
"QFAI-THEMA-012",
|
|
2276
|
+
docId.length > 0 ? `thema \u306E id \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${docId}` : "thema \u306E id \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2277
|
+
"error",
|
|
2278
|
+
file,
|
|
2279
|
+
"contracts.thema.idFormat",
|
|
2280
|
+
docId.length > 0 ? [docId] : void 0
|
|
2281
|
+
)
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
const name = typeof doc.name === "string" ? doc.name : "";
|
|
2285
|
+
if (!name) {
|
|
2286
|
+
issues.push(
|
|
2287
|
+
issue(
|
|
2288
|
+
"QFAI-THEMA-014",
|
|
2289
|
+
"thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2290
|
+
"error",
|
|
2291
|
+
file,
|
|
2292
|
+
"contracts.thema.name"
|
|
2293
|
+
)
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
if (declaredId && docId && declaredId !== docId) {
|
|
2297
|
+
issues.push(
|
|
2298
|
+
issue(
|
|
2299
|
+
"QFAI-THEMA-013",
|
|
2300
|
+
`thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
|
|
2301
|
+
"error",
|
|
2302
|
+
file,
|
|
2303
|
+
"contracts.thema.idMismatch",
|
|
2304
|
+
[declaredId, docId]
|
|
2148
2305
|
)
|
|
2149
2306
|
);
|
|
2150
2307
|
}
|
|
@@ -2174,6 +2331,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
2174
2331
|
"UI",
|
|
2175
2332
|
"API",
|
|
2176
2333
|
"DB",
|
|
2334
|
+
"THEMA",
|
|
2177
2335
|
"ADR"
|
|
2178
2336
|
]);
|
|
2179
2337
|
if (invalidIds.length > 0) {
|
|
@@ -2242,6 +2400,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
2242
2400
|
"UI",
|
|
2243
2401
|
"API",
|
|
2244
2402
|
"DB",
|
|
2403
|
+
"THEMA",
|
|
2245
2404
|
"ADR"
|
|
2246
2405
|
]);
|
|
2247
2406
|
if (invalidIds.length > 0) {
|
|
@@ -2348,6 +2507,278 @@ function validateDuplicateContractIds(contractIndex) {
|
|
|
2348
2507
|
function hasOpenApi(doc) {
|
|
2349
2508
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
2350
2509
|
}
|
|
2510
|
+
async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
|
|
2511
|
+
const issues = [];
|
|
2512
|
+
if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
|
|
2513
|
+
const themaRef = doc.themaRef;
|
|
2514
|
+
if (typeof themaRef !== "string" || themaRef.length === 0) {
|
|
2515
|
+
issues.push(
|
|
2516
|
+
issue(
|
|
2517
|
+
"QFAI-UI-020",
|
|
2518
|
+
"themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2519
|
+
"error",
|
|
2520
|
+
file,
|
|
2521
|
+
"contracts.ui.themaRef"
|
|
2522
|
+
)
|
|
2523
|
+
);
|
|
2524
|
+
} else if (!THEMA_ID_RE.test(themaRef)) {
|
|
2525
|
+
issues.push(
|
|
2526
|
+
issue(
|
|
2527
|
+
"QFAI-UI-020",
|
|
2528
|
+
`themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
|
|
2529
|
+
"error",
|
|
2530
|
+
file,
|
|
2531
|
+
"contracts.ui.themaRef",
|
|
2532
|
+
[themaRef]
|
|
2533
|
+
)
|
|
2534
|
+
);
|
|
2535
|
+
} else if (!themaIds.has(themaRef)) {
|
|
2536
|
+
issues.push(
|
|
2537
|
+
issue(
|
|
2538
|
+
"QFAI-UI-020",
|
|
2539
|
+
`themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
|
|
2540
|
+
"error",
|
|
2541
|
+
file,
|
|
2542
|
+
"contracts.ui.themaRef",
|
|
2543
|
+
[themaRef]
|
|
2544
|
+
)
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const assets = doc.assets;
|
|
2549
|
+
if (assets && typeof assets === "object") {
|
|
2550
|
+
issues.push(
|
|
2551
|
+
...await validateUiAssets(
|
|
2552
|
+
assets,
|
|
2553
|
+
file,
|
|
2554
|
+
uiRoot
|
|
2555
|
+
)
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
return issues;
|
|
2559
|
+
}
|
|
2560
|
+
async function validateUiAssets(assets, file, uiRoot) {
|
|
2561
|
+
const issues = [];
|
|
2562
|
+
const packValue = assets.pack;
|
|
2563
|
+
const useValue = assets.use;
|
|
2564
|
+
if (packValue === void 0 && useValue === void 0) {
|
|
2565
|
+
return issues;
|
|
2566
|
+
}
|
|
2567
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2568
|
+
issues.push(
|
|
2569
|
+
issue(
|
|
2570
|
+
"QFAI-ASSET-001",
|
|
2571
|
+
"assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2572
|
+
"error",
|
|
2573
|
+
file,
|
|
2574
|
+
"assets.pack"
|
|
2575
|
+
)
|
|
2576
|
+
);
|
|
2577
|
+
return issues;
|
|
2578
|
+
}
|
|
2579
|
+
if (!isSafeRelativePath(packValue)) {
|
|
2580
|
+
issues.push(
|
|
2581
|
+
issue(
|
|
2582
|
+
"QFAI-ASSET-001",
|
|
2583
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
|
|
2584
|
+
"error",
|
|
2585
|
+
file,
|
|
2586
|
+
"assets.pack",
|
|
2587
|
+
[packValue]
|
|
2588
|
+
)
|
|
2589
|
+
);
|
|
2590
|
+
return issues;
|
|
2591
|
+
}
|
|
2592
|
+
const packDir = path17.resolve(uiRoot, packValue);
|
|
2593
|
+
const packRelative = path17.relative(uiRoot, packDir);
|
|
2594
|
+
if (packRelative.startsWith("..") || path17.isAbsolute(packRelative)) {
|
|
2595
|
+
issues.push(
|
|
2596
|
+
issue(
|
|
2597
|
+
"QFAI-ASSET-001",
|
|
2598
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
|
|
2599
|
+
"error",
|
|
2600
|
+
file,
|
|
2601
|
+
"assets.pack",
|
|
2602
|
+
[packValue]
|
|
2603
|
+
)
|
|
2604
|
+
);
|
|
2605
|
+
return issues;
|
|
2606
|
+
}
|
|
2607
|
+
if (!await exists6(packDir)) {
|
|
2608
|
+
issues.push(
|
|
2609
|
+
issue(
|
|
2610
|
+
"QFAI-ASSET-001",
|
|
2611
|
+
`assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
|
|
2612
|
+
"error",
|
|
2613
|
+
file,
|
|
2614
|
+
"assets.pack",
|
|
2615
|
+
[packValue]
|
|
2616
|
+
)
|
|
2617
|
+
);
|
|
2618
|
+
return issues;
|
|
2619
|
+
}
|
|
2620
|
+
const assetsYamlPath = path17.join(packDir, "assets.yaml");
|
|
2621
|
+
if (!await exists6(assetsYamlPath)) {
|
|
2622
|
+
issues.push(
|
|
2623
|
+
issue(
|
|
2624
|
+
"QFAI-ASSET-002",
|
|
2625
|
+
`assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
|
|
2626
|
+
"error",
|
|
2627
|
+
assetsYamlPath,
|
|
2628
|
+
"assets.yaml"
|
|
2629
|
+
)
|
|
2630
|
+
);
|
|
2631
|
+
return issues;
|
|
2632
|
+
}
|
|
2633
|
+
let manifest;
|
|
2634
|
+
try {
|
|
2635
|
+
const manifestText = await readFile7(assetsYamlPath, "utf-8");
|
|
2636
|
+
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
2637
|
+
} catch (error2) {
|
|
2638
|
+
issues.push(
|
|
2639
|
+
issue(
|
|
2640
|
+
"QFAI-ASSET-002",
|
|
2641
|
+
`assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
|
|
2642
|
+
"error",
|
|
2643
|
+
assetsYamlPath,
|
|
2644
|
+
"assets.yaml"
|
|
2645
|
+
)
|
|
2646
|
+
);
|
|
2647
|
+
return issues;
|
|
2648
|
+
}
|
|
2649
|
+
const items = Array.isArray(manifest.items) ? manifest.items : [];
|
|
2650
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
2651
|
+
const itemPaths = [];
|
|
2652
|
+
for (const item of items) {
|
|
2653
|
+
if (!item || typeof item !== "object") {
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
const record2 = item;
|
|
2657
|
+
const id = typeof record2.id === "string" ? record2.id : void 0;
|
|
2658
|
+
const pathValue = typeof record2.path === "string" ? record2.path : void 0;
|
|
2659
|
+
if (id) {
|
|
2660
|
+
itemIds.add(id);
|
|
2661
|
+
}
|
|
2662
|
+
itemPaths.push({ id, path: pathValue });
|
|
2663
|
+
}
|
|
2664
|
+
if (useValue !== void 0) {
|
|
2665
|
+
if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
|
|
2666
|
+
issues.push(
|
|
2667
|
+
issue(
|
|
2668
|
+
"QFAI-ASSET-003",
|
|
2669
|
+
"assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2670
|
+
"error",
|
|
2671
|
+
file,
|
|
2672
|
+
"assets.use"
|
|
2673
|
+
)
|
|
2674
|
+
);
|
|
2675
|
+
} else {
|
|
2676
|
+
const missing = useValue.filter((entry) => !itemIds.has(entry));
|
|
2677
|
+
if (missing.length > 0) {
|
|
2678
|
+
issues.push(
|
|
2679
|
+
issue(
|
|
2680
|
+
"QFAI-ASSET-003",
|
|
2681
|
+
`assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
|
|
2682
|
+
"error",
|
|
2683
|
+
file,
|
|
2684
|
+
"assets.use",
|
|
2685
|
+
missing
|
|
2686
|
+
)
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
for (const entry of itemPaths) {
|
|
2692
|
+
if (!entry.path) {
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
2696
|
+
issues.push(
|
|
2697
|
+
issue(
|
|
2698
|
+
"QFAI-ASSET-004",
|
|
2699
|
+
`assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
|
|
2700
|
+
"error",
|
|
2701
|
+
assetsYamlPath,
|
|
2702
|
+
"assets.path",
|
|
2703
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2704
|
+
)
|
|
2705
|
+
);
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
const assetPath = path17.resolve(packDir, entry.path);
|
|
2709
|
+
const assetRelative = path17.relative(packDir, assetPath);
|
|
2710
|
+
if (assetRelative.startsWith("..") || path17.isAbsolute(assetRelative)) {
|
|
2711
|
+
issues.push(
|
|
2712
|
+
issue(
|
|
2713
|
+
"QFAI-ASSET-004",
|
|
2714
|
+
`assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
|
|
2715
|
+
"error",
|
|
2716
|
+
assetsYamlPath,
|
|
2717
|
+
"assets.path",
|
|
2718
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2719
|
+
)
|
|
2720
|
+
);
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
if (!await exists6(assetPath)) {
|
|
2724
|
+
issues.push(
|
|
2725
|
+
issue(
|
|
2726
|
+
"QFAI-ASSET-004",
|
|
2727
|
+
`assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
|
|
2728
|
+
"error",
|
|
2729
|
+
assetsYamlPath,
|
|
2730
|
+
"assets.path",
|
|
2731
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2732
|
+
)
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return issues;
|
|
2737
|
+
}
|
|
2738
|
+
function shouldIgnoreInvalidId(value, doc) {
|
|
2739
|
+
if (!doc) {
|
|
2740
|
+
return false;
|
|
2741
|
+
}
|
|
2742
|
+
const assets = doc.assets;
|
|
2743
|
+
if (!assets || typeof assets !== "object") {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
const packValue = assets.pack;
|
|
2747
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2748
|
+
return false;
|
|
2749
|
+
}
|
|
2750
|
+
const normalized = packValue.replace(/\\/g, "/");
|
|
2751
|
+
const basename = path17.posix.basename(normalized);
|
|
2752
|
+
if (!basename) {
|
|
2753
|
+
return false;
|
|
2754
|
+
}
|
|
2755
|
+
return value.toLowerCase() === basename.toLowerCase();
|
|
2756
|
+
}
|
|
2757
|
+
function isSafeRelativePath(value) {
|
|
2758
|
+
if (!value) {
|
|
2759
|
+
return false;
|
|
2760
|
+
}
|
|
2761
|
+
if (path17.isAbsolute(value)) {
|
|
2762
|
+
return false;
|
|
2763
|
+
}
|
|
2764
|
+
const normalized = value.replace(/\\/g, "/");
|
|
2765
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
const segments = normalized.split("/");
|
|
2769
|
+
if (segments.some((segment) => segment === "..")) {
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
return true;
|
|
2773
|
+
}
|
|
2774
|
+
async function exists6(target) {
|
|
2775
|
+
try {
|
|
2776
|
+
await access6(target);
|
|
2777
|
+
return true;
|
|
2778
|
+
} catch {
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2351
2782
|
function formatError4(error2) {
|
|
2352
2783
|
if (error2 instanceof Error) {
|
|
2353
2784
|
return error2.message;
|
|
@@ -2378,12 +2809,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2378
2809
|
|
|
2379
2810
|
// src/core/validators/delta.ts
|
|
2380
2811
|
import { readFile as readFile8 } from "fs/promises";
|
|
2381
|
-
import
|
|
2382
|
-
var SECTION_RE = /^##\s+変更区分/m;
|
|
2383
|
-
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2384
|
-
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
2385
|
-
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
2386
|
-
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
2812
|
+
import path18 from "path";
|
|
2387
2813
|
async function validateDeltas(root, config) {
|
|
2388
2814
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2389
2815
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
@@ -2392,10 +2818,9 @@ async function validateDeltas(root, config) {
|
|
|
2392
2818
|
}
|
|
2393
2819
|
const issues = [];
|
|
2394
2820
|
for (const pack of packs) {
|
|
2395
|
-
const deltaPath =
|
|
2396
|
-
let text;
|
|
2821
|
+
const deltaPath = path18.join(pack, "delta.md");
|
|
2397
2822
|
try {
|
|
2398
|
-
|
|
2823
|
+
await readFile8(deltaPath, "utf-8");
|
|
2399
2824
|
} catch (error2) {
|
|
2400
2825
|
if (isMissingFileError2(error2)) {
|
|
2401
2826
|
issues.push(
|
|
@@ -2404,41 +2829,16 @@ async function validateDeltas(root, config) {
|
|
|
2404
2829
|
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2405
2830
|
"error",
|
|
2406
2831
|
deltaPath,
|
|
2407
|
-
"delta.exists"
|
|
2832
|
+
"delta.exists",
|
|
2833
|
+
void 0,
|
|
2834
|
+
"change",
|
|
2835
|
+
"spec-xxxx/delta.md \u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30C6\u30F3\u30D7\u30EC\u306F init \u751F\u6210\u7269\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\uFF09\u3002"
|
|
2408
2836
|
)
|
|
2409
2837
|
);
|
|
2410
2838
|
continue;
|
|
2411
2839
|
}
|
|
2412
2840
|
throw error2;
|
|
2413
2841
|
}
|
|
2414
|
-
const hasSection = SECTION_RE.test(text);
|
|
2415
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
2416
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
2417
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
2418
|
-
issues.push(
|
|
2419
|
-
issue2(
|
|
2420
|
-
"QFAI-DELTA-002",
|
|
2421
|
-
"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",
|
|
2422
|
-
"error",
|
|
2423
|
-
deltaPath,
|
|
2424
|
-
"delta.section"
|
|
2425
|
-
)
|
|
2426
|
-
);
|
|
2427
|
-
continue;
|
|
2428
|
-
}
|
|
2429
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
2430
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
2431
|
-
if (compatibilityChecked === changeChecked) {
|
|
2432
|
-
issues.push(
|
|
2433
|
-
issue2(
|
|
2434
|
-
"QFAI-DELTA-003",
|
|
2435
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
2436
|
-
"error",
|
|
2437
|
-
deltaPath,
|
|
2438
|
-
"delta.classification"
|
|
2439
|
-
)
|
|
2440
|
-
);
|
|
2441
|
-
}
|
|
2442
2842
|
}
|
|
2443
2843
|
return issues;
|
|
2444
2844
|
}
|
|
@@ -2472,7 +2872,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2472
2872
|
|
|
2473
2873
|
// src/core/validators/ids.ts
|
|
2474
2874
|
import { readFile as readFile9 } from "fs/promises";
|
|
2475
|
-
import
|
|
2875
|
+
import path19 from "path";
|
|
2476
2876
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2477
2877
|
async function validateDefinedIds(root, config) {
|
|
2478
2878
|
const issues = [];
|
|
@@ -2538,7 +2938,7 @@ function recordId(out, id, file) {
|
|
|
2538
2938
|
}
|
|
2539
2939
|
function formatFileList(files, root) {
|
|
2540
2940
|
return files.map((file) => {
|
|
2541
|
-
const relative =
|
|
2941
|
+
const relative = path19.relative(root, file);
|
|
2542
2942
|
return relative.length > 0 ? relative : file;
|
|
2543
2943
|
}).join(", ");
|
|
2544
2944
|
}
|
|
@@ -2596,7 +2996,8 @@ async function validatePromptsIntegrity(root) {
|
|
|
2596
2996
|
}
|
|
2597
2997
|
|
|
2598
2998
|
// src/core/validators/scenario.ts
|
|
2599
|
-
import { readFile as readFile10 } from "fs/promises";
|
|
2999
|
+
import { access as access7, readFile as readFile10 } from "fs/promises";
|
|
3000
|
+
import path20 from "path";
|
|
2600
3001
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2601
3002
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2602
3003
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2619,6 +3020,18 @@ async function validateScenarios(root, config) {
|
|
|
2619
3020
|
}
|
|
2620
3021
|
const issues = [];
|
|
2621
3022
|
for (const entry of entries) {
|
|
3023
|
+
const legacyScenarioPath = path20.join(entry.dir, "scenario.md");
|
|
3024
|
+
if (await fileExists(legacyScenarioPath)) {
|
|
3025
|
+
issues.push(
|
|
3026
|
+
issue4(
|
|
3027
|
+
"QFAI-SC-004",
|
|
3028
|
+
"scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3029
|
+
"error",
|
|
3030
|
+
legacyScenarioPath,
|
|
3031
|
+
"scenario.legacy"
|
|
3032
|
+
)
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
2622
3035
|
let text;
|
|
2623
3036
|
try {
|
|
2624
3037
|
text = await readFile10(entry.scenarioPath, "utf-8");
|
|
@@ -2650,6 +3063,7 @@ function validateScenarioContent(text, file) {
|
|
|
2650
3063
|
"UI",
|
|
2651
3064
|
"API",
|
|
2652
3065
|
"DB",
|
|
3066
|
+
"THEMA",
|
|
2653
3067
|
"ADR"
|
|
2654
3068
|
]);
|
|
2655
3069
|
if (invalidIds.length > 0) {
|
|
@@ -2793,6 +3207,14 @@ function isMissingFileError3(error2) {
|
|
|
2793
3207
|
}
|
|
2794
3208
|
return error2.code === "ENOENT";
|
|
2795
3209
|
}
|
|
3210
|
+
async function fileExists(target) {
|
|
3211
|
+
try {
|
|
3212
|
+
await access7(target);
|
|
3213
|
+
return true;
|
|
3214
|
+
} catch {
|
|
3215
|
+
return false;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
2796
3218
|
|
|
2797
3219
|
// src/core/validators/spec.ts
|
|
2798
3220
|
import { readFile as readFile11 } from "fs/promises";
|
|
@@ -2852,6 +3274,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2852
3274
|
"UI",
|
|
2853
3275
|
"API",
|
|
2854
3276
|
"DB",
|
|
3277
|
+
"THEMA",
|
|
2855
3278
|
"ADR"
|
|
2856
3279
|
]);
|
|
2857
3280
|
if (invalidIds.length > 0) {
|
|
@@ -3031,7 +3454,7 @@ async function validateTraceability(root, config) {
|
|
|
3031
3454
|
"QFAI-TRACE-021",
|
|
3032
3455
|
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
3033
3456
|
", "
|
|
3034
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3457
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3035
3458
|
"error",
|
|
3036
3459
|
file,
|
|
3037
3460
|
"traceability.specContractRefFormat",
|
|
@@ -3095,7 +3518,7 @@ async function validateTraceability(root, config) {
|
|
|
3095
3518
|
"QFAI-TRACE-032",
|
|
3096
3519
|
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
3097
3520
|
", "
|
|
3098
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3521
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3099
3522
|
"error",
|
|
3100
3523
|
file,
|
|
3101
3524
|
"traceability.scenarioContractRefFormat",
|
|
@@ -3474,17 +3897,25 @@ function countIssues(issues) {
|
|
|
3474
3897
|
}
|
|
3475
3898
|
|
|
3476
3899
|
// src/core/report.ts
|
|
3477
|
-
var ID_PREFIXES2 = [
|
|
3900
|
+
var ID_PREFIXES2 = [
|
|
3901
|
+
"SPEC",
|
|
3902
|
+
"BR",
|
|
3903
|
+
"SC",
|
|
3904
|
+
"UI",
|
|
3905
|
+
"API",
|
|
3906
|
+
"DB",
|
|
3907
|
+
"THEMA"
|
|
3908
|
+
];
|
|
3478
3909
|
async function createReportData(root, validation, configResult) {
|
|
3479
|
-
const resolvedRoot =
|
|
3910
|
+
const resolvedRoot = path21.resolve(root);
|
|
3480
3911
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3481
3912
|
const config = resolved.config;
|
|
3482
3913
|
const configPath = resolved.configPath;
|
|
3483
3914
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3484
3915
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3485
|
-
const apiRoot =
|
|
3486
|
-
const uiRoot =
|
|
3487
|
-
const dbRoot =
|
|
3916
|
+
const apiRoot = path21.join(contractsRoot, "api");
|
|
3917
|
+
const uiRoot = path21.join(contractsRoot, "ui");
|
|
3918
|
+
const dbRoot = path21.join(contractsRoot, "db");
|
|
3488
3919
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3489
3920
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3490
3921
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3492,7 +3923,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3492
3923
|
const {
|
|
3493
3924
|
api: apiFiles,
|
|
3494
3925
|
ui: uiFiles,
|
|
3495
|
-
db: dbFiles
|
|
3926
|
+
db: dbFiles,
|
|
3927
|
+
thema: themaFiles
|
|
3496
3928
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
3497
3929
|
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
3498
3930
|
const contractIdList = Array.from(contractIndex.ids);
|
|
@@ -3519,7 +3951,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3519
3951
|
...scenarioFiles,
|
|
3520
3952
|
...apiFiles,
|
|
3521
3953
|
...uiFiles,
|
|
3522
|
-
...dbFiles
|
|
3954
|
+
...dbFiles,
|
|
3955
|
+
...themaFiles
|
|
3523
3956
|
]);
|
|
3524
3957
|
const upstreamIds = await collectUpstreamIds([
|
|
3525
3958
|
...specFiles,
|
|
@@ -3556,7 +3989,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3556
3989
|
contracts: {
|
|
3557
3990
|
api: apiFiles.length,
|
|
3558
3991
|
ui: uiFiles.length,
|
|
3559
|
-
db: dbFiles.length
|
|
3992
|
+
db: dbFiles.length,
|
|
3993
|
+
thema: themaFiles.length
|
|
3560
3994
|
},
|
|
3561
3995
|
counts: normalizedValidation.counts
|
|
3562
3996
|
},
|
|
@@ -3566,7 +4000,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3566
4000
|
sc: idsByPrefix.SC,
|
|
3567
4001
|
ui: idsByPrefix.UI,
|
|
3568
4002
|
api: idsByPrefix.API,
|
|
3569
|
-
db: idsByPrefix.DB
|
|
4003
|
+
db: idsByPrefix.DB,
|
|
4004
|
+
thema: idsByPrefix.THEMA
|
|
3570
4005
|
},
|
|
3571
4006
|
traceability: {
|
|
3572
4007
|
upstreamIdsFound: upstreamIds.size,
|
|
@@ -3636,7 +4071,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3636
4071
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
3637
4072
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
3638
4073
|
lines.push(
|
|
3639
|
-
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
4074
|
+
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
|
|
3640
4075
|
);
|
|
3641
4076
|
lines.push(
|
|
3642
4077
|
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
@@ -3780,6 +4215,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3780
4215
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
3781
4216
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3782
4217
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
4218
|
+
lines.push(formatIdLine("THEMA", data.ids.thema));
|
|
3783
4219
|
lines.push("");
|
|
3784
4220
|
lines.push("## Traceability");
|
|
3785
4221
|
lines.push("");
|
|
@@ -3944,12 +4380,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3944
4380
|
"- 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"
|
|
3945
4381
|
);
|
|
3946
4382
|
}
|
|
3947
|
-
lines.push(
|
|
3948
|
-
|
|
3949
|
-
);
|
|
3950
|
-
lines.push(
|
|
3951
|
-
"- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
|
|
3952
|
-
);
|
|
4383
|
+
lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
|
|
4384
|
+
lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md`");
|
|
3953
4385
|
return lines.join("\n");
|
|
3954
4386
|
}
|
|
3955
4387
|
function formatReportJson(data) {
|
|
@@ -4001,7 +4433,8 @@ async function collectIds(files) {
|
|
|
4001
4433
|
SC: /* @__PURE__ */ new Set(),
|
|
4002
4434
|
UI: /* @__PURE__ */ new Set(),
|
|
4003
4435
|
API: /* @__PURE__ */ new Set(),
|
|
4004
|
-
DB: /* @__PURE__ */ new Set()
|
|
4436
|
+
DB: /* @__PURE__ */ new Set(),
|
|
4437
|
+
THEMA: /* @__PURE__ */ new Set()
|
|
4005
4438
|
};
|
|
4006
4439
|
for (const file of files) {
|
|
4007
4440
|
const text = await readFile13(file, "utf-8");
|
|
@@ -4016,7 +4449,8 @@ async function collectIds(files) {
|
|
|
4016
4449
|
SC: toSortedArray2(result.SC),
|
|
4017
4450
|
UI: toSortedArray2(result.UI),
|
|
4018
4451
|
API: toSortedArray2(result.API),
|
|
4019
|
-
DB: toSortedArray2(result.DB)
|
|
4452
|
+
DB: toSortedArray2(result.DB),
|
|
4453
|
+
THEMA: toSortedArray2(result.THEMA)
|
|
4020
4454
|
};
|
|
4021
4455
|
}
|
|
4022
4456
|
async function collectUpstreamIds(files) {
|
|
@@ -4180,7 +4614,7 @@ function warnIfTruncated(scan, context) {
|
|
|
4180
4614
|
|
|
4181
4615
|
// src/cli/commands/report.ts
|
|
4182
4616
|
async function runReport(options) {
|
|
4183
|
-
const root =
|
|
4617
|
+
const root = path22.resolve(options.root);
|
|
4184
4618
|
const configResult = await loadConfig(root);
|
|
4185
4619
|
let validation;
|
|
4186
4620
|
if (options.runValidate) {
|
|
@@ -4197,7 +4631,7 @@ async function runReport(options) {
|
|
|
4197
4631
|
validation = normalized;
|
|
4198
4632
|
} else {
|
|
4199
4633
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
4200
|
-
const inputPath =
|
|
4634
|
+
const inputPath = path22.isAbsolute(input) ? input : path22.resolve(root, input);
|
|
4201
4635
|
try {
|
|
4202
4636
|
validation = await readValidationResult(inputPath);
|
|
4203
4637
|
} catch (err) {
|
|
@@ -4224,10 +4658,10 @@ async function runReport(options) {
|
|
|
4224
4658
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4225
4659
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4226
4660
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4227
|
-
const defaultOut = options.format === "json" ?
|
|
4661
|
+
const defaultOut = options.format === "json" ? path22.join(outRoot, "report.json") : path22.join(outRoot, "report.md");
|
|
4228
4662
|
const out = options.outPath ?? defaultOut;
|
|
4229
|
-
const outPath =
|
|
4230
|
-
await mkdir3(
|
|
4663
|
+
const outPath = path22.isAbsolute(out) ? out : path22.resolve(root, out);
|
|
4664
|
+
await mkdir3(path22.dirname(outPath), { recursive: true });
|
|
4231
4665
|
await writeFile2(outPath, `${output}
|
|
4232
4666
|
`, "utf-8");
|
|
4233
4667
|
info(
|
|
@@ -4292,15 +4726,15 @@ function isMissingFileError5(error2) {
|
|
|
4292
4726
|
return record2.code === "ENOENT";
|
|
4293
4727
|
}
|
|
4294
4728
|
async function writeValidationResult(root, outputPath, result) {
|
|
4295
|
-
const abs =
|
|
4296
|
-
await mkdir3(
|
|
4729
|
+
const abs = path22.isAbsolute(outputPath) ? outputPath : path22.resolve(root, outputPath);
|
|
4730
|
+
await mkdir3(path22.dirname(abs), { recursive: true });
|
|
4297
4731
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
4298
4732
|
`, "utf-8");
|
|
4299
4733
|
}
|
|
4300
4734
|
|
|
4301
4735
|
// src/cli/commands/validate.ts
|
|
4302
4736
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
4303
|
-
import
|
|
4737
|
+
import path23 from "path";
|
|
4304
4738
|
|
|
4305
4739
|
// src/cli/lib/failOn.ts
|
|
4306
4740
|
function shouldFail(result, failOn) {
|
|
@@ -4315,7 +4749,7 @@ function shouldFail(result, failOn) {
|
|
|
4315
4749
|
|
|
4316
4750
|
// src/cli/commands/validate.ts
|
|
4317
4751
|
async function runValidate(options) {
|
|
4318
|
-
const root =
|
|
4752
|
+
const root = path23.resolve(options.root);
|
|
4319
4753
|
const configResult = await loadConfig(root);
|
|
4320
4754
|
const result = await validateProject(root, configResult);
|
|
4321
4755
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -4440,12 +4874,12 @@ function issueKey(issue7) {
|
|
|
4440
4874
|
}
|
|
4441
4875
|
async function emitJson(result, root, jsonPath) {
|
|
4442
4876
|
const abs = resolveJsonPath(root, jsonPath);
|
|
4443
|
-
await mkdir4(
|
|
4877
|
+
await mkdir4(path23.dirname(abs), { recursive: true });
|
|
4444
4878
|
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
4445
4879
|
`, "utf-8");
|
|
4446
4880
|
}
|
|
4447
4881
|
function resolveJsonPath(root, jsonPath) {
|
|
4448
|
-
return
|
|
4882
|
+
return path23.isAbsolute(jsonPath) ? jsonPath : path23.resolve(root, jsonPath);
|
|
4449
4883
|
}
|
|
4450
4884
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
4451
4885
|
|