qfai 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +13 -3
  2. package/assets/init/.qfai/README.md +2 -2
  3. package/assets/init/.qfai/contracts/README.md +21 -1
  4. package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/assets.yaml +6 -0
  5. package/assets/init/.qfai/contracts/ui/assets/thema-001-facebook-like/palette.png +0 -0
  6. package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/assets.yaml +6 -0
  7. package/assets/init/.qfai/contracts/ui/assets/ui-0001-sample/snapshots/login__desktop__light__default.png +0 -0
  8. package/assets/init/.qfai/contracts/ui/thema-001-facebook-like.yml +13 -0
  9. package/assets/init/.qfai/contracts/ui/ui-0001-sample.yaml +9 -0
  10. package/assets/init/.qfai/prompts/makeBusinessFlow.md +1 -1
  11. package/assets/init/.qfai/prompts/makeOverview.md +1 -1
  12. package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +1 -1
  13. package/assets/init/.qfai/prompts/require-to-spec.md +2 -2
  14. package/assets/init/.qfai/samples/analyze/analysis.md +1 -1
  15. package/assets/init/.qfai/specs/README.md +3 -3
  16. package/assets/init/.qfai/specs/spec-0001/delta.md +1 -1
  17. package/assets/init/.qfai/specs/spec-0001/{scenario.md → scenario.feature} +1 -1
  18. package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
  19. package/dist/cli/index.cjs +589 -114
  20. package/dist/cli/index.cjs.map +1 -1
  21. package/dist/cli/index.mjs +591 -116
  22. package/dist/cli/index.mjs.map +1 -1
  23. package/dist/index.cjs +542 -67
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +3 -1
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.mjs +544 -69
  28. package/dist/index.mjs.map +1 -1
  29. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -456,7 +456,15 @@ function isRecord(value) {
456
456
  }
457
457
 
458
458
  // src/core/ids.ts
