qfai 1.0.2 → 1.0.3

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