qfai 1.0.2 → 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 +11 -1
- 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/specs/README.md +1 -1
- package/assets/init/.qfai/specs/spec-0001/scenario.feature +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
- package/dist/cli/index.cjs +585 -109
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +587 -111
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +538 -62
- 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 +540 -64
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -399,7 +399,15 @@ function isRecord(value) {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
// src/core/ids.ts
|
|
402
|
-
var ID_PREFIXES = [
|
|
402
|
+
var ID_PREFIXES = [
|
|
403
|
+
"SPEC",
|
|
404
|
+
"BR",
|
|
405
|
+
"SC",
|
|
406
|
+
"UI",
|
|
407
|
+
"API",
|
|
408
|
+
"DB",
|
|
409
|
+
"THEMA"
|
|
410
|
+
];
|
|
403
411
|
var STRICT_ID_PATTERNS = {
|
|
404
412
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
405
413
|
BR: /\bBR-\d{4}\b/g,
|
|
@@ -407,6 +415,7 @@ var STRICT_ID_PATTERNS = {
|
|
|
407
415
|
UI: /\bUI-\d{4}\b/g,
|
|
408
416
|
API: /\bAPI-\d{4}\b/g,
|
|
409
417
|
DB: /\bDB-\d{4}\b/g,
|
|
418
|
+
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
410
419
|
ADR: /\bADR-\d{4}\b/g
|
|
411
420
|
};
|
|
412
421
|
var LOOSE_ID_PATTERNS = {
|
|
@@ -416,6 +425,7 @@ var LOOSE_ID_PATTERNS = {
|
|
|
416
425
|
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
417
426
|
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
418
427
|
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
428
|
+
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
419
429
|
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
420
430
|
};
|
|
421
431
|
function extractIds(text, prefix) {
|
|
@@ -453,14 +463,15 @@ function isValidId(value, prefix) {
|
|
|
453
463
|
|
|
454
464
|
// src/core/report.ts
|
|
455
465
|
import { readFile as readFile12 } from "fs/promises";
|
|
456
|
-
import
|
|
466
|
+
import path16 from "path";
|
|
457
467
|
|
|
458
468
|
// src/core/contractIndex.ts
|
|
459
469
|
import { readFile as readFile2 } from "fs/promises";
|
|
460
|
-
import
|
|
470
|
+
import path5 from "path";
|
|
461
471
|
|
|
462
472
|
// src/core/discovery.ts
|
|
463
473
|
import { access as access3 } from "fs/promises";
|
|
474
|
+
import path4 from "path";
|
|
464
475
|
|
|
465
476
|
// src/core/fs.ts
|
|
466
477
|
import { access as access2, readdir } from "fs/promises";
|
|
@@ -609,7 +620,12 @@ async function collectScenarioFiles(specsRoot) {
|
|
|
609
620
|
return filterExisting(entries.map((entry) => entry.scenarioPath));
|
|
610
621
|
}
|
|
611
622
|
async function collectUiContractFiles(uiRoot) {
|
|
612
|
-
|
|
623
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
624
|
+
return filterByBasenamePrefix(files, "ui-");
|
|
625
|
+
}
|
|
626
|
+
async function collectThemaContractFiles(uiRoot) {
|
|
627
|
+
const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
|
|
628
|
+
return filterByBasenamePrefix(files, "thema-");
|
|
613
629
|
}
|
|
614
630
|
async function collectApiContractFiles(apiRoot) {
|
|
615
631
|
return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
|
|
@@ -618,12 +634,13 @@ async function collectDbContractFiles(dbRoot) {
|
|
|
618
634
|
return collectFiles(dbRoot, { extensions: [".sql"] });
|
|
619
635
|
}
|
|
620
636
|
async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
621
|
-
const [ui, api, db] = await Promise.all([
|
|
637
|
+
const [ui, thema, api, db] = await Promise.all([
|
|
622
638
|
collectUiContractFiles(uiRoot),
|
|
639
|
+
collectThemaContractFiles(uiRoot),
|
|
623
640
|
collectApiContractFiles(apiRoot),
|
|
624
641
|
collectDbContractFiles(dbRoot)
|
|
625
642
|
]);
|
|
626
|
-
return { ui, api, db };
|
|
643
|
+
return { ui, thema, api, db };
|
|
627
644
|
}
|
|
628
645
|
async function filterExisting(files) {
|
|
629
646
|
const existing = [];
|
|
@@ -642,10 +659,16 @@ async function exists3(target) {
|
|
|
642
659
|
return false;
|
|
643
660
|
}
|
|
644
661
|
}
|
|
662
|
+
function filterByBasenamePrefix(files, prefix) {
|
|
663
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
664
|
+
return files.filter(
|
|
665
|
+
(file) => path4.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
645
668
|
|
|
646
669
|
// src/core/contractsDecl.ts
|
|
647
|
-
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
|
|
648
|
-
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
|
|
670
|
+
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
671
|
+
var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
|
|
649
672
|
function extractDeclaredContractIds(text) {
|
|
650
673
|
const ids = [];
|
|
651
674
|
for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
|
|
@@ -663,20 +686,22 @@ function stripContractDeclarationLines(text) {
|
|
|
663
686
|
// src/core/contractIndex.ts
|
|
664
687
|
async function buildContractIndex(root, config) {
|
|
665
688
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
666
|
-
const uiRoot =
|
|
667
|
-
const apiRoot =
|
|
668
|
-
const dbRoot =
|
|
669
|
-
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
689
|
+
const uiRoot = path5.join(contractsRoot, "ui");
|
|
690
|
+
const apiRoot = path5.join(contractsRoot, "api");
|
|
691
|
+
const dbRoot = path5.join(contractsRoot, "db");
|
|
692
|
+
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
670
693
|
collectUiContractFiles(uiRoot),
|
|
694
|
+
collectThemaContractFiles(uiRoot),
|
|
671
695
|
collectApiContractFiles(apiRoot),
|
|
672
696
|
collectDbContractFiles(dbRoot)
|
|
673
697
|
]);
|
|
674
698
|
const index = {
|
|
675
699
|
ids: /* @__PURE__ */ new Set(),
|
|
676
700
|
idToFiles: /* @__PURE__ */ new Map(),
|
|
677
|
-
files: { ui: uiFiles, api: apiFiles, db: dbFiles }
|
|
701
|
+
files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
|
|
678
702
|
};
|
|
679
703
|
await indexContractFiles(uiFiles, index);
|
|
704
|
+
await indexContractFiles(themaFiles, index);
|
|
680
705
|
await indexContractFiles(apiFiles, index);
|
|
681
706
|
await indexContractFiles(dbFiles, index);
|
|
682
707
|
return index;
|
|
@@ -695,15 +720,15 @@ function record(index, id, file) {
|
|
|
695
720
|
}
|
|
696
721
|
|
|
697
722
|
// src/core/paths.ts
|
|
698
|
-
import
|
|
723
|
+
import path6 from "path";
|
|
699
724
|
function toRelativePath(root, target) {
|
|
700
725
|
if (!target) {
|
|
701
726
|
return target;
|
|
702
727
|
}
|
|
703
|
-
if (!
|
|
728
|
+
if (!path6.isAbsolute(target)) {
|
|
704
729
|
return toPosixPath(target);
|
|
705
730
|
}
|
|
706
|
-
const relative =
|
|
731
|
+
const relative = path6.relative(root, target);
|
|
707
732
|
if (!relative) {
|
|
708
733
|
return ".";
|
|
709
734
|
}
|
|
@@ -751,7 +776,7 @@ function normalizeValidationResult(root, result) {
|
|
|
751
776
|
}
|
|
752
777
|
|
|
753
778
|
// src/core/parse/contractRefs.ts
|
|
754
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
779
|
+
var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
|
|
755
780
|
function parseContractRefs(text, options = {}) {
|
|
756
781
|
const linePattern = buildLinePattern(options);
|
|
757
782
|
const lines = [];
|
|
@@ -922,7 +947,7 @@ function parseSpec(md, file) {
|
|
|
922
947
|
|
|
923
948
|
// src/core/traceability.ts
|
|
924
949
|
import { readFile as readFile3 } from "fs/promises";
|
|
925
|
-
import
|
|
950
|
+
import path7 from "path";
|
|
926
951
|
|
|
927
952
|
// src/core/gherkin/parse.ts
|
|
928
953
|
import {
|
|
@@ -1154,7 +1179,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1154
1179
|
};
|
|
1155
1180
|
}
|
|
1156
1181
|
const normalizedFiles = Array.from(
|
|
1157
|
-
new Set(scanResult.files.map((file) =>
|
|
1182
|
+
new Set(scanResult.files.map((file) => path7.normalize(file)))
|
|
1158
1183
|
);
|
|
1159
1184
|
for (const file of normalizedFiles) {
|
|
1160
1185
|
const text = await readFile3(file, "utf-8");
|
|
@@ -1217,11 +1242,11 @@ function formatError3(error) {
|
|
|
1217
1242
|
|
|
1218
1243
|
// src/core/version.ts
|
|
1219
1244
|
import { readFile as readFile4 } from "fs/promises";
|
|
1220
|
-
import
|
|
1245
|
+
import path8 from "path";
|
|
1221
1246
|
import { fileURLToPath } from "url";
|
|
1222
1247
|
async function resolveToolVersion() {
|
|
1223
|
-
if ("1.0.
|
|
1224
|
-
return "1.0.
|
|
1248
|
+
if ("1.0.3".length > 0) {
|
|
1249
|
+
return "1.0.3";
|
|
1225
1250
|
}
|
|
1226
1251
|
try {
|
|
1227
1252
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1236,18 +1261,18 @@ async function resolveToolVersion() {
|
|
|
1236
1261
|
function resolvePackageJsonPath() {
|
|
1237
1262
|
const base = import.meta.url;
|
|
1238
1263
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
1239
|
-
return
|
|
1264
|
+
return path8.resolve(path8.dirname(basePath), "../../package.json");
|
|
1240
1265
|
}
|
|
1241
1266
|
|
|
1242
1267
|
// src/core/validators/contracts.ts
|
|
1243
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
1244
|
-
import
|
|
1268
|
+
import { access as access4, readFile as readFile5 } from "fs/promises";
|
|
1269
|
+
import path10 from "path";
|
|
1245
1270
|
|
|
1246
1271
|
// src/core/contracts.ts
|
|
1247
|
-
import
|
|
1272
|
+
import path9 from "path";
|
|
1248
1273
|
import { parse as parseYaml2 } from "yaml";
|
|
1249
1274
|
function parseStructuredContract(file, text) {
|
|
1250
|
-
const ext =
|
|
1275
|
+
const ext = path9.extname(file).toLowerCase();
|
|
1251
1276
|
if (ext === ".json") {
|
|
1252
1277
|
return JSON.parse(text);
|
|
1253
1278
|
}
|
|
@@ -1264,17 +1289,23 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1264
1289
|
label: "ALTER TABLE ... DROP"
|
|
1265
1290
|
}
|
|
1266
1291
|
];
|
|
1292
|
+
var THEMA_ID_RE = /^THEMA-\d{3}$/;
|
|
1267
1293
|
async function validateContracts(root, config) {
|
|
1268
1294
|
const issues = [];
|
|
1269
|
-
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1270
|
-
issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
|
|
1271
|
-
issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
|
|
1272
|
-
issues.push(...await validateDbContracts(path9.join(contractsRoot, "db")));
|
|
1273
1295
|
const contractIndex = await buildContractIndex(root, config);
|
|
1296
|
+
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1297
|
+
const uiRoot = path10.join(contractsRoot, "ui");
|
|
1298
|
+
const themaIds = new Set(
|
|
1299
|
+
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
1300
|
+
);
|
|
1301
|
+
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
1302
|
+
issues.push(...await validateThemaContracts(uiRoot));
|
|
1303
|
+
issues.push(...await validateApiContracts(path10.join(contractsRoot, "api")));
|
|
1304
|
+
issues.push(...await validateDbContracts(path10.join(contractsRoot, "db")));
|
|
1274
1305
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1275
1306
|
return issues;
|
|
1276
1307
|
}
|
|
1277
|
-
async function validateUiContracts(uiRoot) {
|
|
1308
|
+
async function validateUiContracts(uiRoot, themaIds) {
|
|
1278
1309
|
const files = await collectUiContractFiles(uiRoot);
|
|
1279
1310
|
if (files.length === 0) {
|
|
1280
1311
|
return [
|
|
@@ -1288,6 +1319,60 @@ async function validateUiContracts(uiRoot) {
|
|
|
1288
1319
|
];
|
|
1289
1320
|
}
|
|
1290
1321
|
const issues = [];
|
|
1322
|
+
for (const file of files) {
|
|
1323
|
+
const text = await readFile5(file, "utf-8");
|
|
1324
|
+
const declaredIds = extractDeclaredContractIds(text);
|
|
1325
|
+
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1326
|
+
let doc = null;
|
|
1327
|
+
try {
|
|
1328
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
issues.push(
|
|
1331
|
+
issue(
|
|
1332
|
+
"QFAI-CONTRACT-001",
|
|
1333
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
1334
|
+
"error",
|
|
1335
|
+
file,
|
|
1336
|
+
"contracts.ui.parse"
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const invalidIds = extractInvalidIds(text, [
|
|
1341
|
+
"SPEC",
|
|
1342
|
+
"BR",
|
|
1343
|
+
"SC",
|
|
1344
|
+
"UI",
|
|
1345
|
+
"API",
|
|
1346
|
+
"DB",
|
|
1347
|
+
"THEMA",
|
|
1348
|
+
"ADR"
|
|
1349
|
+
]).filter((id) => !shouldIgnoreInvalidId(id, doc));
|
|
1350
|
+
if (invalidIds.length > 0) {
|
|
1351
|
+
issues.push(
|
|
1352
|
+
issue(
|
|
1353
|
+
"QFAI-ID-002",
|
|
1354
|
+
`ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
|
|
1355
|
+
"error",
|
|
1356
|
+
file,
|
|
1357
|
+
"id.format",
|
|
1358
|
+
invalidIds
|
|
1359
|
+
)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
if (doc) {
|
|
1363
|
+
issues.push(
|
|
1364
|
+
...await validateUiContractDoc(doc, file, uiRoot, themaIds)
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return issues;
|
|
1369
|
+
}
|
|
1370
|
+
async function validateThemaContracts(uiRoot) {
|
|
1371
|
+
const files = await collectThemaContractFiles(uiRoot);
|
|
1372
|
+
if (files.length === 0) {
|
|
1373
|
+
return [];
|
|
1374
|
+
}
|
|
1375
|
+
const issues = [];
|
|
1291
1376
|
for (const file of files) {
|
|
1292
1377
|
const text = await readFile5(file, "utf-8");
|
|
1293
1378
|
const invalidIds = extractInvalidIds(text, [
|
|
@@ -1297,6 +1382,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
1297
1382
|
"UI",
|
|
1298
1383
|
"API",
|
|
1299
1384
|
"DB",
|
|
1385
|
+
"THEMA",
|
|
1300
1386
|
"ADR"
|
|
1301
1387
|
]);
|
|
1302
1388
|
if (invalidIds.length > 0) {
|
|
@@ -1312,17 +1398,95 @@ async function validateUiContracts(uiRoot) {
|
|
|
1312
1398
|
);
|
|
1313
1399
|
}
|
|
1314
1400
|
const declaredIds = extractDeclaredContractIds(text);
|
|
1315
|
-
|
|
1401
|
+
if (declaredIds.length === 0) {
|
|
1402
|
+
issues.push(
|
|
1403
|
+
issue(
|
|
1404
|
+
"QFAI-THEMA-010",
|
|
1405
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
|
|
1406
|
+
"error",
|
|
1407
|
+
file,
|
|
1408
|
+
"contracts.thema.declaration"
|
|
1409
|
+
)
|
|
1410
|
+
);
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
if (declaredIds.length > 1) {
|
|
1414
|
+
issues.push(
|
|
1415
|
+
issue(
|
|
1416
|
+
"QFAI-THEMA-011",
|
|
1417
|
+
`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(
|
|
1418
|
+
", "
|
|
1419
|
+
)}`,
|
|
1420
|
+
"error",
|
|
1421
|
+
file,
|
|
1422
|
+
"contracts.thema.declaration",
|
|
1423
|
+
declaredIds
|
|
1424
|
+
)
|
|
1425
|
+
);
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
const declaredId = declaredIds[0] ?? "";
|
|
1429
|
+
if (!THEMA_ID_RE.test(declaredId)) {
|
|
1430
|
+
issues.push(
|
|
1431
|
+
issue(
|
|
1432
|
+
"QFAI-THEMA-012",
|
|
1433
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
|
|
1434
|
+
"error",
|
|
1435
|
+
file,
|
|
1436
|
+
"contracts.thema.idFormat",
|
|
1437
|
+
[declaredId]
|
|
1438
|
+
)
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
let doc;
|
|
1316
1442
|
try {
|
|
1317
|
-
parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1443
|
+
doc = parseStructuredContract(file, stripContractDeclarationLines(text));
|
|
1318
1444
|
} catch (error) {
|
|
1319
1445
|
issues.push(
|
|
1320
1446
|
issue(
|
|
1321
|
-
"QFAI-
|
|
1322
|
-
`
|
|
1447
|
+
"QFAI-THEMA-001",
|
|
1448
|
+
`thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
|
|
1323
1449
|
"error",
|
|
1324
1450
|
file,
|
|
1325
|
-
"contracts.
|
|
1451
|
+
"contracts.thema.parse"
|
|
1452
|
+
)
|
|
1453
|
+
);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
const docId = typeof doc.id === "string" ? doc.id : "";
|
|
1457
|
+
if (!THEMA_ID_RE.test(docId)) {
|
|
1458
|
+
issues.push(
|
|
1459
|
+
issue(
|
|
1460
|
+
"QFAI-THEMA-012",
|
|
1461
|
+
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",
|
|
1462
|
+
"error",
|
|
1463
|
+
file,
|
|
1464
|
+
"contracts.thema.idFormat",
|
|
1465
|
+
docId.length > 0 ? [docId] : void 0
|
|
1466
|
+
)
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const name = typeof doc.name === "string" ? doc.name : "";
|
|
1470
|
+
if (!name) {
|
|
1471
|
+
issues.push(
|
|
1472
|
+
issue(
|
|
1473
|
+
"QFAI-THEMA-014",
|
|
1474
|
+
"thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1475
|
+
"error",
|
|
1476
|
+
file,
|
|
1477
|
+
"contracts.thema.name"
|
|
1478
|
+
)
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
if (declaredId && docId && declaredId !== docId) {
|
|
1482
|
+
issues.push(
|
|
1483
|
+
issue(
|
|
1484
|
+
"QFAI-THEMA-013",
|
|
1485
|
+
`thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
|
|
1486
|
+
"error",
|
|
1487
|
+
file,
|
|
1488
|
+
"contracts.thema.idMismatch",
|
|
1489
|
+
[declaredId, docId]
|
|
1326
1490
|
)
|
|
1327
1491
|
);
|
|
1328
1492
|
}
|
|
@@ -1352,6 +1516,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1352
1516
|
"UI",
|
|
1353
1517
|
"API",
|
|
1354
1518
|
"DB",
|
|
1519
|
+
"THEMA",
|
|
1355
1520
|
"ADR"
|
|
1356
1521
|
]);
|
|
1357
1522
|
if (invalidIds.length > 0) {
|
|
@@ -1420,6 +1585,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1420
1585
|
"UI",
|
|
1421
1586
|
"API",
|
|
1422
1587
|
"DB",
|
|
1588
|
+
"THEMA",
|
|
1423
1589
|
"ADR"
|
|
1424
1590
|
]);
|
|
1425
1591
|
if (invalidIds.length > 0) {
|
|
@@ -1526,6 +1692,278 @@ function validateDuplicateContractIds(contractIndex) {
|
|
|
1526
1692
|
function hasOpenApi(doc) {
|
|
1527
1693
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
1528
1694
|
}
|
|
1695
|
+
async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
|
|
1696
|
+
const issues = [];
|
|
1697
|
+
if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
|
|
1698
|
+
const themaRef = doc.themaRef;
|
|
1699
|
+
if (typeof themaRef !== "string" || themaRef.length === 0) {
|
|
1700
|
+
issues.push(
|
|
1701
|
+
issue(
|
|
1702
|
+
"QFAI-UI-020",
|
|
1703
|
+
"themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1704
|
+
"error",
|
|
1705
|
+
file,
|
|
1706
|
+
"contracts.ui.themaRef"
|
|
1707
|
+
)
|
|
1708
|
+
);
|
|
1709
|
+
} else if (!THEMA_ID_RE.test(themaRef)) {
|
|
1710
|
+
issues.push(
|
|
1711
|
+
issue(
|
|
1712
|
+
"QFAI-UI-020",
|
|
1713
|
+
`themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
|
|
1714
|
+
"error",
|
|
1715
|
+
file,
|
|
1716
|
+
"contracts.ui.themaRef",
|
|
1717
|
+
[themaRef]
|
|
1718
|
+
)
|
|
1719
|
+
);
|
|
1720
|
+
} else if (!themaIds.has(themaRef)) {
|
|
1721
|
+
issues.push(
|
|
1722
|
+
issue(
|
|
1723
|
+
"QFAI-UI-020",
|
|
1724
|
+
`themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
|
|
1725
|
+
"error",
|
|
1726
|
+
file,
|
|
1727
|
+
"contracts.ui.themaRef",
|
|
1728
|
+
[themaRef]
|
|
1729
|
+
)
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const assets = doc.assets;
|
|
1734
|
+
if (assets && typeof assets === "object") {
|
|
1735
|
+
issues.push(
|
|
1736
|
+
...await validateUiAssets(
|
|
1737
|
+
assets,
|
|
1738
|
+
file,
|
|
1739
|
+
uiRoot
|
|
1740
|
+
)
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
return issues;
|
|
1744
|
+
}
|
|
1745
|
+
async function validateUiAssets(assets, file, uiRoot) {
|
|
1746
|
+
const issues = [];
|
|
1747
|
+
const packValue = assets.pack;
|
|
1748
|
+
const useValue = assets.use;
|
|
1749
|
+
if (packValue === void 0 && useValue === void 0) {
|
|
1750
|
+
return issues;
|
|
1751
|
+
}
|
|
1752
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
1753
|
+
issues.push(
|
|
1754
|
+
issue(
|
|
1755
|
+
"QFAI-ASSET-001",
|
|
1756
|
+
"assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1757
|
+
"error",
|
|
1758
|
+
file,
|
|
1759
|
+
"assets.pack"
|
|
1760
|
+
)
|
|
1761
|
+
);
|
|
1762
|
+
return issues;
|
|
1763
|
+
}
|
|
1764
|
+
if (!isSafeRelativePath(packValue)) {
|
|
1765
|
+
issues.push(
|
|
1766
|
+
issue(
|
|
1767
|
+
"QFAI-ASSET-001",
|
|
1768
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
|
|
1769
|
+
"error",
|
|
1770
|
+
file,
|
|
1771
|
+
"assets.pack",
|
|
1772
|
+
[packValue]
|
|
1773
|
+
)
|
|
1774
|
+
);
|
|
1775
|
+
return issues;
|
|
1776
|
+
}
|
|
1777
|
+
const packDir = path10.resolve(uiRoot, packValue);
|
|
1778
|
+
const packRelative = path10.relative(uiRoot, packDir);
|
|
1779
|
+
if (packRelative.startsWith("..") || path10.isAbsolute(packRelative)) {
|
|
1780
|
+
issues.push(
|
|
1781
|
+
issue(
|
|
1782
|
+
"QFAI-ASSET-001",
|
|
1783
|
+
`assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
|
|
1784
|
+
"error",
|
|
1785
|
+
file,
|
|
1786
|
+
"assets.pack",
|
|
1787
|
+
[packValue]
|
|
1788
|
+
)
|
|
1789
|
+
);
|
|
1790
|
+
return issues;
|
|
1791
|
+
}
|
|
1792
|
+
if (!await exists4(packDir)) {
|
|
1793
|
+
issues.push(
|
|
1794
|
+
issue(
|
|
1795
|
+
"QFAI-ASSET-001",
|
|
1796
|
+
`assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
|
|
1797
|
+
"error",
|
|
1798
|
+
file,
|
|
1799
|
+
"assets.pack",
|
|
1800
|
+
[packValue]
|
|
1801
|
+
)
|
|
1802
|
+
);
|
|
1803
|
+
return issues;
|
|
1804
|
+
}
|
|
1805
|
+
const assetsYamlPath = path10.join(packDir, "assets.yaml");
|
|
1806
|
+
if (!await exists4(assetsYamlPath)) {
|
|
1807
|
+
issues.push(
|
|
1808
|
+
issue(
|
|
1809
|
+
"QFAI-ASSET-002",
|
|
1810
|
+
`assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
|
|
1811
|
+
"error",
|
|
1812
|
+
assetsYamlPath,
|
|
1813
|
+
"assets.yaml"
|
|
1814
|
+
)
|
|
1815
|
+
);
|
|
1816
|
+
return issues;
|
|
1817
|
+
}
|
|
1818
|
+
let manifest;
|
|
1819
|
+
try {
|
|
1820
|
+
const manifestText = await readFile5(assetsYamlPath, "utf-8");
|
|
1821
|
+
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
issues.push(
|
|
1824
|
+
issue(
|
|
1825
|
+
"QFAI-ASSET-002",
|
|
1826
|
+
`assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error)})`,
|
|
1827
|
+
"error",
|
|
1828
|
+
assetsYamlPath,
|
|
1829
|
+
"assets.yaml"
|
|
1830
|
+
)
|
|
1831
|
+
);
|
|
1832
|
+
return issues;
|
|
1833
|
+
}
|
|
1834
|
+
const items = Array.isArray(manifest.items) ? manifest.items : [];
|
|
1835
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
1836
|
+
const itemPaths = [];
|
|
1837
|
+
for (const item of items) {
|
|
1838
|
+
if (!item || typeof item !== "object") {
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
const record2 = item;
|
|
1842
|
+
const id = typeof record2.id === "string" ? record2.id : void 0;
|
|
1843
|
+
const pathValue = typeof record2.path === "string" ? record2.path : void 0;
|
|
1844
|
+
if (id) {
|
|
1845
|
+
itemIds.add(id);
|
|
1846
|
+
}
|
|
1847
|
+
itemPaths.push({ id, path: pathValue });
|
|
1848
|
+
}
|
|
1849
|
+
if (useValue !== void 0) {
|
|
1850
|
+
if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
|
|
1851
|
+
issues.push(
|
|
1852
|
+
issue(
|
|
1853
|
+
"QFAI-ASSET-003",
|
|
1854
|
+
"assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1855
|
+
"error",
|
|
1856
|
+
file,
|
|
1857
|
+
"assets.use"
|
|
1858
|
+
)
|
|
1859
|
+
);
|
|
1860
|
+
} else {
|
|
1861
|
+
const missing = useValue.filter((entry) => !itemIds.has(entry));
|
|
1862
|
+
if (missing.length > 0) {
|
|
1863
|
+
issues.push(
|
|
1864
|
+
issue(
|
|
1865
|
+
"QFAI-ASSET-003",
|
|
1866
|
+
`assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
|
|
1867
|
+
"error",
|
|
1868
|
+
file,
|
|
1869
|
+
"assets.use",
|
|
1870
|
+
missing
|
|
1871
|
+
)
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
for (const entry of itemPaths) {
|
|
1877
|
+
if (!entry.path) {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
1881
|
+
issues.push(
|
|
1882
|
+
issue(
|
|
1883
|
+
"QFAI-ASSET-004",
|
|
1884
|
+
`assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
|
|
1885
|
+
"error",
|
|
1886
|
+
assetsYamlPath,
|
|
1887
|
+
"assets.path",
|
|
1888
|
+
entry.id ? [entry.id] : [entry.path]
|
|
1889
|
+
)
|
|
1890
|
+
);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const assetPath = path10.resolve(packDir, entry.path);
|
|
1894
|
+
const assetRelative = path10.relative(packDir, assetPath);
|
|
1895
|
+
if (assetRelative.startsWith("..") || path10.isAbsolute(assetRelative)) {
|
|
1896
|
+
issues.push(
|
|
1897
|
+
issue(
|
|
1898
|
+
"QFAI-ASSET-004",
|
|
1899
|
+
`assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
|
|
1900
|
+
"error",
|
|
1901
|
+
assetsYamlPath,
|
|
1902
|
+
"assets.path",
|
|
1903
|
+
entry.id ? [entry.id] : [entry.path]
|
|
1904
|
+
)
|
|
1905
|
+
);
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
if (!await exists4(assetPath)) {
|
|
1909
|
+
issues.push(
|
|
1910
|
+
issue(
|
|
1911
|
+
"QFAI-ASSET-004",
|
|
1912
|
+
`assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
|
|
1913
|
+
"error",
|
|
1914
|
+
assetsYamlPath,
|
|
1915
|
+
"assets.path",
|
|
1916
|
+
entry.id ? [entry.id] : [entry.path]
|
|
1917
|
+
)
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return issues;
|
|
1922
|
+
}
|
|
1923
|
+
function shouldIgnoreInvalidId(value, doc) {
|
|
1924
|
+
if (!doc) {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
const assets = doc.assets;
|
|
1928
|
+
if (!assets || typeof assets !== "object") {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
const packValue = assets.pack;
|
|
1932
|
+
if (typeof packValue !== "string" || packValue.length === 0) {
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
const normalized = packValue.replace(/\\/g, "/");
|
|
1936
|
+
const basename = path10.posix.basename(normalized);
|
|
1937
|
+
if (!basename) {
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
return value.toLowerCase() === basename.toLowerCase();
|
|
1941
|
+
}
|
|
1942
|
+
function isSafeRelativePath(value) {
|
|
1943
|
+
if (!value) {
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
if (path10.isAbsolute(value)) {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
const normalized = value.replace(/\\/g, "/");
|
|
1950
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
const segments = normalized.split("/");
|
|
1954
|
+
if (segments.some((segment) => segment === "..")) {
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
async function exists4(target) {
|
|
1960
|
+
try {
|
|
1961
|
+
await access4(target);
|
|
1962
|
+
return true;
|
|
1963
|
+
} catch {
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1529
1967
|
function formatError4(error) {
|
|
1530
1968
|
if (error instanceof Error) {
|
|
1531
1969
|
return error.message;
|
|
@@ -1556,7 +1994,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
1556
1994
|
|
|
1557
1995
|
// src/core/validators/delta.ts
|
|
1558
1996
|
import { readFile as readFile6 } from "fs/promises";
|
|
1559
|
-
import
|
|
1997
|
+
import path11 from "path";
|
|
1560
1998
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1561
1999
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1562
2000
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1570,7 +2008,7 @@ async function validateDeltas(root, config) {
|
|
|
1570
2008
|
}
|
|
1571
2009
|
const issues = [];
|
|
1572
2010
|
for (const pack of packs) {
|
|
1573
|
-
const deltaPath =
|
|
2011
|
+
const deltaPath = path11.join(pack, "delta.md");
|
|
1574
2012
|
let text;
|
|
1575
2013
|
try {
|
|
1576
2014
|
text = await readFile6(deltaPath, "utf-8");
|
|
@@ -1650,7 +2088,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
1650
2088
|
|
|
1651
2089
|
// src/core/validators/ids.ts
|
|
1652
2090
|
import { readFile as readFile7 } from "fs/promises";
|
|
1653
|
-
import
|
|
2091
|
+
import path12 from "path";
|
|
1654
2092
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1655
2093
|
async function validateDefinedIds(root, config) {
|
|
1656
2094
|
const issues = [];
|
|
@@ -1716,7 +2154,7 @@ function recordId(out, id, file) {
|
|
|
1716
2154
|
}
|
|
1717
2155
|
function formatFileList(files, root) {
|
|
1718
2156
|
return files.map((file) => {
|
|
1719
|
-
const relative =
|
|
2157
|
+
const relative = path12.relative(root, file);
|
|
1720
2158
|
return relative.length > 0 ? relative : file;
|
|
1721
2159
|
}).join(", ");
|
|
1722
2160
|
}
|
|
@@ -1744,19 +2182,19 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
|
|
|
1744
2182
|
|
|
1745
2183
|
// src/core/promptsIntegrity.ts
|
|
1746
2184
|
import { readFile as readFile8 } from "fs/promises";
|
|
1747
|
-
import
|
|
2185
|
+
import path14 from "path";
|
|
1748
2186
|
|
|
1749
2187
|
// src/shared/assets.ts
|
|
1750
2188
|
import { existsSync } from "fs";
|
|
1751
|
-
import
|
|
2189
|
+
import path13 from "path";
|
|
1752
2190
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1753
2191
|
function getInitAssetsDir() {
|
|
1754
2192
|
const base = import.meta.url;
|
|
1755
2193
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1756
|
-
const baseDir =
|
|
2194
|
+
const baseDir = path13.dirname(basePath);
|
|
1757
2195
|
const candidates = [
|
|
1758
|
-
|
|
1759
|
-
|
|
2196
|
+
path13.resolve(baseDir, "../../../assets/init"),
|
|
2197
|
+
path13.resolve(baseDir, "../../assets/init")
|
|
1760
2198
|
];
|
|
1761
2199
|
for (const candidate of candidates) {
|
|
1762
2200
|
if (existsSync(candidate)) {
|
|
@@ -1774,10 +2212,10 @@ function getInitAssetsDir() {
|
|
|
1774
2212
|
|
|
1775
2213
|
// src/core/promptsIntegrity.ts
|
|
1776
2214
|
async function diffProjectPromptsAgainstInitAssets(root) {
|
|
1777
|
-
const promptsDir =
|
|
2215
|
+
const promptsDir = path14.resolve(root, ".qfai", "prompts");
|
|
1778
2216
|
let templateDir;
|
|
1779
2217
|
try {
|
|
1780
|
-
templateDir =
|
|
2218
|
+
templateDir = path14.join(getInitAssetsDir(), ".qfai", "prompts");
|
|
1781
2219
|
} catch {
|
|
1782
2220
|
return {
|
|
1783
2221
|
status: "skipped_missing_assets",
|
|
@@ -1854,7 +2292,7 @@ function normalizeNewlines(text) {
|
|
|
1854
2292
|
return text.replace(/\r\n/g, "\n");
|
|
1855
2293
|
}
|
|
1856
2294
|
function toRel(base, abs) {
|
|
1857
|
-
const rel =
|
|
2295
|
+
const rel = path14.relative(base, abs);
|
|
1858
2296
|
return rel.replace(/[\\/]+/g, "/");
|
|
1859
2297
|
}
|
|
1860
2298
|
function intersectKeys(a, b) {
|
|
@@ -1899,7 +2337,8 @@ async function validatePromptsIntegrity(root) {
|
|
|
1899
2337
|
}
|
|
1900
2338
|
|
|
1901
2339
|
// src/core/validators/scenario.ts
|
|
1902
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
2340
|
+
import { access as access5, readFile as readFile9 } from "fs/promises";
|
|
2341
|
+
import path15 from "path";
|
|
1903
2342
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1904
2343
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1905
2344
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -1922,6 +2361,18 @@ async function validateScenarios(root, config) {
|
|
|
1922
2361
|
}
|
|
1923
2362
|
const issues = [];
|
|
1924
2363
|
for (const entry of entries) {
|
|
2364
|
+
const legacyScenarioPath = path15.join(entry.dir, "scenario.md");
|
|
2365
|
+
if (await fileExists(legacyScenarioPath)) {
|
|
2366
|
+
issues.push(
|
|
2367
|
+
issue4(
|
|
2368
|
+
"QFAI-SC-004",
|
|
2369
|
+
"scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
2370
|
+
"error",
|
|
2371
|
+
legacyScenarioPath,
|
|
2372
|
+
"scenario.legacy"
|
|
2373
|
+
)
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
1925
2376
|
let text;
|
|
1926
2377
|
try {
|
|
1927
2378
|
text = await readFile9(entry.scenarioPath, "utf-8");
|
|
@@ -1953,6 +2404,7 @@ function validateScenarioContent(text, file) {
|
|
|
1953
2404
|
"UI",
|
|
1954
2405
|
"API",
|
|
1955
2406
|
"DB",
|
|
2407
|
+
"THEMA",
|
|
1956
2408
|
"ADR"
|
|
1957
2409
|
]);
|
|
1958
2410
|
if (invalidIds.length > 0) {
|
|
@@ -2096,6 +2548,14 @@ function isMissingFileError3(error) {
|
|
|
2096
2548
|
}
|
|
2097
2549
|
return error.code === "ENOENT";
|
|
2098
2550
|
}
|
|
2551
|
+
async function fileExists(target) {
|
|
2552
|
+
try {
|
|
2553
|
+
await access5(target);
|
|
2554
|
+
return true;
|
|
2555
|
+
} catch {
|
|
2556
|
+
return false;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2099
2559
|
|
|
2100
2560
|
// src/core/validators/spec.ts
|
|
2101
2561
|
import { readFile as readFile10 } from "fs/promises";
|
|
@@ -2155,6 +2615,7 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
2155
2615
|
"UI",
|
|
2156
2616
|
"API",
|
|
2157
2617
|
"DB",
|
|
2618
|
+
"THEMA",
|
|
2158
2619
|
"ADR"
|
|
2159
2620
|
]);
|
|
2160
2621
|
if (invalidIds.length > 0) {
|
|
@@ -2334,7 +2795,7 @@ async function validateTraceability(root, config) {
|
|
|
2334
2795
|
"QFAI-TRACE-021",
|
|
2335
2796
|
`Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
|
|
2336
2797
|
", "
|
|
2337
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
2798
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
2338
2799
|
"error",
|
|
2339
2800
|
file,
|
|
2340
2801
|
"traceability.specContractRefFormat",
|
|
@@ -2398,7 +2859,7 @@ async function validateTraceability(root, config) {
|
|
|
2398
2859
|
"QFAI-TRACE-032",
|
|
2399
2860
|
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2400
2861
|
", "
|
|
2401
|
-
)} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
|
|
2862
|
+
)} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
|
|
2402
2863
|
"error",
|
|
2403
2864
|
file,
|
|
2404
2865
|
"traceability.scenarioContractRefFormat",
|
|
@@ -2777,17 +3238,25 @@ function countIssues(issues) {
|
|
|
2777
3238
|
}
|
|
2778
3239
|
|
|
2779
3240
|
// src/core/report.ts
|
|
2780
|
-
var ID_PREFIXES2 = [
|
|
3241
|
+
var ID_PREFIXES2 = [
|
|
3242
|
+
"SPEC",
|
|
3243
|
+
"BR",
|
|
3244
|
+
"SC",
|
|
3245
|
+
"UI",
|
|
3246
|
+
"API",
|
|
3247
|
+
"DB",
|
|
3248
|
+
"THEMA"
|
|
3249
|
+
];
|
|
2781
3250
|
async function createReportData(root, validation, configResult) {
|
|
2782
|
-
const resolvedRoot =
|
|
3251
|
+
const resolvedRoot = path16.resolve(root);
|
|
2783
3252
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2784
3253
|
const config = resolved.config;
|
|
2785
3254
|
const configPath = resolved.configPath;
|
|
2786
3255
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2787
3256
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2788
|
-
const apiRoot =
|
|
2789
|
-
const uiRoot =
|
|
2790
|
-
const dbRoot =
|
|
3257
|
+
const apiRoot = path16.join(contractsRoot, "api");
|
|
3258
|
+
const uiRoot = path16.join(contractsRoot, "ui");
|
|
3259
|
+
const dbRoot = path16.join(contractsRoot, "db");
|
|
2791
3260
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2792
3261
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2793
3262
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2795,7 +3264,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2795
3264
|
const {
|
|
2796
3265
|
api: apiFiles,
|
|
2797
3266
|
ui: uiFiles,
|
|
2798
|
-
db: dbFiles
|
|
3267
|
+
db: dbFiles,
|
|
3268
|
+
thema: themaFiles
|
|
2799
3269
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2800
3270
|
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2801
3271
|
const contractIdList = Array.from(contractIndex.ids);
|
|
@@ -2822,7 +3292,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2822
3292
|
...scenarioFiles,
|
|
2823
3293
|
...apiFiles,
|
|
2824
3294
|
...uiFiles,
|
|
2825
|
-
...dbFiles
|
|
3295
|
+
...dbFiles,
|
|
3296
|
+
...themaFiles
|
|
2826
3297
|
]);
|
|
2827
3298
|
const upstreamIds = await collectUpstreamIds([
|
|
2828
3299
|
...specFiles,
|
|
@@ -2859,7 +3330,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2859
3330
|
contracts: {
|
|
2860
3331
|
api: apiFiles.length,
|
|
2861
3332
|
ui: uiFiles.length,
|
|
2862
|
-
db: dbFiles.length
|
|
3333
|
+
db: dbFiles.length,
|
|
3334
|
+
thema: themaFiles.length
|
|
2863
3335
|
},
|
|
2864
3336
|
counts: normalizedValidation.counts
|
|
2865
3337
|
},
|
|
@@ -2869,7 +3341,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2869
3341
|
sc: idsByPrefix.SC,
|
|
2870
3342
|
ui: idsByPrefix.UI,
|
|
2871
3343
|
api: idsByPrefix.API,
|
|
2872
|
-
db: idsByPrefix.DB
|
|
3344
|
+
db: idsByPrefix.DB,
|
|
3345
|
+
thema: idsByPrefix.THEMA
|
|
2873
3346
|
},
|
|
2874
3347
|
traceability: {
|
|
2875
3348
|
upstreamIdsFound: upstreamIds.size,
|
|
@@ -2939,7 +3412,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
2939
3412
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2940
3413
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2941
3414
|
lines.push(
|
|
2942
|
-
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
|
|
3415
|
+
`- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
|
|
2943
3416
|
);
|
|
2944
3417
|
lines.push(
|
|
2945
3418
|
`- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
|
|
@@ -3083,6 +3556,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3083
3556
|
lines.push(formatIdLine("UI", data.ids.ui));
|
|
3084
3557
|
lines.push(formatIdLine("API", data.ids.api));
|
|
3085
3558
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
3559
|
+
lines.push(formatIdLine("THEMA", data.ids.thema));
|
|
3086
3560
|
lines.push("");
|
|
3087
3561
|
lines.push("## Traceability");
|
|
3088
3562
|
lines.push("");
|
|
@@ -3304,7 +3778,8 @@ async function collectIds(files) {
|
|
|
3304
3778
|
SC: /* @__PURE__ */ new Set(),
|
|
3305
3779
|
UI: /* @__PURE__ */ new Set(),
|
|
3306
3780
|
API: /* @__PURE__ */ new Set(),
|
|
3307
|
-
DB: /* @__PURE__ */ new Set()
|
|
3781
|
+
DB: /* @__PURE__ */ new Set(),
|
|
3782
|
+
THEMA: /* @__PURE__ */ new Set()
|
|
3308
3783
|
};
|
|
3309
3784
|
for (const file of files) {
|
|
3310
3785
|
const text = await readFile12(file, "utf-8");
|
|
@@ -3319,7 +3794,8 @@ async function collectIds(files) {
|
|
|
3319
3794
|
SC: toSortedArray2(result.SC),
|
|
3320
3795
|
UI: toSortedArray2(result.UI),
|
|
3321
3796
|
API: toSortedArray2(result.API),
|
|
3322
|
-
DB: toSortedArray2(result.DB)
|
|
3797
|
+
DB: toSortedArray2(result.DB),
|
|
3798
|
+
THEMA: toSortedArray2(result.THEMA)
|
|
3323
3799
|
};
|
|
3324
3800
|
}
|
|
3325
3801
|
async function collectUpstreamIds(files) {
|