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.cjs
CHANGED
|
@@ -231,11 +231,11 @@ function emitPromptNotFound(promptName, candidates) {
|
|
|
231
231
|
|
|
232
232
|
// src/cli/commands/doctor.ts
|
|
233
233
|
var import_promises10 = require("fs/promises");
|
|
234
|
-
var
|
|
234
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
235
235
|
|
|
236
236
|
// src/core/doctor.ts
|
|
237
237
|
var import_promises9 = require("fs/promises");
|
|
238
|
-
var
|
|
238
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
239
239
|
|
|
240
240
|
// src/core/config.ts
|
|
241
241
|
var import_promises3 = require("fs/promises");
|
|
@@ -639,6 +639,7 @@ function isRecord(value) {
|
|
|
639
639
|
|
|
640
640
|
// src/core/discovery.ts
|
|
641
641
|
var import_promises5 = require("fs/promises");
|
|
642
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
642
643
|
|
|
643
644
|
// src/core/specLayout.ts
|
|
644
645
|
var import_promises4 = require("fs/promises");
|
|
@@ -650,7 +651,7 @@ async function collectSpecEntries(specsRoot) {
|
|
|
650
651
|
dir,
|
|
651
652
|
specPath: import_node_path4.default.join(dir, "spec.md"),
|
|
652
653
|
deltaPath: import_node_path4.default.join(dir, "delta.md"),
|
|
653
|
-
scenarioPath: import_node_path4.default.join(dir, "scenario.
|
|
654
|
+
scenarioPath: import_node_path4.default.join(dir, "scenario.feature")
|
|
654
655
|
}));
|
|
655
656
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
656
657
|
}
|
|
@@ -686,7 +687,12 @@ async function collectScenarioFiles(specsRoot) {
|
|
|
686
687
|
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
687
688
|
}
|
|
688
689
|
async function collectUiContractFiles(uiRoot) {
|
|
689
|
-
|
|
690
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
691
|
+
return filterByBasenamePrefix(files, "ui-");
|
|
692
|
+
}
|
|
693
|
+
async function collectThemaContractFiles(uiRoot) {
|
|
694
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
695
|
+
return filterByBasenamePrefix(files, "thema-");
|
|
690
696
|
}
|
|
691
697
|
async function collectApiContractFiles(apiRoot) {
|
|
692
698
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
@@ -695,12 +701,13 @@ async function collectDbContractFiles(dbRoot) {
|
|
|
695
701
|
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
696
702
|
}
|
|
697
703
|
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
698
|
-
const [ui, api, db] = await Promise.all([
|
|
704
|
+
const [ui, thema, api, db] = await Promise.all([
|
|
699
705
|
collectUiContractFiles(uiRoot),
|
|
706
|
+
collectThemaContractFiles(uiRoot),
|
|
700
707
|
collectApiContractFiles(apiRoot),
|
|
701
708
|
collectDbContractFiles(dbRoot)
|
|
702
709
|
]);
|
|
703
|
-
return { ui, api, db };
|
|
710
|
+
return { ui, thema, api, db };
|
|
704
711
|
}
|
|
705
712
|
async function filterExisting(files) {
|
|
706
713
|
const existing = [];
|
|
@@ -719,17 +726,23 @@ async function exists3(target) {
|
|
|
719
726
|
return false;
|
|
720
727
|
}
|
|
721
728
|
}
|
|
729
|
+
function filterByBasenamePrefix(files, prefix) {
|
|
730
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
731
|
+
return files.filter(
|
|
732
|
+
(file) => import_node_path5.default.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
733
|
+
);
|
|
734
|
+
}
|
|
722
735
|
|
|
723
736
|
// src/core/paths.ts
|
|
724
|
-
var
|
|
737
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
725
738
|
function toRelativePath(root, target) {
|
|
726
739
|
if (!target) {
|
|
727
740
|
return target;
|
|
728
741
|
}
|
|
729
|
-
if (!
|
|
742
|
+
if (!import_node_path6.default.isAbsolute(target)) {
|
|
730
743
|
return toPosixPath(target);
|
|
731
744
|
}
|
|
732
|
-
const relative =
|
|
745
|
+
const relative = import_node_path6.default.relative(root, target);
|
|
733
746
|
if (!relative) {
|
|
734
747
|
return ".";
|
|
735
748
|
}
|
|
@@ -741,7 +754,7 @@ function toPosixPath(value) {
|
|
|
741
754
|
|
|
742
755
|
// src/core/traceability.ts
|
|
743
756
|
var import_promises6 = require("fs/promises");
|
|
744
|
-
var
|
|
757
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
745
758
|
|
|
746
759
|
// src/core/gherkin/parse.ts
|
|
747
760
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -969,7 +982,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
969
982
|
};
|
|
970
983
|
}
|
|
971
984
|
const normalizedFiles = Array.from(
|
|
972
|
-
new Set(scanResult.files.map((file) =>
|
|
985
|
+
new Set(scanResult.files.map((file) => import_node_path7.default.normalize(file)))
|
|
973
986
|
);
|
|
974
987
|
for (const file of normalizedFiles) {
|
|
975
988
|
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
@@ -1032,19 +1045,19 @@ function formatError3(error2) {
|
|
|
1032
1045
|
|
|
1033
1046
|
// src/core/promptsIntegrity.ts
|
|
1034
1047
|
var import_promises7 = require("fs/promises");
|
|
1035
|
-
var
|
|
1048
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1036
1049
|
|
|
1037
1050
|
// src/shared/assets.ts
|
|
1038
1051
|
var import_node_fs = require("fs");
|
|
1039
|
-
var
|
|
1052
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1040
1053
|
var import_node_url = require("url");
|
|
1041
1054
|
function getInitAssetsDir() {
|
|
1042
1055
|
const base = __filename;
|
|
1043
1056
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
1044
|
-
const baseDir =
|
|
1057
|
+
const baseDir = import_node_path8.default.dirname(basePath);
|
|
1045
1058
|
const candidates = [
|
|
1046
|
-
|
|
1047
|
-
|
|
1059
|
+
import_node_path8.default.resolve(baseDir, "../../../assets/init"),
|
|
1060
|
+
import_node_path8.default.resolve(baseDir, "../../assets/init")
|
|
1048
1061
|
];
|
|
1049
1062
|
for (const candidate of candidates) {
|
|
1050
1063
|
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
@@ -1062,10 +1075,10 @@ function getInitAssetsDir() {
|
|
|
1062
1075
|
|
|
1063
1076
|
// src/core/promptsIntegrity.ts
|
|
1064
1077
|
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1065
|
-
const promptsDir =
|
|
1078
|
+
const promptsDir = import_node_path9.default.resolve(root, ".qfai", "prompts");
|
|
1066
1079
|
let templateDir;
|
|
1067
1080
|
try {
|
|
1068
|
-
templateDir =
|
|
1081
|
+
templateDir = import_node_path9.default.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1069
1082
|
} catch {
|
|
1070
1083
|
return {
|
|
1071
1084
|
status: "skipped_missing_assets",
|
|
@@ -1142,7 +1155,7 @@ function normalizeNewlines(text) {
|
|
|
1142
1155
|
return text.replace(/\r\n/g, "\n");
|
|
1143
1156
|
}
|
|
1144
1157
|
function toRel(base, abs) {
|
|
1145
|
-
const rel =
|
|
1158
|
+
const rel = import_node_path9.default.relative(base, abs);
|
|
1146
1159
|
return rel.replace(/[\\/]+/g, "/");
|
|
1147
1160
|
}
|
|
1148
1161
|
function intersectKeys(a, b) {
|
|
@@ -1157,11 +1170,11 @@ function intersectKeys(a, b) {
|
|
|
1157
1170
|
|
|
1158
1171
|
// src/core/version.ts
|
|
1159
1172
|
var import_promises8 = require("fs/promises");
|
|
1160
|
-
var
|
|
1173
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1161
1174
|
var import_node_url2 = require("url");
|
|
1162
1175
|
async function resolveToolVersion() {
|
|
1163
|
-
if ("1.0.
|
|
1164
|
-
return "1.0.
|
|
1176
|
+
if ("1.0.3".length > 0) {
|
|
1177
|
+
return "1.0.3";
|
|
1165
1178
|
}
|
|
1166
1179
|
try {
|
|
1167
1180
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1176,7 +1189,7 @@ async function resolveToolVersion() {
|
|
|
1176
1189
|
function resolvePackageJsonPath() {
|
|
1177
1190
|
const base = __filename;
|
|
1178
1191
|
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1179
|
-
return
|
|
1192
|
+
return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
|
|
1180
1193
|
}
|
|
1181
1194
|
|
|
1182
1195
|
// src/core/doctor.ts
|
|
@@ -1202,7 +1215,7 @@ function normalizeGlobs2(values) {
|
|
|
1202
1215
|
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1203
1216
|
}
|
|
1204
1217
|
async function createDoctorData(options) {
|
|
1205
|
-
const startDir =
|
|
1218
|
+
const startDir = import_node_path11.default.resolve(options.startDir);
|
|
1206
1219
|
const checks = [];
|
|
1207
1220
|
const configPath = getConfigPath(startDir);
|
|
1208
1221
|
const search = options.rootExplicit ? {
|
|
@@ -1265,9 +1278,9 @@ async function createDoctorData(options) {
|
|
|
1265
1278
|
details: { path: toRelativePath(root, resolved) }
|
|
1266
1279
|
});
|
|
1267
1280
|
if (key === "promptsDir") {
|
|
1268
|
-
const promptsLocalDir =
|
|
1269
|
-
|
|
1270
|
-
`${
|
|
1281
|
+
const promptsLocalDir = import_node_path11.default.join(
|
|
1282
|
+
import_node_path11.default.dirname(resolved),
|
|
1283
|
+
`${import_node_path11.default.basename(resolved)}.local`
|
|
1271
1284
|
);
|
|
1272
1285
|
const found = await exists4(promptsLocalDir);
|
|
1273
1286
|
addCheck(checks, {
|
|
@@ -1340,7 +1353,7 @@ async function createDoctorData(options) {
|
|
|
1340
1353
|
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1341
1354
|
details: { specPacks: entries.length, missingFiles }
|
|
1342
1355
|
});
|
|
1343
|
-
const validateJsonAbs =
|
|
1356
|
+
const validateJsonAbs = import_node_path11.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path11.default.resolve(root, config.output.validateJsonPath);
|
|
1344
1357
|
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1345
1358
|
addCheck(checks, {
|
|
1346
1359
|
id: "output.validateJson",
|
|
@@ -1350,8 +1363,8 @@ async function createDoctorData(options) {
|
|
|
1350
1363
|
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1351
1364
|
});
|
|
1352
1365
|
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1353
|
-
const rel =
|
|
1354
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
1366
|
+
const rel = import_node_path11.default.relative(outDirAbs, validateJsonAbs);
|
|
1367
|
+
const inside = rel !== "" && !rel.startsWith("..") && !import_node_path11.default.isAbsolute(rel);
|
|
1355
1368
|
addCheck(checks, {
|
|
1356
1369
|
id: "output.pathAlignment",
|
|
1357
1370
|
severity: inside ? "ok" : "warning",
|
|
@@ -1474,12 +1487,12 @@ async function detectOutDirCollisions(root) {
|
|
|
1474
1487
|
});
|
|
1475
1488
|
const configPaths = configScan.files;
|
|
1476
1489
|
const configRoots = Array.from(
|
|
1477
|
-
new Set(configPaths.map((configPath) =>
|
|
1490
|
+
new Set(configPaths.map((configPath) => import_node_path11.default.dirname(configPath)))
|
|
1478
1491
|
).sort((a, b) => a.localeCompare(b));
|
|
1479
1492
|
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1480
1493
|
for (const configRoot of configRoots) {
|
|
1481
1494
|
const { config } = await loadConfig(configRoot);
|
|
1482
|
-
const outDir =
|
|
1495
|
+
const outDir = import_node_path11.default.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1483
1496
|
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1484
1497
|
roots.add(configRoot);
|
|
1485
1498
|
outDirToRoots.set(outDir, roots);
|
|
@@ -1505,20 +1518,20 @@ async function detectOutDirCollisions(root) {
|
|
|
1505
1518
|
};
|
|
1506
1519
|
}
|
|
1507
1520
|
async function findMonorepoRoot(startDir) {
|
|
1508
|
-
let current =
|
|
1521
|
+
let current = import_node_path11.default.resolve(startDir);
|
|
1509
1522
|
while (true) {
|
|
1510
|
-
const gitPath =
|
|
1511
|
-
const workspacePath =
|
|
1523
|
+
const gitPath = import_node_path11.default.join(current, ".git");
|
|
1524
|
+
const workspacePath = import_node_path11.default.join(current, "pnpm-workspace.yaml");
|
|
1512
1525
|
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1513
1526
|
return current;
|
|
1514
1527
|
}
|
|
1515
|
-
const parent =
|
|
1528
|
+
const parent = import_node_path11.default.dirname(current);
|
|
1516
1529
|
if (parent === current) {
|
|
1517
1530
|
break;
|
|
1518
1531
|
}
|
|
1519
1532
|
current = parent;
|
|
1520
1533
|
}
|
|
1521
|
-
return
|
|
1534
|
+
return import_node_path11.default.resolve(startDir);
|
|
1522
1535
|
}
|
|
1523
1536
|
|
|
1524
1537
|
// src/cli/lib/logger.ts
|
|
@@ -1560,8 +1573,8 @@ async function runDoctor(options) {
|
|
|
1560
1573
|
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1561
1574
|
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1562
1575
|
if (options.outPath) {
|
|
1563
|
-
const outAbs =
|
|
1564
|
-
await (0, import_promises10.mkdir)(
|
|
1576
|
+
const outAbs = import_node_path12.default.isAbsolute(options.outPath) ? options.outPath : import_node_path12.default.resolve(process.cwd(), options.outPath);
|
|
1577
|
+
await (0, import_promises10.mkdir)(import_node_path12.default.dirname(outAbs), { recursive: true });
|
|
1565
1578
|
await (0, import_promises10.writeFile)(outAbs, `${output}
|
|
1566
1579
|
`, "utf-8");
|
|
1567
1580
|
info(`doctor: wrote ${outAbs}`);
|
|
@@ -1581,11 +1594,11 @@ function shouldFailDoctor(summary, failOn) {
|
|
|
1581
1594
|
}
|
|
1582
1595
|
|
|
1583
1596
|
// src/cli/commands/init.ts
|
|
1584
|
-
var
|
|
1597
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
1585
1598
|
|
|
1586
1599
|
// src/cli/lib/fs.ts
|
|
1587
1600
|
var import_promises11 = require("fs/promises");
|
|
1588
|
-
var
|
|
1601
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1589
1602
|
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1590
1603
|
const files = await collectTemplateFiles(sourceRoot);
|
|
1591
1604
|
return copyFiles(files, sourceRoot, destRoot, options);
|
|
@@ -1593,7 +1606,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
|
1593
1606
|
async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
|
|
1594
1607
|
const allFiles = [];
|
|
1595
1608
|
for (const relPath of relativePaths) {
|
|
1596
|
-
const fullPath =
|
|
1609
|
+
const fullPath = import_node_path13.default.join(sourceRoot, relPath);
|
|
1597
1610
|
const files = await collectTemplateFiles(fullPath);
|
|
1598
1611
|
allFiles.push(...files);
|
|
1599
1612
|
}
|
|
@@ -1603,13 +1616,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1603
1616
|
const copied = [];
|
|
1604
1617
|
const skipped = [];
|
|
1605
1618
|
const conflicts = [];
|
|
1606
|
-
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
1607
|
-
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
1619
|
+
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
|
|
1620
|
+
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
|
|
1608
1621
|
const isProtectedRelative = (relative) => {
|
|
1609
1622
|
if (protectPrefixes.length === 0) {
|
|
1610
1623
|
return false;
|
|
1611
1624
|
}
|
|
1612
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1625
|
+
const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
|
|
1613
1626
|
return protectPrefixes.some(
|
|
1614
1627
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1615
1628
|
);
|
|
@@ -1618,7 +1631,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1618
1631
|
if (excludePrefixes.length === 0) {
|
|
1619
1632
|
return false;
|
|
1620
1633
|
}
|
|
1621
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1634
|
+
const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
|
|
1622
1635
|
return excludePrefixes.some(
|
|
1623
1636
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1624
1637
|
);
|
|
@@ -1626,14 +1639,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1626
1639
|
const conflictPolicy = options.conflictPolicy ?? "error";
|
|
1627
1640
|
if (!options.force && conflictPolicy === "error") {
|
|
1628
1641
|
for (const file of files) {
|
|
1629
|
-
const relative =
|
|
1642
|
+
const relative = import_node_path13.default.relative(sourceRoot, file);
|
|
1630
1643
|
if (isExcludedRelative(relative)) {
|
|
1631
1644
|
continue;
|
|
1632
1645
|
}
|
|
1633
1646
|
if (isProtectedRelative(relative)) {
|
|
1634
1647
|
continue;
|
|
1635
1648
|
}
|
|
1636
|
-
const dest =
|
|
1649
|
+
const dest = import_node_path13.default.join(destRoot, relative);
|
|
1637
1650
|
if (!await shouldWrite(dest, options.force)) {
|
|
1638
1651
|
conflicts.push(dest);
|
|
1639
1652
|
}
|
|
@@ -1643,18 +1656,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1643
1656
|
}
|
|
1644
1657
|
}
|
|
1645
1658
|
for (const file of files) {
|
|
1646
|
-
const relative =
|
|
1659
|
+
const relative = import_node_path13.default.relative(sourceRoot, file);
|
|
1647
1660
|
if (isExcludedRelative(relative)) {
|
|
1648
1661
|
continue;
|
|
1649
1662
|
}
|
|
1650
|
-
const dest =
|
|
1663
|
+
const dest = import_node_path13.default.join(destRoot, relative);
|
|
1651
1664
|
const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
|
|
1652
1665
|
if (!await shouldWrite(dest, forceForThisFile)) {
|
|
1653
1666
|
skipped.push(dest);
|
|
1654
1667
|
continue;
|
|
1655
1668
|
}
|
|
1656
1669
|
if (!options.dryRun) {
|
|
1657
|
-
await (0, import_promises11.mkdir)(
|
|
1670
|
+
await (0, import_promises11.mkdir)(import_node_path13.default.dirname(dest), { recursive: true });
|
|
1658
1671
|
await (0, import_promises11.copyFile)(file, dest);
|
|
1659
1672
|
}
|
|
1660
1673
|
copied.push(dest);
|
|
@@ -1678,7 +1691,7 @@ async function collectTemplateFiles(root) {
|
|
|
1678
1691
|
}
|
|
1679
1692
|
const items = await (0, import_promises11.readdir)(root, { withFileTypes: true });
|
|
1680
1693
|
for (const item of items) {
|
|
1681
|
-
const fullPath =
|
|
1694
|
+
const fullPath = import_node_path13.default.join(root, item.name);
|
|
1682
1695
|
if (item.isDirectory()) {
|
|
1683
1696
|
const nested = await collectTemplateFiles(fullPath);
|
|
1684
1697
|
entries.push(...nested);
|
|
@@ -1708,10 +1721,10 @@ async function exists5(target) {
|
|
|
1708
1721
|
// src/cli/commands/init.ts
|
|
1709
1722
|
async function runInit(options) {
|
|
1710
1723
|
const assetsRoot = getInitAssetsDir();
|
|
1711
|
-
const rootAssets =
|
|
1712
|
-
const qfaiAssets =
|
|
1713
|
-
const destRoot =
|
|
1714
|
-
const destQfai =
|
|
1724
|
+
const rootAssets = import_node_path14.default.join(assetsRoot, "root");
|
|
1725
|
+
const qfaiAssets = import_node_path14.default.join(assetsRoot, ".qfai");
|
|
1726
|
+
const destRoot = import_node_path14.default.resolve(options.dir);
|
|
1727
|
+
const destQfai = import_node_path14.default.join(destRoot, ".qfai");
|
|
1715
1728
|
if (options.force) {
|
|
1716
1729
|
info(
|
|
1717
1730
|
"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"
|
|
@@ -1759,7 +1772,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1759
1772
|
|
|
1760
1773
|
// src/cli/commands/report.ts
|
|
1761
1774
|
var import_promises20 = require("fs/promises");
|
|
1762
|
-
var
|
|
1775
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
1763
1776
|
|
|
1764
1777
|
// src/core/normalize.ts
|
|
1765
1778
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1800,15 +1813,15 @@ function normalizeValidationResult(root, result) {
|
|
|
1800
1813
|
|
|
1801
1814
|
// src/core/report.ts
|
|
1802
1815
|
var import_promises19 = require("fs/promises");
|
|
1803
|
-
var
|
|
1816
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
1804
1817
|
|
|
1805
1818
|
// src/core/contractIndex.ts
|
|
1806
1819
|
var import_promises12 = require("fs/promises");
|
|
1807
|
-
var
|
|
1820
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
1808
1821
|
|
|
1809
1822
|
// src/core/contractsDecl.ts
|
|
1810
|
-
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
1811
|
-
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
1823
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
1824
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
|
|
1812
1825
|
function extractDeclaredContractIds(text) {
|
|
1813
1826
|
const ids = [];
|
|
1814
1827
|
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
@@ -1826,20 +1839,22 @@ function stripContractDeclarationLines(text) {
|
|
|
1826
1839
|
// src/core/contractIndex.ts
|
|
1827
1840
|
async function buildContractIndex(root, config) {
|
|
1828
1841
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1829
|
-
const uiRoot =
|
|
1830
|
-
const apiRoot =
|
|
1831
|
-
const dbRoot =
|
|
1832
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1842
|
+
const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
|
|
1843
|
+
const apiRoot = import_node_path15.default.join(contractsRoot, "api");
|
|
1844
|
+
const dbRoot = import_node_path15.default.join(contractsRoot, "db");
|
|
1845
|
+
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1833
1846
|
collectUiContractFiles(uiRoot),
|
|
1847
|
+
collectThemaContractFiles(uiRoot),
|
|
1834
1848
|
collectApiContractFiles(apiRoot),
|
|
1835
1849
|
collectDbContractFiles(dbRoot)
|
|
1836
1850
|
]);
|
|
1837
1851
|
const index = {
|
|
1838
1852
|
ids: /* @__PURE__ */ new Set(),
|
|
1839
1853
|
idToFiles: /* @__PURE__ */ new Map(),
|
|
1840
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1854
|
+
files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
|
|
1841
1855
|
};
|
|
1842
1856
|
await indexContractFiles(uiFiles, index);
|
|
1857
|
+
await indexContractFiles(themaFiles, index);
|
|
1843
1858
|
await indexContractFiles(apiFiles, index);
|
|
1844
1859
|
await indexContractFiles(dbFiles, index);
|
|
1845
1860
|
return index;
|
|
@@ -1858,7 +1873,15 @@ function record(index, id, file) {
|
|
|
1858
1873
|
}
|
|
1859
1874
|
|
|
1860
1875
|
// src/core/ids.ts
|
|
1861
|
-
var ID_PREFIXES = [
|
|
1876
|
+
var ID_PREFIXES = [
|
|
1877
|
+
"SPEC",
|
|
1878
|
+
"BR",
|
|
1879
|
+
"SC",
|
|
1880
|
+
"UI",
|
|
1881
|
+
"API",
|
|
1882
|
+
"DB",
|
|
1883
|
+
"THEMA"
|
|
1884
|
+
];
|
|
1862
1885
|
var STRICT_ID_PATTERNS = {
|
|
1863
1886
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1864
1887
|
BR: /\bBR-\d{4}\b/g,
|
|
@@ -1866,6 +1889,7 @@ var STRICT_ID_PATTERNS = {
|
|
|
1866
1889
|
UI: /\bUI-\d{4}\b/g,
|
|
1867
1890
|
API: /\bAPI-\d{4}\b/g,
|
|
1868
1891
|
DB: /\bDB-\d{4}\b/g,
|
|
1892
|
+
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
1869
1893
|
ADR: /\bADR-\d{4}\b/g
|
|
1870
1894
|
};
|
|
1871
1895
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -1875,6 +1899,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
1875
1899
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1876
1900
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1877
1901
|
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1902
|
+
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
1878
1903
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1879
1904
|
};
|
|
1880
1905
|
function extractIds(text, prefix) {
|
|
@@ -1911,7 +1936,7 @@ function isValidId(value, prefix) {
|
|
|
1911
1936
|
}
|
|
1912
1937
|
|
|
1913
1938
|
// src/core/parse/contractRefs.ts
|
|
1914
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1939
|
+
var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
|
|
1915
1940
|
function parseContractRefs(text, options = {}) {
|
|
1916
1941
|
const linePattern = buildLinePattern(options);
|
|
1917
1942
|
const lines = [];
|
|
@@ -2082,13 +2107,13 @@ function parseSpec(md, file) {
|
|
|
2082
2107
|
|
|
2083
2108
|
// src/core/validators/contracts.ts
|
|
2084
2109
|
var import_promises13 = require("fs/promises");
|
|
2085
|
-
var
|
|
2110
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2086
2111
|
|
|
2087
2112
|
// src/core/contracts.ts
|
|
2088
|
-
var
|
|
2113
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2089
2114
|
var import_yaml2 = require("yaml");
|
|
2090
2115
|
function parseStructuredContract(file, text) {
|
|
2091
|
-
const ext =
|
|
2116
|
+
const ext = import_node_path16.default.extname(file).toLowerCase();
|
|
2092
2117
|
if (ext === ".json") {
|
|
2093
2118
|
return JSON.parse(text);
|
|
2094
2119
|
}
|
|
@@ -2105,17 +2130,23 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
2105
2130
|
label: "ALTER TABLE ... DROP"
|
|
2106
2131
|
}
|
|
2107
2132
|
];
|
|
2133
|
+
var THEMA_ID_RE = /^THEMA-\d{3}$/;
|
|
2108
2134
|
async function validateContracts(root, config) {
|
|
2109
2135
|
const issues = [];
|
|
2110
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2111
|
-
issues.push(...await validateUiContracts(import_node_path16.default.join(contractsRoot, "ui")));
|
|
2112
|
-
issues.push(...await validateApiContracts(import_node_path16.default.join(contractsRoot, "api")));
|
|
2113
|
-
issues.push(...await validateDbContracts(import_node_path16.default.join(contractsRoot, "db")));
|
|
2114
2136
|
const contractIndex = await buildContractIndex(root, config);
|
|
2137
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2138
|
+
const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
|
|
2139
|
+
const themaIds = new Set(
|
|
2140
|
+
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
2141
|
+
);
|
|
2142
|
+
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
2143
|
+
issues.push(...await validateThemaContracts(uiRoot));
|
|
2144
|
+
issues.push(...await validateApiContracts(import_node_path17.default.join(contractsRoot, "api")));
|
|
2145
|
+
issues.push(...await validateDbContracts(import_node_path17.default.join(contractsRoot, "db")));
|
|
2115
2146
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
2116
2147
|
return issues;
|
|
2117
2148
|
}
|
|
2118
|
-
async function validateUiContracts(uiRoot) {
|
|
2149
|
+
async function validateUiContracts(uiRoot, themaIds) {
|
|
2119
2150
|
const files = await collectUiContractFiles(uiRoot);
|
|
2120
2151
|
if (files.length === 0) {
|
|
2121
2152
|
return [
|
|
@@ -2131,6 +2162,22 @@ async function validateUiContracts(uiRoot) {
|
|
|
2131
2162
|
const issues = [];
|
|
2132
2163
|
for (const file of files) {
|
|
2133
2164
|
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2165
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
2166
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
2167
|
+
let doc = null;
|
|
2168
|
+
try {
|
|
2169
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2170
|
+
} catch (error2) {
|
|
2171
|
+
issues.push(
|
|
2172
|
+
issue(
|
|
2173
|
+
"QFAI-CONTRACT-001",
|
|
2174
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2175
|
+
"error",
|
|
2176
|
+
file,
|
|
2177
|
+
"contracts.ui.parse"
|
|
2178
|
+
)
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2134
2181
|
const invalidIds = extractInvalidIds(text, [
|
|
2135
2182
|
"SPEC",
|
|
2136
2183
|
"BR",
|
|
@@ -2138,6 +2185,45 @@ async function validateUiContracts(uiRoot) {
|
|
|
2138
2185
|
"UI",
|
|
2139
2186
|
"API",
|
|
2140
2187
|
"DB",
|
|
2188
|
+
"THEMA",
|
|
2189
|
+
"ADR"
|
|
2190
|
+
]).filter((id) => !shouldIgnoreInvalidId(id, doc));
|
|
2191
|
+
if (invalidIds.length > 0) {
|
|
2192
|
+
issues.push(
|
|
2193
|
+
issue(
|
|
2194
|
+
"QFAI-ID-002",
|
|
2195
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
2196
|
+
"error",
|
|
2197
|
+
file,
|
|
2198
|
+
"id.format",
|
|
2199
|
+
invalidIds
|
|
2200
|
+
)
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
if (doc) {
|
|
2204
|
+
issues.push(
|
|
2205
|
+
...await validateUiContractDoc(doc, file, uiRoot, themaIds)
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return issues;
|
|
2210
|
+
}
|
|
2211
|
+
async function validateThemaContracts(uiRoot) {
|
|
2212
|
+
const files = await collectThemaContractFiles(uiRoot);
|
|
2213
|
+
if (files.length === 0) {
|
|
2214
|
+
return [];
|
|
2215
|
+
}
|
|
2216
|
+
const issues = [];
|
|
2217
|
+
for (const file of files) {
|
|
2218
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2219
|
+
const invalidIds = extractInvalidIds(text, [
|
|
2220
|
+
"SPEC",
|
|
2221
|
+
"BR",
|
|
2222
|
+
"SC",
|
|
2223
|
+
"UI",
|
|
2224
|
+
"API",
|
|
2225
|
+
"DB",
|
|
2226
|
+
"THEMA",
|
|
2141
2227
|
"ADR"
|
|
2142
2228
|
]);
|
|
2143
2229
|
if (invalidIds.length > 0) {
|
|
@@ -2153,17 +2239,95 @@ async function validateUiContracts(uiRoot) {
|
|
|
2153
2239
|
);
|
|
2154
2240
|
}
|
|
2155
2241
|
const declaredIds = extractDeclaredContractIds(text);
|
|
2156
|
-
|
|
2242
|
+
if (declaredIds.length === 0) {
|
|
2243
|
+
issues.push(
|
|
2244
|
+
issue(
|
|
2245
|
+
"QFAI-THEMA-010",
|
|
2246
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
2247
|
+
"error",
|
|
2248
|
+
file,
|
|
2249
|
+
"contracts.thema.declaration"
|
|
2250
|
+
)
|
|
2251
|
+
);
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
if (declaredIds.length > 1) {
|
|
2255
|
+
issues.push(
|
|
2256
|
+
issue(
|
|
2257
|
+
"QFAI-THEMA-011",
|
|
2258
|
+
`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(
|
|
2259
|
+
", "
|
|
2260
|
+
)}`,
|
|
2261
|
+
"error",
|
|
2262
|
+
file,
|
|
2263
|
+
"contracts.thema.declaration",
|
|
2264
|
+
declaredIds
|
|
2265
|
+
)
|
|
2266
|
+
);
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
const declaredId = declaredIds[0] ?? "";
|
|
2270
|
+
if (!THEMA_ID_RE.test(declaredId)) {
|
|
2271
|
+
issues.push(
|
|
2272
|
+
issue(
|
|
2273
|
+
"QFAI-THEMA-012",
|
|
2274
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
|
|
2275
|
+
"error",
|
|
2276
|
+
file,
|
|
2277
|
+
"contracts.thema.idFormat",
|
|
2278
|
+
[declaredId]
|
|
2279
|
+
)
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
let doc;
|
|
2157
2283
|
try {
|
|
2158
|
-
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2284
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2159
2285
|
} catch (error2) {
|
|
2160
2286
|
issues.push(
|
|
2161
2287
|
issue(
|
|
2162
|
-
"QFAI-
|
|
2163
|
-
`
|
|
2288
|
+
"QFAI-THEMA-001",
|
|
2289
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2164
2290
|
"error",
|
|
2165
2291
|
file,
|
|
2166
|
-
"contracts.
|
|
2292
|
+
"contracts.thema.parse"
|
|
2293
|
+
)
|
|
2294
|
+
);
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
const docId = typeof doc.id === "string" ? doc.id : "";
|
|
2298
|
+
if (!THEMA_ID_RE.test(docId)) {
|
|
2299
|
+
issues.push(
|
|
2300
|
+
issue(
|
|
2301
|
+
"QFAI-THEMA-012",
|
|
2302
|
+
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",
|
|
2303
|
+
"error",
|
|
2304
|
+
file,
|
|
2305
|
+
"contracts.thema.idFormat",
|
|
2306
|
+
docId.length > 0 ? [docId] : void 0
|
|
2307
|
+
)
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const name = typeof doc.name === "string" ? doc.name : "";
|
|
2311
|
+
if (!name) {
|
|
2312
|
+
issues.push(
|
|
2313
|
+
issue(
|
|
2314
|
+
"QFAI-THEMA-014",
|
|
2315
|
+
"thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2316
|
+
"error",
|
|
2317
|
+
file,
|
|
2318
|
+
"contracts.thema.name"
|
|
2319
|
+
)
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
if (declaredId && docId && declaredId !== docId) {
|
|
2323
|
+
issues.push(
|
|
2324
|
+
issue(
|
|
2325
|
+
"QFAI-THEMA-013",
|
|
2326
|
+
`thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
|
|
2327
|
+
"error",
|
|
2328
|
+
file,
|
|
2329
|
+
"contracts.thema.idMismatch",
|
|
2330
|
+
[declaredId, docId]
|
|
2167
2331
|
)
|
|
2168
2332
|
);
|
|
2169
2333
|
}
|
|
@@ -2193,6 +2357,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
2193
2357
|
"UI",
|
|
2194
2358
|
"API",
|
|
2195
2359
|
"DB",
|
|
2360
|
+
"THEMA",
|
|
2196
2361
|
"ADR"
|
|
2197
2362
|
]);
|
|
2198
2363
|
if (invalidIds.length > 0) {
|
|
@@ -2261,6 +2426,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
2261
2426
|
"UI",
|
|
2262
2427
|
"API",
|
|
2263
2428
|
"DB",
|
|
2429
|
+
"THEMA",
|
|
2264
2430
|
"ADR"
|
|
2265
2431
|
]);
|
|
2266
2432
|
if (invalidIds.length > 0) {
|
|
@@ -2367,6 +2533,278 @@ function validateDuplicateContractIds(contractIndex) {
|
|
|
2367
2533
|
function hasOpenApi(doc) {
|
|
2368
2534
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
2369
2535
|
}
|
|
2536
|
+
async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
|
|
2537
|
+
const issues = [];
|
|
2538
|
+
if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
|
|
2539
|
+
const themaRef = doc.themaRef;
|
|
2540
|
+
if (typeof themaRef !== "string" || themaRef.length === 0) {
|
|
2541
|
+
issues.push(
|
|
2542
|
+
issue(
|
|
2543
|
+
"QFAI-UI-020",
|
|
2544
|
+
"themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2545
|
+
"error",
|
|
2546
|
+
file,
|
|
2547
|
+
"contracts.ui.themaRef"
|
|
2548
|
+
)
|
|
2549
|
+
);
|
|
2550
|
+
} else if (!THEMA_ID_RE.test(themaRef)) {
|
|
2551
|
+
issues.push(
|
|
2552
|
+
issue(
|
|
2553
|
+
"QFAI-UI-020",
|
|
2554
|
+
`themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
|
|
2555
|
+
"error",
|
|
2556
|
+
file,
|
|
2557
|
+
"contracts.ui.themaRef",
|
|
2558
|
+
[themaRef]
|
|
2559
|
+
)
|
|
2560
|
+
);
|
|
2561
|
+
} else if (!themaIds.has(themaRef)) {
|
|
2562
|
+
issues.push(
|
|
2563
|
+
issue(
|
|
2564
|
+
"QFAI-UI-020",
|
|
2565
|
+
`themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
|
|
2566
|
+
"error",
|
|
2567
|
+
file,
|
|
2568
|
+
"contracts.ui.themaRef",
|
|
2569
|
+
[themaRef]
|
|
2570
|
+
)
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
const assets = doc.assets;
|
|
2575
|
+
if (assets && typeof assets === "object") {
|
|
2576
|
+
issues.push(
|
|
2577
|
+
...await validateUiAssets(
|
|
2578
|
+
assets,
|
|
2579
|
+
file,
|
|
2580
|
+
uiRoot
|
|
2581
|
+
)
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
return issues;
|
|
2585
|
+
}
|
|
2586
|
+
async function validateUiAssets(assets, file, uiRoot) {
|
|
2587
|
+
const issues = [];
|
|
2588
|
+
const packValue = assets.pack;
|
|
2589
|
+
const useValue = assets.use;
|
|
2590
|
+
if (packValue === void 0 && useValue === void 0) {
|
|
2591
|
+
return issues;
|
|
2592
|
+
}
|
|
2593
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2594
|
+
issues.push(
|
|
2595
|
+
issue(
|
|
2596
|
+
"QFAI-ASSET-001",
|
|
2597
|
+
"assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2598
|
+
"error",
|
|
2599
|
+
file,
|
|
2600
|
+
"assets.pack"
|
|
2601
|
+
)
|
|
2602
|
+
);
|
|
2603
|
+
return issues;
|
|
2604
|
+
}
|
|
2605
|
+
if (!isSafeRelativePath(packValue)) {
|
|
2606
|
+
issues.push(
|
|
2607
|
+
issue(
|
|
2608
|
+
"QFAI-ASSET-001",
|
|
2609
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
|
|
2610
|
+
"error",
|
|
2611
|
+
file,
|
|
2612
|
+
"assets.pack",
|
|
2613
|
+
[packValue]
|
|
2614
|
+
)
|
|
2615
|
+
);
|
|
2616
|
+
return issues;
|
|
2617
|
+
}
|
|
2618
|
+
const packDir = import_node_path17.default.resolve(uiRoot, packValue);
|
|
2619
|
+
const packRelative = import_node_path17.default.relative(uiRoot, packDir);
|
|
2620
|
+
if (packRelative.startsWith("..") || import_node_path17.default.isAbsolute(packRelative)) {
|
|
2621
|
+
issues.push(
|
|
2622
|
+
issue(
|
|
2623
|
+
"QFAI-ASSET-001",
|
|
2624
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
|
|
2625
|
+
"error",
|
|
2626
|
+
file,
|
|
2627
|
+
"assets.pack",
|
|
2628
|
+
[packValue]
|
|
2629
|
+
)
|
|
2630
|
+
);
|
|
2631
|
+
return issues;
|
|
2632
|
+
}
|
|
2633
|
+
if (!await exists6(packDir)) {
|
|
2634
|
+
issues.push(
|
|
2635
|
+
issue(
|
|
2636
|
+
"QFAI-ASSET-001",
|
|
2637
|
+
`assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
|
|
2638
|
+
"error",
|
|
2639
|
+
file,
|
|
2640
|
+
"assets.pack",
|
|
2641
|
+
[packValue]
|
|
2642
|
+
)
|
|
2643
|
+
);
|
|
2644
|
+
return issues;
|
|
2645
|
+
}
|
|
2646
|
+
const assetsYamlPath = import_node_path17.default.join(packDir, "assets.yaml");
|
|
2647
|
+
if (!await exists6(assetsYamlPath)) {
|
|
2648
|
+
issues.push(
|
|
2649
|
+
issue(
|
|
2650
|
+
"QFAI-ASSET-002",
|
|
2651
|
+
`assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
|
|
2652
|
+
"error",
|
|
2653
|
+
assetsYamlPath,
|
|
2654
|
+
"assets.yaml"
|
|
2655
|
+
)
|
|
2656
|
+
);
|
|
2657
|
+
return issues;
|
|
2658
|
+
}
|
|
2659
|
+
let manifest;
|
|
2660
|
+
try {
|
|
2661
|
+
const manifestText = await (0, import_promises13.readFile)(assetsYamlPath, "utf-8");
|
|
2662
|
+
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
2663
|
+
} catch (error2) {
|
|
2664
|
+
issues.push(
|
|
2665
|
+
issue(
|
|
2666
|
+
"QFAI-ASSET-002",
|
|
2667
|
+
`assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
|
|
2668
|
+
"error",
|
|
2669
|
+
assetsYamlPath,
|
|
2670
|
+
"assets.yaml"
|
|
2671
|
+
)
|
|
2672
|
+
);
|
|
2673
|
+
return issues;
|
|
2674
|
+
}
|
|
2675
|
+
const items = Array.isArray(manifest.items) ? manifest.items : [];
|
|
2676
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
2677
|
+
const itemPaths = [];
|
|
2678
|
+
for (const item of items) {
|
|
2679
|
+
if (!item || typeof item !== "object") {
|
|
2680
|
+
continue;
|
|
2681
|
+
}
|
|
2682
|
+
const record2 = item;
|
|
2683
|
+
const id = typeof record2.id === "string" ? record2.id : void 0;
|
|
2684
|
+
const pathValue = typeof record2.path === "string" ? record2.path : void 0;
|
|
2685
|
+
if (id) {
|
|
2686
|
+
itemIds.add(id);
|
|
2687
|
+
}
|
|
2688
|
+
itemPaths.push({ id, path: pathValue });
|
|
2689
|
+
}
|
|
2690
|
+
if (useValue !== void 0) {
|
|
2691
|
+
if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
|
|
2692
|
+
issues.push(
|
|
2693
|
+
issue(
|
|
2694
|
+
"QFAI-ASSET-003",
|
|
2695
|
+
"assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2696
|
+
"error",
|
|
2697
|
+
file,
|
|
2698
|
+
"assets.use"
|
|
2699
|
+
)
|
|
2700
|
+
);
|
|
2701
|
+
} else {
|
|
2702
|
+
const missing = useValue.filter((entry) => !itemIds.has(entry));
|
|
2703
|
+
if (missing.length > 0) {
|
|
2704
|
+
issues.push(
|
|
2705
|
+
issue(
|
|
2706
|
+
"QFAI-ASSET-003",
|
|
2707
|
+
`assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
|
|
2708
|
+
"error",
|
|
2709
|
+
file,
|
|
2710
|
+
"assets.use",
|
|
2711
|
+
missing
|
|
2712
|
+
)
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
for (const entry of itemPaths) {
|
|
2718
|
+
if (!entry.path) {
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
2722
|
+
issues.push(
|
|
2723
|
+
issue(
|
|
2724
|
+
"QFAI-ASSET-004",
|
|
2725
|
+
`assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
|
|
2726
|
+
"error",
|
|
2727
|
+
assetsYamlPath,
|
|
2728
|
+
"assets.path",
|
|
2729
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2730
|
+
)
|
|
2731
|
+
);
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
const assetPath = import_node_path17.default.resolve(packDir, entry.path);
|
|
2735
|
+
const assetRelative = import_node_path17.default.relative(packDir, assetPath);
|
|
2736
|
+
if (assetRelative.startsWith("..") || import_node_path17.default.isAbsolute(assetRelative)) {
|
|
2737
|
+
issues.push(
|
|
2738
|
+
issue(
|
|
2739
|
+
"QFAI-ASSET-004",
|
|
2740
|
+
`assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
|
|
2741
|
+
"error",
|
|
2742
|
+
assetsYamlPath,
|
|
2743
|
+
"assets.path",
|
|
2744
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2745
|
+
)
|
|
2746
|
+
);
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
if (!await exists6(assetPath)) {
|
|
2750
|
+
issues.push(
|
|
2751
|
+
issue(
|
|
2752
|
+
"QFAI-ASSET-004",
|
|
2753
|
+
`assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
|
|
2754
|
+
"error",
|
|
2755
|
+
assetsYamlPath,
|
|
2756
|
+
"assets.path",
|
|
2757
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2758
|
+
)
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
return issues;
|
|
2763
|
+
}
|
|
2764
|
+
function shouldIgnoreInvalidId(value, doc) {
|
|
2765
|
+
if (!doc) {
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
const assets = doc.assets;
|
|
2769
|
+
if (!assets || typeof assets !== "object") {
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
const packValue = assets.pack;
|
|
2773
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2774
|
+
return false;
|
|
2775
|
+
}
|
|
2776
|
+
const normalized = packValue.replace(/\\/g, "/");
|
|
2777
|
+
const basename = import_node_path17.default.posix.basename(normalized);
|
|
2778
|
+
if (!basename) {
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
return value.toLowerCase() === basename.toLowerCase();
|
|
2782
|
+
}
|
|
2783
|
+
function isSafeRelativePath(value) {
|
|
2784
|
+
if (!value) {
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
if (import_node_path17.default.isAbsolute(value)) {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
const normalized = value.replace(/\\/g, "/");
|
|
2791
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
2792
|
+
return false;
|
|
2793
|
+
}
|
|
2794
|
+
const segments = normalized.split("/");
|
|
2795
|
+
if (segments.some((segment) => segment === "..")) {
|
|
2796
|
+
return false;
|
|
2797
|
+
}
|
|
2798
|
+
return true;
|
|
2799
|
+
}
|
|
2800
|
+
async function exists6(target) {
|
|
2801
|
+
try {
|
|
2802
|
+
await (0, import_promises13.access)(target);
|
|
2803
|
+
return true;
|
|
2804
|
+
} catch {
|
|
2805
|
+
return false;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2370
2808
|
function formatError4(error2) {
|
|
2371
2809
|
if (error2 instanceof Error) {
|
|
2372
2810
|
return error2.message;
|
|
@@ -2397,7 +2835,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2397
2835
|
|
|
2398
2836
|
// src/core/validators/delta.ts
|
|
2399
2837
|
var import_promises14 = require("fs/promises");
|
|
2400
|
-
var
|
|
2838
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
2401
2839
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
2402
2840
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2403
2841
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -2411,7 +2849,7 @@ async function validateDeltas(root, config) {
|
|
|
2411
2849
|
}
|
|
2412
2850
|
const issues = [];
|
|
2413
2851
|
for (const pack of packs) {
|
|
2414
|
-
const deltaPath =
|
|
2852
|
+
const deltaPath = import_node_path18.default.join(pack, "delta.md");
|
|
2415
2853
|
let text;
|
|
2416
2854
|
try {
|
|
2417
2855
|
text = await (0, import_promises14.readFile)(deltaPath, "utf-8");
|
|
@@ -2491,7 +2929,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2491
2929
|
|
|
2492
2930
|
// src/core/validators/ids.ts
|
|
2493
2931
|
var import_promises15 = require("fs/promises");
|
|
2494
|
-
var
|
|
2932
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
2495
2933
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2496
2934
|
async function validateDefinedIds(root, config) {
|
|
2497
2935
|
const issues = [];
|
|
@@ -2557,7 +2995,7 @@ function recordId(out, id, file) {
|
|
|
2557
2995
|
}
|
|
2558
2996
|
function formatFileList(files, root) {
|
|
2559
2997
|
return files.map((file) => {
|
|
2560
|
-
const relative =
|
|
2998
|
+
const relative = import_node_path19.default.relative(root, file);
|
|
2561
2999
|
return relative.length > 0 ? relative : file;
|
|
2562
3000
|
}).join(", ");
|
|
2563
3001
|
}
|
|
@@ -2616,6 +3054,7 @@ async function validatePromptsIntegrity(root) {
|
|
|
2616
3054
|
|
|
2617
3055
|
// src/core/validators/scenario.ts
|
|
2618
3056
|
var import_promises16 = require("fs/promises");
|
|
3057
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
2619
3058
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2620
3059
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2621
3060
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2625,12 +3064,11 @@ async function validateScenarios(root, config) {
|
|
|
2625
3064
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2626
3065
|
const entries = await collectSpecEntries(specsRoot);
|
|
2627
3066
|
if (entries.length === 0) {
|
|
2628
|
-
const expected = "spec-0001/scenario.
|
|
2629
|
-
const legacy = "spec-001/scenario.md";
|
|
3067
|
+
const expected = "spec-0001/scenario.feature";
|
|
2630
3068
|
return [
|
|
2631
3069
|
issue4(
|
|
2632
3070
|
"QFAI-SC-000",
|
|
2633
|
-
`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}
|
|
3071
|
+
`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}`,
|
|
2634
3072
|
"info",
|
|
2635
3073
|
specsRoot,
|
|
2636
3074
|
"scenario.files"
|
|
@@ -2639,6 +3077,18 @@ async function validateScenarios(root, config) {
|
|
|
2639
3077
|
}
|
|
2640
3078
|
const issues = [];
|
|
2641
3079
|
for (const entry of entries) {
|
|
3080
|
+
const legacyScenarioPath = import_node_path20.default.join(entry.dir, "scenario.md");
|
|
3081
|
+
if (await fileExists(legacyScenarioPath)) {
|
|
3082
|
+
issues.push(
|
|
3083
|
+
issue4(
|
|
3084
|
+
"QFAI-SC-004",
|
|
3085
|
+
"scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3086
|
+
"error",
|
|
3087
|
+
legacyScenarioPath,
|
|
3088
|
+
"scenario.legacy"
|
|
3089
|
+
)
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
2642
3092
|
let text;
|
|
2643
3093
|
try {
|
|
2644
3094
|
text = await (0, import_promises16.readFile)(entry.scenarioPath, "utf-8");
|
|
@@ -2647,7 +3097,7 @@ async function validateScenarios(root, config) {
|
|
|
2647
3097
|
issues.push(
|
|
2648
3098
|
issue4(
|
|
2649
3099
|
"QFAI-SC-001",
|
|
2650
|
-
"scenario.
|
|
3100
|
+
"scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2651
3101
|
"error",
|
|
2652
3102
|
entry.scenarioPath,
|
|
2653
3103
|
"scenario.exists"
|
|
@@ -2670,6 +3120,7 @@ function validateScenarioContent(text, file) {
|
|
|
2670
3120
|
"UI",
|
|
2671
3121
|
"API",
|
|
2672
3122
|
"DB",
|
|
3123
|
+
"THEMA",
|
|
2673
3124
|
"ADR"
|
|
2674
3125
|
]);
|
|
2675
3126
|
if (invalidIds.length > 0) {
|
|
@@ -2813,6 +3264,14 @@ function isMissingFileError3(error2) {
|
|
|
2813
3264
|
}
|
|
2814
3265
|
return error2.code === "ENOENT";
|
|
2815
3266
|
}
|
|
3267
|
+
async function fileExists(target) {
|
|
3268
|
+
try {
|
|
3269
|
+
await (0, import_promises16.access)(target);
|
|
3270
|
+
return true;
|
|
3271
|
+
} catch {
|
|
3272
|
+
return false;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
2816
3275
|
|
|
2817
3276
|
// src/core/validators/spec.ts
|
|
2818
3277
|
var import_promises17 = require("fs/promises");
|
|
@@ -2872,6 +3331,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2872
3331
|
"UI",
|
|
2873
3332
|
"API",
|
|
2874
3333
|
"DB",
|
|
3334
|
+
"THEMA",
|
|
2875
3335
|
"ADR"
|
|
2876
3336
|
]);
|
|
2877
3337
|
if (invalidIds.length > 0) {
|
|
@@ -3051,7 +3511,7 @@ async function validateTraceability(root, config) {
|
|
|
3051
3511
|
"QFAI-TRACE-021",
|
|
3052
3512
|
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
3053
3513
|
", "
|
|
3054
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3514
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3055
3515
|
"error",
|
|
3056
3516
|
file,
|
|
3057
3517
|
"traceability.specContractRefFormat",
|
|
@@ -3115,7 +3575,7 @@ async function validateTraceability(root, config) {
|
|
|
3115
3575
|
"QFAI-TRACE-032",
|
|
3116
3576
|
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
3117
3577
|
", "
|
|
3118
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3578
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3119
3579
|
"error",
|
|
3120
3580
|
file,
|
|
3121
3581
|
"traceability.scenarioContractRefFormat",
|
|
@@ -3494,17 +3954,25 @@ function countIssues(issues) {
|
|
|
3494
3954
|
}
|
|
3495
3955
|
|
|
3496
3956
|
// src/core/report.ts
|
|
3497
|
-
var ID_PREFIXES2 = [
|
|
3957
|
+
var ID_PREFIXES2 = [
|
|
3958
|
+
"SPEC",
|
|
3959
|
+
"BR",
|
|
3960
|
+
"SC",
|
|
3961
|
+
"UI",
|
|
3962
|
+
"API",
|
|
3963
|
+
"DB",
|
|
3964
|
+
"THEMA"
|
|
3965
|
+
];
|
|
3498
3966
|
async function createReportData(root, validation, configResult) {
|
|
3499
|
-
const resolvedRoot =
|
|
3967
|
+
const resolvedRoot = import_node_path21.default.resolve(root);
|
|
3500
3968
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3501
3969
|
const config = resolved.config;
|
|
3502
3970
|
const configPath = resolved.configPath;
|
|
3503
3971
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3504
3972
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3505
|
-
const apiRoot =
|
|
3506
|
-
const uiRoot =
|
|
3507
|
-
const dbRoot =
|
|
3973
|
+
const apiRoot = import_node_path21.default.join(contractsRoot, "api");
|
|
3974
|
+
const uiRoot = import_node_path21.default.join(contractsRoot, "ui");
|
|
3975
|
+
const dbRoot = import_node_path21.default.join(contractsRoot, "db");
|
|
3508
3976
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3509
3977
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3510
3978
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3512,7 +3980,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3512
3980
|
const {
|
|
3513
3981
|
api: apiFiles,
|
|
3514
3982
|
ui: uiFiles,
|
|
3515
|
-
db: dbFiles
|
|
3983
|
+
db: dbFiles,
|
|
3984
|
+
thema: themaFiles
|
|
3516
3985
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
3517
3986
|
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
3518
3987
|
const contractIdList = Array.from(contractIndex.ids);
|
|
@@ -3539,7 +4008,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3539
4008
|
...scenarioFiles,
|
|
3540
4009
|
...apiFiles,
|
|
3541
4010
|
...uiFiles,
|
|
3542
|
-
...dbFiles
|
|
4011
|
+
...dbFiles,
|
|
4012
|
+
...themaFiles
|
|
3543
4013
|
]);
|
|
3544
4014
|
const upstreamIds = await collectUpstreamIds([
|
|
3545
4015
|
...specFiles,
|
|
@@ -3576,7 +4046,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3576
4046
|
contracts: {
|
|
3577
4047
|
api: apiFiles.length,
|
|
3578
4048
|
ui: uiFiles.length,
|
|
3579
|
-
db: dbFiles.length
|
|
4049
|
+
db: dbFiles.length,
|
|
4050
|
+
thema: themaFiles.length
|
|
3580
4051
|
},
|
|
3581
4052
|
counts: normalizedValidation.counts
|
|
3582
4053
|
},
|
|
@@ -3586,7 +4057,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3586
4057
|
sc: idsByPrefix.SC,
|
|
3587
4058
|
ui: idsByPrefix.UI,
|
|
3588
4059
|
api: idsByPrefix.API,
|
|
3589
|
-
db: idsByPrefix.DB
|
|
4060
|
+
db: idsByPrefix.DB,
|
|
4061
|
+
thema: idsByPrefix.THEMA
|
|
3590
4062
|
},
|
|
3591
4063
|
traceability: {
|
|
3592
4064
|
upstreamIdsFound: upstreamIds.size,
|
|
@@ -3656,7 +4128,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3656
4128
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
3657
4129
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
3658
4130
|
lines.push(
|
|
3659
|
-
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
4131
|
+
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
|
|
3660
4132
|
);
|
|
3661
4133
|
lines.push(
|
|
3662
4134
|
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
@@ -3800,6 +4272,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3800
4272
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
3801
4273
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3802
4274
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
4275
|
+
lines.push(formatIdLine("THEMA", data.ids.thema));
|
|
3803
4276
|
lines.push("");
|
|
3804
4277
|
lines.push("## Traceability");
|
|
3805
4278
|
lines.push("");
|
|
@@ -4021,7 +4494,8 @@ async function collectIds(files) {
|
|
|
4021
4494
|
SC: /* @__PURE__ */ new Set(),
|
|
4022
4495
|
UI: /* @__PURE__ */ new Set(),
|
|
4023
4496
|
API: /* @__PURE__ */ new Set(),
|
|
4024
|
-
DB: /* @__PURE__ */ new Set()
|
|
4497
|
+
DB: /* @__PURE__ */ new Set(),
|
|
4498
|
+
THEMA: /* @__PURE__ */ new Set()
|
|
4025
4499
|
};
|
|
4026
4500
|
for (const file of files) {
|
|
4027
4501
|
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
@@ -4036,7 +4510,8 @@ async function collectIds(files) {
|
|
|
4036
4510
|
SC: toSortedArray2(result.SC),
|
|
4037
4511
|
UI: toSortedArray2(result.UI),
|
|
4038
4512
|
API: toSortedArray2(result.API),
|
|
4039
|
-
DB: toSortedArray2(result.DB)
|
|
4513
|
+
DB: toSortedArray2(result.DB),
|
|
4514
|
+
THEMA: toSortedArray2(result.THEMA)
|
|
4040
4515
|
};
|
|
4041
4516
|
}
|
|
4042
4517
|
async function collectUpstreamIds(files) {
|
|
@@ -4200,7 +4675,7 @@ function warnIfTruncated(scan, context) {
|
|
|
4200
4675
|
|
|
4201
4676
|
// src/cli/commands/report.ts
|
|
4202
4677
|
async function runReport(options) {
|
|
4203
|
-
const root =
|
|
4678
|
+
const root = import_node_path22.default.resolve(options.root);
|
|
4204
4679
|
const configResult = await loadConfig(root);
|
|
4205
4680
|
let validation;
|
|
4206
4681
|
if (options.runValidate) {
|
|
@@ -4217,7 +4692,7 @@ async function runReport(options) {
|
|
|
4217
4692
|
validation = normalized;
|
|
4218
4693
|
} else {
|
|
4219
4694
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
4220
|
-
const inputPath =
|
|
4695
|
+
const inputPath = import_node_path22.default.isAbsolute(input) ? input : import_node_path22.default.resolve(root, input);
|
|
4221
4696
|
try {
|
|
4222
4697
|
validation = await readValidationResult(inputPath);
|
|
4223
4698
|
} catch (err) {
|
|
@@ -4244,10 +4719,10 @@ async function runReport(options) {
|
|
|
4244
4719
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4245
4720
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4246
4721
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4247
|
-
const defaultOut = options.format === "json" ?
|
|
4722
|
+
const defaultOut = options.format === "json" ? import_node_path22.default.join(outRoot, "report.json") : import_node_path22.default.join(outRoot, "report.md");
|
|
4248
4723
|
const out = options.outPath ?? defaultOut;
|
|
4249
|
-
const outPath =
|
|
4250
|
-
await (0, import_promises20.mkdir)(
|
|
4724
|
+
const outPath = import_node_path22.default.isAbsolute(out) ? out : import_node_path22.default.resolve(root, out);
|
|
4725
|
+
await (0, import_promises20.mkdir)(import_node_path22.default.dirname(outPath), { recursive: true });
|
|
4251
4726
|
await (0, import_promises20.writeFile)(outPath, `${output}
|
|
4252
4727
|
`, "utf-8");
|
|
4253
4728
|
info(
|
|
@@ -4312,15 +4787,15 @@ function isMissingFileError5(error2) {
|
|
|
4312
4787
|
return record2.code === "ENOENT";
|
|
4313
4788
|
}
|
|
4314
4789
|
async function writeValidationResult(root, outputPath, result) {
|
|
4315
|
-
const abs =
|
|
4316
|
-
await (0, import_promises20.mkdir)(
|
|
4790
|
+
const abs = import_node_path22.default.isAbsolute(outputPath) ? outputPath : import_node_path22.default.resolve(root, outputPath);
|
|
4791
|
+
await (0, import_promises20.mkdir)(import_node_path22.default.dirname(abs), { recursive: true });
|
|
4317
4792
|
await (0, import_promises20.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
4318
4793
|
`, "utf-8");
|
|
4319
4794
|
}
|
|
4320
4795
|
|
|
4321
4796
|
// src/cli/commands/validate.ts
|
|
4322
4797
|
var import_promises21 = require("fs/promises");
|
|
4323
|
-
var
|
|
4798
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
4324
4799
|
|
|
4325
4800
|
// src/cli/lib/failOn.ts
|
|
4326
4801
|
function shouldFail(result, failOn) {
|
|
@@ -4335,7 +4810,7 @@ function shouldFail(result, failOn) {
|
|
|
4335
4810
|
|
|
4336
4811
|
// src/cli/commands/validate.ts
|
|
4337
4812
|
async function runValidate(options) {
|
|
4338
|
-
const root =
|
|
4813
|
+
const root = import_node_path23.default.resolve(options.root);
|
|
4339
4814
|
const configResult = await loadConfig(root);
|
|
4340
4815
|
const result = await validateProject(root, configResult);
|
|
4341
4816
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -4460,12 +4935,12 @@ function issueKey(issue7) {
|
|
|
4460
4935
|
}
|
|
4461
4936
|
async function emitJson(result, root, jsonPath) {
|
|
4462
4937
|
const abs = resolveJsonPath(root, jsonPath);
|
|
4463
|
-
await (0, import_promises21.mkdir)(
|
|
4938
|
+
await (0, import_promises21.mkdir)(import_node_path23.default.dirname(abs), { recursive: true });
|
|
4464
4939
|
await (0, import_promises21.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
4465
4940
|
`, "utf-8");
|
|
4466
4941
|
}
|
|
4467
4942
|
function resolveJsonPath(root, jsonPath) {
|
|
4468
|
-
return
|
|
4943
|
+
return import_node_path23.default.isAbsolute(jsonPath) ? jsonPath : import_node_path23.default.resolve(root, jsonPath);
|
|
4469
4944
|
}
|
|
4470
4945
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
4471
4946
|
|