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.
Files changed (39) hide show
  1. package/README.md +13 -12
  2. package/assets/init/.qfai/README.md +2 -7
  3. package/assets/init/.qfai/contracts/README.md +20 -0
  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/promptpack/commands/plan.md +1 -1
  11. package/assets/init/.qfai/promptpack/commands/review.md +1 -2
  12. package/assets/init/.qfai/promptpack/constitution.md +1 -1
  13. package/assets/init/.qfai/prompts/README.md +1 -3
  14. package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +3 -3
  15. package/assets/init/.qfai/prompts/require-to-spec.md +1 -2
  16. package/assets/init/.qfai/specs/README.md +3 -4
  17. package/assets/init/.qfai/specs/spec-0001/delta.md +0 -5
  18. package/assets/init/.qfai/specs/spec-0001/scenario.feature +1 -1
  19. package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
  20. package/assets/init/root/qfai.config.yaml +0 -1
  21. package/dist/cli/index.cjs +596 -162
  22. package/dist/cli/index.cjs.map +1 -1
  23. package/dist/cli/index.mjs +598 -164
  24. package/dist/cli/index.mjs.map +1 -1
  25. package/dist/index.cjs +549 -114
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +3 -2
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.mjs +551 -116
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +1 -1
  32. package/assets/init/.qfai/promptpack/modes/change.md +0 -5
  33. package/assets/init/.qfai/promptpack/modes/compatibility.md +0 -6
  34. package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +0 -42
  35. package/assets/init/.qfai/prompts/qfai-classify-change.md +0 -33
  36. package/assets/init/.qfai/rules/conventions.md +0 -27
  37. package/assets/init/.qfai/rules/pnpm.md +0 -29
  38. package/assets/init/.qfai/samples/analyze/analysis.md +0 -38
  39. package/assets/init/.qfai/samples/analyze/input_bundle.md +0 -54
