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.
- package/README.md +13 -12
- package/assets/init/.qfai/README.md +2 -7
- package/assets/init/.qfai/contracts/README.md +20 -0
- 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/promptpack/commands/plan.md +1 -1
- package/assets/init/.qfai/promptpack/commands/review.md +1 -2
- package/assets/init/.qfai/promptpack/constitution.md +1 -1
- package/assets/init/.qfai/prompts/README.md +1 -3
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +3 -3
- package/assets/init/.qfai/prompts/require-to-spec.md +1 -2
- package/assets/init/.qfai/specs/README.md +3 -4
- package/assets/init/.qfai/specs/spec-0001/delta.md +0 -5
- package/assets/init/.qfai/specs/spec-0001/scenario.feature +1 -1
- package/assets/init/.qfai/specs/spec-0001/spec.md +1 -1
- package/assets/init/root/qfai.config.yaml +0 -1
- package/dist/cli/index.cjs +596 -162
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +598 -164
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +549 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +551 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/promptpack/modes/change.md +0 -5
- package/assets/init/.qfai/promptpack/modes/compatibility.md +0 -6
- package/assets/init/.qfai/promptpack/steering/compatibility-vs-change.md +0 -42
- package/assets/init/.qfai/prompts/qfai-classify-change.md +0 -33
- package/assets/init/.qfai/rules/conventions.md +0 -27
- package/assets/init/.qfai/rules/pnpm.md +0 -29
- package/assets/init/.qfai/samples/analyze/analysis.md +0 -38
- 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 = [
|
|
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
|
|
458
|
+
import path16 from "path";
|
|
457
459
|
|
|
458
460
|
// src/core/contractIndex.ts
|
|
459
461
|
import { readFile as readFile2 } from "fs/promises";
|
|
460
|
-
import
|
|
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
|
-
|
|
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 =
|
|
667
|
-
const apiRoot =
|
|
668
|
-
const dbRoot =
|
|
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
|
|
715
|
+
import path6 from "path";
|
|
699
716
|
function toRelativePath(root, target) {
|
|
700
717
|
if (!target) {
|
|
701
718
|
return target;
|
|
702
719
|
}
|
|
703
|
-
if (!
|
|
720
|
+
if (!path6.isAbsolute(target)) {
|
|
704
721
|
return toPosixPath(target);
|
|
705
722
|
}
|
|
706
|
-
const relative =
|
|
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
|
|
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) =>
|
|
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
|
|
1237
|
+
import path8 from "path";
|
|
1221
1238
|
import { fileURLToPath } from "url";
|
|
1222
1239
|
async function resolveToolVersion() {
|
|
1223
|
-
if ("1.0.
|
|
1224
|
-
return "1.0.
|
|
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
|
|
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
|
|
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
|
|
1264
|
+
import path9 from "path";
|
|
1248
1265
|
import { parse as parseYaml2 } from "yaml";
|
|
1249
1266
|
function parseStructuredContract(file, text) {
|
|
1250
|
-
const ext =
|
|
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
|
-
|
|
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-
|
|
1322
|
-
`
|
|
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.
|
|
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
|
|
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 =
|
|
1574
|
-
let text;
|
|
1998
|
+
const deltaPath = path11.join(pack, "delta.md");
|
|
1575
1999
|
try {
|
|
1576
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2146
|
+
import path14 from "path";
|
|
1748
2147
|
|
|
1749
2148
|
// src/shared/assets.ts
|
|
1750
2149
|
import { existsSync } from "fs";
|
|
1751
|
-
import
|
|
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 =
|
|
2155
|
+
const baseDir = path13.dirname(basePath);
|
|
1757
2156
|
const candidates = [
|
|
1758
|
-
|
|
1759
|
-
|
|
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 =
|
|
2177
|
+
const promptsDir = path14.resolve(root, ".qfai", "prompts");
|
|
1778
2178
|
let templateDir;
|
|
1779
2179
|
try {
|
|
1780
|
-
templateDir =
|
|
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 ||
|
|
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:
|
|
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 =
|
|
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 = [
|
|
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 =
|
|
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 =
|
|
2789
|
-
const uiRoot =
|
|
2790
|
-
const dbRoot =
|
|
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
|
-
|
|
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) {
|