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.mjs CHANGED
@@ -6,7 +6,6 @@ var defaultConfig = {
6
6
  paths: {
7
7
  contractsDir: ".qfai/contracts",
8
8
  specsDir: ".qfai/specs",
9
- rulesDir: ".qfai/rules",
10
9
  outDir: ".qfai/out",
11
10
  promptsDir: ".qfai/prompts",
12
11
  srcDir: "src",
@@ -119,13 +118,6 @@ function normalizePaths(raw, configPath, issues) {
119
118
  configPath,
120
119
  issues
121
120
  ),
122
- rulesDir: readString(
123
- raw.rulesDir,
124
- base.rulesDir,
125
- "paths.rulesDir",
126
- configPath,
127
- issues
128
- ),
129
121
  outDir: readString(
130
122
  raw.outDir,
131
123
  base.outDir,
@@ -399,7 +391,15 @@ function isRecord(value) {
399
391
  }
400
392
 
401
393
  // src/core/ids.ts
402
- var ID_PREFIXES = ["SPEC", "BR", "SC", "UI", "API", "DB"];
394
+ var ID_PREFIXES = [
395
+ "SPEC",
396
+ "BR",
397
+ "SC",
398
+ "UI",
399
+ "API",
400
+ "DB",
401
+ "THEMA"
402
+ ];
403
403
  var STRICT_ID_PATTERNS = {
404
404
  SPEC: /\bSPEC-\d{4}\b/g,
405
405
  BR: /\bBR-\d{4}\b/g,
@@ -407,6 +407,7 @@ var STRICT_ID_PATTERNS = {
407
407
  UI: /\bUI-\d{4}\b/g,
408
408
  API: /\bAPI-\d{4}\b/g,
409
409
  DB: /\bDB-\d{4}\b/g,
410
+ THEMA: /\bTHEMA-\d{3}\b/g,
410
411
  ADR: /\bADR-\d{4}\b/g
411
412
  };
412
413
  var LOOSE_ID_PATTERNS = {
@@ -416,6 +417,7 @@ var LOOSE_ID_PATTERNS = {
416
417
  UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
417
418
  API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
418
419
  DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
420
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
419
421
  ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
420
422
  };
421
423
  function extractIds(text, prefix) {
@@ -453,14 +455,15 @@ function isValidId(value, prefix) {
453
455
 
454
456
  // src/core/report.ts
455
457
  import { readFile as readFile12 } from "fs/promises";
456
- import path14 from "path";
458
+ import path16 from "path";
457
459
 
458
460
  // src/core/contractIndex.ts
459
461
  import { readFile as readFile2 } from "fs/promises";
460
- import path4 from "path";
462
+ import path5 from "path";
461
463
 
462
464
  // src/core/discovery.ts
463
465
  import { access as access3 } from "fs/promises";
466
+ import path4 from "path";
464
467
 
465
468
  // src/core/fs.ts
466
469
  import { access as access2, readdir } from "fs/promises";
@@ -609,7 +612,12 @@ async function collectScenarioFiles(specsRoot) {
609
612
  return filterExisting(entries.map((entry) => entry.scenarioPath));
610
613
  }
611
614
  async function collectUiContractFiles(uiRoot) {
612
- return collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
615
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
616
+ return filterByBasenamePrefix(files, "ui-");
617
+ }
618
+ async function collectThemaContractFiles(uiRoot) {
619
+ const files = await collectFiles(uiRoot, { extensions: [".yaml", ".yml"] });
620
+ return filterByBasenamePrefix(files, "thema-");
613
621
  }
614
622
  async function collectApiContractFiles(apiRoot) {
615
623
  return collectFiles(apiRoot, { extensions: [".yaml", ".yml", ".json"] });
@@ -618,12 +626,13 @@ async function collectDbContractFiles(dbRoot) {
618
626
  return collectFiles(dbRoot, { extensions: [".sql"] });
619
627
  }
620
628
  async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
621
- const [ui, api, db] = await Promise.all([
629
+ const [ui, thema, api, db] = await Promise.all([
622
630
  collectUiContractFiles(uiRoot),
631
+ collectThemaContractFiles(uiRoot),
623
632
  collectApiContractFiles(apiRoot),
624
633
  collectDbContractFiles(dbRoot)
625
634
  ]);
626
- return { ui, api, db };
635
+ return { ui, thema, api, db };
627
636
  }
628
637
  async function filterExisting(files) {
629
638
  const existing = [];
@@ -642,10 +651,16 @@ async function exists3(target) {
642
651
  return false;
643
652
  }
644
653
  }
654
+ function filterByBasenamePrefix(files, prefix) {
655
+ const lowerPrefix = prefix.toLowerCase();
656
+ return files.filter(
657
+ (file) => path4.basename(file).toLowerCase().startsWith(lowerPrefix)
658
+ );
659
+ }
645
660
 
646
661
  // 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*$/;
662
+ var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
663
+ var CONTRACT_DECLARATION_LINE_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/;
649
664
  function extractDeclaredContractIds(text) {
650
665
  const ids = [];
651
666
  for (const match of text.matchAll(CONTRACT_DECLARATION_RE)) {
@@ -663,20 +678,22 @@ function stripContractDeclarationLines(text) {
663
678
  // src/core/contractIndex.ts
664
679
  async function buildContractIndex(root, config) {
665
680
  const contractsRoot = resolvePath(root, config, "contractsDir");
666
- const uiRoot = path4.join(contractsRoot, "ui");
667
- const apiRoot = path4.join(contractsRoot, "api");
668
- const dbRoot = path4.join(contractsRoot, "db");
669
- const [uiFiles, apiFiles, dbFiles] = await Promise.all([
681
+ const uiRoot = path5.join(contractsRoot, "ui");
682
+ const apiRoot = path5.join(contractsRoot, "api");
683
+ const dbRoot = path5.join(contractsRoot, "db");
684
+ const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
670
685
  collectUiContractFiles(uiRoot),
686
+ collectThemaContractFiles(uiRoot),
671
687
  collectApiContractFiles(apiRoot),
672
688
  collectDbContractFiles(dbRoot)
673
689
  ]);
674
690
  const index = {
675
691
  ids: /* @__PURE__ */ new Set(),
676
692
  idToFiles: /* @__PURE__ */ new Map(),
677
- files: { ui: uiFiles, api: apiFiles, db: dbFiles }
693
+ files: { ui: uiFiles, thema: themaFiles, api: apiFiles, db: dbFiles }
678
694
  };
679
695
  await indexContractFiles(uiFiles, index);
696
+ await indexContractFiles(themaFiles, index);
680
697
  await indexContractFiles(apiFiles, index);
681
698
  await indexContractFiles(dbFiles, index);
682
699
  return index;
@@ -695,15 +712,15 @@ function record(index, id, file) {
695
712
  }
696
713
 
697
714
  // src/core/paths.ts
698
- import path5 from "path";
715
+ import path6 from "path";
699
716
  function toRelativePath(root, target) {
700
717
  if (!target) {
701
718
  return target;
702
719
  }
703
- if (!path5.isAbsolute(target)) {
720
+ if (!path6.isAbsolute(target)) {
704
721
  return toPosixPath(target);
705
722
  }
706
- const relative = path5.relative(root, target);
723
+ const relative = path6.relative(root, target);
707
724
  if (!relative) {
708
725
  return ".";
709
726
  }
@@ -751,7 +768,7 @@ function normalizeValidationResult(root, result) {
751
768
  }
752
769
 
753
770
  // src/core/parse/contractRefs.ts
754
- var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
771
+ var CONTRACT_REF_ID_RE = /^(?:(?:API|UI|DB)-\d{4}|THEMA-\d{3})$/;
755
772
  function parseContractRefs(text, options = {}) {
756
773
  const linePattern = buildLinePattern(options);
757
774
  const lines = [];
@@ -922,7 +939,7 @@ function parseSpec(md, file) {
922
939
 
923
940
  // src/core/traceability.ts
924
941
  import { readFile as readFile3 } from "fs/promises";
925
- import path6 from "path";
942
+ import path7 from "path";
926
943
 
927
944
  // src/core/gherkin/parse.ts
928
945
  import {
@@ -1154,7 +1171,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1154
1171
  };
1155
1172
  }
1156
1173
  const normalizedFiles = Array.from(
1157
- new Set(scanResult.files.map((file) => path6.normalize(file)))
1174
+ new Set(scanResult.files.map((file) => path7.normalize(file)))
1158
1175
  );
1159
1176
  for (const file of normalizedFiles) {
1160
1177
  const text = await readFile3(file, "utf-8");
@@ -1217,11 +1234,11 @@ function formatError3(error) {
1217
1234
 
1218
1235
  // src/core/version.ts
1219
1236
  import { readFile as readFile4 } from "fs/promises";
1220
- import path7 from "path";
1237
+ import path8 from "path";
1221
1238
  import { fileURLToPath } from "url";
1222
1239
  async function resolveToolVersion() {
1223
- if ("1.0.2".length > 0) {
1224
- return "1.0.2";
1240
+ if ("1.0.4".length > 0) {
1241
+ return "1.0.4";
1225
1242
  }
1226
1243
  try {
1227
1244
  const packagePath = resolvePackageJsonPath();
@@ -1236,18 +1253,18 @@ async function resolveToolVersion() {
1236
1253
  function resolvePackageJsonPath() {
1237
1254
  const base = import.meta.url;
1238
1255
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
1239
- return path7.resolve(path7.dirname(basePath), "../../package.json");
1256
+ return path8.resolve(path8.dirname(basePath), "../../package.json");
1240
1257
  }
1241
1258
 
1242
1259
  // src/core/validators/contracts.ts
1243
- import { readFile as readFile5 } from "fs/promises";
1244
- import path9 from "path";
1260
+ import { access as access4, readFile as readFile5 } from "fs/promises";
1261
+ import path10 from "path";
1245
1262
 
1246
1263
  // src/core/contracts.ts
1247
- import path8 from "path";
1264
+ import path9 from "path";
1248
1265
  import { parse as parseYaml2 } from "yaml";
1249
1266
  function parseStructuredContract(file, text) {
1250
- const ext = path8.extname(file).toLowerCase();
1267
+ const ext = path9.extname(file).toLowerCase();
1251
1268
  if (ext === ".json") {
1252
1269
  return JSON.parse(text);
1253
1270
  }
@@ -1264,17 +1281,23 @@ var SQL_DANGEROUS_PATTERNS = [
1264
1281
  label: "ALTER TABLE ... DROP"
1265
1282
  }
1266
1283
  ];
1284
+ var THEMA_ID_RE = /^THEMA-\d{3}$/;
1267
1285
  async function validateContracts(root, config) {
1268
1286
  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
1287
  const contractIndex = await buildContractIndex(root, config);
1288
+ const contractsRoot = resolvePath(root, config, "contractsDir");
1289
+ const uiRoot = path10.join(contractsRoot, "ui");
1290
+ const themaIds = new Set(
1291
+ Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
1292
+ );
1293
+ issues.push(...await validateUiContracts(uiRoot, themaIds));
1294
+ issues.push(...await validateThemaContracts(uiRoot));
1295
+ issues.push(...await validateApiContracts(path10.join(contractsRoot, "api")));
1296
+ issues.push(...await validateDbContracts(path10.join(contractsRoot, "db")));
1274
1297
  issues.push(...validateDuplicateContractIds(contractIndex));
1275
1298
  return issues;
1276
1299
  }
1277
- async function validateUiContracts(uiRoot) {
1300
+ async function validateUiContracts(uiRoot, themaIds) {
1278
1301
  const files = await collectUiContractFiles(uiRoot);
1279
1302
  if (files.length === 0) {
1280
1303
  return [
@@ -1288,6 +1311,60 @@ async function validateUiContracts(uiRoot) {
1288
1311
  ];
1289
1312
  }
1290
1313
  const issues = [];
1314
+ for (const file of files) {
1315
+ const text = await readFile5(file, "utf-8");
1316
+ const declaredIds = extractDeclaredContractIds(text);
1317
+ issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1318
+ let doc = null;
1319
+ try {
1320
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1321
+ } catch (error) {
1322
+ issues.push(
1323
+ issue(
1324
+ "QFAI-CONTRACT-001",
1325
+ `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1326
+ "error",
1327
+ file,
1328
+ "contracts.ui.parse"
1329
+ )
1330
+ );
1331
+ }
1332
+ const invalidIds = extractInvalidIds(text, [
1333
+ "SPEC",
1334
+ "BR",
1335
+ "SC",
1336
+ "UI",
1337
+ "API",
1338
+ "DB",
1339
+ "THEMA",
1340
+ "ADR"
1341
+ ]).filter((id) => !shouldIgnoreInvalidId(id, doc));
1342
+ if (invalidIds.length > 0) {
1343
+ issues.push(
1344
+ issue(
1345
+ "QFAI-ID-002",
1346
+ `ID \u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u304C\u4E0D\u6B63\u3067\u3059: ${invalidIds.join(", ")}`,
1347
+ "error",
1348
+ file,
1349
+ "id.format",
1350
+ invalidIds
1351
+ )
1352
+ );
1353
+ }
1354
+ if (doc) {
1355
+ issues.push(
1356
+ ...await validateUiContractDoc(doc, file, uiRoot, themaIds)
1357
+ );
1358
+ }
1359
+ }
1360
+ return issues;
1361
+ }
1362
+ async function validateThemaContracts(uiRoot) {
1363
+ const files = await collectThemaContractFiles(uiRoot);
1364
+ if (files.length === 0) {
1365
+ return [];
1366
+ }
1367
+ const issues = [];
1291
1368
  for (const file of files) {
1292
1369
  const text = await readFile5(file, "utf-8");
1293
1370
  const invalidIds = extractInvalidIds(text, [
@@ -1297,6 +1374,7 @@ async function validateUiContracts(uiRoot) {
1297
1374
  "UI",
1298
1375
  "API",
1299
1376
  "DB",
1377
+ "THEMA",
1300
1378
  "ADR"
1301
1379
  ]);
1302
1380
  if (invalidIds.length > 0) {
@@ -1312,17 +1390,95 @@ async function validateUiContracts(uiRoot) {
1312
1390
  );
1313
1391
  }
1314
1392
  const declaredIds = extractDeclaredContractIds(text);
1315
- issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1393
+ if (declaredIds.length === 0) {
1394
+ issues.push(
1395
+ issue(
1396
+ "QFAI-THEMA-010",
1397
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306B QFAI-CONTRACT-ID \u304C\u3042\u308A\u307E\u305B\u3093: ${file}`,
1398
+ "error",
1399
+ file,
1400
+ "contracts.thema.declaration"
1401
+ )
1402
+ );
1403
+ continue;
1404
+ }
1405
+ if (declaredIds.length > 1) {
1406
+ issues.push(
1407
+ issue(
1408
+ "QFAI-THEMA-011",
1409
+ `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(
1410
+ ", "
1411
+ )}`,
1412
+ "error",
1413
+ file,
1414
+ "contracts.thema.declaration",
1415
+ declaredIds
1416
+ )
1417
+ );
1418
+ continue;
1419
+ }
1420
+ const declaredId = declaredIds[0] ?? "";
1421
+ if (!THEMA_ID_RE.test(declaredId)) {
1422
+ issues.push(
1423
+ issue(
1424
+ "QFAI-THEMA-012",
1425
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E ID \u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${declaredId}`,
1426
+ "error",
1427
+ file,
1428
+ "contracts.thema.idFormat",
1429
+ [declaredId]
1430
+ )
1431
+ );
1432
+ }
1433
+ let doc;
1316
1434
  try {
1317
- parseStructuredContract(file, stripContractDeclarationLines(text));
1435
+ doc = parseStructuredContract(file, stripContractDeclarationLines(text));
1318
1436
  } catch (error) {
1319
1437
  issues.push(
1320
1438
  issue(
1321
- "QFAI-CONTRACT-001",
1322
- `UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1439
+ "QFAI-THEMA-001",
1440
+ `thema \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error)})`,
1323
1441
  "error",
1324
1442
  file,
1325
- "contracts.ui.parse"
1443
+ "contracts.thema.parse"
1444
+ )
1445
+ );
1446
+ continue;
1447
+ }
1448
+ const docId = typeof doc.id === "string" ? doc.id : "";
1449
+ if (!THEMA_ID_RE.test(docId)) {
1450
+ issues.push(
1451
+ issue(
1452
+ "QFAI-THEMA-012",
1453
+ 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",
1454
+ "error",
1455
+ file,
1456
+ "contracts.thema.idFormat",
1457
+ docId.length > 0 ? [docId] : void 0
1458
+ )
1459
+ );
1460
+ }
1461
+ const name = typeof doc.name === "string" ? doc.name : "";
1462
+ if (!name) {
1463
+ issues.push(
1464
+ issue(
1465
+ "QFAI-THEMA-014",
1466
+ "thema \u306E name \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1467
+ "error",
1468
+ file,
1469
+ "contracts.thema.name"
1470
+ )
1471
+ );
1472
+ }
1473
+ if (declaredId && docId && declaredId !== docId) {
1474
+ issues.push(
1475
+ issue(
1476
+ "QFAI-THEMA-013",
1477
+ `thema \u306E\u5BA3\u8A00 ID \u3068 id \u304C\u4E00\u81F4\u3057\u307E\u305B\u3093: ${declaredId} / ${docId}`,
1478
+ "error",
1479
+ file,
1480
+ "contracts.thema.idMismatch",
1481
+ [declaredId, docId]
1326
1482
  )
1327
1483
  );
1328
1484
  }
@@ -1352,6 +1508,7 @@ async function validateApiContracts(apiRoot) {
1352
1508
  "UI",
1353
1509
  "API",
1354
1510
  "DB",
1511
+ "THEMA",
1355
1512
  "ADR"
1356
1513
  ]);
1357
1514
  if (invalidIds.length > 0) {
@@ -1420,6 +1577,7 @@ async function validateDbContracts(dbRoot) {
1420
1577
  "UI",
1421
1578
  "API",
1422
1579
  "DB",
1580
+ "THEMA",
1423
1581
  "ADR"
1424
1582
  ]);
1425
1583
  if (invalidIds.length > 0) {
@@ -1526,6 +1684,278 @@ function validateDuplicateContractIds(contractIndex) {
1526
1684
  function hasOpenApi(doc) {
1527
1685
  return typeof doc.openapi === "string" && doc.openapi.length > 0;
1528
1686
  }
1687
+ async function validateUiContractDoc(doc, file, uiRoot, themaIds) {
1688
+ const issues = [];
1689
+ if (Object.prototype.hasOwnProperty.call(doc, "themaRef")) {
1690
+ const themaRef = doc.themaRef;
1691
+ if (typeof themaRef !== "string" || themaRef.length === 0) {
1692
+ issues.push(
1693
+ issue(
1694
+ "QFAI-UI-020",
1695
+ "themaRef \u306F THEMA-001 \u5F62\u5F0F\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1696
+ "error",
1697
+ file,
1698
+ "contracts.ui.themaRef"
1699
+ )
1700
+ );
1701
+ } else if (!THEMA_ID_RE.test(themaRef)) {
1702
+ issues.push(
1703
+ issue(
1704
+ "QFAI-UI-020",
1705
+ `themaRef \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${themaRef}`,
1706
+ "error",
1707
+ file,
1708
+ "contracts.ui.themaRef",
1709
+ [themaRef]
1710
+ )
1711
+ );
1712
+ } else if (!themaIds.has(themaRef)) {
1713
+ issues.push(
1714
+ issue(
1715
+ "QFAI-UI-020",
1716
+ `themaRef \u304C\u5B58\u5728\u3057\u306A\u3044 THEMA \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${themaRef}`,
1717
+ "error",
1718
+ file,
1719
+ "contracts.ui.themaRef",
1720
+ [themaRef]
1721
+ )
1722
+ );
1723
+ }
1724
+ }
1725
+ const assets = doc.assets;
1726
+ if (assets && typeof assets === "object") {
1727
+ issues.push(
1728
+ ...await validateUiAssets(
1729
+ assets,
1730
+ file,
1731
+ uiRoot
1732
+ )
1733
+ );
1734
+ }
1735
+ return issues;
1736
+ }
1737
+ async function validateUiAssets(assets, file, uiRoot) {
1738
+ const issues = [];
1739
+ const packValue = assets.pack;
1740
+ const useValue = assets.use;
1741
+ if (packValue === void 0 && useValue === void 0) {
1742
+ return issues;
1743
+ }
1744
+ if (typeof packValue !== "string" || packValue.length === 0) {
1745
+ issues.push(
1746
+ issue(
1747
+ "QFAI-ASSET-001",
1748
+ "assets.pack \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1749
+ "error",
1750
+ file,
1751
+ "assets.pack"
1752
+ )
1753
+ );
1754
+ return issues;
1755
+ }
1756
+ if (!isSafeRelativePath(packValue)) {
1757
+ issues.push(
1758
+ issue(
1759
+ "QFAI-ASSET-001",
1760
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306E\u76F8\u5BFE\u30D1\u30B9\u306E\u307F\u8A31\u53EF\u3055\u308C\u307E\u3059: ${packValue}`,
1761
+ "error",
1762
+ file,
1763
+ "assets.pack",
1764
+ [packValue]
1765
+ )
1766
+ );
1767
+ return issues;
1768
+ }
1769
+ const packDir = path10.resolve(uiRoot, packValue);
1770
+ const packRelative = path10.relative(uiRoot, packDir);
1771
+ if (packRelative.startsWith("..") || path10.isAbsolute(packRelative)) {
1772
+ issues.push(
1773
+ issue(
1774
+ "QFAI-ASSET-001",
1775
+ `assets.pack \u306F ui/ \u914D\u4E0B\u306B\u9650\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044: ${packValue}`,
1776
+ "error",
1777
+ file,
1778
+ "assets.pack",
1779
+ [packValue]
1780
+ )
1781
+ );
1782
+ return issues;
1783
+ }
1784
+ if (!await exists4(packDir)) {
1785
+ issues.push(
1786
+ issue(
1787
+ "QFAI-ASSET-001",
1788
+ `assets.pack \u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${packValue}`,
1789
+ "error",
1790
+ file,
1791
+ "assets.pack",
1792
+ [packValue]
1793
+ )
1794
+ );
1795
+ return issues;
1796
+ }
1797
+ const assetsYamlPath = path10.join(packDir, "assets.yaml");
1798
+ if (!await exists4(assetsYamlPath)) {
1799
+ issues.push(
1800
+ issue(
1801
+ "QFAI-ASSET-002",
1802
+ `assets.yaml \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${assetsYamlPath}`,
1803
+ "error",
1804
+ assetsYamlPath,
1805
+ "assets.yaml"
1806
+ )
1807
+ );
1808
+ return issues;
1809
+ }
1810
+ let manifest;
1811
+ try {
1812
+ const manifestText = await readFile5(assetsYamlPath, "utf-8");
1813
+ manifest = parseStructuredContract(assetsYamlPath, manifestText);
1814
+ } catch (error) {
1815
+ issues.push(
1816
+ issue(
1817
+ "QFAI-ASSET-002",
1818
+ `assets.yaml \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${assetsYamlPath} (${formatError4(error)})`,
1819
+ "error",
1820
+ assetsYamlPath,
1821
+ "assets.yaml"
1822
+ )
1823
+ );
1824
+ return issues;
1825
+ }
1826
+ const items = Array.isArray(manifest.items) ? manifest.items : [];
1827
+ const itemIds = /* @__PURE__ */ new Set();
1828
+ const itemPaths = [];
1829
+ for (const item of items) {
1830
+ if (!item || typeof item !== "object") {
1831
+ continue;
1832
+ }
1833
+ const record2 = item;
1834
+ const id = typeof record2.id === "string" ? record2.id : void 0;
1835
+ const pathValue = typeof record2.path === "string" ? record2.path : void 0;
1836
+ if (id) {
1837
+ itemIds.add(id);
1838
+ }
1839
+ itemPaths.push({ id, path: pathValue });
1840
+ }
1841
+ if (useValue !== void 0) {
1842
+ if (!Array.isArray(useValue) || useValue.some((entry) => typeof entry !== "string")) {
1843
+ issues.push(
1844
+ issue(
1845
+ "QFAI-ASSET-003",
1846
+ "assets.use \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1847
+ "error",
1848
+ file,
1849
+ "assets.use"
1850
+ )
1851
+ );
1852
+ } else {
1853
+ const missing = useValue.filter((entry) => !itemIds.has(entry));
1854
+ if (missing.length > 0) {
1855
+ issues.push(
1856
+ issue(
1857
+ "QFAI-ASSET-003",
1858
+ `assets.use \u304C assets.yaml \u306B\u5B58\u5728\u3057\u307E\u305B\u3093: ${missing.join(", ")}`,
1859
+ "error",
1860
+ file,
1861
+ "assets.use",
1862
+ missing
1863
+ )
1864
+ );
1865
+ }
1866
+ }
1867
+ }
1868
+ for (const entry of itemPaths) {
1869
+ if (!entry.path) {
1870
+ continue;
1871
+ }
1872
+ if (!isSafeRelativePath(entry.path)) {
1873
+ issues.push(
1874
+ issue(
1875
+ "QFAI-ASSET-004",
1876
+ `assets.yaml \u306E path \u304C\u4E0D\u6B63\u3067\u3059: ${entry.path}`,
1877
+ "error",
1878
+ assetsYamlPath,
1879
+ "assets.path",
1880
+ entry.id ? [entry.id] : [entry.path]
1881
+ )
1882
+ );
1883
+ continue;
1884
+ }
1885
+ const assetPath = path10.resolve(packDir, entry.path);
1886
+ const assetRelative = path10.relative(packDir, assetPath);
1887
+ if (assetRelative.startsWith("..") || path10.isAbsolute(assetRelative)) {
1888
+ issues.push(
1889
+ issue(
1890
+ "QFAI-ASSET-004",
1891
+ `assets.yaml \u306E path \u304C packDir \u3092\u9038\u8131\u3057\u3066\u3044\u307E\u3059: ${entry.path}`,
1892
+ "error",
1893
+ assetsYamlPath,
1894
+ "assets.path",
1895
+ entry.id ? [entry.id] : [entry.path]
1896
+ )
1897
+ );
1898
+ continue;
1899
+ }
1900
+ if (!await exists4(assetPath)) {
1901
+ issues.push(
1902
+ issue(
1903
+ "QFAI-ASSET-004",
1904
+ `assets.yaml \u306E path \u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${entry.path}`,
1905
+ "error",
1906
+ assetsYamlPath,
1907
+ "assets.path",
1908
+ entry.id ? [entry.id] : [entry.path]
1909
+ )
1910
+ );
1911
+ }
1912
+ }
1913
+ return issues;
1914
+ }
1915
+ function shouldIgnoreInvalidId(value, doc) {
1916
+ if (!doc) {
1917
+ return false;
1918
+ }
1919
+ const assets = doc.assets;
1920
+ if (!assets || typeof assets !== "object") {
1921
+ return false;
1922
+ }
1923
+ const packValue = assets.pack;
1924
+ if (typeof packValue !== "string" || packValue.length === 0) {
1925
+ return false;
1926
+ }
1927
+ const normalized = packValue.replace(/\\/g, "/");
1928
+ const basename = path10.posix.basename(normalized);
1929
+ if (!basename) {
1930
+ return false;
1931
+ }
1932
+ return value.toLowerCase() === basename.toLowerCase();
1933
+ }
1934
+ function isSafeRelativePath(value) {
1935
+ if (!value) {
1936
+ return false;
1937
+ }
1938
+ if (path10.isAbsolute(value)) {
1939
+ return false;
1940
+ }
1941
+ const normalized = value.replace(/\\/g, "/");
1942
+ if (/^[A-Za-z]:/.test(normalized)) {
1943
+ return false;
1944
+ }
1945
+ const segments = normalized.split("/");
1946
+ if (segments.some((segment) => segment === "..")) {
1947
+ return false;
1948
+ }
1949
+ return true;
1950
+ }
1951
+ async function exists4(target) {
1952
+ try {
1953
+ await access4(target);
1954
+ return true;
1955
+ } catch {
1956
+ return false;
1957
+ }
1958
+ }
1529
1959
  function formatError4(error) {
1530
1960
  if (error instanceof Error) {
1531
1961
  return error.message;
@@ -1556,12 +1986,7 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
1556
1986
 
1557
1987
  // src/core/validators/delta.ts
1558
1988
  import { readFile as readFile6 } from "fs/promises";
1559
- import path10 from "path";
1560
- var SECTION_RE = /^##\s+変更区分/m;
1561
- var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
1562
- var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
1563
- var COMPAT_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Compatibility\b/m;
1564
- var CHANGE_CHECKED_RE = /^\s*-\s*\[[xX]\]\s*Change\/Improvement\b/m;
1989
+ import path11 from "path";
1565
1990
  async function validateDeltas(root, config) {
1566
1991
  const specsRoot = resolvePath(root, config, "specsDir");
1567
1992
  const packs = await collectSpecPackDirs(specsRoot);
@@ -1570,10 +1995,9 @@ async function validateDeltas(root, config) {
1570
1995
  }
1571
1996
  const issues = [];
1572
1997
  for (const pack of packs) {
1573
- const deltaPath = path10.join(pack, "delta.md");
1574
- let text;
1998
+ const deltaPath = path11.join(pack, "delta.md");
1575
1999
  try {
1576
- text = await readFile6(deltaPath, "utf-8");
2000
+ await readFile6(deltaPath, "utf-8");
1577
2001
  } catch (error) {
1578
2002
  if (isMissingFileError2(error)) {
1579
2003
  issues.push(
@@ -1582,41 +2006,16 @@ async function validateDeltas(root, config) {
1582
2006
  "delta.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
1583
2007
  "error",
1584
2008
  deltaPath,
1585
- "delta.exists"
2009
+ "delta.exists",
2010
+ void 0,
2011
+ "change",
2012
+ "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"
1586
2013
  )
1587
2014
  );
1588
2015
  continue;
1589
2016
  }
1590
2017
  throw error;
1591
2018
  }
1592
- const hasSection = SECTION_RE.test(text);
1593
- const hasCompatibility = COMPAT_LINE_RE.test(text);
1594
- const hasChange = CHANGE_LINE_RE.test(text);
1595
- if (!hasSection || !hasCompatibility || !hasChange) {
1596
- issues.push(
1597
- issue2(
1598
- "QFAI-DELTA-002",
1599
- "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",
1600
- "error",
1601
- deltaPath,
1602
- "delta.section"
1603
- )
1604
- );
1605
- continue;
1606
- }
1607
- const compatibilityChecked = COMPAT_CHECKED_RE.test(text);
1608
- const changeChecked = CHANGE_CHECKED_RE.test(text);
1609
- if (compatibilityChecked === changeChecked) {
1610
- issues.push(
1611
- issue2(
1612
- "QFAI-DELTA-003",
1613
- "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",
1614
- "error",
1615
- deltaPath,
1616
- "delta.classification"
1617
- )
1618
- );
1619
- }
1620
2019
  }
1621
2020
  return issues;
1622
2021
  }
@@ -1650,7 +2049,7 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
1650
2049
 
1651
2050
  // src/core/validators/ids.ts
1652
2051
  import { readFile as readFile7 } from "fs/promises";
1653
- import path11 from "path";
2052
+ import path12 from "path";
1654
2053
  var SC_TAG_RE3 = /^SC-\d{4}$/;
1655
2054
  async function validateDefinedIds(root, config) {
1656
2055
  const issues = [];
@@ -1716,7 +2115,7 @@ function recordId(out, id, file) {
1716
2115
  }
1717
2116
  function formatFileList(files, root) {
1718
2117
  return files.map((file) => {
1719
- const relative = path11.relative(root, file);
2118
+ const relative = path12.relative(root, file);
1720
2119
  return relative.length > 0 ? relative : file;
1721
2120
  }).join(", ");
1722
2121
  }
@@ -1744,19 +2143,19 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
1744
2143
 
1745
2144
  // src/core/promptsIntegrity.ts
1746
2145
  import { readFile as readFile8 } from "fs/promises";
1747
- import path13 from "path";
2146
+ import path14 from "path";
1748
2147
 
1749
2148
  // src/shared/assets.ts
1750
2149
  import { existsSync } from "fs";
1751
- import path12 from "path";
2150
+ import path13 from "path";
1752
2151
  import { fileURLToPath as fileURLToPath2 } from "url";
1753
2152
  function getInitAssetsDir() {
1754
2153
  const base = import.meta.url;
1755
2154
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
1756
- const baseDir = path12.dirname(basePath);
2155
+ const baseDir = path13.dirname(basePath);
1757
2156
  const candidates = [
1758
- path12.resolve(baseDir, "../../../assets/init"),
1759
- path12.resolve(baseDir, "../../assets/init")
2157
+ path13.resolve(baseDir, "../../../assets/init"),
2158
+ path13.resolve(baseDir, "../../assets/init")
1760
2159
  ];
1761
2160
  for (const candidate of candidates) {
1762
2161
  if (existsSync(candidate)) {
@@ -1773,11 +2172,12 @@ function getInitAssetsDir() {
1773
2172
  }
1774
2173
 
1775
2174
  // src/core/promptsIntegrity.ts
2175
+ var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
1776
2176
  async function diffProjectPromptsAgainstInitAssets(root) {
1777
- const promptsDir = path13.resolve(root, ".qfai", "prompts");
2177
+ const promptsDir = path14.resolve(root, ".qfai", "prompts");
1778
2178
  let templateDir;
1779
2179
  try {
1780
- templateDir = path13.join(getInitAssetsDir(), ".qfai", "prompts");
2180
+ templateDir = path14.join(getInitAssetsDir(), ".qfai", "prompts");
1781
2181
  } catch {
1782
2182
  return {
1783
2183
  status: "skipped_missing_assets",
@@ -1821,6 +2221,7 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1821
2221
  extra.push(rel);
1822
2222
  }
1823
2223
  }
2224
+ const filteredExtra = extra.filter((rel) => !LEGACY_OK_EXTRA.has(rel));
1824
2225
  const common = intersectKeys(templateByRel, projectByRel);
1825
2226
  for (const rel of common) {
1826
2227
  const templateAbs = templateByRel.get(rel);
@@ -1840,13 +2241,13 @@ async function diffProjectPromptsAgainstInitAssets(root) {
1840
2241
  changed.push(rel);
1841
2242
  }
1842
2243
  }
1843
- const status = missing.length > 0 || extra.length > 0 || changed.length > 0 ? "modified" : "ok";
2244
+ const status = missing.length > 0 || filteredExtra.length > 0 || changed.length > 0 ? "modified" : "ok";
1844
2245
  return {
1845
2246
  status,
1846
2247
  promptsDir,
1847
2248
  templateDir,
1848
2249
  missing: missing.sort(),
1849
- extra: extra.sort(),
2250
+ extra: filteredExtra.sort(),
1850
2251
  changed: changed.sort()
1851
2252
  };
1852
2253
  }
@@ -1854,7 +2255,7 @@ function normalizeNewlines(text) {
1854
2255
  return text.replace(/\r\n/g, "\n");
1855
2256
  }
1856
2257
  function toRel(base, abs) {
1857
- const rel = path13.relative(base, abs);
2258
+ const rel = path14.relative(base, abs);
1858
2259
  return rel.replace(/[\\/]+/g, "/");
1859
2260
  }
1860
2261
  function intersectKeys(a, b) {
@@ -1899,7 +2300,8 @@ async function validatePromptsIntegrity(root) {
1899
2300
  }
1900
2301
 
1901
2302
  // src/core/validators/scenario.ts
1902
- import { readFile as readFile9 } from "fs/promises";
2303
+ import { access as access5, readFile as readFile9 } from "fs/promises";
2304
+ import path15 from "path";
1903
2305
  var GIVEN_PATTERN = /\bGiven\b/;
1904
2306
  var WHEN_PATTERN = /\bWhen\b/;
1905
2307
  var THEN_PATTERN = /\bThen\b/;
@@ -1922,6 +2324,18 @@ async function validateScenarios(root, config) {
1922
2324
  }
1923
2325
  const issues = [];
1924
2326
  for (const entry of entries) {
2327
+ const legacyScenarioPath = path15.join(entry.dir, "scenario.md");
2328
+ if (await fileExists(legacyScenarioPath)) {
2329
+ issues.push(
2330
+ issue4(
2331
+ "QFAI-SC-004",
2332
+ "scenario.md \u306F\u975E\u5BFE\u5FDC\u3067\u3059\u3002scenario.feature \u3078\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2333
+ "error",
2334
+ legacyScenarioPath,
2335
+ "scenario.legacy"
2336
+ )
2337
+ );
2338
+ }
1925
2339
  let text;
1926
2340
  try {
1927
2341
  text = await readFile9(entry.scenarioPath, "utf-8");
@@ -1953,6 +2367,7 @@ function validateScenarioContent(text, file) {
1953
2367
  "UI",
1954
2368
  "API",
1955
2369
  "DB",
2370
+ "THEMA",
1956
2371
  "ADR"
1957
2372
  ]);
1958
2373
  if (invalidIds.length > 0) {
@@ -2096,6 +2511,14 @@ function isMissingFileError3(error) {
2096
2511
  }
2097
2512
  return error.code === "ENOENT";
2098
2513
  }
2514
+ async function fileExists(target) {
2515
+ try {
2516
+ await access5(target);
2517
+ return true;
2518
+ } catch {
2519
+ return false;
2520
+ }
2521
+ }
2099
2522
 
2100
2523
  // src/core/validators/spec.ts
2101
2524
  import { readFile as readFile10 } from "fs/promises";
@@ -2155,6 +2578,7 @@ function validateSpecContent(text, file, requiredSections) {
2155
2578
  "UI",
2156
2579
  "API",
2157
2580
  "DB",
2581
+ "THEMA",
2158
2582
  "ADR"
2159
2583
  ]);
2160
2584
  if (invalidIds.length > 0) {
@@ -2334,7 +2758,7 @@ async function validateTraceability(root, config) {
2334
2758
  "QFAI-TRACE-021",
2335
2759
  `Spec \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${contractRefs.invalidTokens.join(
2336
2760
  ", "
2337
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2761
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
2338
2762
  "error",
2339
2763
  file,
2340
2764
  "traceability.specContractRefFormat",
@@ -2398,7 +2822,7 @@ async function validateTraceability(root, config) {
2398
2822
  "QFAI-TRACE-032",
2399
2823
  `Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
2400
2824
  ", "
2401
- )} (\u4F8B: UI-0001 / API-0001 / DB-0001)`,
2825
+ )} (\u4F8B: UI-0001 / API-0001 / DB-0001 / THEMA-001)`,
2402
2826
  "error",
2403
2827
  file,
2404
2828
  "traceability.scenarioContractRefFormat",
@@ -2777,17 +3201,25 @@ function countIssues(issues) {
2777
3201
  }
2778
3202
 
2779
3203
  // src/core/report.ts
2780
- var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
3204
+ var ID_PREFIXES2 = [
3205
+ "SPEC",
3206
+ "BR",
3207
+ "SC",
3208
+ "UI",
3209
+ "API",
3210
+ "DB",
3211
+ "THEMA"
3212
+ ];
2781
3213
  async function createReportData(root, validation, configResult) {
2782
- const resolvedRoot = path14.resolve(root);
3214
+ const resolvedRoot = path16.resolve(root);
2783
3215
  const resolved = configResult ?? await loadConfig(resolvedRoot);
2784
3216
  const config = resolved.config;
2785
3217
  const configPath = resolved.configPath;
2786
3218
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
2787
3219
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
2788
- const apiRoot = path14.join(contractsRoot, "api");
2789
- const uiRoot = path14.join(contractsRoot, "ui");
2790
- const dbRoot = path14.join(contractsRoot, "db");
3220
+ const apiRoot = path16.join(contractsRoot, "api");
3221
+ const uiRoot = path16.join(contractsRoot, "ui");
3222
+ const dbRoot = path16.join(contractsRoot, "db");
2791
3223
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
2792
3224
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
2793
3225
  const specFiles = await collectSpecFiles(specsRoot);
@@ -2795,7 +3227,8 @@ async function createReportData(root, validation, configResult) {
2795
3227
  const {
2796
3228
  api: apiFiles,
2797
3229
  ui: uiFiles,
2798
- db: dbFiles
3230
+ db: dbFiles,
3231
+ thema: themaFiles
2799
3232
  } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
2800
3233
  const contractIndex = await buildContractIndex(resolvedRoot, config);
2801
3234
  const contractIdList = Array.from(contractIndex.ids);
@@ -2822,7 +3255,8 @@ async function createReportData(root, validation, configResult) {
2822
3255
  ...scenarioFiles,
2823
3256
  ...apiFiles,
2824
3257
  ...uiFiles,
2825
- ...dbFiles
3258
+ ...dbFiles,
3259
+ ...themaFiles
2826
3260
  ]);
2827
3261
  const upstreamIds = await collectUpstreamIds([
2828
3262
  ...specFiles,
@@ -2859,7 +3293,8 @@ async function createReportData(root, validation, configResult) {
2859
3293
  contracts: {
2860
3294
  api: apiFiles.length,
2861
3295
  ui: uiFiles.length,
2862
- db: dbFiles.length
3296
+ db: dbFiles.length,
3297
+ thema: themaFiles.length
2863
3298
  },
2864
3299
  counts: normalizedValidation.counts
2865
3300
  },
@@ -2869,7 +3304,8 @@ async function createReportData(root, validation, configResult) {
2869
3304
  sc: idsByPrefix.SC,
2870
3305
  ui: idsByPrefix.UI,
2871
3306
  api: idsByPrefix.API,
2872
- db: idsByPrefix.DB
3307
+ db: idsByPrefix.DB,
3308
+ thema: idsByPrefix.THEMA
2873
3309
  },
2874
3310
  traceability: {
2875
3311
  upstreamIdsFound: upstreamIds.size,
@@ -2939,7 +3375,7 @@ function formatReportMarkdown(data, options = {}) {
2939
3375
  lines.push(`- specs: ${data.summary.specs}`);
2940
3376
  lines.push(`- scenarios: ${data.summary.scenarios}`);
2941
3377
  lines.push(
2942
- `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db}`
3378
+ `- contracts: api ${data.summary.contracts.api} / ui ${data.summary.contracts.ui} / db ${data.summary.contracts.db} / thema ${data.summary.contracts.thema}`
2943
3379
  );
2944
3380
  lines.push(
2945
3381
  `- issues(total): info ${data.summary.counts.info} / warning ${data.summary.counts.warning} / error ${data.summary.counts.error}`
@@ -3083,6 +3519,7 @@ function formatReportMarkdown(data, options = {}) {
3083
3519
  lines.push(formatIdLine("UI", data.ids.ui));
3084
3520
  lines.push(formatIdLine("API", data.ids.api));
3085
3521
  lines.push(formatIdLine("DB", data.ids.db));
3522
+ lines.push(formatIdLine("THEMA", data.ids.thema));
3086
3523
  lines.push("");
3087
3524
  lines.push("## Traceability");
3088
3525
  lines.push("");
@@ -3247,12 +3684,8 @@ function formatReportMarkdown(data, options = {}) {
3247
3684
  "- 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"
3248
3685
  );
3249
3686
  }
3250
- lines.push(
3251
- "- \u5909\u66F4\u533A\u5206\uFF08Compatibility / Change/Improvement\uFF09\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002"
3252
- );
3253
- lines.push(
3254
- "- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md` / `.qfai/promptpack/steering/compatibility-vs-change.md`"
3255
- );
3687
+ lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
3688
+ lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/promptpack/steering/traceability.md`");
3256
3689
  return lines.join("\n");
3257
3690
  }
3258
3691
  function formatReportJson(data) {
@@ -3304,7 +3737,8 @@ async function collectIds(files) {
3304
3737
  SC: /* @__PURE__ */ new Set(),
3305
3738
  UI: /* @__PURE__ */ new Set(),
3306
3739
  API: /* @__PURE__ */ new Set(),
3307
- DB: /* @__PURE__ */ new Set()
3740
+ DB: /* @__PURE__ */ new Set(),
3741
+ THEMA: /* @__PURE__ */ new Set()
3308
3742
  };
3309
3743
  for (const file of files) {
3310
3744
  const text = await readFile12(file, "utf-8");
@@ -3319,7 +3753,8 @@ async function collectIds(files) {
3319
3753
  SC: toSortedArray2(result.SC),
3320
3754
  UI: toSortedArray2(result.UI),
3321
3755
  API: toSortedArray2(result.API),
3322
- DB: toSortedArray2(result.DB)
3756
+ DB: toSortedArray2(result.DB),
3757
+ THEMA: toSortedArray2(result.THEMA)
3323
3758
  };
3324
3759
  }
3325
3760
  async function collectUpstreamIds(files) {