qfai 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -12
- package/assets/init/.qfai/README.md +2 -7
- package/assets/init/.qfai/contracts/README.md +20 -0
- package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/assets.yaml +6 -0
- package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/palette.png +0 -0
- package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/assets.yaml +6 -0
- package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/snapshots/login__desktop__light__default.png +0 -0
- package/assets/init/.qfai/contracts/ui/thema-001-facebook-like.yml +13 -0
- package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +9 -0
- package/assets/init/.qfai/promptpack/commands/plan.md +1 -1
- package/assets/init/.qfai/promptpack/commands/review.md +1 -2
- package/assets/init/.qfai/promptpack/constitution.md +1 -1
- package/assets/init/.qfai/prompts/README.md +1 -3
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +3 -3
- package/assets/init/.qfai/prompts/require-to-spec.md +1 -2
- package/assets/init/.qfai/specs/README.md +3 -4
- package/assets/init/.qfai/specs/spec-0001/delta.md +0 -5
- package/assets/init/.qfai/specs/spec-0001/scenario.feature +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
- package/assets/init/root/qfai.config.yaml +0 -1
- package/dist/cli/index.cjs +596 -162
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +598 -164
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +549 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +551 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/promptpack/modes/change.md +0 -5
- package/assets/init/.qfai/promptpack/modes/compatibility.md +0 -6
- package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +0 -42
- package/assets/init/.qfai/prompts/qfai-classify-change.md +0 -33
- package/assets/init/.qfai/rules/conventions.md +0 -27
- package/assets/init/.qfai/rules/pnpm.md +0 -29
- package/assets/init/.qfai/samples/analyze/analysis.md +0 -38
- package/assets/init/.qfai/samples/analyze/input_bundle.md +0 -54
package/dist/cli/index.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");
|
|
@@ -245,7 +245,6 @@ var defaultConfig = {
|
|
|
245
245
|
paths: {
|
|
246
246
|
contractsDir: ".qfai/contracts",
|
|
247
247
|
specsDir: ".qfai/specs",
|
|
248
|
-
rulesDir: ".qfai/rules",
|
|
249
248
|
outDir: ".qfai/out",
|
|
250
249
|
promptsDir: ".qfai/prompts",
|
|
251
250
|
srcDir: "src",
|
|
@@ -358,13 +357,6 @@ function normalizePaths(raw, configPath, issues) {
|
|
|
358
357
|
configPath,
|
|
359
358
|
issues
|
|
360
359
|
),
|
|
361
|
-
rulesDir: readString(
|
|
362
|
-
raw.rulesDir,
|
|
363
|
-
base.rulesDir,
|
|
364
|
-
"paths.rulesDir",
|
|
365
|
-
configPath,
|
|
366
|
-
issues
|
|
367
|
-
),
|
|
368
360
|
outDir: readString(
|
|
369
361
|
raw.outDir,
|
|
370
362
|
base.outDir,
|
|
@@ -639,6 +631,7 @@ function isRecord(value) {
|
|
|
639
631
|
|
|
640
632
|
// src/core/discovery.ts
|
|
641
633
|
var import_promises5 = require("fs/promises");
|
|
634
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
642
635
|
|
|
643
636
|
// src/core/specLayout.ts
|
|
644
637
|
var import_promises4 = require("fs/promises");
|
|
@@ -686,7 +679,12 @@ async function collectScenarioFiles(specsRoot) {
|
|
|
686
679
|
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
687
680
|
}
|
|
688
681
|
async function collectUiContractFiles(uiRoot) {
|
|
689
|
-
|
|
682
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
683
|
+
return filterByBasenamePrefix(files, "ui-");
|
|
684
|
+
}
|
|
685
|
+
async function collectThemaContractFiles(uiRoot) {
|
|
686
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
687
|
+
return filterByBasenamePrefix(files, "thema-");
|
|
690
688
|
}
|
|
691
689
|
async function collectApiContractFiles(apiRoot) {
|
|
692
690
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
@@ -695,12 +693,13 @@ async function collectDbContractFiles(dbRoot) {
|
|
|
695
693
|
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
696
694
|
}
|
|
697
695
|
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
698
|
-
const [ui, api, db] = await Promise.all([
|
|
696
|
+
const [ui, thema, api, db] = await Promise.all([
|
|
699
697
|
collectUiContractFiles(uiRoot),
|
|
698
|
+
collectThemaContractFiles(uiRoot),
|
|
700
699
|
collectApiContractFiles(apiRoot),
|
|
701
700
|
collectDbContractFiles(dbRoot)
|
|
702
701
|
]);
|
|
703
|
-
return { ui, api, db };
|
|
702
|
+
return { ui, thema, api, db };
|
|
704
703
|
}
|
|
705
704
|
async function filterExisting(files) {
|
|
706
705
|
const existing = [];
|
|
@@ -719,17 +718,23 @@ async function exists3(target) {
|
|
|
719
718
|
return false;
|
|
720
719
|
}
|
|
721
720
|
}
|
|
721
|
+
function filterByBasenamePrefix(files, prefix) {
|
|
722
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
723
|
+
return files.filter(
|
|
724
|
+
(file) => import_node_path5.default.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
725
|
+
);
|
|
726
|
+
}
|
|
722
727
|
|
|
723
728
|
// src/core/paths.ts
|
|
724
|
-
var
|
|
729
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
725
730
|
function toRelativePath(root, target) {
|
|
726
731
|
if (!target) {
|
|
727
732
|
return target;
|
|
728
733
|
}
|
|
729
|
-
if (!
|
|
734
|
+
if (!import_node_path6.default.isAbsolute(target)) {
|
|
730
735
|
return toPosixPath(target);
|
|
731
736
|
}
|
|
732
|
-
const relative =
|
|
737
|
+
const relative = import_node_path6.default.relative(root, target);
|
|
733
738
|
if (!relative) {
|
|
734
739
|
return ".";
|
|
735
740
|
}
|
|
@@ -741,7 +746,7 @@ function toPosixPath(value) {
|
|
|
741
746
|
|
|
742
747
|
// src/core/traceability.ts
|
|
743
748
|
var import_promises6 = require("fs/promises");
|
|
744
|
-
var
|
|
749
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
745
750
|
|
|
746
751
|
// src/core/gherkin/parse.ts
|
|
747
752
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -969,7 +974,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
969
974
|
};
|
|
970
975
|
}
|
|
971
976
|
const normalizedFiles = Array.from(
|
|
972
|
-
new Set(scanResult.files.map((file) =>
|
|
977
|
+
new Set(scanResult.files.map((file) => import_node_path7.default.normalize(file)))
|
|
973
978
|
);
|
|
974
979
|
for (const file of normalizedFiles) {
|
|
975
980
|
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
@@ -1032,19 +1037,19 @@ function formatError3(error2) {
|
|
|
1032
1037
|
|
|
1033
1038
|
// src/core/promptsIntegrity.ts
|
|
1034
1039
|
var import_promises7 = require("fs/promises");
|
|
1035
|
-
var
|
|
1040
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1036
1041
|
|
|
1037
1042
|
// src/shared/assets.ts
|
|
1038
1043
|
var import_node_fs = require("fs");
|
|
1039
|
-
var
|
|
1044
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1040
1045
|
var import_node_url = require("url");
|
|
1041
1046
|
function getInitAssetsDir() {
|
|
1042
1047
|
const base = __filename;
|
|
1043
1048
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
1044
|
-
const baseDir =
|
|
1049
|
+
const baseDir = import_node_path8.default.dirname(basePath);
|
|
1045
1050
|
const candidates = [
|
|
1046
|
-
|
|
1047
|
-
|
|
1051
|
+
import_node_path8.default.resolve(baseDir, "../../../assets/init"),
|
|
1052
|
+
import_node_path8.default.resolve(baseDir, "../../assets/init")
|
|
1048
1053
|
];
|
|
1049
1054
|
for (const candidate of candidates) {
|
|
1050
1055
|
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
@@ -1061,11 +1066,12 @@ function getInitAssetsDir() {
|
|
|
1061
1066
|
}
|
|
1062
1067
|
|
|
1063
1068
|
// src/core/promptsIntegrity.ts
|
|
1069
|
+
var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
|
|
1064
1070
|
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1065
|
-
const promptsDir =
|
|
1071
|
+
const promptsDir = import_node_path9.default.resolve(root, ".qfai", "prompts");
|
|
1066
1072
|
let templateDir;
|
|
1067
1073
|
try {
|
|
1068
|
-
templateDir =
|
|
1074
|
+
templateDir = import_node_path9.default.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1069
1075
|
} catch {
|
|
1070
1076
|
return {
|
|
1071
1077
|
status: "skipped_missing_assets",
|
|
@@ -1109,6 +1115,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1109
1115
|
extra.push(rel);
|
|
1110
1116
|
}
|
|
1111
1117
|
}
|
|
1118
|
+
const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
|
|
1112
1119
|
const common = intersectKeys(templateByRel, projectByRel);
|
|
1113
1120
|
for (const rel of common) {
|
|
1114
1121
|
const templateAbs = templateByRel.get(rel);
|
|
@@ -1128,13 +1135,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
|
|
|
1128
1135
|
changed.push(rel);
|
|
1129
1136
|
}
|
|
1130
1137
|
}
|
|
1131
|
-
const status = missing.length > 0 ||
|
|
1138
|
+
const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
|
|
1132
1139
|
return {
|
|
1133
1140
|
status,
|
|
1134
1141
|
promptsDir,
|
|
1135
1142
|
templateDir,
|
|
1136
1143
|
missing: missing.sort(),
|
|
1137
|
-
extra:
|
|
1144
|
+
extra: filteredExtra.sort(),
|
|
1138
1145
|
changed: changed.sort()
|
|
1139
1146
|
};
|
|
1140
1147
|
}
|
|
@@ -1142,7 +1149,7 @@ function normalizeNewlines(text) {
|
|
|
1142
1149
|
return text.replace(/\r\n/g, "\n");
|
|
1143
1150
|
}
|
|
1144
1151
|
function toRel(base, abs) {
|
|
1145
|
-
const rel =
|
|
1152
|
+
const rel = import_node_path9.default.relative(base, abs);
|
|
1146
1153
|
return rel.replace(/[\\/]+/g, "/");
|
|
1147
1154
|
}
|
|
1148
1155
|
function intersectKeys(a, b) {
|
|
@@ -1157,11 +1164,11 @@ function intersectKeys(a, b) {
|
|
|
1157
1164
|
|
|
1158
1165
|
// src/core/version.ts
|
|
1159
1166
|
var import_promises8 = require("fs/promises");
|
|
1160
|
-
var
|
|
1167
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1161
1168
|
var import_node_url2 = require("url");
|
|
1162
1169
|
async function resolveToolVersion() {
|
|
1163
|
-
if ("1.0.
|
|
1164
|
-
return "1.0.
|
|
1170
|
+
if ("1.0.4".length > 0) {
|
|
1171
|
+
return "1.0.4";
|
|
1165
1172
|
}
|
|
1166
1173
|
try {
|
|
1167
1174
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1176,7 +1183,7 @@ async function resolveToolVersion() {
|
|
|
1176
1183
|
function resolvePackageJsonPath() {
|
|
1177
1184
|
const base = __filename;
|
|
1178
1185
|
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1179
|
-
return
|
|
1186
|
+
return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
|
|
1180
1187
|
}
|
|
1181
1188
|
|
|
1182
1189
|
// src/core/doctor.ts
|
|
@@ -1202,7 +1209,7 @@ function normalizeGlobs2(values) {
|
|
|
1202
1209
|
return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1203
1210
|
}
|
|
1204
1211
|
async function createDoctorData(options) {
|
|
1205
|
-
const startDir =
|
|
1212
|
+
const startDir = import_node_path11.default.resolve(options.startDir);
|
|
1206
1213
|
const checks = [];
|
|
1207
1214
|
const configPath = getConfigPath(startDir);
|
|
1208
1215
|
const search = options.rootExplicit ? {
|
|
@@ -1251,7 +1258,6 @@ async function createDoctorData(options) {
|
|
|
1251
1258
|
"outDir",
|
|
1252
1259
|
"srcDir",
|
|
1253
1260
|
"testsDir",
|
|
1254
|
-
"rulesDir",
|
|
1255
1261
|
"promptsDir"
|
|
1256
1262
|
];
|
|
1257
1263
|
for (const key of pathKeys) {
|
|
@@ -1265,9 +1271,9 @@ async function createDoctorData(options) {
|
|
|
1265
1271
|
details: { path: toRelativePath(root, resolved) }
|
|
1266
1272
|
});
|
|
1267
1273
|
if (key === "promptsDir") {
|
|
1268
|
-
const promptsLocalDir =
|
|
1269
|
-
|
|
1270
|
-
`${
|
|
1274
|
+
const promptsLocalDir = import_node_path11.default.join(
|
|
1275
|
+
import_node_path11.default.dirname(resolved),
|
|
1276
|
+
`${import_node_path11.default.basename(resolved)}.local`
|
|
1271
1277
|
);
|
|
1272
1278
|
const found = await exists4(promptsLocalDir);
|
|
1273
1279
|
addCheck(checks, {
|
|
@@ -1340,7 +1346,7 @@ async function createDoctorData(options) {
|
|
|
1340
1346
|
message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
|
|
1341
1347
|
details: { specPacks: entries.length, missingFiles }
|
|
1342
1348
|
});
|
|
1343
|
-
const validateJsonAbs =
|
|
1349
|
+
const validateJsonAbs = import_node_path11.default.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : import_node_path11.default.resolve(root, config.output.validateJsonPath);
|
|
1344
1350
|
const validateJsonExists = await exists4(validateJsonAbs);
|
|
1345
1351
|
addCheck(checks, {
|
|
1346
1352
|
id: "output.validateJson",
|
|
@@ -1350,8 +1356,8 @@ async function createDoctorData(options) {
|
|
|
1350
1356
|
details: { path: toRelativePath(root, validateJsonAbs) }
|
|
1351
1357
|
});
|
|
1352
1358
|
const outDirAbs = resolvePath(root, config, "outDir");
|
|
1353
|
-
const rel =
|
|
1354
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
1359
|
+
const rel = import_node_path11.default.relative(outDirAbs, validateJsonAbs);
|
|
1360
|
+
const inside = rel !== "" && !rel.startsWith("..") && !import_node_path11.default.isAbsolute(rel);
|
|
1355
1361
|
addCheck(checks, {
|
|
1356
1362
|
id: "output.pathAlignment",
|
|
1357
1363
|
severity: inside ? "ok" : "warning",
|
|
@@ -1474,12 +1480,12 @@ async function detectOutDirCollisions(root) {
|
|
|
1474
1480
|
});
|
|
1475
1481
|
const configPaths = configScan.files;
|
|
1476
1482
|
const configRoots = Array.from(
|
|
1477
|
-
new Set(configPaths.map((configPath) =>
|
|
1483
|
+
new Set(configPaths.map((configPath) => import_node_path11.default.dirname(configPath)))
|
|
1478
1484
|
).sort((a, b) => a.localeCompare(b));
|
|
1479
1485
|
const outDirToRoots = /* @__PURE__ */ new Map();
|
|
1480
1486
|
for (const configRoot of configRoots) {
|
|
1481
1487
|
const { config } = await loadConfig(configRoot);
|
|
1482
|
-
const outDir =
|
|
1488
|
+
const outDir = import_node_path11.default.normalize(resolvePath(configRoot, config, "outDir"));
|
|
1483
1489
|
const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
|
|
1484
1490
|
roots.add(configRoot);
|
|
1485
1491
|
outDirToRoots.set(outDir, roots);
|
|
@@ -1505,20 +1511,20 @@ async function detectOutDirCollisions(root) {
|
|
|
1505
1511
|
};
|
|
1506
1512
|
}
|
|
1507
1513
|
async function findMonorepoRoot(startDir) {
|
|
1508
|
-
let current =
|
|
1514
|
+
let current = import_node_path11.default.resolve(startDir);
|
|
1509
1515
|
while (true) {
|
|
1510
|
-
const gitPath =
|
|
1511
|
-
const workspacePath =
|
|
1516
|
+
const gitPath = import_node_path11.default.join(current, ".git");
|
|
1517
|
+
const workspacePath = import_node_path11.default.join(current, "pnpm-workspace.yaml");
|
|
1512
1518
|
if (await exists4(gitPath) || await exists4(workspacePath)) {
|
|
1513
1519
|
return current;
|
|
1514
1520
|
}
|
|
1515
|
-
const parent =
|
|
1521
|
+
const parent = import_node_path11.default.dirname(current);
|
|
1516
1522
|
if (parent === current) {
|
|
1517
1523
|
break;
|
|
1518
1524
|
}
|
|
1519
1525
|
current = parent;
|
|
1520
1526
|
}
|
|
1521
|
-
return
|
|
1527
|
+
return import_node_path11.default.resolve(startDir);
|
|
1522
1528
|
}
|
|
1523
1529
|
|
|
1524
1530
|
// src/cli/lib/logger.ts
|
|
@@ -1560,8 +1566,8 @@ async function runDoctor(options) {
|
|
|
1560
1566
|
const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
|
|
1561
1567
|
const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
|
|
1562
1568
|
if (options.outPath) {
|
|
1563
|
-
const outAbs =
|
|
1564
|
-
await (0, import_promises10.mkdir)(
|
|
1569
|
+
const outAbs = import_node_path12.default.isAbsolute(options.outPath) ? options.outPath : import_node_path12.default.resolve(process.cwd(), options.outPath);
|
|
1570
|
+
await (0, import_promises10.mkdir)(import_node_path12.default.dirname(outAbs), { recursive: true });
|
|
1565
1571
|
await (0, import_promises10.writeFile)(outAbs, `${output}
|
|
1566
1572
|
`, "utf-8");
|
|
1567
1573
|
info(`doctor: wrote ${outAbs}`);
|
|
@@ -1581,11 +1587,11 @@ function shouldFailDoctor(summary, failOn) {
|
|
|
1581
1587
|
}
|
|
1582
1588
|
|
|
1583
1589
|
// src/cli/commands/init.ts
|
|
1584
|
-
var
|
|
1590
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
1585
1591
|
|
|
1586
1592
|
// src/cli/lib/fs.ts
|
|
1587
1593
|
var import_promises11 = require("fs/promises");
|
|
1588
|
-
var
|
|
1594
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1589
1595
|
async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
1590
1596
|
const files = await collectTemplateFiles(sourceRoot);
|
|
1591
1597
|
return copyFiles(files, sourceRoot, destRoot, options);
|
|
@@ -1593,7 +1599,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
|
|
|
1593
1599
|
async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
|
|
1594
1600
|
const allFiles = [];
|
|
1595
1601
|
for (const relPath of relativePaths) {
|
|
1596
|
-
const fullPath =
|
|
1602
|
+
const fullPath = import_node_path13.default.join(sourceRoot, relPath);
|
|
1597
1603
|
const files = await collectTemplateFiles(fullPath);
|
|
1598
1604
|
allFiles.push(...files);
|
|
1599
1605
|
}
|
|
@@ -1603,13 +1609,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1603
1609
|
const copied = [];
|
|
1604
1610
|
const skipped = [];
|
|
1605
1611
|
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 +
|
|
1612
|
+
const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
|
|
1613
|
+
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + import_node_path13.default.sep);
|
|
1608
1614
|
const isProtectedRelative = (relative) => {
|
|
1609
1615
|
if (protectPrefixes.length === 0) {
|
|
1610
1616
|
return false;
|
|
1611
1617
|
}
|
|
1612
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1618
|
+
const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
|
|
1613
1619
|
return protectPrefixes.some(
|
|
1614
1620
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1615
1621
|
);
|
|
@@ -1618,7 +1624,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1618
1624
|
if (excludePrefixes.length === 0) {
|
|
1619
1625
|
return false;
|
|
1620
1626
|
}
|
|
1621
|
-
const normalized = relative.replace(/[\\/]+/g,
|
|
1627
|
+
const normalized = relative.replace(/[\\/]+/g, import_node_path13.default.sep);
|
|
1622
1628
|
return excludePrefixes.some(
|
|
1623
1629
|
(prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
|
|
1624
1630
|
);
|
|
@@ -1626,14 +1632,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1626
1632
|
const conflictPolicy = options.conflictPolicy ?? "error";
|
|
1627
1633
|
if (!options.force && conflictPolicy === "error") {
|
|
1628
1634
|
for (const file of files) {
|
|
1629
|
-
const relative =
|
|
1635
|
+
const relative = import_node_path13.default.relative(sourceRoot, file);
|
|
1630
1636
|
if (isExcludedRelative(relative)) {
|
|
1631
1637
|
continue;
|
|
1632
1638
|
}
|
|
1633
1639
|
if (isProtectedRelative(relative)) {
|
|
1634
1640
|
continue;
|
|
1635
1641
|
}
|
|
1636
|
-
const dest =
|
|
1642
|
+
const dest = import_node_path13.default.join(destRoot, relative);
|
|
1637
1643
|
if (!await shouldWrite(dest, options.force)) {
|
|
1638
1644
|
conflicts.push(dest);
|
|
1639
1645
|
}
|
|
@@ -1643,18 +1649,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
|
|
|
1643
1649
|
}
|
|
1644
1650
|
}
|
|
1645
1651
|
for (const file of files) {
|
|
1646
|
-
const relative =
|
|
1652
|
+
const relative = import_node_path13.default.relative(sourceRoot, file);
|
|
1647
1653
|
if (isExcludedRelative(relative)) {
|
|
1648
1654
|
continue;
|
|
1649
1655
|
}
|
|
1650
|
-
const dest =
|
|
1656
|
+
const dest = import_node_path13.default.join(destRoot, relative);
|
|
1651
1657
|
const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
|
|
1652
1658
|
if (!await shouldWrite(dest, forceForThisFile)) {
|
|
1653
1659
|
skipped.push(dest);
|
|
1654
1660
|
continue;
|
|
1655
1661
|
}
|
|
1656
1662
|
if (!options.dryRun) {
|
|
1657
|
-
await (0, import_promises11.mkdir)(
|
|
1663
|
+
await (0, import_promises11.mkdir)(import_node_path13.default.dirname(dest), { recursive: true });
|
|
1658
1664
|
await (0, import_promises11.copyFile)(file, dest);
|
|
1659
1665
|
}
|
|
1660
1666
|
copied.push(dest);
|
|
@@ -1678,7 +1684,7 @@ async function collectTemplateFiles(root) {
|
|
|
1678
1684
|
}
|
|
1679
1685
|
const items = await (0, import_promises11.readdir)(root, { withFileTypes: true });
|
|
1680
1686
|
for (const item of items) {
|
|
1681
|
-
const fullPath =
|
|
1687
|
+
const fullPath = import_node_path13.default.join(root, item.name);
|
|
1682
1688
|
if (item.isDirectory()) {
|
|
1683
1689
|
const nested = await collectTemplateFiles(fullPath);
|
|
1684
1690
|
entries.push(...nested);
|
|
@@ -1708,10 +1714,10 @@ async function exists5(target) {
|
|
|
1708
1714
|
// src/cli/commands/init.ts
|
|
1709
1715
|
async function runInit(options) {
|
|
1710
1716
|
const assetsRoot = getInitAssetsDir();
|
|
1711
|
-
const rootAssets =
|
|
1712
|
-
const qfaiAssets =
|
|
1713
|
-
const destRoot =
|
|
1714
|
-
const destQfai =
|
|
1717
|
+
const rootAssets = import_node_path14.default.join(assetsRoot, "root");
|
|
1718
|
+
const qfaiAssets = import_node_path14.default.join(assetsRoot, ".qfai");
|
|
1719
|
+
const destRoot = import_node_path14.default.resolve(options.dir);
|
|
1720
|
+
const destQfai = import_node_path14.default.join(destRoot, ".qfai");
|
|
1715
1721
|
if (options.force) {
|
|
1716
1722
|
info(
|
|
1717
1723
|
"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 +1765,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
1759
1765
|
|
|
1760
1766
|
// src/cli/commands/report.ts
|
|
1761
1767
|
var import_promises20 = require("fs/promises");
|
|
1762
|
-
var
|
|
1768
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
1763
1769
|
|
|
1764
1770
|
// src/core/normalize.ts
|
|
1765
1771
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1800,15 +1806,15 @@ function normalizeValidationResult(root, result) {
|
|
|
1800
1806
|
|
|
1801
1807
|
// src/core/report.ts
|
|
1802
1808
|
var import_promises19 = require("fs/promises");
|
|
1803
|
-
var
|
|
1809
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
1804
1810
|
|
|
1805
1811
|
// src/core/contractIndex.ts
|
|
1806
1812
|
var import_promises12 = require("fs/promises");
|
|
1807
|
-
var
|
|
1813
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
1808
1814
|
|
|
1809
1815
|
// 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*$/;
|
|
1816
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
1817
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
|
|
1812
1818
|
function extractDeclaredContractIds(text) {
|
|
1813
1819
|
const ids = [];
|
|
1814
1820
|
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
@@ -1826,20 +1832,22 @@ function stripContractDeclarationLines(text) {
|
|
|
1826
1832
|
// src/core/contractIndex.ts
|
|
1827
1833
|
async function buildContractIndex(root, config) {
|
|
1828
1834
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1829
|
-
const uiRoot =
|
|
1830
|
-
const apiRoot =
|
|
1831
|
-
const dbRoot =
|
|
1832
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1835
|
+
const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
|
|
1836
|
+
const apiRoot = import_node_path15.default.join(contractsRoot, "api");
|
|
1837
|
+
const dbRoot = import_node_path15.default.join(contractsRoot, "db");
|
|
1838
|
+
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1833
1839
|
collectUiContractFiles(uiRoot),
|
|
1840
|
+
collectThemaContractFiles(uiRoot),
|
|
1834
1841
|
collectApiContractFiles(apiRoot),
|
|
1835
1842
|
collectDbContractFiles(dbRoot)
|
|
1836
1843
|
]);
|
|
1837
1844
|
const index = {
|
|
1838
1845
|
ids: /* @__PURE__ */ new Set(),
|
|
1839
1846
|
idToFiles: /* @__PURE__ */ new Map(),
|
|
1840
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
1847
|
+
files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
|
|
1841
1848
|
};
|
|
1842
1849
|
await indexContractFiles(uiFiles, index);
|
|
1850
|
+
await indexContractFiles(themaFiles, index);
|
|
1843
1851
|
await indexContractFiles(apiFiles, index);
|
|
1844
1852
|
await indexContractFiles(dbFiles, index);
|
|
1845
1853
|
return index;
|
|
@@ -1858,7 +1866,15 @@ function record(index, id, file) {
|
|
|
1858
1866
|
}
|
|
1859
1867
|
|
|
1860
1868
|
// src/core/ids.ts
|
|
1861
|
-
var ID_PREFIXES = [
|
|
1869
|
+
var ID_PREFIXES = [
|
|
1870
|
+
"SPEC",
|
|
1871
|
+
"BR",
|
|
1872
|
+
"SC",
|
|
1873
|
+
"UI",
|
|
1874
|
+
"API",
|
|
1875
|
+
"DB",
|
|
1876
|
+
"THEMA"
|
|
1877
|
+
];
|
|
1862
1878
|
var STRICT_ID_PATTERNS = {
|
|
1863
1879
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1864
1880
|
BR: /\bBR-\d{4}\b/g,
|
|
@@ -1866,6 +1882,7 @@ var STRICT_ID_PATTERNS = {
|
|
|
1866
1882
|
UI: /\bUI-\d{4}\b/g,
|
|
1867
1883
|
API: /\bAPI-\d{4}\b/g,
|
|
1868
1884
|
DB: /\bDB-\d{4}\b/g,
|
|
1885
|
+
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
1869
1886
|
ADR: /\bADR-\d{4}\b/g
|
|
1870
1887
|
};
|
|
1871
1888
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -1875,6 +1892,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
1875
1892
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1876
1893
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1877
1894
|
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1895
|
+
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
1878
1896
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1879
1897
|
};
|
|
1880
1898
|
function extractIds(text, prefix) {
|
|
@@ -1911,7 +1929,7 @@ function isValidId(value, prefix) {
|
|
|
1911
1929
|
}
|
|
1912
1930
|
|
|
1913
1931
|
// src/core/parse/contractRefs.ts
|
|
1914
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
1932
|
+
var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
|
|
1915
1933
|
function parseContractRefs(text, options = {}) {
|
|
1916
1934
|
const linePattern = buildLinePattern(options);
|
|
1917
1935
|
const lines = [];
|
|
@@ -2082,13 +2100,13 @@ function parseSpec(md, file) {
|
|
|
2082
2100
|
|
|
2083
2101
|
// src/core/validators/contracts.ts
|
|
2084
2102
|
var import_promises13 = require("fs/promises");
|
|
2085
|
-
var
|
|
2103
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2086
2104
|
|
|
2087
2105
|
// src/core/contracts.ts
|
|
2088
|
-
var
|
|
2106
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2089
2107
|
var import_yaml2 = require("yaml");
|
|
2090
2108
|
function parseStructuredContract(file, text) {
|
|
2091
|
-
const ext =
|
|
2109
|
+
const ext = import_node_path16.default.extname(file).toLowerCase();
|
|
2092
2110
|
if (ext === ".json") {
|
|
2093
2111
|
return JSON.parse(text);
|
|
2094
2112
|
}
|
|
@@ -2105,17 +2123,23 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
2105
2123
|
label: "ALTER TABLE ... DROP"
|
|
2106
2124
|
}
|
|
2107
2125
|
];
|
|
2126
|
+
var THEMA_ID_RE = /^THEMA-\d{3}$/;
|
|
2108
2127
|
async function validateContracts(root, config) {
|
|
2109
2128
|
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
2129
|
const contractIndex = await buildContractIndex(root, config);
|
|
2130
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2131
|
+
const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
|
|
2132
|
+
const themaIds = new Set(
|
|
2133
|
+
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
2134
|
+
);
|
|
2135
|
+
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
2136
|
+
issues.push(...await validateThemaContracts(uiRoot));
|
|
2137
|
+
issues.push(...await validateApiContracts(import_node_path17.default.join(contractsRoot, "api")));
|
|
2138
|
+
issues.push(...await validateDbContracts(import_node_path17.default.join(contractsRoot, "db")));
|
|
2115
2139
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
2116
2140
|
return issues;
|
|
2117
2141
|
}
|
|
2118
|
-
async function validateUiContracts(uiRoot) {
|
|
2142
|
+
async function validateUiContracts(uiRoot, themaIds) {
|
|
2119
2143
|
const files = await collectUiContractFiles(uiRoot);
|
|
2120
2144
|
if (files.length === 0) {
|
|
2121
2145
|
return [
|
|
@@ -2131,6 +2155,22 @@ async function validateUiContracts(uiRoot) {
|
|
|
2131
2155
|
const issues = [];
|
|
2132
2156
|
for (const file of files) {
|
|
2133
2157
|
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2158
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
2159
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
2160
|
+
let doc = null;
|
|
2161
|
+
try {
|
|
2162
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2163
|
+
} catch (error2) {
|
|
2164
|
+
issues.push(
|
|
2165
|
+
issue(
|
|
2166
|
+
"QFAI-CONTRACT-001",
|
|
2167
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2168
|
+
"error",
|
|
2169
|
+
file,
|
|
2170
|
+
"contracts.ui.parse"
|
|
2171
|
+
)
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2134
2174
|
const invalidIds = extractInvalidIds(text, [
|
|
2135
2175
|
"SPEC",
|
|
2136
2176
|
"BR",
|
|
@@ -2138,6 +2178,45 @@ async function validateUiContracts(uiRoot) {
|
|
|
2138
2178
|
"UI",
|
|
2139
2179
|
"API",
|
|
2140
2180
|
"DB",
|
|
2181
|
+
"THEMA",
|
|
2182
|
+
"ADR"
|
|
2183
|
+
]).filter((id) => !shouldIgnoreInvalidId(id, doc));
|
|
2184
|
+
if (invalidIds.length > 0) {
|
|
2185
|
+
issues.push(
|
|
2186
|
+
issue(
|
|
2187
|
+
"QFAI-ID-002",
|
|
2188
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
2189
|
+
"error",
|
|
2190
|
+
file,
|
|
2191
|
+
"id.format",
|
|
2192
|
+
invalidIds
|
|
2193
|
+
)
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
if (doc) {
|
|
2197
|
+
issues.push(
|
|
2198
|
+
...await validateUiContractDoc(doc, file, uiRoot, themaIds)
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return issues;
|
|
2203
|
+
}
|
|
2204
|
+
async function validateThemaContracts(uiRoot) {
|
|
2205
|
+
const files = await collectThemaContractFiles(uiRoot);
|
|
2206
|
+
if (files.length === 0) {
|
|
2207
|
+
return [];
|
|
2208
|
+
}
|
|
2209
|
+
const issues = [];
|
|
2210
|
+
for (const file of files) {
|
|
2211
|
+
const text = await (0, import_promises13.readFile)(file, "utf-8");
|
|
2212
|
+
const invalidIds = extractInvalidIds(text, [
|
|
2213
|
+
"SPEC",
|
|
2214
|
+
"BR",
|
|
2215
|
+
"SC",
|
|
2216
|
+
"UI",
|
|
2217
|
+
"API",
|
|
2218
|
+
"DB",
|
|
2219
|
+
"THEMA",
|
|
2141
2220
|
"ADR"
|
|
2142
2221
|
]);
|
|
2143
2222
|
if (invalidIds.length > 0) {
|
|
@@ -2153,17 +2232,95 @@ async function validateUiContracts(uiRoot) {
|
|
|
2153
2232
|
);
|
|
2154
2233
|
}
|
|
2155
2234
|
const declaredIds = extractDeclaredContractIds(text);
|
|
2156
|
-
|
|
2235
|
+
if (declaredIds.length === 0) {
|
|
2236
|
+
issues.push(
|
|
2237
|
+
issue(
|
|
2238
|
+
"QFAI-THEMA-010",
|
|
2239
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
2240
|
+
"error",
|
|
2241
|
+
file,
|
|
2242
|
+
"contracts.thema.declaration"
|
|
2243
|
+
)
|
|
2244
|
+
);
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
if (declaredIds.length > 1) {
|
|
2248
|
+
issues.push(
|
|
2249
|
+
issue(
|
|
2250
|
+
"QFAI-THEMA-011",
|
|
2251
|
+
`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(
|
|
2252
|
+
", "
|
|
2253
|
+
)}`,
|
|
2254
|
+
"error",
|
|
2255
|
+
file,
|
|
2256
|
+
"contracts.thema.declaration",
|
|
2257
|
+
declaredIds
|
|
2258
|
+
)
|
|
2259
|
+
);
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
const declaredId = declaredIds[0] ?? "";
|
|
2263
|
+
if (!THEMA_ID_RE.test(declaredId)) {
|
|
2264
|
+
issues.push(
|
|
2265
|
+
issue(
|
|
2266
|
+
"QFAI-THEMA-012",
|
|
2267
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
|
|
2268
|
+
"error",
|
|
2269
|
+
file,
|
|
2270
|
+
"contracts.thema.idFormat",
|
|
2271
|
+
[declaredId]
|
|
2272
|
+
)
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
let doc;
|
|
2157
2276
|
try {
|
|
2158
|
-
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2277
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
2159
2278
|
} catch (error2) {
|
|
2160
2279
|
issues.push(
|
|
2161
2280
|
issue(
|
|
2162
|
-
"QFAI-
|
|
2163
|
-
`
|
|
2281
|
+
"QFAI-THEMA-001",
|
|
2282
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
2164
2283
|
"error",
|
|
2165
2284
|
file,
|
|
2166
|
-
"contracts.
|
|
2285
|
+
"contracts.thema.parse"
|
|
2286
|
+
)
|
|
2287
|
+
);
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
const docId = typeof doc.id === "string" ? doc.id : "";
|
|
2291
|
+
if (!THEMA_ID_RE.test(docId)) {
|
|
2292
|
+
issues.push(
|
|
2293
|
+
issue(
|
|
2294
|
+
"QFAI-THEMA-012",
|
|
2295
|
+
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",
|
|
2296
|
+
"error",
|
|
2297
|
+
file,
|
|
2298
|
+
"contracts.thema.idFormat",
|
|
2299
|
+
docId.length > 0 ? [docId] : void 0
|
|
2300
|
+
)
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
const name = typeof doc.name === "string" ? doc.name : "";
|
|
2304
|
+
if (!name) {
|
|
2305
|
+
issues.push(
|
|
2306
|
+
issue(
|
|
2307
|
+
"QFAI-THEMA-014",
|
|
2308
|
+
"thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2309
|
+
"error",
|
|
2310
|
+
file,
|
|
2311
|
+
"contracts.thema.name"
|
|
2312
|
+
)
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
if (declaredId && docId && declaredId !== docId) {
|
|
2316
|
+
issues.push(
|
|
2317
|
+
issue(
|
|
2318
|
+
"QFAI-THEMA-013",
|
|
2319
|
+
`thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
|
|
2320
|
+
"error",
|
|
2321
|
+
file,
|
|
2322
|
+
"contracts.thema.idMismatch",
|
|
2323
|
+
[declaredId, docId]
|
|
2167
2324
|
)
|
|
2168
2325
|
);
|
|
2169
2326
|
}
|
|
@@ -2193,6 +2350,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
2193
2350
|
"UI",
|
|
2194
2351
|
"API",
|
|
2195
2352
|
"DB",
|
|
2353
|
+
"THEMA",
|
|
2196
2354
|
"ADR"
|
|
2197
2355
|
]);
|
|
2198
2356
|
if (invalidIds.length > 0) {
|
|
@@ -2261,6 +2419,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
2261
2419
|
"UI",
|
|
2262
2420
|
"API",
|
|
2263
2421
|
"DB",
|
|
2422
|
+
"THEMA",
|
|
2264
2423
|
"ADR"
|
|
2265
2424
|
]);
|
|
2266
2425
|
if (invalidIds.length > 0) {
|
|
@@ -2367,6 +2526,278 @@ function validateDuplicateContractIds(contractIndex) {
|
|
|
2367
2526
|
function hasOpenApi(doc) {
|
|
2368
2527
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
2369
2528
|
}
|
|
2529
|
+
async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
|
|
2530
|
+
const issues = [];
|
|
2531
|
+
if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
|
|
2532
|
+
const themaRef = doc.themaRef;
|
|
2533
|
+
if (typeof themaRef !== "string" || themaRef.length === 0) {
|
|
2534
|
+
issues.push(
|
|
2535
|
+
issue(
|
|
2536
|
+
"QFAI-UI-020",
|
|
2537
|
+
"themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2538
|
+
"error",
|
|
2539
|
+
file,
|
|
2540
|
+
"contracts.ui.themaRef"
|
|
2541
|
+
)
|
|
2542
|
+
);
|
|
2543
|
+
} else if (!THEMA_ID_RE.test(themaRef)) {
|
|
2544
|
+
issues.push(
|
|
2545
|
+
issue(
|
|
2546
|
+
"QFAI-UI-020",
|
|
2547
|
+
`themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
|
|
2548
|
+
"error",
|
|
2549
|
+
file,
|
|
2550
|
+
"contracts.ui.themaRef",
|
|
2551
|
+
[themaRef]
|
|
2552
|
+
)
|
|
2553
|
+
);
|
|
2554
|
+
} else if (!themaIds.has(themaRef)) {
|
|
2555
|
+
issues.push(
|
|
2556
|
+
issue(
|
|
2557
|
+
"QFAI-UI-020",
|
|
2558
|
+
`themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
|
|
2559
|
+
"error",
|
|
2560
|
+
file,
|
|
2561
|
+
"contracts.ui.themaRef",
|
|
2562
|
+
[themaRef]
|
|
2563
|
+
)
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
const assets = doc.assets;
|
|
2568
|
+
if (assets && typeof assets === "object") {
|
|
2569
|
+
issues.push(
|
|
2570
|
+
...await validateUiAssets(
|
|
2571
|
+
assets,
|
|
2572
|
+
file,
|
|
2573
|
+
uiRoot
|
|
2574
|
+
)
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
return issues;
|
|
2578
|
+
}
|
|
2579
|
+
async function validateUiAssets(assets, file, uiRoot) {
|
|
2580
|
+
const issues = [];
|
|
2581
|
+
const packValue = assets.pack;
|
|
2582
|
+
const useValue = assets.use;
|
|
2583
|
+
if (packValue === void 0 && useValue === void 0) {
|
|
2584
|
+
return issues;
|
|
2585
|
+
}
|
|
2586
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2587
|
+
issues.push(
|
|
2588
|
+
issue(
|
|
2589
|
+
"QFAI-ASSET-001",
|
|
2590
|
+
"assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2591
|
+
"error",
|
|
2592
|
+
file,
|
|
2593
|
+
"assets.pack"
|
|
2594
|
+
)
|
|
2595
|
+
);
|
|
2596
|
+
return issues;
|
|
2597
|
+
}
|
|
2598
|
+
if (!isSafeRelativePath(packValue)) {
|
|
2599
|
+
issues.push(
|
|
2600
|
+
issue(
|
|
2601
|
+
"QFAI-ASSET-001",
|
|
2602
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
|
|
2603
|
+
"error",
|
|
2604
|
+
file,
|
|
2605
|
+
"assets.pack",
|
|
2606
|
+
[packValue]
|
|
2607
|
+
)
|
|
2608
|
+
);
|
|
2609
|
+
return issues;
|
|
2610
|
+
}
|
|
2611
|
+
const packDir = import_node_path17.default.resolve(uiRoot, packValue);
|
|
2612
|
+
const packRelative = import_node_path17.default.relative(uiRoot, packDir);
|
|
2613
|
+
if (packRelative.startsWith("..") || import_node_path17.default.isAbsolute(packRelative)) {
|
|
2614
|
+
issues.push(
|
|
2615
|
+
issue(
|
|
2616
|
+
"QFAI-ASSET-001",
|
|
2617
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
|
|
2618
|
+
"error",
|
|
2619
|
+
file,
|
|
2620
|
+
"assets.pack",
|
|
2621
|
+
[packValue]
|
|
2622
|
+
)
|
|
2623
|
+
);
|
|
2624
|
+
return issues;
|
|
2625
|
+
}
|
|
2626
|
+
if (!await exists6(packDir)) {
|
|
2627
|
+
issues.push(
|
|
2628
|
+
issue(
|
|
2629
|
+
"QFAI-ASSET-001",
|
|
2630
|
+
`assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
|
|
2631
|
+
"error",
|
|
2632
|
+
file,
|
|
2633
|
+
"assets.pack",
|
|
2634
|
+
[packValue]
|
|
2635
|
+
)
|
|
2636
|
+
);
|
|
2637
|
+
return issues;
|
|
2638
|
+
}
|
|
2639
|
+
const assetsYamlPath = import_node_path17.default.join(packDir, "assets.yaml");
|
|
2640
|
+
if (!await exists6(assetsYamlPath)) {
|
|
2641
|
+
issues.push(
|
|
2642
|
+
issue(
|
|
2643
|
+
"QFAI-ASSET-002",
|
|
2644
|
+
`assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
|
|
2645
|
+
"error",
|
|
2646
|
+
assetsYamlPath,
|
|
2647
|
+
"assets.yaml"
|
|
2648
|
+
)
|
|
2649
|
+
);
|
|
2650
|
+
return issues;
|
|
2651
|
+
}
|
|
2652
|
+
let manifest;
|
|
2653
|
+
try {
|
|
2654
|
+
const manifestText = await (0, import_promises13.readFile)(assetsYamlPath, "utf-8");
|
|
2655
|
+
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
2656
|
+
} catch (error2) {
|
|
2657
|
+
issues.push(
|
|
2658
|
+
issue(
|
|
2659
|
+
"QFAI-ASSET-002",
|
|
2660
|
+
`assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error2)})`,
|
|
2661
|
+
"error",
|
|
2662
|
+
assetsYamlPath,
|
|
2663
|
+
"assets.yaml"
|
|
2664
|
+
)
|
|
2665
|
+
);
|
|
2666
|
+
return issues;
|
|
2667
|
+
}
|
|
2668
|
+
const items = Array.isArray(manifest.items) ? manifest.items : [];
|
|
2669
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
2670
|
+
const itemPaths = [];
|
|
2671
|
+
for (const item of items) {
|
|
2672
|
+
if (!item || typeof item !== "object") {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
const record2 = item;
|
|
2676
|
+
const id = typeof record2.id === "string" ? record2.id : void 0;
|
|
2677
|
+
const pathValue = typeof record2.path === "string" ? record2.path : void 0;
|
|
2678
|
+
if (id) {
|
|
2679
|
+
itemIds.add(id);
|
|
2680
|
+
}
|
|
2681
|
+
itemPaths.push({ id, path: pathValue });
|
|
2682
|
+
}
|
|
2683
|
+
if (useValue !== void 0) {
|
|
2684
|
+
if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
|
|
2685
|
+
issues.push(
|
|
2686
|
+
issue(
|
|
2687
|
+
"QFAI-ASSET-003",
|
|
2688
|
+
"assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2689
|
+
"error",
|
|
2690
|
+
file,
|
|
2691
|
+
"assets.use"
|
|
2692
|
+
)
|
|
2693
|
+
);
|
|
2694
|
+
} else {
|
|
2695
|
+
const missing = useValue.filter((entry) => !itemIds.has(entry));
|
|
2696
|
+
if (missing.length > 0) {
|
|
2697
|
+
issues.push(
|
|
2698
|
+
issue(
|
|
2699
|
+
"QFAI-ASSET-003",
|
|
2700
|
+
`assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
|
|
2701
|
+
"error",
|
|
2702
|
+
file,
|
|
2703
|
+
"assets.use",
|
|
2704
|
+
missing
|
|
2705
|
+
)
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
for (const entry of itemPaths) {
|
|
2711
|
+
if (!entry.path) {
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
2715
|
+
issues.push(
|
|
2716
|
+
issue(
|
|
2717
|
+
"QFAI-ASSET-004",
|
|
2718
|
+
`assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
|
|
2719
|
+
"error",
|
|
2720
|
+
assetsYamlPath,
|
|
2721
|
+
"assets.path",
|
|
2722
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2723
|
+
)
|
|
2724
|
+
);
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
const assetPath = import_node_path17.default.resolve(packDir, entry.path);
|
|
2728
|
+
const assetRelative = import_node_path17.default.relative(packDir, assetPath);
|
|
2729
|
+
if (assetRelative.startsWith("..") || import_node_path17.default.isAbsolute(assetRelative)) {
|
|
2730
|
+
issues.push(
|
|
2731
|
+
issue(
|
|
2732
|
+
"QFAI-ASSET-004",
|
|
2733
|
+
`assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
|
|
2734
|
+
"error",
|
|
2735
|
+
assetsYamlPath,
|
|
2736
|
+
"assets.path",
|
|
2737
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2738
|
+
)
|
|
2739
|
+
);
|
|
2740
|
+
continue;
|
|
2741
|
+
}
|
|
2742
|
+
if (!await exists6(assetPath)) {
|
|
2743
|
+
issues.push(
|
|
2744
|
+
issue(
|
|
2745
|
+
"QFAI-ASSET-004",
|
|
2746
|
+
`assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
|
|
2747
|
+
"error",
|
|
2748
|
+
assetsYamlPath,
|
|
2749
|
+
"assets.path",
|
|
2750
|
+
entry.id ? [entry.id] : [entry.path]
|
|
2751
|
+
)
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return issues;
|
|
2756
|
+
}
|
|
2757
|
+
function shouldIgnoreInvalidId(value, doc) {
|
|
2758
|
+
if (!doc) {
|
|
2759
|
+
return false;
|
|
2760
|
+
}
|
|
2761
|
+
const assets = doc.assets;
|
|
2762
|
+
if (!assets || typeof assets !== "object") {
|
|
2763
|
+
return false;
|
|
2764
|
+
}
|
|
2765
|
+
const packValue = assets.pack;
|
|
2766
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
2767
|
+
return false;
|
|
2768
|
+
}
|
|
2769
|
+
const normalized = packValue.replace(/\\/g, "/");
|
|
2770
|
+
const basename = import_node_path17.default.posix.basename(normalized);
|
|
2771
|
+
if (!basename) {
|
|
2772
|
+
return false;
|
|
2773
|
+
}
|
|
2774
|
+
return value.toLowerCase() === basename.toLowerCase();
|
|
2775
|
+
}
|
|
2776
|
+
function isSafeRelativePath(value) {
|
|
2777
|
+
if (!value) {
|
|
2778
|
+
return false;
|
|
2779
|
+
}
|
|
2780
|
+
if (import_node_path17.default.isAbsolute(value)) {
|
|
2781
|
+
return false;
|
|
2782
|
+
}
|
|
2783
|
+
const normalized = value.replace(/\\/g, "/");
|
|
2784
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
const segments = normalized.split("/");
|
|
2788
|
+
if (segments.some((segment) => segment === "..")) {
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
return true;
|
|
2792
|
+
}
|
|
2793
|
+
async function exists6(target) {
|
|
2794
|
+
try {
|
|
2795
|
+
await (0, import_promises13.access)(target);
|
|
2796
|
+
return true;
|
|
2797
|
+
} catch {
|
|
2798
|
+
return false;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2370
2801
|
function formatError4(error2) {
|
|
2371
2802
|
if (error2 instanceof Error) {
|
|
2372
2803
|
return error2.message;
|
|
@@ -2397,12 +2828,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2397
2828
|
|
|
2398
2829
|
// src/core/validators/delta.ts
|
|
2399
2830
|
var import_promises14 = require("fs/promises");
|
|
2400
|
-
var
|
|
2401
|
-
var SECTION_RE = /^##\s+変更区分/m;
|
|
2402
|
-
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
2403
|
-
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
2404
|
-
var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
|
|
2405
|
-
var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
|
|
2831
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
2406
2832
|
async function validateDeltas(root, config) {
|
|
2407
2833
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2408
2834
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
@@ -2411,10 +2837,9 @@ async function validateDeltas(root, config) {
|
|
|
2411
2837
|
}
|
|
2412
2838
|
const issues = [];
|
|
2413
2839
|
for (const pack of packs) {
|
|
2414
|
-
const deltaPath =
|
|
2415
|
-
let text;
|
|
2840
|
+
const deltaPath = import_node_path18.default.join(pack, "delta.md");
|
|
2416
2841
|
try {
|
|
2417
|
-
|
|
2842
|
+
await (0, import_promises14.readFile)(deltaPath, "utf-8");
|
|
2418
2843
|
} catch (error2) {
|
|
2419
2844
|
if (isMissingFileError2(error2)) {
|
|
2420
2845
|
issues.push(
|
|
@@ -2423,41 +2848,16 @@ async function validateDeltas(root, config) {
|
|
|
2423
2848
|
"delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2424
2849
|
"error",
|
|
2425
2850
|
deltaPath,
|
|
2426
|
-
"delta.exists"
|
|
2851
|
+
"delta.exists",
|
|
2852
|
+
void 0,
|
|
2853
|
+
"change",
|
|
2854
|
+
"spec-xxxx/delta.md \u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30C6\u30F3\u30D7\u30EC\u306F init \u751F\u6210\u7269\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\uFF09\u3002"
|
|
2427
2855
|
)
|
|
2428
2856
|
);
|
|
2429
2857
|
continue;
|
|
2430
2858
|
}
|
|
2431
2859
|
throw error2;
|
|
2432
2860
|
}
|
|
2433
|
-
const hasSection = SECTION_RE.test(text);
|
|
2434
|
-
const hasCompatibility = COMPAT_LINE_RE.test(text);
|
|
2435
|
-
const hasChange = CHANGE_LINE_RE.test(text);
|
|
2436
|
-
if (!hasSection || !hasCompatibility || !hasChange) {
|
|
2437
|
-
issues.push(
|
|
2438
|
-
issue2(
|
|
2439
|
-
"QFAI-DELTA-002",
|
|
2440
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059\u3002`## \u5909\u66F4\u533A\u5206` \u3068\u30C1\u30A7\u30C3\u30AF\u30DC\u30C3\u30AF\u30B9\uFF08Compatibility / Change/Improvement\uFF09\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2441
|
-
"error",
|
|
2442
|
-
deltaPath,
|
|
2443
|
-
"delta.section"
|
|
2444
|
-
)
|
|
2445
|
-
);
|
|
2446
|
-
continue;
|
|
2447
|
-
}
|
|
2448
|
-
const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
|
|
2449
|
-
const changeChecked = CHANGE_CHECKED_RE.test(text);
|
|
2450
|
-
if (compatibilityChecked === changeChecked) {
|
|
2451
|
-
issues.push(
|
|
2452
|
-
issue2(
|
|
2453
|
-
"QFAI-DELTA-003",
|
|
2454
|
-
"delta.md \u306E\u5909\u66F4\u533A\u5206\u306F\u3069\u3061\u3089\u304B1\u3064\u3060\u3051\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4E21\u65B9ON/\u4E21\u65B9OFF\u306F\u7121\u52B9\u3067\u3059\uFF09\u3002",
|
|
2455
|
-
"error",
|
|
2456
|
-
deltaPath,
|
|
2457
|
-
"delta.classification"
|
|
2458
|
-
)
|
|
2459
|
-
);
|
|
2460
|
-
}
|
|
2461
2861
|
}
|
|
2462
2862
|
return issues;
|
|
2463
2863
|
}
|
|
@@ -2491,7 +2891,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2491
2891
|
|
|
2492
2892
|
// src/core/validators/ids.ts
|
|
2493
2893
|
var import_promises15 = require("fs/promises");
|
|
2494
|
-
var
|
|
2894
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
2495
2895
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2496
2896
|
async function validateDefinedIds(root, config) {
|
|
2497
2897
|
const issues = [];
|
|
@@ -2557,7 +2957,7 @@ function recordId(out, id, file) {
|
|
|
2557
2957
|
}
|
|
2558
2958
|
function formatFileList(files, root) {
|
|
2559
2959
|
return files.map((file) => {
|
|
2560
|
-
const relative =
|
|
2960
|
+
const relative = import_node_path19.default.relative(root, file);
|
|
2561
2961
|
return relative.length > 0 ? relative : file;
|
|
2562
2962
|
}).join(", ");
|
|
2563
2963
|
}
|
|
@@ -2616,6 +3016,7 @@ async function validatePromptsIntegrity(root) {
|
|
|
2616
3016
|
|
|
2617
3017
|
// src/core/validators/scenario.ts
|
|
2618
3018
|
var import_promises16 = require("fs/promises");
|
|
3019
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
2619
3020
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2620
3021
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2621
3022
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2638,6 +3039,18 @@ async function validateScenarios(root, config) {
|
|
|
2638
3039
|
}
|
|
2639
3040
|
const issues = [];
|
|
2640
3041
|
for (const entry of entries) {
|
|
3042
|
+
const legacyScenarioPath = import_node_path20.default.join(entry.dir, "scenario.md");
|
|
3043
|
+
if (await fileExists(legacyScenarioPath)) {
|
|
3044
|
+
issues.push(
|
|
3045
|
+
issue4(
|
|
3046
|
+
"QFAI-SC-004",
|
|
3047
|
+
"scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3048
|
+
"error",
|
|
3049
|
+
legacyScenarioPath,
|
|
3050
|
+
"scenario.legacy"
|
|
3051
|
+
)
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
2641
3054
|
let text;
|
|
2642
3055
|
try {
|
|
2643
3056
|
text = await (0, import_promises16.readFile)(entry.scenarioPath, "utf-8");
|
|
@@ -2669,6 +3082,7 @@ function validateScenarioContent(text, file) {
|
|
|
2669
3082
|
"UI",
|
|
2670
3083
|
"API",
|
|
2671
3084
|
"DB",
|
|
3085
|
+
"THEMA",
|
|
2672
3086
|
"ADR"
|
|
2673
3087
|
]);
|
|
2674
3088
|
if (invalidIds.length > 0) {
|
|
@@ -2812,6 +3226,14 @@ function isMissingFileError3(error2) {
|
|
|
2812
3226
|
}
|
|
2813
3227
|
return error2.code === "ENOENT";
|
|
2814
3228
|
}
|
|
3229
|
+
async function fileExists(target) {
|
|
3230
|
+
try {
|
|
3231
|
+
await (0, import_promises16.access)(target);
|
|
3232
|
+
return true;
|
|
3233
|
+
} catch {
|
|
3234
|
+
return false;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
2815
3237
|
|
|
2816
3238
|
// src/core/validators/spec.ts
|
|
2817
3239
|
var import_promises17 = require("fs/promises");
|
|
@@ -2871,6 +3293,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2871
3293
|
"UI",
|
|
2872
3294
|
"API",
|
|
2873
3295
|
"DB",
|
|
3296
|
+
"THEMA",
|
|
2874
3297
|
"ADR"
|
|
2875
3298
|
]);
|
|
2876
3299
|
if (invalidIds.length > 0) {
|
|
@@ -3050,7 +3473,7 @@ async function validateTraceability(root, config) {
|
|
|
3050
3473
|
"QFAI-TRACE-021",
|
|
3051
3474
|
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
3052
3475
|
", "
|
|
3053
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3476
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3054
3477
|
"error",
|
|
3055
3478
|
file,
|
|
3056
3479
|
"traceability.specContractRefFormat",
|
|
@@ -3114,7 +3537,7 @@ async function validateTraceability(root, config) {
|
|
|
3114
3537
|
"QFAI-TRACE-032",
|
|
3115
3538
|
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
3116
3539
|
", "
|
|
3117
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
3540
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
3118
3541
|
"error",
|
|
3119
3542
|
file,
|
|
3120
3543
|
"traceability.scenarioContractRefFormat",
|
|
@@ -3493,17 +3916,25 @@ function countIssues(issues) {
|
|
|
3493
3916
|
}
|
|
3494
3917
|
|
|
3495
3918
|
// src/core/report.ts
|
|
3496
|
-
var ID_PREFIXES2 = [
|
|
3919
|
+
var ID_PREFIXES2 = [
|
|
3920
|
+
"SPEC",
|
|
3921
|
+
"BR",
|
|
3922
|
+
"SC",
|
|
3923
|
+
"UI",
|
|
3924
|
+
"API",
|
|
3925
|
+
"DB",
|
|
3926
|
+
"THEMA"
|
|
3927
|
+
];
|
|
3497
3928
|
async function createReportData(root, validation, configResult) {
|
|
3498
|
-
const resolvedRoot =
|
|
3929
|
+
const resolvedRoot = import_node_path21.default.resolve(root);
|
|
3499
3930
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3500
3931
|
const config = resolved.config;
|
|
3501
3932
|
const configPath = resolved.configPath;
|
|
3502
3933
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3503
3934
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3504
|
-
const apiRoot =
|
|
3505
|
-
const uiRoot =
|
|
3506
|
-
const dbRoot =
|
|
3935
|
+
const apiRoot = import_node_path21.default.join(contractsRoot, "api");
|
|
3936
|
+
const uiRoot = import_node_path21.default.join(contractsRoot, "ui");
|
|
3937
|
+
const dbRoot = import_node_path21.default.join(contractsRoot, "db");
|
|
3507
3938
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3508
3939
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3509
3940
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3511,7 +3942,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3511
3942
|
const {
|
|
3512
3943
|
api: apiFiles,
|
|
3513
3944
|
ui: uiFiles,
|
|
3514
|
-
db: dbFiles
|
|
3945
|
+
db: dbFiles,
|
|
3946
|
+
thema: themaFiles
|
|
3515
3947
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
3516
3948
|
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
3517
3949
|
const contractIdList = Array.from(contractIndex.ids);
|
|
@@ -3538,7 +3970,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3538
3970
|
...scenarioFiles,
|
|
3539
3971
|
...apiFiles,
|
|
3540
3972
|
...uiFiles,
|
|
3541
|
-
...dbFiles
|
|
3973
|
+
...dbFiles,
|
|
3974
|
+
...themaFiles
|
|
3542
3975
|
]);
|
|
3543
3976
|
const upstreamIds = await collectUpstreamIds([
|
|
3544
3977
|
...specFiles,
|
|
@@ -3575,7 +4008,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3575
4008
|
contracts: {
|
|
3576
4009
|
api: apiFiles.length,
|
|
3577
4010
|
ui: uiFiles.length,
|
|
3578
|
-
db: dbFiles.length
|
|
4011
|
+
db: dbFiles.length,
|
|
4012
|
+
thema: themaFiles.length
|
|
3579
4013
|
},
|
|
3580
4014
|
counts: normalizedValidation.counts
|
|
3581
4015
|
},
|
|
@@ -3585,7 +4019,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
3585
4019
|
sc: idsByPrefix.SC,
|
|
3586
4020
|
ui: idsByPrefix.UI,
|
|
3587
4021
|
api: idsByPrefix.API,
|
|
3588
|
-
db: idsByPrefix.DB
|
|
4022
|
+
db: idsByPrefix.DB,
|
|
4023
|
+
thema: idsByPrefix.THEMA
|
|
3589
4024
|
},
|
|
3590
4025
|
traceability: {
|
|
3591
4026
|
upstreamIdsFound: upstreamIds.size,
|
|
@@ -3655,7 +4090,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3655
4090
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
3656
4091
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
3657
4092
|
lines.push(
|
|
3658
|
-
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
4093
|
+
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
|
|
3659
4094
|
);
|
|
3660
4095
|
lines.push(
|
|
3661
4096
|
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
@@ -3799,6 +4234,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3799
4234
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
3800
4235
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3801
4236
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
4237
|
+
lines.push(formatIdLine("THEMA", data.ids.thema));
|
|
3802
4238
|
lines.push("");
|
|
3803
4239
|
lines.push("## Traceability");
|
|
3804
4240
|
lines.push("");
|
|
@@ -3963,12 +4399,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3963
4399
|
"- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3964
4400
|
);
|
|
3965
4401
|
}
|
|
3966
|
-
lines.push(
|
|
3967
|
-
|
|
3968
|
-
);
|
|
3969
|
-
lines.push(
|
|
3970
|
-
"- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
|
|
3971
|
-
);
|
|
4402
|
+
lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
|
|
4403
|
+
lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md`");
|
|
3972
4404
|
return lines.join("\n");
|
|
3973
4405
|
}
|
|
3974
4406
|
function formatReportJson(data) {
|
|
@@ -4020,7 +4452,8 @@ async function collectIds(files) {
|
|
|
4020
4452
|
SC: /* @__PURE__ */ new Set(),
|
|
4021
4453
|
UI: /* @__PURE__ */ new Set(),
|
|
4022
4454
|
API: /* @__PURE__ */ new Set(),
|
|
4023
|
-
DB: /* @__PURE__ */ new Set()
|
|
4455
|
+
DB: /* @__PURE__ */ new Set(),
|
|
4456
|
+
THEMA: /* @__PURE__ */ new Set()
|
|
4024
4457
|
};
|
|
4025
4458
|
for (const file of files) {
|
|
4026
4459
|
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
@@ -4035,7 +4468,8 @@ async function collectIds(files) {
|
|
|
4035
4468
|
SC: toSortedArray2(result.SC),
|
|
4036
4469
|
UI: toSortedArray2(result.UI),
|
|
4037
4470
|
API: toSortedArray2(result.API),
|
|
4038
|
-
DB: toSortedArray2(result.DB)
|
|
4471
|
+
DB: toSortedArray2(result.DB),
|
|
4472
|
+
THEMA: toSortedArray2(result.THEMA)
|
|
4039
4473
|
};
|
|
4040
4474
|
}
|
|
4041
4475
|
async function collectUpstreamIds(files) {
|
|
@@ -4199,7 +4633,7 @@ function warnIfTruncated(scan, context) {
|
|
|
4199
4633
|
|
|
4200
4634
|
// src/cli/commands/report.ts
|
|
4201
4635
|
async function runReport(options) {
|
|
4202
|
-
const root =
|
|
4636
|
+
const root = import_node_path22.default.resolve(options.root);
|
|
4203
4637
|
const configResult = await loadConfig(root);
|
|
4204
4638
|
let validation;
|
|
4205
4639
|
if (options.runValidate) {
|
|
@@ -4216,7 +4650,7 @@ async function runReport(options) {
|
|
|
4216
4650
|
validation = normalized;
|
|
4217
4651
|
} else {
|
|
4218
4652
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
4219
|
-
const inputPath =
|
|
4653
|
+
const inputPath = import_node_path22.default.isAbsolute(input) ? input : import_node_path22.default.resolve(root, input);
|
|
4220
4654
|
try {
|
|
4221
4655
|
validation = await readValidationResult(inputPath);
|
|
4222
4656
|
} catch (err) {
|
|
@@ -4243,10 +4677,10 @@ async function runReport(options) {
|
|
|
4243
4677
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4244
4678
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4245
4679
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4246
|
-
const defaultOut = options.format === "json" ?
|
|
4680
|
+
const defaultOut = options.format === "json" ? import_node_path22.default.join(outRoot, "report.json") : import_node_path22.default.join(outRoot, "report.md");
|
|
4247
4681
|
const out = options.outPath ?? defaultOut;
|
|
4248
|
-
const outPath =
|
|
4249
|
-
await (0, import_promises20.mkdir)(
|
|
4682
|
+
const outPath = import_node_path22.default.isAbsolute(out) ? out : import_node_path22.default.resolve(root, out);
|
|
4683
|
+
await (0, import_promises20.mkdir)(import_node_path22.default.dirname(outPath), { recursive: true });
|
|
4250
4684
|
await (0, import_promises20.writeFile)(outPath, `${output}
|
|
4251
4685
|
`, "utf-8");
|
|
4252
4686
|
info(
|
|
@@ -4311,15 +4745,15 @@ function isMissingFileError5(error2) {
|
|
|
4311
4745
|
return record2.code === "ENOENT";
|
|
4312
4746
|
}
|
|
4313
4747
|
async function writeValidationResult(root, outputPath, result) {
|
|
4314
|
-
const abs =
|
|
4315
|
-
await (0, import_promises20.mkdir)(
|
|
4748
|
+
const abs = import_node_path22.default.isAbsolute(outputPath) ? outputPath : import_node_path22.default.resolve(root, outputPath);
|
|
4749
|
+
await (0, import_promises20.mkdir)(import_node_path22.default.dirname(abs), { recursive: true });
|
|
4316
4750
|
await (0, import_promises20.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
4317
4751
|
`, "utf-8");
|
|
4318
4752
|
}
|
|
4319
4753
|
|
|
4320
4754
|
// src/cli/commands/validate.ts
|
|
4321
4755
|
var import_promises21 = require("fs/promises");
|
|
4322
|
-
var
|
|
4756
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
4323
4757
|
|
|
4324
4758
|
// src/cli/lib/failOn.ts
|
|
4325
4759
|
function shouldFail(result, failOn) {
|
|
@@ -4334,7 +4768,7 @@ function shouldFail(result, failOn) {
|
|
|
4334
4768
|
|
|
4335
4769
|
// src/cli/commands/validate.ts
|
|
4336
4770
|
async function runValidate(options) {
|
|
4337
|
-
const root =
|
|
4771
|
+
const root = import_node_path23.default.resolve(options.root);
|
|
4338
4772
|
const configResult = await loadConfig(root);
|
|
4339
4773
|
const result = await validateProject(root, configResult);
|
|
4340
4774
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -4459,12 +4893,12 @@ function issueKey(issue7) {
|
|
|
4459
4893
|
}
|
|
4460
4894
|
async function emitJson(result, root, jsonPath) {
|
|
4461
4895
|
const abs = resolveJsonPath(root, jsonPath);
|
|
4462
|
-
await (0, import_promises21.mkdir)(
|
|
4896
|
+
await (0, import_promises21.mkdir)(import_node_path23.default.dirname(abs), { recursive: true });
|
|
4463
4897
|
await (0, import_promises21.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
4464
4898
|
`, "utf-8");
|
|
4465
4899
|
}
|
|
4466
4900
|
function resolveJsonPath(root, jsonPath) {
|
|
4467
|
-
return
|
|
4901
|
+
return import_node_path23.default.isAbsolute(jsonPath) ? jsonPath : import_node_path23.default.resolve(root, jsonPath);
|
|
4468
4902
|
}
|
|
4469
4903
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
4470
4904
|
|