qfai 1.0.1 → 1.0.3
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 -3
- package/assets/init/.qfai/README.md +2 -2
- package/assets/init/.qfai/contracts/README.md +21 -1
- 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/prompts/makeBusinessFlow.md +1 -1
- package/assets/init/.qfai/prompts/makeOverview.md +1 -1
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +1 -1
- package/assets/init/.qfai/prompts/require-to-spec.md +2 -2
- package/assets/init/.qfai/samples/analyze/analysis.md +1 -1
- package/assets/init/.qfai/specs/README.md +3 -3
- package/assets/init/.qfai/specs/spec-0001/delta.md +1 -1
- package/assets/init/.qfai/specs/spec-0001/{scenario.md → scenario.feature} +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
- package/dist/cli/index.cjs +589 -114
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +591 -116
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +542 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +544 -69
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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";
|
|
@@ -616,6 +616,7 @@ function isRecord(value) {
|
|
|
616
616
|
|
|
617
617
|
// src/core/discovery.ts
|
|
618
618
|
import { access as access3 } from "fs/promises";
|
|
619
|
+
import path5 from "path";
|
|
619
620
|
|
|
620
621
|
// src/core/specLayout.ts
|
|
621
622
|
import { readdir as readdir2 } from "fs/promises";
|
|
@@ -627,7 +628,7 @@ async function collectSpecEntries(specsRoot) {
|
|
|
627
628
|
dir,
|
|
628
629
|
specPath: path4.join(dir, "spec.md"),
|
|
629
630
|
deltaPath: path4.join(dir, "delta.md"),
|
|
630
|
-
scenarioPath: path4.join(dir, "scenario.
|
|
631
|
+
scenarioPath: path4.join(dir, "scenario.feature")
|
|
631
632
|
}));
|
|
632
633
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
633
634
|
}
|
|
@@ -663,7 +664,12 @@ async function collectScenarioFiles(specsRoot) {
|
|
|
663
664
|
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
664
665
|
}
|
|
665
666
|
async function collectUiContractFiles(uiRoot) {
|
|
666
|
-
|
|
667
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
668
|
+
return filterByBasenamePrefix(files, "ui-");
|
|
669
|
+
}
|
|
670
|
+
async function collectThemaContractFiles(uiRoot) {
|
|
671
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
672
|
+
return filterByBasenamePrefix(files, "thema-");
|
|
667
673
|
}
|
|
668
674
|
async function collectApiContractFiles(apiRoot) {
|
|
669
675
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
@@ -672,12 +678,13 @@ async function collectDbContractFiles(dbRoot) {
|
|
|
672
678
|
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
673
679
|
}
|
|
674
680
|
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
675
|
-
const [ui, api, db] = await Promise.all([
|
|
681
|
+
const [ui, thema, api, db] = await Promise.all([
|
|
676
682
|
collectUiContractFiles(uiRoot),
|
|
683
|
+
collectThemaContractFiles(uiRoot),
|
|
677
684
|
collectApiContractFiles(apiRoot),
|
|
678
685
|
collectDbContractFiles(dbRoot)
|
|
679
686
|
]);
|
|
680
|
-
return { ui, api, db };
|
|
687
|
+
return { ui, thema, api, db };
|
|
681
688
|
}
|
|
682
689
|
async function filterExisting(files) {
|
|
683
690
|
const existing = [];
|
|
@@ -696,17 +703,23 @@ async function exists3(target) {
|
|
|
696
703
|
return false;
|
|
697
704
|
}
|
|
698
705
|
}
|
|
706
|
+
function filterByBasenamePrefix(files, prefix) {
|
|
707
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
708
|
+
return files.filter(
|
|
709
|
+
(file) => path5.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
710
|
+
);
|
|
711
|
+
}
|
|
699
712
|
|
|
700
713
|
// src/core/paths.ts
|
|
701
|
-
import
|
|
714
|
+
import path6 from "path";
|
|
702
715
|
function toRelativePath(root, target) {
|
|
703
716
|
if (!target) {
|
|
704
717
|
return target;
|
|
705
718
|
}
|
|
706
|
-
if (!
|
|
719
|
+
if (!path6.isAbsolute(target)) {
|
|
707
720
|
return toPosixPath(target);
|
|
708
721
|
}
|
|
709
|
-
const relative =
|
|
722
|
+
const relative = path6.relative(root, target);
|
|
710
723
|
if (!relative) {
|
|
711
724
|
return ".";
|
|
712
725
|
}
|
|
@@ -718,7 +731,7 @@ function toPosixPath(value) {
|
|
|
718
731
|
|
|
719
732
|
// src/core/traceability.ts
|
|
720
733
|
import { readFile as readFile3 } from "fs/promises";
|
|
721
|
-
import
|
|
734
|
+
import path7 from "path";
|
|
722
735
|
|
|
723
736
|
// src/core/gherkin/parse.ts
|
|
724
737
|
import {
|
|
@@ -950,7 +963,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
950
963
|
};
|
|
951
964
|
}
|
|
952
965
|
const normalizedFiles = Array.from(
|
|
953
|
-
new Set(scanResult.files.map((file) =>
|
|
966
|
+
new Set(scanResult.files.map((file) => path7.normalize(file)))
|
|
954
967
|
);
|
|
955
968
|
for (const file of normalizedFiles) {
|
|
956
969
|
const text = await readFile3(file, "utf-8");
|
|
@@ -1013,19 +1026,19 @@ function formatError3(error2) {
|
|
|
1013
1026
|
|
|
1014
1027
|
// src/core/promptsIntegrity.ts
|
|
1015
1028
|
import { readFile as readFile4 } from "fs/promises";
|
|
1016
|
-
import
|
|
1029
|
+
import path9 from "path";
|
|
1017
1030
|
|
|
1018
1031
|
// src/shared/assets.ts
|
|
1019
1032
|
import { existsSync } from "fs";
|
|
1020
|
-
import
|
|
1033
|
+
import path8 from "path";
|
|
1021
1034
|
import { fileURLToPath } from "url";
|
|
1022
1035
|
function getInitAssetsDir() {
|
|
1023
1036
|
const base = import.meta.url;
|
|
1024
1037
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
1025
|
-
const baseDir =
|
|
1038
|
+
const baseDir = path8.dirname(basePath);
|
|
1026
1039
|
const candidates = [
|
|
1027
|
-
|
|
1028
|
-
|
|
1040
|
+
path8.resolve(baseDir, "../../../assets/init"),
|
|
1041
|
+
path8.resolve(baseDir, "../../assets/init")
|
|
1029
1042
|
];
|
|
1030
1043
|
for (const candidate of candidates) {
|
|
1031
1044
|
if (existsSync(candidate)) {
|
|
@@ -1043,10 +1056,10 @@ function getInitAssetsDir() {
|
|
|
1043
1056
|
|
|
1044
1057
|
// src/core/promptsIntegrity.ts
|
|
1045
1058
|
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1046
|
-
const promptsDir =
|
|
1059
|
+
const promptsDir = path9.resolve(root, ".qfai", "prompts");
|
|
1047
1060
|
let templateDir;
|
|
1048
1061
|
try {
|
|
1049
|
-
templateDir =
|
|
1062
|
+
templateDir = path9.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1050
1063
|
} catch {
|
|
1051
1064
|
return {
|
|
1052
1065
|
status: "skipped_missing_assets",
|
|
@@ -1123,7 +1136,7 @@ function normalizeNewlines(text) {
|
|
|
1123
1136
|
return text.replace(/\r\n/g, "\n");
|
|
1124
1137
|
}
|
|
1125
1138
|
function toRel(base, abs) {
|
|
1126
|
-
const rel =
|
|
1139
|
+
const rel = path9.relative(base, abs);
|
|
1127
1140
|
return rel.replace(/[\\/]+/g, "/");
|
|
1128
1141
|
}
|
|
1129
1142
|
function intersectKeys(a, b) {
|
|
@@ -1138,11 +1151,11 @@ function intersectKeys(a, b) {
|
|
|
1138
1151
|
|
|
1139
1152
|
// src/core/version.ts
|
|
1140
1153
|
import { readFile as readFile5 } from "fs/promises";
|
|
1141
|
-
import
|
|
1154
|
+
import path10 from "path";
|
|
1142
1155
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1143
1156
|
async function resolveToolVersion() {
|
|
1144
|
-
if ("1.0.
|
|
1145
|
-
return "1.0.
|
|
1157
|
+
if ("1.0.3".length > 0) {
|
|
1158
|
+
return "1.0.3";
|
|
1146
1159
|
}
|
|
1147
1160
|
try {
|
|
1148
1161
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1157,7 +1170,7 @@ async function resolveToolVersion() {
|
|
|
1157
1170
|
function resolvePackageJsonPath() {
|
|
1158
1171
|
const base = import.meta.url;
|
|
1159
1172
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1160
|
-
return
|
|
1173
|
+
return path10.resolve(path10.dirname(basePath), "../../package.json");
|
|
1161
1174
|
}
|
|
1162
1175
|
|
|
1163
1176
|
// src/core/doctor.ts
|
|
@@ -1183,7 +1196,7 @@ function normalizeGlobs2(values) {
|
|
|
1183
1196
|
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1184
1197
|
}
|
|
1185
1198
|
async function createDoctorData(options) {
|
|
1186
|
-
const startDir =
|
|
1199
|
+
const startDir = path11.resolve(options.startDir);
|
|
1187
1200
|
const checks = [];
|
|
1188
1201
|
const configPath = getConfigPath(startDir);
|
|
1189
1202
|
const search = options.rootExplicit ? {
|
|
@@ -1246,9 +1259,9 @@ async function createDoctorData(options) {
|
|
|
1246
1259
|
details: { path: toRelativePath(root, resolved) }
|
|
1247
1260
|
});
|
|
1248
1261
|
if (key === "promptsDir") {
|
|
1249
|
-
const promptsLocalDir =
|
|
1250
|
-
|
|
1251
|
-
`${
|
|
1262
|
+
const promptsLocalDir = path11.join(
|
|
1263
|
+
path11.dirname(resolved),
|
|
1264
|
+
`${path11.basename(resolved)}.local`
|
|
1252
1265
|
);
|
|
1253
1266
|
const found = await exists4(promptsLocalDir);
|
|
1254
1267
|
addCheck(checks, {
|
|
@@ -1321,7 +1334,7 @@ async function createDoctorData(options) {
|
|
|
1321
1334
|
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1322
1335
|
details: { specPacks: entries.length, missingFiles }
|
|
1323
1336
|
});
|
|
1324
|
-
const validateJsonAbs =
|
|
1337
|
+
const validateJsonAbs = path11.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path11.resolve(root, config.output.validateJsonPath);
|
|
1325
1338
|
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1326
1339
|
addCheck(checks, {
|
|
1327
1340
|
id: "output.validateJson",
|
|
@@ -1331,8 +1344,8 @@ async function createDoctorData(options) {
|
|
|
1331
1344
|
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1332
1345
|
});
|
|
1333
1346
|
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1334
|
-
const rel =
|
|
1335
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
1347
|
+
const rel = path11.relative(outDirAbs, validateJsonAbs);
|
|
1348
|
+
const inside = rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
1336
1349
|
addCheck(checks, {
|
|
1337
1350
|
id: "output.pathAlignment",
|
|
1338
1351
|
severity: inside ? "ok" : "warning",
|
|
@@ -1455,12 +1468,12 @@ async function detectOutDirCollisions(root) {
|
|
|
1455
1468
|
});
|
|
1456
1469
|
const configPaths = configScan.files;
|
|
1457
1470
|
const configRoots = Array.from(
|
|
1458
|
-
new Set(configPaths.map((configPath) =>
|
|
1471
|
+
new Set(configPaths.map((configPath) => path11.dirname(configPath)))
|
|
1459
1472
|
).sort((a, b) => a.localeCompare(b));
|
|
1460
1473
|
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1461
1474
|
for (const configRoot of configRoots) {
|
|
1462
1475
|
const { config } = await loadConfig(configRoot);
|
|
1463
|
-
const outDir =
|
|
1476
|
+
const outDir = path11.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1464
1477
|
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1465
1478
|
roots.add(configRoot);
|
|
1466
1479
|
outDirToRoots.set(outDir, roots);
|
|
@@ -1486,20 +1499,20 @@ async function detectOutDirCollisions(root) {
|
|
|
1486
1499
|
};
|
|
1487
1500
|
}
|
|
1488
1501
|
async function findMonorepoRoot(startDir) {
|
|
1489
|
-
let current =
|
|
1502
|
+
let current = path11.resolve(startDir);
|
|
1490
1503
|
while (true) {
|
|
1491
|
-
const gitPath =
|
|
1492
|
-
const workspacePath =
|
|
1504
|
+
const gitPath = path11.join(current, ".git");
|
|
1505
|
+
const workspacePath = path11.join(current, "pnpm-workspace.yaml");
|
|
1493
1506
|
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1494
1507
|
return current;
|
|
1495
1508
|
}
|
|
1496
|
-
const parent =
|
|
1509
|
+
const parent = path11.dirname(current);
|
|
1497
1510
|
if (parent === current) {
|
|
1498
1511
|
break;
|
|
1499
1512
|
}
|
|
1500
1513
|
current = parent;
|
|
1501
1514
|
}
|
|
1502
|
-
return
|
|
1515
|
+
return path11.resolve(startDir);
|
|
1503
1516
|
}
|
|
1504
1517
|
|
|
1505
1518
|
// src/cli/lib/logger.ts
|
|
@@ -1541,8 +1554,8 @@ async function runDoctor(options) {
|
|
|
1541
1554
|
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1542
1555
|
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1543
1556
|
if (options.outPath) {
|
|
1544
|
-
const outAbs =
|
|
1545
|
-
await mkdir(
|
|
1557
|
+
const outAbs = path12.isAbsolute(options.outPath) ? options.outPath : path12.resolve(process.cwd(), options.outPath);
|
|
1558
|
+
await mkdir(path12.dirname(outAbs), { recursive: true });
|
|
1546
1559
|
await writeFile(outAbs, `${output}
|
|
1547
1560
|
`, "utf-8");
|
|
1548
1561
|
info(`doctor: wrote ${outAbs}`);
|
|
@@ -1562,11 +1575,11 @@ function shouldFailDoctor(summary, failOn) {
|
|
|
1562
1575
|
}
|
|
1563
1576
|
|
|
1564
1577
|
// src/cli/commands/init.ts
|
|
1565
|
-
import
|
|
1578
|
+
import path14 from "path";
|
|
1566
1579
|
|
|
1567
1580
|
// src/cli/lib/fs.ts
|
|
1568
1581
|
import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
|
|
1569
|
-
import
|
|
1582
|
+
import path13 from "path";
|
|
1570
1583
|
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1571
1584
|
const files = await collectTemplateFiles(sourceRoot);
|
|
1572
1585
|
return copyFiles(files, sourceRoot, destRoot, options);
|
|
@@ -1574,7 +1587,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
|
1574
1587
|
async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
|
|
1575
1588
|
const allFiles = [];
|
|
1576
1589
|
for (const relPath of relativePaths) {
|
|
1577
|
-
const fullPath =
|
|
1590
|
+
const fullPath = path13.join(sourceRoot, relPath);
|
|
1578
1591
|
const files = await collectTemplateFiles(fullPath);
|
|
1579
1592
|
allFiles.push(...files);
|
|
1580
1593
|
}
|
|
@@ -1584,13 +1597,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1584
1597
|
const copied = [];
|
|
1585
1598
|
const skipped = [];
|
|
1586
1599
|
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 +
|
|
1600
|
+
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
|
|
1601
|
+
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path13.sep);
|
|
1589
1602
|
const isProtectedRelative = (relative) => {
|
|
1590
1603
|
if (protectPrefixes.length === 0) {
|
|
1591
1604
|
return false;
|
|
1592
1605
|
}
|
|
1593
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1606
|
+
const normalized = relative.replace(/[\\/]+/g, path13.sep);
|
|
1594
1607
|
return protectPrefixes.some(
|
|
1595
1608
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1596
1609
|
);
|
|
@@ -1599,7 +1612,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1599
1612
|
if (excludePrefixes.length === 0) {
|
|
1600
1613
|
return false;
|
|
1601
1614
|
}
|
|
1602
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1615
|
+
const normalized = relative.replace(/[\\/]+/g, path13.sep);
|
|
1603
1616
|
return excludePrefixes.some(
|
|
1604
1617
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1605
1618
|
);
|
|
@@ -1607,14 +1620,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1607
1620
|
const conflictPolicy = options.conflictPolicy ?? "error";
|
|
1608
1621
|
if (!options.force && conflictPolicy === "error") {
|
|
1609
1622
|
for (const file of files) {
|
|
1610
|
-
const relative =
|
|
1623
|
+
const relative = path13.relative(sourceRoot, file);
|
|
1611
1624
|
if (isExcludedRelative(relative)) {
|
|
1612
1625
|
continue;
|
|
1613
1626
|
}
|
|
1614
1627
|
if (isProtectedRelative(relative)) {
|
|
1615
1628
|
continue;
|
|
1616
1629
|
}
|
|
1617
|
-
const dest =
|
|
1630
|
+
const dest = path13.join(destRoot, relative);
|
|
1618
1631
|
if (!await shouldWrite(dest, options.force)) {
|
|
1619
1632
|
conflicts.push(dest);
|
|
1620
1633
|
}
|
|
@@ -1624,18 +1637,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1624
1637
|
}
|
|
1625
1638
|
}
|
|
1626
1639
|
for (const file of files) {
|
|
1627
|
-
const relative =
|
|
1640
|
+
const relative = path13.relative(sourceRoot, file);
|
|
1628
1641
|
if (isExcludedRelative(relative)) {
|
|
1629
1642
|
continue;
|
|
1630
1643
|
}
|
|
1631
|
-
const dest =
|
|
1644
|
+
const dest = path13.join(destRoot, relative);
|
|
1632
1645
|
const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
|
|
1633
1646
|
if (!await shouldWrite(dest, forceForThisFile)) {
|
|
1634
1647
|
skipped.push(dest);
|
|
1635
1648
|
continue;
|
|
1636
1649
|
}
|
|
1637
1650
|
if (!options.dryRun) {
|
|
1638
|
-
await mkdir2(
|
|
1651
|
+
await mkdir2(path13.dirname(dest), { recursive: true });
|
|
1639
1652
|
await copyFile(file, dest);
|
|
1640
1653
|
}
|
|
1641
1654
|
copied.push(dest);
|
|
@@ -1659,7 +1672,7 @@ async function collectTemplateFiles(root) {
|
|
|
1659
1672
|
}
|
|
1660
1673
|
const items = await readdir3(root, { withFileTypes: true });
|
|
1661
1674
|
for (const item of items) {
|
|
1662
|
-
const fullPath =
|
|
1675
|
+
const fullPath = path13.join(root, item.name);
|
|
1663
1676
|
if (item.isDirectory()) {
|
|
1664
1677
|
const nested = await collectTemplateFiles(fullPath);
|
|
1665
1678
|
entries.push(...nested);
|
|
@@ -1689,10 +1702,10 @@ async function exists5(target) {
|
|
|
1689
1702
|
// src/cli/commands/init.ts
|
|
1690
1703
|
async function runInit(options) {
|
|
1691
1704
|
const assetsRoot = getInitAssetsDir();
|
|
1692
|
-
const rootAssets =
|
|
1693
|
-
const qfaiAssets =
|
|
1694
|
-
const destRoot =
|
|
1695
|
-
const destQfai =
|
|
1705
|
+
const rootAssets = path14.join(assetsRoot, "root");
|
|
1706
|
+
const qfaiAssets = path14.join(assetsRoot, ".qfai");
|
|
1707
|
+
const destRoot = path14.resolve(options.dir);
|
|
1708
|
+
const destQfai = path14.join(destRoot, ".qfai");
|
|
1696
1709
|
if (options.force) {
|
|
1697
1710
|
info(
|
|
1698
1711
|
"NOTE: --force \u306F .qfai/prompts/** \u306E\u307F\u4E0A\u66F8\u304D\u3057\u307E\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u3001specs/contracts \u7B49\u306F\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\uFF09\u3002"
|
|
@@ -1740,7 +1753,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1740
1753
|
|
|
1741
1754
|
// src/cli/commands/report.ts
|
|
1742
1755
|
import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
|
|
1743
|
-
import
|
|
1756
|
+
import path22 from "path";
|
|
1744
1757
|
|
|
1745
1758
|
// src/core/normalize.ts
|
|
1746
1759
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1781,15 +1794,15 @@ function normalizeValidationResult(root, result) {
|
|
|
1781
1794
|
|
|
1782
1795
|
// src/core/report.ts
|
|
1783
1796
|
import { readFile as readFile13 } from "fs/promises";
|
|
1784
|
-
import
|
|
1797
|
+
import path21 from "path";
|
|
1785
1798
|
|
|
1786
1799
|
// src/core/contractIndex.ts
|
|
1787
1800
|
import { readFile as readFile6 } from "fs/promises";
|
|
1788
|
-
import
|
|
1801
|
+
import path15 from "path";
|
|
1789
1802
|
|
|
1790
1803
|
// 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*$/;
|
|
1804
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
1805
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
|
|
1793
1806
|
function extractDeclaredContractIds(text) {
|
|
1794
1807
|
const ids = [];
|
|
1795
1808
|
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
@@ -1807,20 +1820,22 @@ function stripContractDeclarationLines(text) {
|
|
|
1807
1820
|
// src/core/contractIndex.ts
|
|
1808
1821
|
async function buildContractIndex(root, config) {
|
|
1809
1822
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1810
|
-
const uiRoot =
|
|
1811
|
-
const apiRoot =
|
|
1812
|
-
const dbRoot =
|
|
1813
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1823
|
+
const uiRoot = path15.join(contractsRoot, "ui");
|
|
1824
|
+
const apiRoot = path15.join(contractsRoot, "api");
|
|
1825
|
+
const dbRoot = path15.join(contractsRoot, "db");
|
|
1826
|
+
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1814
1827
|
collectUiContractFiles(uiRoot),
|
|
1828
|
+
collectThemaContractFiles(uiRoot),
|
|
1815
1829
|
collectApiContractFiles(apiRoot),
|
|
1816
1830
|
collectDbContractFiles(dbRoot)
|
|
1817
1831
|
]);
|
|
1818
1832
|
const index = {
|
|
1819
1833
|
ids: /* @__PURE__ */ new Set(),
|
|
1820
1834
|
idToFiles: /* @__PURE__ */ new Map(),
|
|
1821
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1835
|
+
files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
|
|
1822
1836
|
};
|
|
1823
1837
|
await indexContractFiles(uiFiles, index);
|
|
1838
|
+
await indexContractFiles(themaFiles, index);
|
|
1824
1839
|
await indexContractFiles(apiFiles, index);
|
|
1825
1840
|
await indexContractFiles(dbFiles, index);
|
|
1826
1841
|
return index;
|
|
@@ -1839,7 +1854,15 @@ function record(index, id, file) {
|
|
|
1839
1854
|
}
|
|
1840
1855
|
|
|
1841
1856
|
// src/core/ids.ts
|
|
1842
|
-
var ID_PREFIXES = [
|
|
1857
|
+
var ID_PREFIXES = [
|
|
1858
|
+
"SPEC",
|
|
1859
|
+
"BR",
|
|
1860
|
+
"SC",
|
|
1861
|
+
"UI",
|
|
1862
|
+
"API",
|
|
1863
|
+
"DB",
|
|
1864
|
+
"THEMA"
|
|
1865
|
+
];
|
|
1843
1866
|
var STRICT_ID_PATTERNS = {
|
|
1844
1867
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1845
1868
|
BR: /\bBR-\d{4}\b/g,
|
|
@@ -1847,6 +1870,7 @@ var STRICT_ID_PATTERNS = {
|
|
|
1847
1870
|
UI: /\bUI-\d{4}\b/g,
|
|
1848
1871
|
API: /\bAPI-\d{4}\b/g,
|
|
1849
1872
|
DB: /\bDB-\d{4}\b/g,
|
|
1873
|
+
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
1850
1874
|
ADR: /\bADR-\d{4}\b/g
|
|
1851
1875
|
};
|
|
1852
1876
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -1856,6 +1880,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
1856
1880
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1857
1881
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1858
1882
|
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1883
|
+
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
1859
1884
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1860
1885
|
};
|
|
1861
1886
|
function extractIds(text, prefix) {
|
|
@@ -1892,7 +1917,7 @@ function isValidId(value, prefix) {
|
|
|
1892
1917
|
}
|
|
1893
1918
|
|
|
1894
1919
|
// src/core/parse/contractRefs.ts
|
|
1895
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1920
|
+
var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
|
|
1896
1921
|
function parseContractRefs(text, options = {}) {
|
|
1897
1922
|
const linePattern = buildLinePattern(options);
|
|
1898
1923
|
const lines = [];
|
|
@@ -2062,14 +2087,14 @@ function parseSpec(md, file) {
|
|
|
2062
2087
|
}
|
|
2063
2088
|
|
|
2064
2089
|
// src/core/validators/contracts.ts
|
|
2065
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
2066
|
-
import
|
|
2090
|
+
import { access as access6, readFile as readFile7 } from "fs/promises";
|
|
2091
|
+
import path17 from "path";
|
|
2067
2092
|
|
|
2068
2093
|
// src/core/contracts.ts
|
|
2069
|
-
import
|
|
2094
|
+
import path16 from "path";
|
|
2070
2095
|
import { parse as parseYaml2 } from "yaml";
|
|
2071
2096
|
function parseStructuredContract(file, text) {
|
|
2072
|
-
const ext =
|
|
2097
|
+
const ext = path16.extname(file).toLowerCase();
|
|
2073
2098
|
if (ext === ".json") {
|
|
2074
2099
|
return JSON.parse(text);
|
|
2075
2100
|
}
|
|
@@ -2086,17 +2111,23 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
2086
2111
|
label: "ALTER TABLE ... DROP"
|
|
2087
2112
|
}
|
|
2088
2113
|
];
|
|
2114
|
+
var THEMA_ID_RE = /^THEMA-\d{3}$/;
|
|
2089
2115
|
async function validateContracts(root, config) {
|
|
2090
2116
|
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
2117
|
const contractIndex = await buildContractIndex(root, config);
|
|
2118
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2119
|
+
const uiRoot = path17.join(contractsRoot, "ui");
|
|
2120
|
+
const themaIds = new Set(
|
|
2121
|
+
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
2122
|
+
);
|
|
2123
|
+
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
2124
|
+
issues.push(...await validateThemaContracts(uiRoot));
|
|
2125
|
+
issues.push(...await validateApiContracts(path17.join(contractsRoot, "api")));
|
|
2126
|
+
issues.push(...await validateDbContracts(path17.join(contractsRoot, "db")));
|
|
2096
2127
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
2097
2128
|
return issues;
|
|
2098
2129
|
}
|
|
2099
|
-
async function validateUiContracts(uiRoot) {
|
|
2130
|
+
async function validateUiContracts(uiRoot, themaIds) {
|
|
2100
2131
|
const files = await collectUiContractFiles(uiRoot);
|
|
2101
2132
|
if (files.length === 0) {
|
|
2102
2133
|
return [
|
|
@@ -2112,6 +2143,22 @@ async function validateUiContracts(uiRoot) {
|
|
|
2112
2143
|
const issues = [];
|
|
2113
2144
|
for (const file of files) {
|
|
2114
2145
|
const text = await readFile7(file, "utf-8");
|
|
2146
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
2147
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
2148
|
+
let doc = null;
|
|
2149
|
+
try {
|
|
2150
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2151
|
+
} catch (error2) {
|
|
2152
|
+
issues.push(
|
|
2153
|
+
issue(
|
|
2154
|
+
"QFAI-CONTRACT-001",
|
|
2155
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2156
|
+
"error",
|
|
2157
|
+
file,
|
|
2158
|
+
"contracts.ui.parse"
|
|
2159
|
+
)
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2115
2162
|
const invalidIds = extractInvalidIds(text, [
|
|
2116
2163
|
"SPEC",
|
|
2117
2164
|
"BR",
|
|
@@ -2119,6 +2166,45 @@ async function validateUiContracts(uiRoot) {
|
|
|
2119
2166
|
"UI",
|
|
2120
2167
|
"API",
|
|
2121
2168
|
"DB",
|
|
2169
|
+
"THEMA",
|
|
2170
|
+
"ADR"
|
|
2171
|
+
]).filter((id) => !shouldIgnoreInvalidId(id, doc));
|
|
2172
|
+
if (invalidIds.length > 0) {
|
|
2173
|
+
issues.push(
|
|
2174
|
+
issue(
|
|
2175
|
+
"QFAI-ID-002",
|
|
2176
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
2177
|
+
"error",
|
|
2178
|
+
file,
|
|
2179
|
+
"id.format",
|
|
2180
|
+
invalidIds
|
|
2181
|
+
)
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
if (doc) {
|
|
2185
|
+
issues.push(
|
|
2186
|
+
...await validateUiContractDoc(doc, file, uiRoot, themaIds)
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return issues;
|
|
2191
|
+
}
|
|
2192
|
+
async function validateThemaContracts(uiRoot) {
|
|
2193
|
+
const files = await collectThemaContractFiles(uiRoot);
|
|
2194
|
+
if (files.length === 0) {
|
|
2195
|
+
return [];
|
|
2196
|
+
}
|
|
2197
|
+
const issues = [];
|
|
2198
|
+
for (const file of files) {
|
|
2199
|
+
const text = await readFile7(file, "utf-8");
|
|
2200
|
+
const invalidIds = extractInvalidIds(text, [
|
|
2201
|
+
"SPEC",
|
|
2202
|
+
"BR",
|
|
2203
|
+
"SC",
|
|
2204
|
+
"UI",
|
|
2205
|
+
"API",
|
|
2206
|
+
"DB",
|
|
2207
|
+
"THEMA",
|
|
2122
2208
|
"ADR"
|
|
2123
2209
|
]);
|
|
2124
2210
|
if (invalidIds.length > 0) {
|
|
@@ -2134,17 +2220,95 @@ async function validateUiContracts(uiRoot) {
|
|
|
2134
2220
|
);
|
|
2135
2221
|
}
|
|
2136
2222
|
const declaredIds = extractDeclaredContractIds(text);
|
|
2137
|
-
|
|
2223
|
+
if (declaredIds.length === 0) {
|
|
2224
|
+
issues.push(
|
|
2225
|
+
issue(
|
|
2226
|
+
"QFAI-THEMA-010",
|
|
2227
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
2228
|
+
"error",
|
|
2229
|
+
file,
|
|
2230
|
+
"contracts.thema.declaration"
|
|
2231
|
+
)
|
|
2232
|
+
);
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
if (declaredIds.length > 1) {
|
|
2236
|
+
issues.push(
|
|
2237
|
+
issue(
|
|
2238
|
+
"QFAI-THEMA-011",
|
|
2239
|
+
`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(
|
|
2240
|
+
", "
|
|
2241
|
+
)}`,
|
|
2242
|
+
"error",
|
|
2243
|
+
file,
|
|
2244
|
+
"contracts.thema.declaration",
|
|
2245
|
+
declaredIds
|
|
2246
|
+
)
|
|
2247
|
+
);
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
const declaredId = declaredIds[0] ?? "";
|
|
2251
|
+
if (!THEMA_ID_RE.test(declaredId)) {
|
|
2252
|
+
issues.push(
|
|
2253
|
+
issue(
|
|
2254
|
+
"QFAI-THEMA-012",
|
|
2255
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
|
|
2256
|
+
"error",
|
|
2257
|
+
file,
|
|
2258
|
+
"contracts.thema.idFormat",
|
|
2259
|
+
[declaredId]
|
|
2260
|
+
)
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
let doc;
|
|
2138
2264
|
try {
|
|
2139
|
-
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2265
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2140
2266
|
} catch (error2) {
|
|
2141
2267
|
issues.push(
|
|
2142
2268
|
issue(
|
|
2143
|
-
"QFAI-
|
|
2144
|
-
`
|
|
2269
|
+
"QFAI-THEMA-001",
|
|
2270
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2145
2271
|
"error",
|
|
2146
2272
|
file,
|
|
2147
|
-
"contracts.
|
|
2273
|
+
"contracts.thema.parse"
|
|
2274
|
+
)
|
|
2275
|
+
);
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
const docId = typeof doc.id === "string" ? doc.id : "";
|
|
2279
|
+
if (!THEMA_ID_RE.test(docId)) {
|
|
2280
|
+
issues.push(
|
|
2281
|
+
issue(
|
|
2282
|
+
"QFAI-THEMA-012",
|
|
2283
|
+
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",
|
|
2284
|
+
"error",
|
|
2285
|
+
file,
|
|
2286
|
+
"contracts.thema.idFormat",
|
|
2287
|
+
docId.length > 0 ? [docId] : void 0
|
|
2288
|
+
)
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
const name = typeof doc.name === "string" ? doc.name : "";
|
|
2292
|
+
if (!name) {
|
|
2293
|
+
issues.push(
|
|
2294
|
+
issue(
|
|
2295
|
+
"QFAI-THEMA-014",
|
|
2296
|
+
"thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2297
|
+
"error",
|
|
2298
|
+
file,
|
|
2299
|
+
"contracts.thema.name"
|
|
2300
|
+
)
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
if (declaredId && docId && declaredId !== docId) {
|
|
2304
|
+
issues.push(
|
|
2305
|
+
issue(
|
|
2306
|
+
"QFAI-THEMA-013",
|
|
2307
|
+
`thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
|
|
2308
|
+
"error",
|
|
2309
|
+
file,
|
|
2310
|
+
"contracts.thema.idMismatch",
|
|
2311
|
+
[declaredId, docId]
|
|
2148
2312
|
)
|
|
2149
2313
|
);
|
|
2150
2314
|
}
|
|
@@ -2174,6 +2338,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
2174
2338
|
"UI",
|
|
2175
2339
|
"API",
|
|
2176
2340
|
"DB",
|
|
2341
|
+
"THEMA",
|
|
2177
2342
|
"ADR"
|
|
2178
2343
|
]);
|
|
2179
2344
|
if (invalidIds.length > 0) {
|
|
@@ -2242,6 +2407,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
2242
2407
|
"UI",
|
|
2243
2408
|
"API",
|
|
2244
2409
|
"DB",
|
|
2410
|
+
"THEMA",
|
|
2245
2411
|
"ADR"
|
|
2246
2412
|
]);
|
|
2247
2413
|
if (invalidIds.length > 0) {
|
|
@@ -2348,6 +2514,278 @@ function validateDuplicateContractIds(contractIndex) {
|
|
|
2348
2514
|
function hasOpenApi(doc) {
|
|
2349
2515
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
2350
2516
|
}
|
|
2517
|
+
async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
|
|
2518
|
+
const issues = [];
|
|
2519
|
+
if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
|
|
2520
|
+
const themaRef = doc.themaRef;
|
|
2521
|
+
if (typeof themaRef !== "string" || themaRef.length === 0) {
|
|
2522
|
+
issues.push(
|
|
2523
|
+
issue(
|
|
2524
|
+
"QFAI-UI-020",
|
|
2525
|
+
"themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2526
|
+
"error",
|
|
2527
|
+
file,
|
|
2528
|
+
"contracts.ui.themaRef"
|
|
2529
|
+
)
|
|
2530
|
+
);
|
|
2531
|
+
} else if (!THEMA_ID_RE.test(themaRef)) {
|
|
2532
|
+
issues.push(
|
|
2533
|
+
issue(
|
|
2534
|
+
"QFAI-UI-020",
|
|
2535
|
+
`themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
|
|
2536
|
+
"error",
|
|
2537
|
+
file,
|
|
2538
|
+
"contracts.ui.themaRef",
|
|
2539
|
+
[themaRef]
|
|
2540
|
+
)
|
|
2541
|
+
);
|
|
2542
|
+
} else if (!themaIds.has(themaRef)) {
|
|
2543
|
+
issues.push(
|
|
2544
|
+
issue(
|
|
2545
|
+
"QFAI-UI-020",
|
|
2546
|
+
`themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
|
|
2547
|
+
"error",
|
|
2548
|
+
file,
|
|
2549
|
+
"contracts.ui.themaRef",
|
|
2550
|
+
[themaRef]
|
|
2551
|
+
)
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const assets = doc.assets;
|
|
2556
|
+
if (assets && typeof assets === "object") {
|
|
2557
|
+
issues.push(
|
|
2558
|
+
...await validateUiAssets(
|
|
2559
|
+
assets,
|
|
2560
|
+
file,
|
|
2561
|
+
uiRoot
|
|
2562
|
+
)
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
return issues;
|
|
2566
|
+
}
|
|
2567
|
+
async function validateUiAssets(assets, file, uiRoot) {
|
|
2568
|
+
const issues = [];
|
|
2569
|
+
const packValue = assets.pack;
|
|
2570
|
+
const useValue = assets.use;
|
|
2571
|
+
if (packValue === void 0 && useValue === void 0) {
|
|
2572
|
+
return issues;
|
|
2573
|
+
}
|
|
2574
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2575
|
+
issues.push(
|
|
2576
|
+
issue(
|
|
2577
|
+
"QFAI-ASSET-001",
|
|
2578
|
+
"assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2579
|
+
"error",
|
|
2580
|
+
file,
|
|
2581
|
+
"assets.pack"
|
|
2582
|
+
)
|
|
2583
|
+
);
|
|
2584
|
+
return issues;
|
|
2585
|
+
}
|
|
2586
|
+
if (!isSafeRelativePath(packValue)) {
|
|
2587
|
+
issues.push(
|
|
2588
|
+
issue(
|
|
2589
|
+
"QFAI-ASSET-001",
|
|
2590
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
|
|
2591
|
+
"error",
|
|
2592
|
+
file,
|
|
2593
|
+
"assets.pack",
|
|
2594
|
+
[packValue]
|
|
2595
|
+
)
|
|
2596
|
+
);
|
|
2597
|
+
return issues;
|
|
2598
|
+
}
|
|
2599
|
+
const packDir = path17.resolve(uiRoot, packValue);
|
|
2600
|
+
const packRelative = path17.relative(uiRoot, packDir);
|
|
2601
|
+
if (packRelative.startsWith("..") || path17.isAbsolute(packRelative)) {
|
|
2602
|
+
issues.push(
|
|
2603
|
+
issue(
|
|
2604
|
+
"QFAI-ASSET-001",
|
|
2605
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
|
|
2606
|
+
"error",
|
|
2607
|
+
file,
|
|
2608
|
+
"assets.pack",
|
|
2609
|
+
[packValue]
|
|
2610
|
+
)
|
|
2611
|
+
);
|
|
2612
|
+
return issues;
|
|
2613
|
+
}
|
|
2614
|
+
if (!await exists6(packDir)) {
|
|
2615
|
+
issues.push(
|
|
2616
|
+
issue(
|
|
2617
|
+
"QFAI-ASSET-001",
|
|
2618
|
+
`assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
|
|
2619
|
+
"error",
|
|
2620
|
+
file,
|
|
2621
|
+
"assets.pack",
|
|
2622
|
+
[packValue]
|
|
2623
|
+
)
|
|
2624
|
+
);
|
|
2625
|
+
return issues;
|
|
2626
|
+
}
|
|
2627
|
+
const assetsYamlPath = path17.join(packDir, "assets.yaml");
|
|
2628
|
+
if (!await exists6(assetsYamlPath)) {
|
|
2629
|
+
issues.push(
|
|
2630
|
+
issue(
|
|
2631
|
+
"QFAI-ASSET-002",
|
|
2632
|
+
`assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
|
|
2633
|
+
"error",
|
|
2634
|
+
assetsYamlPath,
|
|
2635
|
+
"assets.yaml"
|
|
2636
|
+
)
|
|
2637
|
+
);
|
|
2638
|
+
return issues;
|
|
2639
|
+
}
|
|
2640
|
+
let manifest;
|
|
2641
|
+
try {
|
|
2642
|
+
const manifestText = await readFile7(assetsYamlPath, "utf-8");
|
|
2643
|
+
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
2644
|
+
} catch (error2) {
|
|
2645
|
+
issues.push(
|
|
2646
|
+
issue(
|
|
2647
|
+
"QFAI-ASSET-002",
|
|
2648
|
+
`assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
|
|
2649
|
+
"error",
|
|
2650
|
+
assetsYamlPath,
|
|
2651
|
+
"assets.yaml"
|
|
2652
|
+
)
|
|
2653
|
+
);
|
|
2654
|
+
return issues;
|
|
2655
|
+
}
|
|
2656
|
+
const items = Array.isArray(manifest.items) ? manifest.items : [];
|
|
2657
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
2658
|
+
const itemPaths = [];
|
|
2659
|
+
for (const item of items) {
|
|
2660
|
+
if (!item || typeof item !== "object") {
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
const record2 = item;
|
|
2664
|
+
const id = typeof record2.id === "string" ? record2.id : void 0;
|
|
2665
|
+
const pathValue = typeof record2.path === "string" ? record2.path : void 0;
|
|
2666
|
+
if (id) {
|
|
2667
|
+
itemIds.add(id);
|
|
2668
|
+
}
|
|
2669
|
+
itemPaths.push({ id, path: pathValue });
|
|
2670
|
+
}
|
|
2671
|
+
if (useValue !== void 0) {
|
|
2672
|
+
if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
|
|
2673
|
+
issues.push(
|
|
2674
|
+
issue(
|
|
2675
|
+
"QFAI-ASSET-003",
|
|
2676
|
+
"assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2677
|
+
"error",
|
|
2678
|
+
file,
|
|
2679
|
+
"assets.use"
|
|
2680
|
+
)
|
|
2681
|
+
);
|
|
2682
|
+
} else {
|
|
2683
|
+
const missing = useValue.filter((entry) => !itemIds.has(entry));
|
|
2684
|
+
if (missing.length > 0) {
|
|
2685
|
+
issues.push(
|
|
2686
|
+
issue(
|
|
2687
|
+
"QFAI-ASSET-003",
|
|
2688
|
+
`assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
|
|
2689
|
+
"error",
|
|
2690
|
+
file,
|
|
2691
|
+
"assets.use",
|
|
2692
|
+
missing
|
|
2693
|
+
)
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
for (const entry of itemPaths) {
|
|
2699
|
+
if (!entry.path) {
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2702
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
2703
|
+
issues.push(
|
|
2704
|
+
issue(
|
|
2705
|
+
"QFAI-ASSET-004",
|
|
2706
|
+
`assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
|
|
2707
|
+
"error",
|
|
2708
|
+
assetsYamlPath,
|
|
2709
|
+
"assets.path",
|
|
2710
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2711
|
+
)
|
|
2712
|
+
);
|
|
2713
|
+
continue;
|
|
2714
|
+
}
|
|
2715
|
+
const assetPath = path17.resolve(packDir, entry.path);
|
|
2716
|
+
const assetRelative = path17.relative(packDir, assetPath);
|
|
2717
|
+
if (assetRelative.startsWith("..") || path17.isAbsolute(assetRelative)) {
|
|
2718
|
+
issues.push(
|
|
2719
|
+
issue(
|
|
2720
|
+
"QFAI-ASSET-004",
|
|
2721
|
+
`assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
|
|
2722
|
+
"error",
|
|
2723
|
+
assetsYamlPath,
|
|
2724
|
+
"assets.path",
|
|
2725
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2726
|
+
)
|
|
2727
|
+
);
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
if (!await exists6(assetPath)) {
|
|
2731
|
+
issues.push(
|
|
2732
|
+
issue(
|
|
2733
|
+
"QFAI-ASSET-004",
|
|
2734
|
+
`assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
|
|
2735
|
+
"error",
|
|
2736
|
+
assetsYamlPath,
|
|
2737
|
+
"assets.path",
|
|
2738
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2739
|
+
)
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
return issues;
|
|
2744
|
+
}
|
|
2745
|
+
function shouldIgnoreInvalidId(value, doc) {
|
|
2746
|
+
if (!doc) {
|
|
2747
|
+
return false;
|
|
2748
|
+
}
|
|
2749
|
+
const assets = doc.assets;
|
|
2750
|
+
if (!assets || typeof assets !== "object") {
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
const packValue = assets.pack;
|
|
2754
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2755
|
+
return false;
|
|
2756
|
+
}
|
|
2757
|
+
const normalized = packValue.replace(/\\/g, "/");
|
|
2758
|
+
const basename = path17.posix.basename(normalized);
|
|
2759
|
+
if (!basename) {
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
return value.toLowerCase() === basename.toLowerCase();
|
|
2763
|
+
}
|
|
2764
|
+
function isSafeRelativePath(value) {
|
|
2765
|
+
if (!value) {
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
if (path17.isAbsolute(value)) {
|
|
2769
|
+
return false;
|
|
2770
|
+
}
|
|
2771
|
+
const normalized = value.replace(/\\/g, "/");
|
|
2772
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
2773
|
+
return false;
|
|
2774
|
+
}
|
|
2775
|
+
const segments = normalized.split("/");
|
|
2776
|
+
if (segments.some((segment) => segment === "..")) {
|
|
2777
|
+
return false;
|
|
2778
|
+
}
|
|
2779
|
+
return true;
|
|
2780
|
+
}
|
|
2781
|
+
async function exists6(target) {
|
|
2782
|
+
try {
|
|
2783
|
+
await access6(target);
|
|
2784
|
+
return true;
|
|
2785
|
+
} catch {
|
|
2786
|
+
return false;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2351
2789
|
function formatError4(error2) {
|
|
2352
2790
|
if (error2 instanceof Error) {
|
|
2353
2791
|
return error2.message;
|
|
@@ -2378,7 +2816,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2378
2816
|
|
|
2379
2817
|
// src/core/validators/delta.ts
|
|
2380
2818
|
import { readFile as readFile8 } from "fs/promises";
|
|
2381
|
-
import
|
|
2819
|
+
import path18 from "path";
|
|
2382
2820
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
2383
2821
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2384
2822
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -2392,7 +2830,7 @@ async function validateDeltas(root, config) {
|
|
|
2392
2830
|
}
|
|
2393
2831
|
const issues = [];
|
|
2394
2832
|
for (const pack of packs) {
|
|
2395
|
-
const deltaPath =
|
|
2833
|
+
const deltaPath = path18.join(pack, "delta.md");
|
|
2396
2834
|
let text;
|
|
2397
2835
|
try {
|
|
2398
2836
|
text = await readFile8(deltaPath, "utf-8");
|
|
@@ -2472,7 +2910,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2472
2910
|
|
|
2473
2911
|
// src/core/validators/ids.ts
|
|
2474
2912
|
import { readFile as readFile9 } from "fs/promises";
|
|
2475
|
-
import
|
|
2913
|
+
import path19 from "path";
|
|
2476
2914
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2477
2915
|
async function validateDefinedIds(root, config) {
|
|
2478
2916
|
const issues = [];
|
|
@@ -2538,7 +2976,7 @@ function recordId(out, id, file) {
|
|
|
2538
2976
|
}
|
|
2539
2977
|
function formatFileList(files, root) {
|
|
2540
2978
|
return files.map((file) => {
|
|
2541
|
-
const relative =
|
|
2979
|
+
const relative = path19.relative(root, file);
|
|
2542
2980
|
return relative.length > 0 ? relative : file;
|
|
2543
2981
|
}).join(", ");
|
|
2544
2982
|
}
|
|
@@ -2596,7 +3034,8 @@ async function validatePromptsIntegrity(root) {
|
|
|
2596
3034
|
}
|
|
2597
3035
|
|
|
2598
3036
|
// src/core/validators/scenario.ts
|
|
2599
|
-
import { readFile as readFile10 } from "fs/promises";
|
|
3037
|
+
import { access as access7, readFile as readFile10 } from "fs/promises";
|
|
3038
|
+
import path20 from "path";
|
|
2600
3039
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2601
3040
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2602
3041
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2606,12 +3045,11 @@ async function validateScenarios(root, config) {
|
|
|
2606
3045
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2607
3046
|
const entries = await collectSpecEntries(specsRoot);
|
|
2608
3047
|
if (entries.length === 0) {
|
|
2609
|
-
const expected = "spec-0001/scenario.
|
|
2610
|
-
const legacy = "spec-001/scenario.md";
|
|
3048
|
+
const expected = "spec-0001/scenario.feature";
|
|
2611
3049
|
return [
|
|
2612
3050
|
issue4(
|
|
2613
3051
|
"QFAI-SC-000",
|
|
2614
|
-
`Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}
|
|
3052
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
|
|
2615
3053
|
"info",
|
|
2616
3054
|
specsRoot,
|
|
2617
3055
|
"scenario.files"
|
|
@@ -2620,6 +3058,18 @@ async function validateScenarios(root, config) {
|
|
|
2620
3058
|
}
|
|
2621
3059
|
const issues = [];
|
|
2622
3060
|
for (const entry of entries) {
|
|
3061
|
+
const legacyScenarioPath = path20.join(entry.dir, "scenario.md");
|
|
3062
|
+
if (await fileExists(legacyScenarioPath)) {
|
|
3063
|
+
issues.push(
|
|
3064
|
+
issue4(
|
|
3065
|
+
"QFAI-SC-004",
|
|
3066
|
+
"scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3067
|
+
"error",
|
|
3068
|
+
legacyScenarioPath,
|
|
3069
|
+
"scenario.legacy"
|
|
3070
|
+
)
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
2623
3073
|
let text;
|
|
2624
3074
|
try {
|
|
2625
3075
|
text = await readFile10(entry.scenarioPath, "utf-8");
|
|
@@ -2628,7 +3078,7 @@ async function validateScenarios(root, config) {
|
|
|
2628
3078
|
issues.push(
|
|
2629
3079
|
issue4(
|
|
2630
3080
|
"QFAI-SC-001",
|
|
2631
|
-
"scenario.
|
|
3081
|
+
"scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2632
3082
|
"error",
|
|
2633
3083
|
entry.scenarioPath,
|
|
2634
3084
|
"scenario.exists"
|
|
@@ -2651,6 +3101,7 @@ function validateScenarioContent(text, file) {
|
|
|
2651
3101
|
"UI",
|
|
2652
3102
|
"API",
|
|
2653
3103
|
"DB",
|
|
3104
|
+
"THEMA",
|
|
2654
3105
|
"ADR"
|
|
2655
3106
|
]);
|
|
2656
3107
|
if (invalidIds.length > 0) {
|
|
@@ -2794,6 +3245,14 @@ function isMissingFileError3(error2) {
|
|
|
2794
3245
|
}
|
|
2795
3246
|
return error2.code === "ENOENT";
|
|
2796
3247
|
}
|
|
3248
|
+
async function fileExists(target) {
|
|
3249
|
+
try {
|
|
3250
|
+
await access7(target);
|
|
3251
|
+
return true;
|
|
3252
|
+
} catch {
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
2797
3256
|
|
|
2798
3257
|
// src/core/validators/spec.ts
|
|
2799
3258
|
import { readFile as readFile11 } from "fs/promises";
|
|
@@ -2853,6 +3312,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2853
3312
|
"UI",
|
|
2854
3313
|
"API",
|
|
2855
3314
|
"DB",
|
|
3315
|
+
"THEMA",
|
|
2856
3316
|
"ADR"
|
|
2857
3317
|
]);
|
|
2858
3318
|
if (invalidIds.length > 0) {
|
|
@@ -3032,7 +3492,7 @@ async function validateTraceability(root, config) {
|
|
|
3032
3492
|
"QFAI-TRACE-021",
|
|
3033
3493
|
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
3034
3494
|
", "
|
|
3035
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3495
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3036
3496
|
"error",
|
|
3037
3497
|
file,
|
|
3038
3498
|
"traceability.specContractRefFormat",
|
|
@@ -3096,7 +3556,7 @@ async function validateTraceability(root, config) {
|
|
|
3096
3556
|
"QFAI-TRACE-032",
|
|
3097
3557
|
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
3098
3558
|
", "
|
|
3099
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3559
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3100
3560
|
"error",
|
|
3101
3561
|
file,
|
|
3102
3562
|
"traceability.scenarioContractRefFormat",
|
|
@@ -3475,17 +3935,25 @@ function countIssues(issues) {
|
|
|
3475
3935
|
}
|
|
3476
3936
|
|
|
3477
3937
|
// src/core/report.ts
|
|
3478
|
-
var ID_PREFIXES2 = [
|
|
3938
|
+
var ID_PREFIXES2 = [
|
|
3939
|
+
"SPEC",
|
|
3940
|
+
"BR",
|
|
3941
|
+
"SC",
|
|
3942
|
+
"UI",
|
|
3943
|
+
"API",
|
|
3944
|
+
"DB",
|
|
3945
|
+
"THEMA"
|
|
3946
|
+
];
|
|
3479
3947
|
async function createReportData(root, validation, configResult) {
|
|
3480
|
-
const resolvedRoot =
|
|
3948
|
+
const resolvedRoot = path21.resolve(root);
|
|
3481
3949
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3482
3950
|
const config = resolved.config;
|
|
3483
3951
|
const configPath = resolved.configPath;
|
|
3484
3952
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3485
3953
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3486
|
-
const apiRoot =
|
|
3487
|
-
const uiRoot =
|
|
3488
|
-
const dbRoot =
|
|
3954
|
+
const apiRoot = path21.join(contractsRoot, "api");
|
|
3955
|
+
const uiRoot = path21.join(contractsRoot, "ui");
|
|
3956
|
+
const dbRoot = path21.join(contractsRoot, "db");
|
|
3489
3957
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3490
3958
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3491
3959
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3493,7 +3961,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3493
3961
|
const {
|
|
3494
3962
|
api: apiFiles,
|
|
3495
3963
|
ui: uiFiles,
|
|
3496
|
-
db: dbFiles
|
|
3964
|
+
db: dbFiles,
|
|
3965
|
+
thema: themaFiles
|
|
3497
3966
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
3498
3967
|
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
3499
3968
|
const contractIdList = Array.from(contractIndex.ids);
|
|
@@ -3520,7 +3989,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3520
3989
|
...scenarioFiles,
|
|
3521
3990
|
...apiFiles,
|
|
3522
3991
|
...uiFiles,
|
|
3523
|
-
...dbFiles
|
|
3992
|
+
...dbFiles,
|
|
3993
|
+
...themaFiles
|
|
3524
3994
|
]);
|
|
3525
3995
|
const upstreamIds = await collectUpstreamIds([
|
|
3526
3996
|
...specFiles,
|
|
@@ -3557,7 +4027,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3557
4027
|
contracts: {
|
|
3558
4028
|
api: apiFiles.length,
|
|
3559
4029
|
ui: uiFiles.length,
|
|
3560
|
-
db: dbFiles.length
|
|
4030
|
+
db: dbFiles.length,
|
|
4031
|
+
thema: themaFiles.length
|
|
3561
4032
|
},
|
|
3562
4033
|
counts: normalizedValidation.counts
|
|
3563
4034
|
},
|
|
@@ -3567,7 +4038,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3567
4038
|
sc: idsByPrefix.SC,
|
|
3568
4039
|
ui: idsByPrefix.UI,
|
|
3569
4040
|
api: idsByPrefix.API,
|
|
3570
|
-
db: idsByPrefix.DB
|
|
4041
|
+
db: idsByPrefix.DB,
|
|
4042
|
+
thema: idsByPrefix.THEMA
|
|
3571
4043
|
},
|
|
3572
4044
|
traceability: {
|
|
3573
4045
|
upstreamIdsFound: upstreamIds.size,
|
|
@@ -3637,7 +4109,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3637
4109
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
3638
4110
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
3639
4111
|
lines.push(
|
|
3640
|
-
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
4112
|
+
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
|
|
3641
4113
|
);
|
|
3642
4114
|
lines.push(
|
|
3643
4115
|
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
@@ -3781,6 +4253,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3781
4253
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
3782
4254
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3783
4255
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
4256
|
+
lines.push(formatIdLine("THEMA", data.ids.thema));
|
|
3784
4257
|
lines.push("");
|
|
3785
4258
|
lines.push("## Traceability");
|
|
3786
4259
|
lines.push("");
|
|
@@ -4002,7 +4475,8 @@ async function collectIds(files) {
|
|
|
4002
4475
|
SC: /* @__PURE__ */ new Set(),
|
|
4003
4476
|
UI: /* @__PURE__ */ new Set(),
|
|
4004
4477
|
API: /* @__PURE__ */ new Set(),
|
|
4005
|
-
DB: /* @__PURE__ */ new Set()
|
|
4478
|
+
DB: /* @__PURE__ */ new Set(),
|
|
4479
|
+
THEMA: /* @__PURE__ */ new Set()
|
|
4006
4480
|
};
|
|
4007
4481
|
for (const file of files) {
|
|
4008
4482
|
const text = await readFile13(file, "utf-8");
|
|
@@ -4017,7 +4491,8 @@ async function collectIds(files) {
|
|
|
4017
4491
|
SC: toSortedArray2(result.SC),
|
|
4018
4492
|
UI: toSortedArray2(result.UI),
|
|
4019
4493
|
API: toSortedArray2(result.API),
|
|
4020
|
-
DB: toSortedArray2(result.DB)
|
|
4494
|
+
DB: toSortedArray2(result.DB),
|
|
4495
|
+
THEMA: toSortedArray2(result.THEMA)
|
|
4021
4496
|
};
|
|
4022
4497
|
}
|
|
4023
4498
|
async function collectUpstreamIds(files) {
|
|
@@ -4181,7 +4656,7 @@ function warnIfTruncated(scan, context) {
|
|
|
4181
4656
|
|
|
4182
4657
|
// src/cli/commands/report.ts
|
|
4183
4658
|
async function runReport(options) {
|
|
4184
|
-
const root =
|
|
4659
|
+
const root = path22.resolve(options.root);
|
|
4185
4660
|
const configResult = await loadConfig(root);
|
|
4186
4661
|
let validation;
|
|
4187
4662
|
if (options.runValidate) {
|
|
@@ -4198,7 +4673,7 @@ async function runReport(options) {
|
|
|
4198
4673
|
validation = normalized;
|
|
4199
4674
|
} else {
|
|
4200
4675
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
4201
|
-
const inputPath =
|
|
4676
|
+
const inputPath = path22.isAbsolute(input) ? input : path22.resolve(root, input);
|
|
4202
4677
|
try {
|
|
4203
4678
|
validation = await readValidationResult(inputPath);
|
|
4204
4679
|
} catch (err) {
|
|
@@ -4225,10 +4700,10 @@ async function runReport(options) {
|
|
|
4225
4700
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4226
4701
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4227
4702
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4228
|
-
const defaultOut = options.format === "json" ?
|
|
4703
|
+
const defaultOut = options.format === "json" ? path22.join(outRoot, "report.json") : path22.join(outRoot, "report.md");
|
|
4229
4704
|
const out = options.outPath ?? defaultOut;
|
|
4230
|
-
const outPath =
|
|
4231
|
-
await mkdir3(
|
|
4705
|
+
const outPath = path22.isAbsolute(out) ? out : path22.resolve(root, out);
|
|
4706
|
+
await mkdir3(path22.dirname(outPath), { recursive: true });
|
|
4232
4707
|
await writeFile2(outPath, `${output}
|
|
4233
4708
|
`, "utf-8");
|
|
4234
4709
|
info(
|
|
@@ -4293,15 +4768,15 @@ function isMissingFileError5(error2) {
|
|
|
4293
4768
|
return record2.code === "ENOENT";
|
|
4294
4769
|
}
|
|
4295
4770
|
async function writeValidationResult(root, outputPath, result) {
|
|
4296
|
-
const abs =
|
|
4297
|
-
await mkdir3(
|
|
4771
|
+
const abs = path22.isAbsolute(outputPath) ? outputPath : path22.resolve(root, outputPath);
|
|
4772
|
+
await mkdir3(path22.dirname(abs), { recursive: true });
|
|
4298
4773
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
4299
4774
|
`, "utf-8");
|
|
4300
4775
|
}
|
|
4301
4776
|
|
|
4302
4777
|
// src/cli/commands/validate.ts
|
|
4303
4778
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
4304
|
-
import
|
|
4779
|
+
import path23 from "path";
|
|
4305
4780
|
|
|
4306
4781
|
// src/cli/lib/failOn.ts
|
|
4307
4782
|
function shouldFail(result, failOn) {
|
|
@@ -4316,7 +4791,7 @@ function shouldFail(result, failOn) {
|
|
|
4316
4791
|
|
|
4317
4792
|
// src/cli/commands/validate.ts
|
|
4318
4793
|
async function runValidate(options) {
|
|
4319
|
-
const root =
|
|
4794
|
+
const root = path23.resolve(options.root);
|
|
4320
4795
|
const configResult = await loadConfig(root);
|
|
4321
4796
|
const result = await validateProject(root, configResult);
|
|
4322
4797
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -4441,12 +4916,12 @@ function issueKey(issue7) {
|
|
|
4441
4916
|
}
|
|
4442
4917
|
async function emitJson(result, root, jsonPath) {
|
|
4443
4918
|
const abs = resolveJsonPath(root, jsonPath);
|
|
4444
|
-
await mkdir4(
|
|
4919
|
+
await mkdir4(path23.dirname(abs), { recursive: true });
|
|
4445
4920
|
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
4446
4921
|
`, "utf-8");
|
|
4447
4922
|
}
|
|
4448
4923
|
function resolveJsonPath(root, jsonPath) {
|
|
4449
|
-
return
|
|
4924
|
+
return path23.isAbsolute(jsonPath) ? jsonPath : path23.resolve(root, jsonPath);
|
|
4450
4925
|
}
|
|
4451
4926
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
4452
4927
|
|