package/dist/index.cjs CHANGED
@@ -63,7 +63,6 @@ var defaultConfig = {
63
63
  paths: {
64
64
  contractsDir: ".qfai/contracts",
65
65
  specsDir: ".qfai/specs",
66
- rulesDir: ".qfai/rules",
67
66
  outDir: ".qfai/out",
68
67
  promptsDir: ".qfai/prompts",
69
68
  srcDir: "src",
@@ -176,13 +175,6 @@ function normalizePaths(raw, configPath, issues) {
176
175
  configPath,
177
176
  issues
178
177
  ),
179
- rulesDir: readString(
180
- raw.rulesDir,
181
- base.rulesDir,
182
- "paths.rulesDir",
183
- configPath,
184
- issues
185
- ),
186
178
  outDir: readString(
187
179
  raw.outDir,
188
180
  base.outDir,
@@ -456,7 +448,15 @@ function isRecord(value) {
456
448
  }
457
449
 
458
450
  // src/core/ids.ts
459
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
451
+ var ID_PREFIXES = [
452
+ "SPEC",
453
+ "BR",
454
+ "SC",
455
+ "UI",
456
+ "API",
457
+ "DB",
458
+ "THEMA"
459
+ ];
460
460
  var STRICT_ID_PATTERNS = {
461
461
  SPEC: /\bSPEC-\d{4}\b/g,
462
462
  BR: /\bBR-\d{4}\b/g,
@@ -464,6 +464,7 @@ var STRICT_ID_PATTERNS = {
464
464
  UI: /\bUI-\d{4}\b/g,
465
465
  API: /\bAPI-\d{4}\b/g,
466
466
  DB: /\bDB-\d{4}\b/g,
467
+ THEMA: /\bTHEMA-\d{3}\b/g,
467
468
  ADR: /\bADR-\d{4}\b/g
468
469
  };
469
470
  var LOOSE_ID_PATTERNS = {
@@ -473,6 +474,7 @@ var LOOSE_ID_PATTERNS = {
473
474
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
474
475
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
475
476
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
477
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
476
478
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
477
479
  };
478
480
  function extractIds(text, prefix) {
@@ -510,14 +512,15 @@ function isValidId(value, prefix) {
510
512
 
511
513
  // src/core/report.ts
512
514
  var import_promises15 = require("fs/promises");
513
- var import_node_path14 = __toESM(require("path"), 1);
515
+ var import_node_path16 = __toESM(require("path"), 1);
514
516
 
515
517
  // src/core/contractIndex.ts
516
518
  var import_promises5 = require("fs/promises");
517
- var import_node_path4 = __toESM(require("path"), 1);
519
+ var import_node_path5 = __toESM(require("path"), 1);
518
520
 
519
521
  // src/core/discovery.ts
520
522
  var import_promises4 = require("fs/promises");
523
+ var import_node_path4 = __toESM(require("path"), 1);
521
524
 
522
525
  // src/core/fs.ts
523
526
  var import_promises2 = require("fs/promises");
@@ -666,7 +669,12 @@ async function collectScenarioFiles(specsRoot) {
666
669
  return filterExisting(entries.map((entry) => entry.scenarioPath));
667
670
  }
668
671
  async function collectUiContractFiles(uiRoot) {
669
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
672
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
673
+ return filterByBasenamePrefix(files, "ui-");
674
+ }
675
+ async function collectThemaContractFiles(uiRoot) {
676
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
677
+ return filterByBasenamePrefix(files, "thema-");
670
678
  }
671
679
  async function collectApiContractFiles(apiRoot) {
672
680
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -675,12 +683,13 @@ async function collectDbContractFiles(dbRoot) {
675
683
  return collectFiles(dbRoot, { extensions: [".sql"] });
676
684
  }
677
685
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
678
- const [ui, api, db] = await Promise.all([
686
+ const [ui, thema, api, db] = await Promise.all([
679
687
  collectUiContractFiles(uiRoot),
688
+ collectThemaContractFiles(uiRoot),
680
689
  collectApiContractFiles(apiRoot),
681
690
  collectDbContractFiles(dbRoot)
682
691
  ]);
683
- return { ui, api, db };
692
+ return { ui, thema, api, db };
684
693
  }
685
694
  async function filterExisting(files) {
686
695
  const existing = [];
@@ -699,10 +708,16 @@ async function exists3(target) {
699
708
  return false;
700
709
  }
701
710
  }
711
+ function filterByBasenamePrefix(files, prefix) {
712
+ const lowerPrefix = prefix.toLowerCase();
713
+ return files.filter(
714
+ (file) => import_node_path4.default.basename(file).toLowerCase().startsWith(lowerPrefix)
715
+ );
716
+ }
702
717
 
703
718
  // 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*$/;
719
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
720
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
706
721
  function extractDeclaredContractIds(text) {
707
722
  const ids = [];
708
723
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -720,20 +735,22 @@ function stripContractDeclarationLines(text) {
720
735
  // src/core/contractIndex.ts
721
736
  async function buildContractIndex(root, config) {
722
737
  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([
738
+ const uiRoot = import_node_path5.default.join(contractsRoot, "ui");
739
+ const apiRoot = import_node_path5.default.join(contractsRoot, "api");
740
+ const dbRoot = import_node_path5.default.join(contractsRoot, "db");
741
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
727
742
  collectUiContractFiles(uiRoot),
743
+ collectThemaContractFiles(uiRoot),
728
744
  collectApiContractFiles(apiRoot),
729
745
  collectDbContractFiles(dbRoot)
730
746
  ]);
731
747
  const index = {
732
748
  ids: /* @__PURE__ */ new Set(),
733
749
  idToFiles: /* @__PURE__ */ new Map(),
734
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
750
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
735
751
  };
736
752
  await indexContractFiles(uiFiles, index);
753
+ await indexContractFiles(themaFiles, index);
737
754
  await indexContractFiles(apiFiles, index);
738
755
  await indexContractFiles(dbFiles, index);
739
756
  return index;
@@ -752,15 +769,15 @@ function record(index, id, file) {
752
769
  }
753
770
 
754
771
  // src/core/paths.ts
755
- var import_node_path5 = __toESM(require("path"), 1);
772
+ var import_node_path6 = __toESM(require("path"), 1);
756
773
  function toRelativePath(root, target) {
757
774
  if (!target) {
758
775
  return target;
759
776
  }
760
- if (!import_node_path5.default.isAbsolute(target)) {
777
+ if (!import_node_path6.default.isAbsolute(target)) {
761
778
  return toPosixPath(target);
762
779
  }
763
- const relative = import_node_path5.default.relative(root, target);
780
+ const relative = import_node_path6.default.relative(root, target);
764
781
  if (!relative) {
765
782
  return ".";
766
783
  }
@@ -808,7 +825,7 @@ function normalizeValidationResult(root, result) {
808
825
  }
809
826
 
810
827
  // src/core/parse/contractRefs.ts
811
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
828
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
812
829
  function parseContractRefs(text, options = {}) {
813
830
  const linePattern = buildLinePattern(options);
814
831
  const lines = [];
@@ -979,7 +996,7 @@ function parseSpec(md, file) {
979
996
 
980
997
  // src/core/traceability.ts
981
998
  var import_promises6 = require("fs/promises");
982
- var import_node_path6 = __toESM(require("path"), 1);
999
+ var import_node_path7 = __toESM(require("path"), 1);
983
1000
 
984
1001
  // src/core/gherkin/parse.ts
985
1002
  var import_gherkin = require("@cucumber/gherkin");
@@ -1207,7 +1224,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1207
1224
  };
1208
1225
  }
1209
1226
  const normalizedFiles = Array.from(
1210
- new Set(scanResult.files.map((file) => import_node_path6.default.normalize(file)))
1227
+ new Set(scanResult.files.map((file) => import_node_path7.default.normalize(file)))
1211
1228
  );
1212
1229
  for (const file of normalizedFiles) {
1213
1230
  const text = await (0, import_promises6.readFile)(file, "utf-8");
@@ -1270,11 +1287,11 @@ function formatError3(error) {
1270
1287
 
1271
1288
  // src/core/version.ts
1272
1289
  var import_promises7 = require("fs/promises");
1273
- var import_node_path7 = __toESM(require("path"), 1);
1290
+ var import_node_path8 = __toESM(require("path"), 1);
1274
1291
  var import_node_url = require("url");
1275
1292
  async function resolveToolVersion() {
1276
- if ("1.0.2".length > 0) {
1277
- return "1.0.2";
1293
+ if ("1.0.4".length > 0) {
1294
+ return "1.0.4";
1278
1295
  }
1279
1296
  try {
1280
1297
  const packagePath = resolvePackageJsonPath();
@@ -1289,18 +1306,18 @@ async function resolveToolVersion() {
1289
1306
  function resolvePackageJsonPath() {
1290
1307
  const base = __filename;
1291
1308
  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");
1309
+ return import_node_path8.default.resolve(import_node_path8.default.dirname(basePath), "../../package.json");
1293
1310
  }
1294
1311
 
1295
1312
  // src/core/validators/contracts.ts
1296
1313
  var import_promises8 = require("fs/promises");
1297
- var import_node_path9 = __toESM(require("path"), 1);
1314
+ var import_node_path10 = __toESM(require("path"), 1);
1298
1315
 
1299
1316
  // src/core/contracts.ts
1300
- var import_node_path8 = __toESM(require("path"), 1);
1317
+ var import_node_path9 = __toESM(require("path"), 1);
1301
1318
  var import_yaml2 = require("yaml");
1302
1319
  function parseStructuredContract(file, text) {
1303
- const ext = import_node_path8.default.extname(file).toLowerCase();
1320
+ const ext = import_node_path9.default.extname(file).toLowerCase();
1304
1321
  if (ext === ".json") {
1305
1322
  return JSON.parse(text);
1306
1323
  }
@@ -1317,17 +1334,23 @@ var SQL_DANGEROUS_PATTERNS = [
1317
1334
  label: "ALTER TABLE ... DROP"
1318
1335
  }
1319
1336
  ];
1337
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
1320
1338
  async function validateContracts(root, config) {
1321
1339
  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
1340
  const contractIndex = await buildContractIndex(root, config);
1341
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1342
+ const uiRoot = import_node_path10.default.join(contractsRoot, "ui");
1343
+ const themaIds = new Set(
1344
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
1345
+ );
1346
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
1347
+ issues.push(...await validateThemaContracts(uiRoot));
1348
+ issues.push(...await validateApiContracts(import_node_path10.default.join(contractsRoot, "api")));
1349
+ issues.push(...await validateDbContracts(import_node_path10.default.join(contractsRoot, "db")));
1327
1350
  issues.push(...validateDuplicateContractIds(contractIndex));
1328
1351
  return issues;
1329
1352
  }
1330
- async function validateUiContracts(uiRoot) {
1353
+ async function validateUiContracts(uiRoot, themaIds) {
1331
1354
  const files = await collectUiContractFiles(uiRoot);
1332
1355
  if (files.length === 0) {
1333
1356
  return [
@@ -1341,6 +1364,60 @@ async function validateUiContracts(uiRoot) {
1341
1364
  ];
1342
1365
  }
1343
1366
  const issues = [];
1367
+ for (const file of files) {
1368
+ const text = await (0, import_promises8.readFile)(file, "utf-8");
1369
+ const declaredIds = extractDeclaredContractIds(text);
1370
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1371
+ let doc = null;
1372
+ try {
1373
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1374
+ } catch (error) {
1375
+ issues.push(
1376
+ issue(
1377
+ "QFAI-CONTRACT-001",
1378
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1379
+ "error",
1380
+ file,
1381
+ "contracts.ui.parse"
1382
+ )
1383
+ );
1384
+ }
1385
+ const invalidIds = extractInvalidIds(text, [
1386
+ "SPEC",
1387
+ "BR",
1388
+ "SC",
1389
+ "UI",
1390
+ "API",
1391
+ "DB",
1392
+ "THEMA",
1393
+ "ADR"
1394
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
1395
+ if (invalidIds.length > 0) {
1396
+ issues.push(
1397
+ issue(
1398
+ "QFAI-ID-002",
1399
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1400
+ "error",
1401
+ file,
1402
+ "id.format",
1403
+ invalidIds
1404
+ )
1405
+ );
1406
+ }
1407
+ if (doc) {
1408
+ issues.push(
1409
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
1410
+ );
1411
+ }
1412
+ }
1413
+ return issues;
1414
+ }
1415
+ async function validateThemaContracts(uiRoot) {
1416
+ const files = await collectThemaContractFiles(uiRoot);
1417
+ if (files.length === 0) {
1418
+ return [];
1419
+ }
1420
+ const issues = [];
1344
1421
  for (const file of files) {
1345
1422
  const text = await (0, import_promises8.readFile)(file, "utf-8");
1346
1423
  const invalidIds = extractInvalidIds(text, [
@@ -1350,6 +1427,7 @@ async function validateUiContracts(uiRoot) {
1350
1427
  "UI",
1351
1428
  "API",
1352
1429
  "DB",
1430
+ "THEMA",
1353
1431
  "ADR"
1354
1432
  ]);
1355
1433
  if (invalidIds.length > 0) {
@@ -1365,17 +1443,95 @@ async function validateUiContracts(uiRoot) {
1365
1443
  );
1366
1444
  }
1367
1445
  const declaredIds = extractDeclaredContractIds(text);
1368
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1446
+ if (declaredIds.length === 0) {
1447
+ issues.push(
1448
+ issue(
1449
+ "QFAI-THEMA-010",
1450
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1451
+ "error",
1452
+ file,
1453
+ "contracts.thema.declaration"
1454
+ )
1455
+ );
1456
+ continue;
1457
+ }
1458
+ if (declaredIds.length > 1) {
1459
+ issues.push(
1460
+ issue(
1461
+ "QFAI-THEMA-011",
1462
+ `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(
1463
+ ", "
1464
+ )}`,
1465
+ "error",
1466
+ file,
1467
+ "contracts.thema.declaration",
1468
+ declaredIds
1469
+ )
1470
+ );
1471
+ continue;
1472
+ }
1473
+ const declaredId = declaredIds[0] ?? "";
1474
+ if (!THEMA_ID_RE.test(declaredId)) {
1475
+ issues.push(
1476
+ issue(
1477
+ "QFAI-THEMA-012",
1478
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
1479
+ "error",
1480
+ file,
1481
+ "contracts.thema.idFormat",
1482
+ [declaredId]
1483
+ )
1484
+ );
1485
+ }
1486
+ let doc;
1369
1487
  try {
1370
- parseStructuredContract(file, stripContractDeclarationLines(text));
1488
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1371
1489
  } catch (error) {
1372
1490
  issues.push(
1373
1491
  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)})`,
1492
+ "QFAI-THEMA-001",
1493
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1376
1494
  "error",
1377
1495
  file,
1378
- "contracts.ui.parse"
1496
+ "contracts.thema.parse"
1497
+ )
1498
+ );
1499
+ continue;
1500
+ }
1501
+ const docId = typeof doc.id === "string" ? doc.id : "";
1502
+ if (!THEMA_ID_RE.test(docId)) {
1503
+ issues.push(
1504
+ issue(
1505
+ "QFAI-THEMA-012",
1506
+ 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",
1507
+ "error",
1508
+ file,
1509
+ "contracts.thema.idFormat",
1510
+ docId.length > 0 ? [docId] : void 0
1511
+ )
1512
+ );
1513
+ }
1514
+ const name = typeof doc.name === "string" ? doc.name : "";
1515
+ if (!name) {
1516
+ issues.push(
1517
+ issue(
1518
+ "QFAI-THEMA-014",
1519
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1520
+ "error",
1521
+ file,
1522
+ "contracts.thema.name"
1523
+ )
1524
+ );
1525
+ }
1526
+ if (declaredId && docId && declaredId !== docId) {
1527
+ issues.push(
1528
+ issue(
1529
+ "QFAI-THEMA-013",
1530
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
1531
+ "error",
1532
+ file,
1533
+ "contracts.thema.idMismatch",
1534
+ [declaredId, docId]
1379
1535
  )
1380
1536
  );
1381
1537
  }
@@ -1405,6 +1561,7 @@ async function validateApiContracts(apiRoot) {
1405
1561
  "UI",
1406
1562
  "API",
1407
1563
  "DB",
1564
+ "THEMA",
1408
1565
  "ADR"
1409
1566
  ]);
1410
1567
  if (invalidIds.length > 0) {
@@ -1473,6 +1630,7 @@ async function validateDbContracts(dbRoot) {
1473
1630
  "UI",
1474
1631
  "API",
1475
1632
  "DB",
1633
+ "THEMA",
1476
1634
  "ADR"
1477
1635
  ]);
1478
1636
  if (invalidIds.length > 0) {
@@ -1579,6 +1737,278 @@ function validateDuplicateContractIds(contractIndex) {
1579
1737
  function hasOpenApi(doc) {
1580
1738
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1581
1739
  }
1740
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
1741
+ const issues = [];
1742
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
1743
+ const themaRef = doc.themaRef;
1744
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
1745
+ issues.push(
1746
+ issue(
1747
+ "QFAI-UI-020",
1748
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1749
+ "error",
1750
+ file,
1751
+ "contracts.ui.themaRef"
1752
+ )
1753
+ );
1754
+ } else if (!THEMA_ID_RE.test(themaRef)) {
1755
+ issues.push(
1756
+ issue(
1757
+ "QFAI-UI-020",
1758
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
1759
+ "error",
1760
+ file,
1761
+ "contracts.ui.themaRef",
1762
+ [themaRef]
1763
+ )
1764
+ );
1765
+ } else if (!themaIds.has(themaRef)) {
1766
+ issues.push(
1767
+ issue(
1768
+ "QFAI-UI-020",
1769
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
1770
+ "error",
1771
+ file,
1772
+ "contracts.ui.themaRef",
1773
+ [themaRef]
1774
+ )
1775
+ );
1776
+ }
1777
+ }
1778
+ const assets = doc.assets;
1779
+ if (assets && typeof assets === "object") {
1780
+ issues.push(
1781
+ ...await validateUiAssets(
1782
+ assets,
1783
+ file,
1784
+ uiRoot
1785
+ )
1786
+ );
1787
+ }
1788
+ return issues;
1789
+ }
1790
+ async function validateUiAssets(assets, file, uiRoot) {
1791
+ const issues = [];
1792
+ const packValue = assets.pack;
1793
+ const useValue = assets.use;
1794
+ if (packValue === void 0 && useValue === void 0) {
1795
+ return issues;
1796
+ }
1797
+ if (typeof packValue !== "string" || packValue.length === 0) {
1798
+ issues.push(
1799
+ issue(
1800
+ "QFAI-ASSET-001",
1801
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1802
+ "error",
1803
+ file,
1804
+ "assets.pack"
1805
+ )
1806
+ );
1807
+ return issues;
1808
+ }
1809
+ if (!isSafeRelativePath(packValue)) {
1810
+ issues.push(
1811
+ issue(
1812
+ "QFAI-ASSET-001",
1813
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
1814
+ "error",
1815
+ file,
1816
+ "assets.pack",
1817
+ [packValue]
1818
+ )
1819
+ );
1820
+ return issues;
1821
+ }
1822
+ const packDir = import_node_path10.default.resolve(uiRoot, packValue);
1823
+ const packRelative = import_node_path10.default.relative(uiRoot, packDir);
1824
+ if (packRelative.startsWith("..") || import_node_path10.default.isAbsolute(packRelative)) {
1825
+ issues.push(
1826
+ issue(
1827
+ "QFAI-ASSET-001",
1828
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
1829
+ "error",
1830
+ file,
1831
+ "assets.pack",
1832
+ [packValue]
1833
+ )
1834
+ );
1835
+ return issues;
1836
+ }
1837
+ if (!await exists4(packDir)) {
1838
+ issues.push(
1839
+ issue(
1840
+ "QFAI-ASSET-001",
1841
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
1842
+ "error",
1843
+ file,
1844
+ "assets.pack",
1845
+ [packValue]
1846
+ )
1847
+ );
1848
+ return issues;
1849
+ }
1850
+ const assetsYamlPath = import_node_path10.default.join(packDir, "assets.yaml");
1851
+ if (!await exists4(assetsYamlPath)) {
1852
+ issues.push(
1853
+ issue(
1854
+ "QFAI-ASSET-002",
1855
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
1856
+ "error",
1857
+ assetsYamlPath,
1858
+ "assets.yaml"
1859
+ )
1860
+ );
1861
+ return issues;
1862
+ }
1863
+ let manifest;
1864
+ try {
1865
+ const manifestText = await (0, import_promises8.readFile)(assetsYamlPath, "utf-8");
1866
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
1867
+ } catch (error) {
1868
+ issues.push(
1869
+ issue(
1870
+ "QFAI-ASSET-002",
1871
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error)})`,
1872
+ "error",
1873
+ assetsYamlPath,
1874
+ "assets.yaml"
1875
+ )
1876
+ );
1877
+ return issues;
1878
+ }
1879
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
1880
+ const itemIds = /* @__PURE__ */ new Set();
1881
+ const itemPaths = [];
1882
+ for (const item of items) {
1883
+ if (!item || typeof item !== "object") {
1884
+ continue;
1885
+ }
1886
+ const record2 = item;
1887
+ const id = typeof record2.id === "string" ? record2.id : void 0;
1888
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
1889
+ if (id) {
1890
+ itemIds.add(id);
1891
+ }
1892
+ itemPaths.push({ id, path: pathValue });
1893
+ }
1894
+ if (useValue !== void 0) {
1895
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
1896
+ issues.push(
1897
+ issue(
1898
+ "QFAI-ASSET-003",
1899
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1900
+ "error",
1901
+ file,
1902
+ "assets.use"
1903
+ )
1904
+ );
1905
+ } else {
1906
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
1907
+ if (missing.length > 0) {
1908
+ issues.push(
1909
+ issue(
1910
+ "QFAI-ASSET-003",
1911
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
1912
+ "error",
1913
+ file,
1914
+ "assets.use",
1915
+ missing
1916
+ )
1917
+ );
1918
+ }
1919
+ }
1920
+ }
1921
+ for (const entry of itemPaths) {
1922
+ if (!entry.path) {
1923
+ continue;
1924
+ }
1925
+ if (!isSafeRelativePath(entry.path)) {
1926
+ issues.push(
1927
+ issue(
1928
+ "QFAI-ASSET-004",
1929
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
1930
+ "error",
1931
+ assetsYamlPath,
1932
+ "assets.path",
1933
+ entry.id ? [entry.id] : [entry.path]
1934
+ )
1935
+ );
1936
+ continue;
1937
+ }
1938
+ const assetPath = import_node_path10.default.resolve(packDir, entry.path);
1939
+ const assetRelative = import_node_path10.default.relative(packDir, assetPath);
1940
+ if (assetRelative.startsWith("..") || import_node_path10.default.isAbsolute(assetRelative)) {
1941
+ issues.push(
1942
+ issue(
1943
+ "QFAI-ASSET-004",
1944
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
1945
+ "error",
1946
+ assetsYamlPath,
1947
+ "assets.path",
1948
+ entry.id ? [entry.id] : [entry.path]
1949
+ )
1950
+ );
1951
+ continue;
1952
+ }
1953
+ if (!await exists4(assetPath)) {
1954
+ issues.push(
1955
+ issue(
1956
+ "QFAI-ASSET-004",
1957
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
1958
+ "error",
1959
+ assetsYamlPath,
1960
+ "assets.path",
1961
+ entry.id ? [entry.id] : [entry.path]
1962
+ )
1963
+ );
1964
+ }
1965
+ }
1966
+ return issues;
1967
+ }
1968
+ function shouldIgnoreInvalidId(value, doc) {
1969
+ if (!doc) {
1970
+ return false;
1971
+ }
1972
+ const assets = doc.assets;
1973
+ if (!assets || typeof assets !== "object") {
1974
+ return false;
1975
+ }
1976
+ const packValue = assets.pack;
1977
+ if (typeof packValue !== "string" || packValue.length === 0) {
1978
+ return false;
1979
+ }
1980
+ const normalized = packValue.replace(/\\/g, "/");
1981
+ const basename = import_node_path10.default.posix.basename(normalized);
1982
+ if (!basename) {
1983
+ return false;
1984
+ }
1985
+ return value.toLowerCase() === basename.toLowerCase();
1986
+ }
1987
+ function isSafeRelativePath(value) {
1988
+ if (!value) {
1989
+ return false;
1990
+ }
1991
+ if (import_node_path10.default.isAbsolute(value)) {
1992
+ return false;
1993
+ }
1994
+ const normalized = value.replace(/\\/g, "/");
1995
+ if (/^[A-Za-z]:/.test(normalized)) {
1996
+ return false;
1997
+ }
1998
+ const segments = normalized.split("/");
1999
+ if (segments.some((segment) => segment === "..")) {
2000
+ return false;
2001
+ }
2002
+ return true;
2003
+ }
2004
+ async function exists4(target) {
2005
+ try {
2006
+ await (0, import_promises8.access)(target);
2007
+ return true;
2008
+ } catch {
2009
+ return false;
2010
+ }
2011
+ }
1582
2012
  function formatError4(error) {
1583
2013
  if (error instanceof Error) {
1584
2014
  return error.message;
@@ -1609,12 +2039,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
1609
2039
 
1610
2040
  // src/core/validators/delta.ts
1611
2041
  var import_promises9 = require("fs/promises");
1612
- var import_node_path10 = __toESM(require("path"), 1);
1613
- var SECTION_RE = /^##\s+変更区分/m;
1614
- var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1615
- var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
1616
- var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
1617
- var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
2042
+ var import_node_path11 = __toESM(require("path"), 1);
1618
2043
  async function validateDeltas(root, config) {
1619
2044
  const specsRoot = resolvePath(root, config, "specsDir");
1620
2045
  const packs = await collectSpecPackDirs(specsRoot);
@@ -1623,10 +2048,9 @@ async function validateDeltas(root, config) {
1623
2048
  }
1624
2049
  const issues = [];
1625
2050
  for (const pack of packs) {
1626
- const deltaPath = import_node_path10.default.join(pack, "delta.md");
1627
- let text;
2051
+ const deltaPath = import_node_path11.default.join(pack, "delta.md");
1628
2052
  try {
1629
- text = await (0, import_promises9.readFile)(deltaPath, "utf-8");
2053
+ await (0, import_promises9.readFile)(deltaPath, "utf-8");
1630
2054
  } catch (error) {
1631
2055
  if (isMissingFileError2(error)) {
1632
2056
  issues.push(
@@ -1635,41 +2059,16 @@ async function validateDeltas(root, config) {
1635
2059
  "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1636
2060
  "error",
1637
2061
  deltaPath,
1638
- "delta.exists"
2062
+ "delta.exists",
2063
+ void 0,
2064
+ "change",
2065
+ "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"
1639
2066
  )
1640
2067
  );
1641
2068
  continue;
1642
2069
  }
1643
2070
  throw error;
1644
2071
  }
1645
- const hasSection = SECTION_RE.test(text);
1646
- const hasCompatibility = COMPAT_LINE_RE.test(text);
1647
- const hasChange = CHANGE_LINE_RE.test(text);
1648
- if (!hasSection || !hasCompatibility || !hasChange) {
1649
- issues.push(
1650
- issue2(
1651
- "QFAI-DELTA-002",
1652
- "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",
1653
- "error",
1654
- deltaPath,
1655
- "delta.section"
1656
- )
1657
- );
1658
- continue;
1659
- }
1660
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1661
- const changeChecked = CHANGE_CHECKED_RE.test(text);
1662
- if (compatibilityChecked === changeChecked) {
1663
- issues.push(
1664
- issue2(
1665
- "QFAI-DELTA-003",
1666
- "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",
1667
- "error",
1668
- deltaPath,
1669
- "delta.classification"
1670
- )
1671
- );
1672
- }
1673
2072
  }
1674
2073
  return issues;
1675
2074
  }
@@ -1703,7 +2102,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
1703
2102
 
1704
2103
  // src/core/validators/ids.ts
1705
2104
  var import_promises10 = require("fs/promises");
1706
- var import_node_path11 = __toESM(require("path"), 1);
2105
+ var import_node_path12 = __toESM(require("path"), 1);
1707
2106
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1708
2107
  async function validateDefinedIds(root, config) {
1709
2108
  const issues = [];
@@ -1769,7 +2168,7 @@ function recordId(out, id, file) {
1769
2168
  }
1770
2169
  function formatFileList(files, root) {
1771
2170
  return files.map((file) => {
1772
- const relative = import_node_path11.default.relative(root, file);
2171
+ const relative = import_node_path12.default.relative(root, file);
1773
2172
  return relative.length > 0 ? relative : file;
1774
2173
  }).join(", ");
1775
2174
  }
@@ -1797,19 +2196,19 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
1797
2196
 
1798
2197
  // src/core/promptsIntegrity.ts
1799
2198
  var import_promises11 = require("fs/promises");
1800
- var import_node_path13 = __toESM(require("path"), 1);
2199
+ var import_node_path14 = __toESM(require("path"), 1);
1801
2200
 
1802
2201
  // src/shared/assets.ts
1803
2202
  var import_node_fs = require("fs");
1804
- var import_node_path12 = __toESM(require("path"), 1);
2203
+ var import_node_path13 = __toESM(require("path"), 1);
1805
2204
  var import_node_url2 = require("url");
1806
2205
  function getInitAssetsDir() {
1807
2206
  const base = __filename;
1808
2207
  const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
1809
- const baseDir = import_node_path12.default.dirname(basePath);
2208
+ const baseDir = import_node_path13.default.dirname(basePath);
1810
2209
  const candidates = [
1811
- import_node_path12.default.resolve(baseDir, "../../../assets/init"),
1812
- import_node_path12.default.resolve(baseDir, "../../assets/init")
2210
+ import_node_path13.default.resolve(baseDir, "../../../assets/init"),
2211
+ import_node_path13.default.resolve(baseDir, "../../assets/init")
1813
2212
  ];
1814
2213
  for (const candidate of candidates) {
1815
2214
  if ((0, import_node_fs.existsSync)(candidate)) {
@@ -1826,11 +2225,12 @@ function getInitAssetsDir() {
1826
2225
  }
1827
2226
 
1828
2227
  // src/core/promptsIntegrity.ts
2228
+ var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
1829
2229
  async function diffProjectPromptsAgainstInitAssets(root) {
1830
- const promptsDir = import_node_path13.default.resolve(root, ".qfai", "prompts");
2230
+ const promptsDir = import_node_path14.default.resolve(root, ".qfai", "prompts");
1831
2231
  let templateDir;
1832
2232
  try {
1833
- templateDir = import_node_path13.default.join(getInitAssetsDir(), ".qfai", "prompts");
2233
+ templateDir = import_node_path14.default.join(getInitAssetsDir(), ".qfai", "prompts");
1834
2234
  } catch {
1835
2235
  return {
1836
2236
  status: "skipped_missing_assets",
@@ -1874,6 +2274,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1874
2274
  extra.push(rel);
1875
2275
  }
1876
2276
  }
2277
+ const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
1877
2278
  const common = intersectKeys(templateByRel, projectByRel);
1878
2279
  for (const rel of common) {
1879
2280
  const templateAbs = templateByRel.get(rel);
@@ -1893,13 +2294,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1893
2294
  changed.push(rel);
1894
2295
  }
1895
2296
  }
1896
- const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
2297
+ const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
1897
2298
  return {
1898
2299
  status,
1899
2300
  promptsDir,
1900
2301
  templateDir,
1901
2302
  missing: missing.sort(),
1902
- extra: extra.sort(),
2303
+ extra: filteredExtra.sort(),
1903
2304
  changed: changed.sort()
1904
2305
  };
1905
2306
  }
@@ -1907,7 +2308,7 @@ function normalizeNewlines(text) {
1907
2308
  return text.replace(/\r\n/g, "\n");
1908
2309
  }
1909
2310
  function toRel(base, abs) {
1910
- const rel = import_node_path13.default.relative(base, abs);
2311
+ const rel = import_node_path14.default.relative(base, abs);
1911
2312
  return rel.replace(/[\\/]+/g, "/");
1912
2313
  }
1913
2314
  function intersectKeys(a, b) {
@@ -1953,6 +2354,7 @@ async function validatePromptsIntegrity(root) {
1953
2354
 
1954
2355
  // src/core/validators/scenario.ts
1955
2356
  var import_promises12 = require("fs/promises");
2357
+ var import_node_path15 = __toESM(require("path"), 1);
1956
2358
  var GIVEN_PATTERN = /\bGiven\b/;
1957
2359
  var WHEN_PATTERN = /\bWhen\b/;
1958
2360
  var THEN_PATTERN = /\bThen\b/;
@@ -1975,6 +2377,18 @@ async function validateScenarios(root, config) {
1975
2377
  }
1976
2378
  const issues = [];
1977
2379
  for (const entry of entries) {
2380
+ const legacyScenarioPath = import_node_path15.default.join(entry.dir, "scenario.md");
2381
+ if (await fileExists(legacyScenarioPath)) {
2382
+ issues.push(
2383
+ issue4(
2384
+ "QFAI-SC-004",
2385
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2386
+ "error",
2387
+ legacyScenarioPath,
2388
+ "scenario.legacy"
2389
+ )
2390
+ );
2391
+ }
1978
2392
  let text;
1979
2393
  try {
1980
2394
  text = await (0, import_promises12.readFile)(entry.scenarioPath, "utf-8");
@@ -2006,6 +2420,7 @@ function validateScenarioContent(text, file) {
2006
2420
  "UI",
2007
2421
  "API",
2008
2422
  "DB",
2423
+ "THEMA",
2009
2424
  "ADR"
2010
2425
  ]);
2011
2426
  if (invalidIds.length > 0) {
@@ -2149,6 +2564,14 @@ function isMissingFileError3(error) {
2149
2564
  }
2150
2565
  return error.code === "ENOENT";
2151
2566
  }
2567
+ async function fileExists(target) {
2568
+ try {
2569
+ await (0, import_promises12.access)(target);
2570
+ return true;
2571
+ } catch {
2572
+ return false;
2573
+ }
2574
+ }
2152
2575
 
2153
2576
  // src/core/validators/spec.ts
2154
2577
  var import_promises13 = require("fs/promises");
@@ -2208,6 +2631,7 @@ function validateSpecContent(text, file, requiredSections) {
2208
2631
  "UI",
2209
2632
  "API",
2210
2633
  "DB",
2634
+ "THEMA",
2211
2635
  "ADR"
2212
2636
  ]);
2213
2637
  if (invalidIds.length > 0) {
@@ -2387,7 +2811,7 @@ async function validateTraceability(root, config) {
2387
2811
  "QFAI-TRACE-021",
2388
2812
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2389
2813
  ", "
2390
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2814
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
2391
2815
  "error",
2392
2816
  file,
2393
2817
  "traceability.specContractRefFormat",
@@ -2451,7 +2875,7 @@ async function validateTraceability(root, config) {
2451
2875
  "QFAI-TRACE-032",
2452
2876
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2453
2877
  ", "
2454
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2878
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
2455
2879
  "error",
2456
2880
  file,
2457
2881
  "traceability.scenarioContractRefFormat",
@@ -2830,17 +3254,25 @@ function countIssues(issues) {
2830
3254
  }
2831
3255
 
2832
3256
  // src/core/report.ts
2833
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3257
+ var ID_PREFIXES2 = [
3258
+ "SPEC",
3259
+ "BR",
3260
+ "SC",
3261
+ "UI",
3262
+ "API",
3263
+ "DB",
3264
+ "THEMA"
3265
+ ];
2834
3266
  async function createReportData(root, validation, configResult) {
2835
- const resolvedRoot = import_node_path14.default.resolve(root);
3267
+ const resolvedRoot = import_node_path16.default.resolve(root);
2836
3268
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2837
3269
  const config = resolved.config;
2838
3270
  const configPath = resolved.configPath;
2839
3271
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2840
3272
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2841
- const apiRoot = import_node_path14.default.join(contractsRoot, "api");
2842
- const uiRoot = import_node_path14.default.join(contractsRoot, "ui");
2843
- const dbRoot = import_node_path14.default.join(contractsRoot, "db");
3273
+ const apiRoot = import_node_path16.default.join(contractsRoot, "api");
3274
+ const uiRoot = import_node_path16.default.join(contractsRoot, "ui");
3275
+ const dbRoot = import_node_path16.default.join(contractsRoot, "db");
2844
3276
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2845
3277
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2846
3278
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2848,7 +3280,8 @@ async function createReportData(root, validation, configResult) {
2848
3280
  const {
2849
3281
  api: apiFiles,
2850
3282
  ui: uiFiles,
2851
- db: dbFiles
3283
+ db: dbFiles,
3284
+ thema: themaFiles
2852
3285
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2853
3286
  const contractIndex = await buildContractIndex(resolvedRoot, config);
2854
3287
  const contractIdList = Array.from(contractIndex.ids);
@@ -2875,7 +3308,8 @@ async function createReportData(root, validation, configResult) {
2875
3308
  ...scenarioFiles,
2876
3309
  ...apiFiles,
2877
3310
  ...uiFiles,
2878
- ...dbFiles
3311
+ ...dbFiles,
3312
+ ...themaFiles
2879
3313
  ]);
2880
3314
  const upstreamIds = await collectUpstreamIds([
2881
3315
  ...specFiles,
@@ -2912,7 +3346,8 @@ async function createReportData(root, validation, configResult) {
2912
3346
  contracts: {
2913
3347
  api: apiFiles.length,
2914
3348
  ui: uiFiles.length,
2915
- db: dbFiles.length
3349
+ db: dbFiles.length,
3350
+ thema: themaFiles.length
2916
3351
  },
2917
3352
  counts: normalizedValidation.counts
2918
3353
  },
@@ -2922,7 +3357,8 @@ async function createReportData(root, validation, configResult) {
2922
3357
  sc: idsByPrefix.SC,
2923
3358
  ui: idsByPrefix.UI,
2924
3359
  api: idsByPrefix.API,
2925
- db: idsByPrefix.DB
3360
+ db: idsByPrefix.DB,
3361
+ thema: idsByPrefix.THEMA
2926
3362
  },
2927
3363
  traceability: {
2928
3364
  upstreamIdsFound: upstreamIds.size,
@@ -2992,7 +3428,7 @@ function formatReportMarkdown(data, options = {}) {
2992
3428
  lines.push(`- specs: ${data.summary.specs}`);
2993
3429
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2994
3430
  lines.push(
2995
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
3431
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
2996
3432
  );
2997
3433
  lines.push(
2998
3434
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3136,6 +3572,7 @@ function formatReportMarkdown(data, options = {}) {
3136
3572
  lines.push(formatIdLine("UI", data.ids.ui));
3137
3573
  lines.push(formatIdLine("API", data.ids.api));
3138
3574
  lines.push(formatIdLine("DB", data.ids.db));
3575
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3139
3576
  lines.push("");
3140
3577
  lines.push("## Traceability");
3141
3578
  lines.push("");
@@ -3300,12 +3737,8 @@ function formatReportMarkdown(data, options = {}) {
3300
3737
  "- 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"
3301
3738
  );
3302
3739
  }
3303
- lines.push(
3304
- "- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
3305
- );
3306
- lines.push(
3307
- "- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
3308
- );
3740
+ lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
3741
+ lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md`");
3309
3742
  return lines.join("\n");
3310
3743
  }
3311
3744
  function formatReportJson(data) {
@@ -3357,7 +3790,8 @@ async function collectIds(files) {
3357
3790
  SC: /* @__PURE__ */ new Set(),
3358
3791
  UI: /* @__PURE__ */ new Set(),
3359
3792
  API: /* @__PURE__ */ new Set(),
3360
- DB: /* @__PURE__ */ new Set()
3793
+ DB: /* @__PURE__ */ new Set(),
3794
+ THEMA: /* @__PURE__ */ new Set()
3361
3795
  };
3362
3796
  for (const file of files) {
3363
3797
  const text = await (0, import_promises15.readFile)(file, "utf-8");
@@ -3372,7 +3806,8 @@ async function collectIds(files) {
3372
3806
  SC: toSortedArray2(result.SC),
3373
3807
  UI: toSortedArray2(result.UI),
3374
3808
  API: toSortedArray2(result.API),
3375
- DB: toSortedArray2(result.DB)
3809
+ DB: toSortedArray2(result.DB),
3810
+ THEMA: toSortedArray2(result.THEMA)
3376
3811
  };
3377
3812
  }
3378
3813
  async function collectUpstreamIds(files) {