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