459
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
459
+ var ID_PREFIXES = [
460
+ "SPEC",
461
+ "BR",
462
+ "SC",
463
+ "UI",
464
+ "API",
465
+ "DB",
466
+ "THEMA"
467
+ ];
460
468
  var STRICT_ID_PATTERNS = {
461
469
  SPEC: /\bSPEC-\d{4}\b/g,
462
470
  BR: /\bBR-\d{4}\b/g,
@@ -464,6 +472,7 @@ var STRICT_ID_PATTERNS = {
464
472
  UI: /\bUI-\d{4}\b/g,
465
473
  API: /\bAPI-\d{4}\b/g,
466
474
  DB: /\bDB-\d{4}\b/g,
475
+ THEMA: /\bTHEMA-\d{3}\b/g,
467
476
  ADR: /\bADR-\d{4}\b/g
468
477
  };
469
478
  var LOOSE_ID_PATTERNS = {
@@ -473,6 +482,7 @@ var LOOSE_ID_PATTERNS = {
473
482
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
474
483
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
475
484
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
485
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
476
486
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
477
487
  };
478
488
  function extractIds(text, prefix) {
@@ -510,14 +520,15 @@ function isValidId(value, prefix) {
510
520
 
511
521
  // src/core/report.ts
512
522
  var import_promises15 = require("fs/promises");
513
- var import_node_path14 = __toESM(require("path"), 1);
523
+ var import_node_path16 = __toESM(require("path"), 1);
514
524
 
515
525
  // src/core/contractIndex.ts
516
526
  var import_promises5 = require("fs/promises");
517
- var import_node_path4 = __toESM(require("path"), 1);
527
+ var import_node_path5 = __toESM(require("path"), 1);
518
528
 
519
529
  // src/core/discovery.ts
520
530
  var import_promises4 = require("fs/promises");
531
+ var import_node_path4 = __toESM(require("path"), 1);
521
532
 
522
533
  // src/core/fs.ts
523
534
  var import_promises2 = require("fs/promises");
@@ -630,7 +641,7 @@ async function collectSpecEntries(specsRoot) {
630
641
  dir,
631
642
  specPath: import_node_path3.default.join(dir, "spec.md"),
632
643
  deltaPath: import_node_path3.default.join(dir, "delta.md"),
633
- scenarioPath: import_node_path3.default.join(dir, "scenario.md")
644
+ scenarioPath: import_node_path3.default.join(dir, "scenario.feature")
634
645
  }));
635
646
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
636
647
  }
@@ -666,7 +677,12 @@ async function collectScenarioFiles(specsRoot) {
666
677
  return filterExisting(entries.map((entry) => entry.scenarioPath));
667
678
  }
668
679
  async function collectUiContractFiles(uiRoot) {
669
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
680
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
681
+ return filterByBasenamePrefix(files, "ui-");
682
+ }
683
+ async function collectThemaContractFiles(uiRoot) {
684
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
685
+ return filterByBasenamePrefix(files, "thema-");
670
686
  }
671
687
  async function collectApiContractFiles(apiRoot) {
672
688
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -675,12 +691,13 @@ async function collectDbContractFiles(dbRoot) {
675
691
  return collectFiles(dbRoot, { extensions: [".sql"] });
676
692
  }
677
693
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
678
- const [ui, api, db] = await Promise.all([
694
+ const [ui, thema, api, db] = await Promise.all([
679
695
  collectUiContractFiles(uiRoot),
696
+ collectThemaContractFiles(uiRoot),
680
697
  collectApiContractFiles(apiRoot),
681
698
  collectDbContractFiles(dbRoot)
682
699
  ]);
683
- return { ui, api, db };
700
+ return { ui, thema, api, db };
684
701
  }
685
702
  async function filterExisting(files) {
686
703
  const existing = [];
@@ -699,10 +716,16 @@ async function exists3(target) {
699
716
  return false;
700
717
  }
701
718
  }
719
+ function filterByBasenamePrefix(files, prefix) {
720
+ const lowerPrefix = prefix.toLowerCase();
721
+ return files.filter(
722
+ (file) => import_node_path4.default.basename(file).toLowerCase().startsWith(lowerPrefix)
723
+ );
724
+ }
702
725
 
703
726
  // src/core/contractsDecl.ts
704
- var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4})\s*(?:\*\/)?\s*$/gm;
705
- var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:API|UI|DB)-\d{4}\s*(?:\*\/)?\s*$/;
727
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
728
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
706
729
  function extractDeclaredContractIds(text) {
707
730
  const ids = [];
708
731
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -720,20 +743,22 @@ function stripContractDeclarationLines(text) {
720
743
  // src/core/contractIndex.ts
721
744
  async function buildContractIndex(root, config) {
722
745
  const contractsRoot = resolvePath(root, config, "contractsDir");
723
- const uiRoot = import_node_path4.default.join(contractsRoot, "ui");
724
- const apiRoot = import_node_path4.default.join(contractsRoot, "api");
725
- const dbRoot = import_node_path4.default.join(contractsRoot, "db");
726
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
746
+ const uiRoot = import_node_path5.default.join(contractsRoot, "ui");
747
+ const apiRoot = import_node_path5.default.join(contractsRoot, "api");
748
+ const dbRoot = import_node_path5.default.join(contractsRoot, "db");
749
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
727
750
  collectUiContractFiles(uiRoot),
751
+ collectThemaContractFiles(uiRoot),
728
752
  collectApiContractFiles(apiRoot),
729
753
  collectDbContractFiles(dbRoot)
730
754
  ]);
731
755
  const index = {
732
756
  ids: /* @__PURE__ */ new Set(),
733
757
  idToFiles: /* @__PURE__ */ new Map(),
734
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
758
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
735
759
  };
736
760
  await indexContractFiles(uiFiles, index);
761
+ await indexContractFiles(themaFiles, index);
737
762
  await indexContractFiles(apiFiles, index);
738
763
  await indexContractFiles(dbFiles, index);
739
764
  return index;
@@ -752,15 +777,15 @@ function record(index, id, file) {
752
777
  }
753
778
 
754
779
  // src/core/paths.ts
755
- var import_node_path5 = __toESM(require("path"), 1);
780
+ var import_node_path6 = __toESM(require("path"), 1);
756
781
  function toRelativePath(root, target) {
757
782
  if (!target) {
758
783
  return target;
759
784
  }
760
- if (!import_node_path5.default.isAbsolute(target)) {
785
+ if (!import_node_path6.default.isAbsolute(target)) {
761
786
  return toPosixPath(target);
762
787
  }
763
- const relative = import_node_path5.default.relative(root, target);
788
+ const relative = import_node_path6.default.relative(root, target);
764
789
  if (!relative) {
765
790
  return ".";
766
791
  }
@@ -808,7 +833,7 @@ function normalizeValidationResult(root, result) {
808
833
  }
809
834
 
810
835
  // src/core/parse/contractRefs.ts
811
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
836
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
812
837
  function parseContractRefs(text, options = {}) {
813
838
  const linePattern = buildLinePattern(options);
814
839
  const lines = [];
@@ -979,7 +1004,7 @@ function parseSpec(md, file) {
979
1004
 
980
1005
  // src/core/traceability.ts
981
1006
  var import_promises6 = require("fs/promises");
982
- var import_node_path6 = __toESM(require("path"), 1);
1007
+ var import_node_path7 = __toESM(require("path"), 1);
983
1008
 
984
1009
  // src/core/gherkin/parse.ts
985
1010
  var import_gherkin = require("@cucumber/gherkin");
@@ -1207,7 +1232,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1207
1232
  };
1208
1233
  }
1209
1234
  const normalizedFiles = Array.from(
1210
- new Set(scanResult.files.map((file) => import_node_path6.default.normalize(file)))
1235
+ new Set(scanResult.files.map((file) => import_node_path7.default.normalize(file)))
1211
1236
  );
1212
1237
  for (const file of normalizedFiles) {
1213
1238
  const text = await (0, import_promises6.readFile)(file, "utf-8");
@@ -1270,11 +1295,11 @@ function formatError3(error) {
1270
1295
 
1271
1296
  // src/core/version.ts
1272
1297
  var import_promises7 = require("fs/promises");
1273
- var import_node_path7 = __toESM(require("path"), 1);
1298
+ var import_node_path8 = __toESM(require("path"), 1);
1274
1299
  var import_node_url = require("url");
1275
1300
  async function resolveToolVersion() {
1276
- if ("1.0.1".length > 0) {
1277
- return "1.0.1";
1301
+ if ("1.0.3".length > 0) {
1302
+ return "1.0.3";
1278
1303
  }
1279
1304
  try {
1280
1305
  const packagePath = resolvePackageJsonPath();
@@ -1289,18 +1314,18 @@ async function resolveToolVersion() {
1289
1314
  function resolvePackageJsonPath() {
1290
1315
  const base = __filename;
1291
1316
  const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
1292
- return import_node_path7.default.resolve(import_node_path7.default.dirname(basePath), "../../package.json");
1317
+ return import_node_path8.default.resolve(import_node_path8.default.dirname(basePath), "../../package.json");
1293
1318
  }
1294
1319
 
1295
1320
  // src/core/validators/contracts.ts
1296
1321
  var import_promises8 = require("fs/promises");
1297
- var import_node_path9 = __toESM(require("path"), 1);
1322
+ var import_node_path10 = __toESM(require("path"), 1);
1298
1323
 
1299
1324
  // src/core/contracts.ts
1300
- var import_node_path8 = __toESM(require("path"), 1);
1325
+ var import_node_path9 = __toESM(require("path"), 1);
1301
1326
  var import_yaml2 = require("yaml");
1302
1327
  function parseStructuredContract(file, text) {
1303
- const ext = import_node_path8.default.extname(file).toLowerCase();
1328
+ const ext = import_node_path9.default.extname(file).toLowerCase();
1304
1329
  if (ext === ".json") {
1305
1330
  return JSON.parse(text);
1306
1331
  }
@@ -1317,17 +1342,23 @@ var SQL_DANGEROUS_PATTERNS = [
1317
1342
  label: "ALTER TABLE ... DROP"
1318
1343
  }
1319
1344
  ];
1345
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
1320
1346
  async function validateContracts(root, config) {
1321
1347
  const issues = [];
1322
- const contractsRoot = resolvePath(root, config, "contractsDir");
1323
- issues.push(...await validateUiContracts(import_node_path9.default.join(contractsRoot, "ui")));
1324
- issues.push(...await validateApiContracts(import_node_path9.default.join(contractsRoot, "api")));
1325
- issues.push(...await validateDbContracts(import_node_path9.default.join(contractsRoot, "db")));
1326
1348
  const contractIndex = await buildContractIndex(root, config);
1349
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1350
+ const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
1351
+ const themaIds = new Set(
1352
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
1353
+ );
1354
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
1355
+ issues.push(...await validateThemaContracts(uiRoot));
1356
+ issues.push(...await validateApiContracts(import_node_path10.default.join(contractsRoot, "api")));
1357
+ issues.push(...await validateDbContracts(import_node_path10.default.join(contractsRoot, "db")));
1327
1358
  issues.push(...validateDuplicateContractIds(contractIndex));
1328
1359
  return issues;
1329
1360
  }
1330
- async function validateUiContracts(uiRoot) {
1361
+ async function validateUiContracts(uiRoot, themaIds) {
1331
1362
  const files = await collectUiContractFiles(uiRoot);
1332
1363
  if (files.length === 0) {
1333
1364
  return [
@@ -1341,6 +1372,60 @@ async function validateUiContracts(uiRoot) {
1341
1372
  ];
1342
1373
  }
1343
1374
  const issues = [];
1375
+ for (const file of files) {
1376
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1377
+ const declaredIds = extractDeclaredContractIds(text);
1378
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1379
+ let doc = null;
1380
+ try {
1381
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1382
+ } catch (error) {
1383
+ issues.push(
1384
+ issue(
1385
+ "QFAI-CONTRACT-001",
1386
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1387
+ "error",
1388
+ file,
1389
+ "contracts.ui.parse"
1390
+ )
1391
+ );
1392
+ }
1393
+ const invalidIds = extractInvalidIds(text, [
1394
+ "SPEC",
1395
+ "BR",
1396
+ "SC",
1397
+ "UI",
1398
+ "API",
1399
+ "DB",
1400
+ "THEMA",
1401
+ "ADR"
1402
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
1403
+ if (invalidIds.length > 0) {
1404
+ issues.push(
1405
+ issue(
1406
+ "QFAI-ID-002",
1407
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1408
+ "error",
1409
+ file,
1410
+ "id.format",
1411
+ invalidIds
1412
+ )
1413
+ );
1414
+ }
1415
+ if (doc) {
1416
+ issues.push(
1417
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
1418
+ );
1419
+ }
1420
+ }
1421
+ return issues;
1422
+ }
1423
+ async function validateThemaContracts(uiRoot) {
1424
+ const files = await collectThemaContractFiles(uiRoot);
1425
+ if (files.length === 0) {
1426
+ return [];
1427
+ }
1428
+ const issues = [];
1344
1429
  for (const file of files) {
1345
1430
  const text = await (0, import_promises8.readFile)(file, "utf-8");
1346
1431
  const invalidIds = extractInvalidIds(text, [
@@ -1350,6 +1435,7 @@ async function validateUiContracts(uiRoot) {
1350
1435
  "UI",
1351
1436
  "API",
1352
1437
  "DB",
1438
+ "THEMA",
1353
1439
  "ADR"
1354
1440
  ]);
1355
1441
  if (invalidIds.length > 0) {
@@ -1365,17 +1451,95 @@ async function validateUiContracts(uiRoot) {
1365
1451
  );
1366
1452
  }
1367
1453
  const declaredIds = extractDeclaredContractIds(text);
1368
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1454
+ if (declaredIds.length === 0) {
1455
+ issues.push(
1456
+ issue(
1457
+ "QFAI-THEMA-010",
1458
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1459
+ "error",
1460
+ file,
1461
+ "contracts.thema.declaration"
1462
+ )
1463
+ );
1464
+ continue;
1465
+ }
1466
+ if (declaredIds.length > 1) {
1467
+ issues.push(
1468
+ issue(
1469
+ "QFAI-THEMA-011",
1470
+ `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(
1471
+ ", "
1472
+ )}`,
1473
+ "error",
1474
+ file,
1475
+ "contracts.thema.declaration",
1476
+ declaredIds
1477
+ )
1478
+ );
1479
+ continue;
1480
+ }
1481
+ const declaredId = declaredIds[0] ?? "";
1482
+ if (!THEMA_ID_RE.test(declaredId)) {
1483
+ issues.push(
1484
+ issue(
1485
+ "QFAI-THEMA-012",
1486
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
1487
+ "error",
1488
+ file,
1489
+ "contracts.thema.idFormat",
1490
+ [declaredId]
1491
+ )
1492
+ );
1493
+ }
1494
+ let doc;
1369
1495
  try {
1370
- parseStructuredContract(file, stripContractDeclarationLines(text));
1496
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1371
1497
  } catch (error) {
1372
1498
  issues.push(
1373
1499
  issue(
1374
- "QFAI-CONTRACT-001",
1375
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1500
+ "QFAI-THEMA-001",
1501
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1376
1502
  "error",
1377
1503
  file,
1378
- "contracts.ui.parse"
1504
+ "contracts.thema.parse"
1505
+ )
1506
+ );
1507
+ continue;
1508
+ }
1509
+ const docId = typeof doc.id === "string" ? doc.id : "";
1510
+ if (!THEMA_ID_RE.test(docId)) {
1511
+ issues.push(
1512
+ issue(
1513
+ "QFAI-THEMA-012",
1514
+ 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",
1515
+ "error",
1516
+ file,
1517
+ "contracts.thema.idFormat",
1518
+ docId.length > 0 ? [docId] : void 0
1519
+ )
1520
+ );
1521
+ }
1522
+ const name = typeof doc.name === "string" ? doc.name : "";
1523
+ if (!name) {
1524
+ issues.push(
1525
+ issue(
1526
+ "QFAI-THEMA-014",
1527
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1528
+ "error",
1529
+ file,
1530
+ "contracts.thema.name"
1531
+ )
1532
+ );
1533
+ }
1534
+ if (declaredId && docId && declaredId !== docId) {
1535
+ issues.push(
1536
+ issue(
1537
+ "QFAI-THEMA-013",
1538
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
1539
+ "error",
1540
+ file,
1541
+ "contracts.thema.idMismatch",
1542
+ [declaredId, docId]
1379
1543
  )
1380
1544
  );
1381
1545
  }
@@ -1405,6 +1569,7 @@ async function validateApiContracts(apiRoot) {
1405
1569
  "UI",
1406
1570
  "API",
1407
1571
  "DB",
1572
+ "THEMA",
1408
1573
  "ADR"
1409
1574
  ]);
1410
1575
  if (invalidIds.length > 0) {
@@ -1473,6 +1638,7 @@ async function validateDbContracts(dbRoot) {
1473
1638
  "UI",
1474
1639
  "API",
1475
1640
  "DB",
1641
+ "THEMA",
1476
1642
  "ADR"
1477
1643
  ]);
1478
1644
  if (invalidIds.length > 0) {
@@ -1579,6 +1745,278 @@ function validateDuplicateContractIds(contractIndex) {
1579
1745
  function hasOpenApi(doc) {
1580
1746
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1581
1747
  }
1748
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
1749
+ const issues = [];
1750
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
1751
+ const themaRef = doc.themaRef;
1752
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
1753
+ issues.push(
1754
+ issue(
1755
+ "QFAI-UI-020",
1756
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1757
+ "error",
1758
+ file,
1759
+ "contracts.ui.themaRef"
1760
+ )
1761
+ );
1762
+ } else if (!THEMA_ID_RE.test(themaRef)) {
1763
+ issues.push(
1764
+ issue(
1765
+ "QFAI-UI-020",
1766
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
1767
+ "error",
1768
+ file,
1769
+ "contracts.ui.themaRef",
1770
+ [themaRef]
1771
+ )
1772
+ );
1773
+ } else if (!themaIds.has(themaRef)) {
1774
+ issues.push(
1775
+ issue(
1776
+ "QFAI-UI-020",
1777
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
1778
+ "error",
1779
+ file,
1780
+ "contracts.ui.themaRef",
1781
+ [themaRef]
1782
+ )
1783
+ );
1784
+ }
1785
+ }
1786
+ const assets = doc.assets;
1787
+ if (assets && typeof assets === "object") {
1788
+ issues.push(
1789
+ ...await validateUiAssets(
1790
+ assets,
1791
+ file,
1792
+ uiRoot
1793
+ )
1794
+ );
1795
+ }
1796
+ return issues;
1797
+ }
1798
+ async function validateUiAssets(assets, file, uiRoot) {
1799
+ const issues = [];
1800
+ const packValue = assets.pack;
1801
+ const useValue = assets.use;
1802
+ if (packValue === void 0 && useValue === void 0) {
1803
+ return issues;
1804
+ }
1805
+ if (typeof packValue !== "string" || packValue.length === 0) {
1806
+ issues.push(
1807
+ issue(
1808
+ "QFAI-ASSET-001",
1809
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1810
+ "error",
1811
+ file,
1812
+ "assets.pack"
1813
+ )
1814
+ );
1815
+ return issues;
1816
+ }
1817
+ if (!isSafeRelativePath(packValue)) {
1818
+ issues.push(
1819
+ issue(
1820
+ "QFAI-ASSET-001",
1821
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
1822
+ "error",
1823
+ file,
1824
+ "assets.pack",
1825
+ [packValue]
1826
+ )
1827
+ );
1828
+ return issues;
1829
+ }
1830
+ const packDir = import_node_path10.default.resolve(uiRoot, packValue);
1831
+ const packRelative = import_node_path10.default.relative(uiRoot, packDir);
1832
+ if (packRelative.startsWith("..") || import_node_path10.default.isAbsolute(packRelative)) {
1833
+ issues.push(
1834
+ issue(
1835
+ "QFAI-ASSET-001",
1836
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
1837
+ "error",
1838
+ file,
1839
+ "assets.pack",
1840
+ [packValue]
1841
+ )
1842
+ );
1843
+ return issues;
1844
+ }
1845
+ if (!await exists4(packDir)) {
1846
+ issues.push(
1847
+ issue(
1848
+ "QFAI-ASSET-001",
1849
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
1850
+ "error",
1851
+ file,
1852
+ "assets.pack",
1853
+ [packValue]
1854
+ )
1855
+ );
1856
+ return issues;
1857
+ }
1858
+ const assetsYamlPath = import_node_path10.default.join(packDir, "assets.yaml");
1859
+ if (!await exists4(assetsYamlPath)) {
1860
+ issues.push(
1861
+ issue(
1862
+ "QFAI-ASSET-002",
1863
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
1864
+ "error",
1865
+ assetsYamlPath,
1866
+ "assets.yaml"
1867
+ )
1868
+ );
1869
+ return issues;
1870
+ }
1871
+ let manifest;
1872
+ try {
1873
+ const manifestText = await (0, import_promises8.readFile)(assetsYamlPath, "utf-8");
1874
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
1875
+ } catch (error) {
1876
+ issues.push(
1877
+ issue(
1878
+ "QFAI-ASSET-002",
1879
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error)})`,
1880
+ "error",
1881
+ assetsYamlPath,
1882
+ "assets.yaml"
1883
+ )
1884
+ );
1885
+ return issues;
1886
+ }
1887
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
1888
+ const itemIds = /* @__PURE__ */ new Set();
1889
+ const itemPaths = [];
1890
+ for (const item of items) {
1891
+ if (!item || typeof item !== "object") {
1892
+ continue;
1893
+ }
1894
+ const record2 = item;
1895
+ const id = typeof record2.id === "string" ? record2.id : void 0;
1896
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
1897
+ if (id) {
1898
+ itemIds.add(id);
1899
+ }
1900
+ itemPaths.push({ id, path: pathValue });
1901
+ }
1902
+ if (useValue !== void 0) {
1903
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
1904
+ issues.push(
1905
+ issue(
1906
+ "QFAI-ASSET-003",
1907
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1908
+ "error",
1909
+ file,
1910
+ "assets.use"
1911
+ )
1912
+ );
1913
+ } else {
1914
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
1915
+ if (missing.length > 0) {
1916
+ issues.push(
1917
+ issue(
1918
+ "QFAI-ASSET-003",
1919
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
1920
+ "error",
1921
+ file,
1922
+ "assets.use",
1923
+ missing
1924
+ )
1925
+ );
1926
+ }
1927
+ }
1928
+ }
1929
+ for (const entry of itemPaths) {
1930
+ if (!entry.path) {
1931
+ continue;
1932
+ }
1933
+ if (!isSafeRelativePath(entry.path)) {
1934
+ issues.push(
1935
+ issue(
1936
+ "QFAI-ASSET-004",
1937
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
1938
+ "error",
1939
+ assetsYamlPath,
1940
+ "assets.path",
1941
+ entry.id ? [entry.id] : [entry.path]
1942
+ )
1943
+ );
1944
+ continue;
1945
+ }
1946
+ const assetPath = import_node_path10.default.resolve(packDir, entry.path);
1947
+ const assetRelative = import_node_path10.default.relative(packDir, assetPath);
1948
+ if (assetRelative.startsWith("..") || import_node_path10.default.isAbsolute(assetRelative)) {
1949
+ issues.push(
1950
+ issue(
1951
+ "QFAI-ASSET-004",
1952
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
1953
+ "error",
1954
+ assetsYamlPath,
1955
+ "assets.path",
1956
+ entry.id ? [entry.id] : [entry.path]
1957
+ )
1958
+ );
1959
+ continue;
1960
+ }
1961
+ if (!await exists4(assetPath)) {
1962
+ issues.push(
1963
+ issue(
1964
+ "QFAI-ASSET-004",
1965
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
1966
+ "error",
1967
+ assetsYamlPath,
1968
+ "assets.path",
1969
+ entry.id ? [entry.id] : [entry.path]
1970
+ )
1971
+ );
1972
+ }
1973
+ }
1974
+ return issues;
1975
+ }
1976
+ function shouldIgnoreInvalidId(value, doc) {
1977
+ if (!doc) {
1978
+ return false;
1979
+ }
1980
+ const assets = doc.assets;
1981
+ if (!assets || typeof assets !== "object") {
1982
+ return false;
1983
+ }
1984
+ const packValue = assets.pack;
1985
+ if (typeof packValue !== "string" || packValue.length === 0) {
1986
+ return false;
1987
+ }
1988
+ const normalized = packValue.replace(/\\/g, "/");
1989
+ const basename = import_node_path10.default.posix.basename(normalized);
1990
+ if (!basename) {
1991
+ return false;
1992
+ }
1993
+ return value.toLowerCase() === basename.toLowerCase();
1994
+ }
1995
+ function isSafeRelativePath(value) {
1996
+ if (!value) {
1997
+ return false;
1998
+ }
1999
+ if (import_node_path10.default.isAbsolute(value)) {
2000
+ return false;
2001
+ }
2002
+ const normalized = value.replace(/\\/g, "/");
2003
+ if (/^[A-Za-z]:/.test(normalized)) {
2004
+ return false;
2005
+ }
2006
+ const segments = normalized.split("/");
2007
+ if (segments.some((segment) => segment === "..")) {
2008
+ return false;
2009
+ }
2010
+ return true;
2011
+ }
2012
+ async function exists4(target) {
2013
+ try {
2014
+ await (0, import_promises8.access)(target);
2015
+ return true;
2016
+ } catch {
2017
+ return false;
2018
+ }
2019
+ }
1582
2020
  function formatError4(error) {
1583
2021
  if (error instanceof Error) {
1584
2022
  return error.message;
@@ -1609,7 +2047,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
1609
2047
 
1610
2048
  // src/core/validators/delta.ts
1611
2049
  var import_promises9 = require("fs/promises");
1612
- var import_node_path10 = __toESM(require("path"), 1);
2050
+ var import_node_path11 = __toESM(require("path"), 1);
1613
2051
  var SECTION_RE = /^##\s+変更区分/m;
1614
2052
  var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1615
2053
  var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
@@ -1623,7 +2061,7 @@ async function validateDeltas(root, config) {
1623
2061
  }
1624
2062
  const issues = [];
1625
2063
  for (const pack of packs) {
1626
- const deltaPath = import_node_path10.default.join(pack, "delta.md");
2064
+ const deltaPath = import_node_path11.default.join(pack, "delta.md");
1627
2065
  let text;
1628
2066
  try {
1629
2067
  text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
@@ -1703,7 +2141,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
1703
2141
 
1704
2142
  // src/core/validators/ids.ts
1705
2143
  var import_promises10 = require("fs/promises");
1706
- var import_node_path11 = __toESM(require("path"), 1);
2144
+ var import_node_path12 = __toESM(require("path"), 1);
1707
2145
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1708
2146
  async function validateDefinedIds(root, config) {
1709
2147
  const issues = [];
@@ -1769,7 +2207,7 @@ function recordId(out, id, file) {
1769
2207
  }
1770
2208
  function formatFileList(files, root) {
1771
2209
  return files.map((file) => {
1772
- const relative = import_node_path11.default.relative(root, file);
2210
+ const relative = import_node_path12.default.relative(root, file);
1773
2211
  return relative.length > 0 ? relative : file;
1774
2212
  }).join(", ");
1775
2213
  }
@@ -1797,19 +2235,19 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
1797
2235
 
1798
2236
  // src/core/promptsIntegrity.ts
1799
2237
  var import_promises11 = require("fs/promises");
1800
- var import_node_path13 = __toESM(require("path"), 1);
2238
+ var import_node_path14 = __toESM(require("path"), 1);
1801
2239
 
1802
2240
  // src/shared/assets.ts
1803
2241
  var import_node_fs = require("fs");
1804
- var import_node_path12 = __toESM(require("path"), 1);
2242
+ var import_node_path13 = __toESM(require("path"), 1);
1805
2243
  var import_node_url2 = require("url");
1806
2244
  function getInitAssetsDir() {
1807
2245
  const base = __filename;
1808
2246
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1809
- const baseDir = import_node_path12.default.dirname(basePath);
2247
+ const baseDir = import_node_path13.default.dirname(basePath);
1810
2248
  const candidates = [
1811
- import_node_path12.default.resolve(baseDir, "../../../assets/init"),
1812
- import_node_path12.default.resolve(baseDir, "../../assets/init")
2249
+ import_node_path13.default.resolve(baseDir, "../../../assets/init"),
2250
+ import_node_path13.default.resolve(baseDir, "../../assets/init")
1813
2251
  ];
1814
2252
  for (const candidate of candidates) {
1815
2253
  if ((0, import_node_fs.existsSync)(candidate)) {
@@ -1827,10 +2265,10 @@ function getInitAssetsDir() {
1827
2265
 
1828
2266
  // src/core/promptsIntegrity.ts
1829
2267
  async function diffProjectPromptsAgainstInitAssets(root) {
1830
- const promptsDir = import_node_path13.default.resolve(root, ".qfai", "prompts");
2268
+ const promptsDir = import_node_path14.default.resolve(root, ".qfai", "prompts");
1831
2269
  let templateDir;
1832
2270
  try {
1833
- templateDir = import_node_path13.default.join(getInitAssetsDir(), ".qfai", "prompts");
2271
+ templateDir = import_node_path14.default.join(getInitAssetsDir(), ".qfai", "prompts");
1834
2272
  } catch {
1835
2273
  return {
1836
2274
  status: "skipped_missing_assets",
@@ -1907,7 +2345,7 @@ function normalizeNewlines(text) {
1907
2345
  return text.replace(/\r\n/g, "\n");
1908
2346
  }
1909
2347
  function toRel(base, abs) {
1910
- const rel = import_node_path13.default.relative(base, abs);
2348
+ const rel = import_node_path14.default.relative(base, abs);
1911
2349
  return rel.replace(/[\\/]+/g, "/");
1912
2350
  }
1913
2351
  function intersectKeys(a, b) {
@@ -1953,6 +2391,7 @@ async function validatePromptsIntegrity(root) {
1953
2391
 
1954
2392
  // src/core/validators/scenario.ts
1955
2393
  var import_promises12 = require("fs/promises");
2394
+ var import_node_path15 = __toESM(require("path"), 1);
1956
2395
  var GIVEN_PATTERN = /\bGiven\b/;
1957
2396
  var WHEN_PATTERN = /\bWhen\b/;
1958
2397
  var THEN_PATTERN = /\bThen\b/;
@@ -1962,12 +2401,11 @@ async function validateScenarios(root, config) {
1962
2401
  const specsRoot = resolvePath(root, config, "specsDir");
1963
2402
  const entries = await collectSpecEntries(specsRoot);
1964
2403
  if (entries.length === 0) {
1965
- const expected = "spec-0001/scenario.md";
1966
- const legacy = "spec-001/scenario.md";
2404
+ const expected = "spec-0001/scenario.feature";
1967
2405
  return [
1968
2406
  issue4(
1969
2407
  "QFAI-SC-000",
1970
- `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected} (${legacy} \u306F\u975E\u5BFE\u5FDC)`,
2408
+ `Scenario \u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u914D\u7F6E\u5834\u6240: ${config.paths.specsDir} / \u671F\u5F85\u30D1\u30BF\u30FC\u30F3: ${expected}`,
1971
2409
  "info",
1972
2410
  specsRoot,
1973
2411
  "scenario.files"
@@ -1976,6 +2414,18 @@ async function validateScenarios(root, config) {
1976
2414
  }
1977
2415
  const issues = [];
1978
2416
  for (const entry of entries) {
2417
+ const legacyScenarioPath = import_node_path15.default.join(entry.dir, "scenario.md");
2418
+ if (await fileExists(legacyScenarioPath)) {
2419
+ issues.push(
2420
+ issue4(
2421
+ "QFAI-SC-004",
2422
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2423
+ "error",
2424
+ legacyScenarioPath,
2425
+ "scenario.legacy"
2426
+ )
2427
+ );
2428
+ }
1979
2429
  let text;
1980
2430
  try {
1981
2431
  text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
@@ -1984,7 +2434,7 @@ async function validateScenarios(root, config) {
1984
2434
  issues.push(
1985
2435
  issue4(
1986
2436
  "QFAI-SC-001",
1987
- "scenario.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
2437
+ "scenario.feature \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1988
2438
  "error",
1989
2439
  entry.scenarioPath,
1990
2440
  "scenario.exists"
@@ -2007,6 +2457,7 @@ function validateScenarioContent(text, file) {
2007
2457
  "UI",
2008
2458
  "API",
2009
2459
  "DB",
2460
+ "THEMA",
2010
2461
  "ADR"
2011
2462
  ]);
2012
2463
  if (invalidIds.length > 0) {
@@ -2150,6 +2601,14 @@ function isMissingFileError3(error) {
2150
2601
  }
2151
2602
  return error.code === "ENOENT";
2152
2603
  }
2604
+ async function fileExists(target) {
2605
+ try {
2606
+ await (0, import_promises12.access)(target);
2607
+ return true;
2608
+ } catch {
2609
+ return false;
2610
+ }
2611
+ }
2153
2612
 
2154
2613
  // src/core/validators/spec.ts
2155
2614
  var import_promises13 = require("fs/promises");
@@ -2209,6 +2668,7 @@ function validateSpecContent(text, file, requiredSections) {
2209
2668
  "UI",
2210
2669
  "API",
2211
2670
  "DB",
2671
+ "THEMA",
2212
2672
  "ADR"
2213
2673
  ]);
2214
2674
  if (invalidIds.length > 0) {
@@ -2388,7 +2848,7 @@ async function validateTraceability(root, config) {
2388
2848
  "QFAI-TRACE-021",
2389
2849
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2390
2850
  ", "
2391
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2851
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
2392
2852
  "error",
2393
2853
  file,
2394
2854
  "traceability.specContractRefFormat",
@@ -2452,7 +2912,7 @@ async function validateTraceability(root, config) {
2452
2912
  "QFAI-TRACE-032",
2453
2913
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2454
2914
  ", "
2455
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2915
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
2456
2916
  "error",
2457
2917
  file,
2458
2918
  "traceability.scenarioContractRefFormat",
@@ -2831,17 +3291,25 @@ function countIssues(issues) {
2831
3291
  }
2832
3292
 
2833
3293
  // src/core/report.ts
2834
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3294
+ var ID_PREFIXES2 = [
3295
+ "SPEC",
3296
+ "BR",
3297
+ "SC",
3298
+ "UI",
3299
+ "API",
3300
+ "DB",
3301
+ "THEMA"
3302
+ ];
2835
3303
  async function createReportData(root, validation, configResult) {
2836
- const resolvedRoot = import_node_path14.default.resolve(root);
3304
+ const resolvedRoot = import_node_path16.default.resolve(root);
2837
3305
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2838
3306
  const config = resolved.config;
2839
3307
  const configPath = resolved.configPath;
2840
3308
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2841
3309
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2842
- const apiRoot = import_node_path14.default.join(contractsRoot, "api");
2843
- const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
2844
- const dbRoot = import_node_path14.default.join(contractsRoot, "db");
3310
+ const apiRoot = import_node_path16.default.join(contractsRoot, "api");
3311
+ const uiRoot = import_node_path16.default.join(contractsRoot, "ui");
3312
+ const dbRoot = import_node_path16.default.join(contractsRoot, "db");
2845
3313
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2846
3314
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2847
3315
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2849,7 +3317,8 @@ async function createReportData(root, validation, configResult) {
2849
3317
  const {
2850
3318
  api: apiFiles,
2851
3319
  ui: uiFiles,
2852
- db: dbFiles
3320
+ db: dbFiles,
3321
+ thema: themaFiles
2853
3322
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2854
3323
  const contractIndex = await buildContractIndex(resolvedRoot, config);
2855
3324
  const contractIdList = Array.from(contractIndex.ids);
@@ -2876,7 +3345,8 @@ async function createReportData(root, validation, configResult) {
2876
3345
  ...scenarioFiles,
2877
3346
  ...apiFiles,
2878
3347
  ...uiFiles,
2879
- ...dbFiles
3348
+ ...dbFiles,
3349
+ ...themaFiles
2880
3350
  ]);
2881
3351
  const upstreamIds = await collectUpstreamIds([
2882
3352
  ...specFiles,
@@ -2913,7 +3383,8 @@ async function createReportData(root, validation, configResult) {
2913
3383
  contracts: {
2914
3384
  api: apiFiles.length,
2915
3385
  ui: uiFiles.length,
2916
- db: dbFiles.length
3386
+ db: dbFiles.length,
3387
+ thema: themaFiles.length
2917
3388
  },
2918
3389
  counts: normalizedValidation.counts
2919
3390
  },
@@ -2923,7 +3394,8 @@ async function createReportData(root, validation, configResult) {
2923
3394
  sc: idsByPrefix.SC,
2924
3395
  ui: idsByPrefix.UI,
2925
3396
  api: idsByPrefix.API,
2926
- db: idsByPrefix.DB
3397
+ db: idsByPrefix.DB,
3398
+ thema: idsByPrefix.THEMA
2927
3399
  },
2928
3400
  traceability: {
2929
3401
  upstreamIdsFound: upstreamIds.size,
@@ -2993,7 +3465,7 @@ function formatReportMarkdown(data, options = {}) {
2993
3465
  lines.push(`- specs: ${data.summary.specs}`);
2994
3466
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2995
3467
  lines.push(
2996
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
3468
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
2997
3469
  );
2998
3470
  lines.push(
2999
3471
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3137,6 +3609,7 @@ function formatReportMarkdown(data, options = {}) {
3137
3609
  lines.push(formatIdLine("UI", data.ids.ui));
3138
3610
  lines.push(formatIdLine("API", data.ids.api));
3139
3611
  lines.push(formatIdLine("DB", data.ids.db));
3612
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3140
3613
  lines.push("");
3141
3614
  lines.push("## Traceability");
3142
3615
  lines.push("");
@@ -3358,7 +3831,8 @@ async function collectIds(files) {
3358
3831
  SC: /* @__PURE__ */ new Set(),
3359
3832
  UI: /* @__PURE__ */ new Set(),
3360
3833
  API: /* @__PURE__ */ new Set(),
3361
- DB: /* @__PURE__ */ new Set()
3834
+ DB: /* @__PURE__ */ new Set(),
3835
+ THEMA: /* @__PURE__ */ new Set()
3362
3836
  };
3363
3837
  for (const file of files) {
3364
3838
  const text = await (0, import_promises15.readFile)(file, "utf-8");
@@ -3373,7 +3847,8 @@ async function collectIds(files) {
3373
3847
  SC: toSortedArray2(result.SC),
3374
3848
  UI: toSortedArray2(result.UI),
3375
3849
  API: toSortedArray2(result.API),
3376
- DB: toSortedArray2(result.DB)
3850
+ DB: toSortedArray2(result.DB),
3851
+ THEMA: toSortedArray2(result.THEMA)
3377
3852
  };
3378
3853
  }
3379
3854
  async function collectUpstreamIds(files) {