qfai 0.4.9 → 0.5.2
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 +10 -3
- package/assets/init/.qfai/README.md +9 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/commands/implement.md +2 -0
- package/assets/init/.qfai/promptpack/commands/plan.md +2 -0
- package/assets/init/.qfai/promptpack/commands/review.md +1 -0
- package/assets/init/.qfai/promptpack/steering/traceability.md +8 -2
- package/assets/init/.qfai/prompts/README.md +16 -0
- package/assets/init/.qfai/prompts/qfai-classify-change.md +33 -0
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +35 -0
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +36 -0
- package/assets/init/.qfai/specs/README.md +3 -1
- package/assets/init/.qfai/specs/spec-0001/scenario.md +2 -1
- package/assets/init/root/qfai.config.yaml +1 -1
- package/dist/cli/index.cjs +551 -211
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +556 -216
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +364 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +368 -160
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/cli/index.cjs
CHANGED
|
@@ -139,6 +139,10 @@ function info(message) {
|
|
|
139
139
|
process.stdout.write(`${message}
|
|
140
140
|
`);
|
|
141
141
|
}
|
|
142
|
+
function warn(message) {
|
|
143
|
+
process.stdout.write(`${message}
|
|
144
|
+
`);
|
|
145
|
+
}
|
|
142
146
|
function error(message) {
|
|
143
147
|
process.stderr.write(`${message}
|
|
144
148
|
`);
|
|
@@ -178,7 +182,7 @@ function report(copied, skipped, dryRun, label) {
|
|
|
178
182
|
|
|
179
183
|
// src/cli/commands/report.ts
|
|
180
184
|
var import_promises16 = require("fs/promises");
|
|
181
|
-
var
|
|
185
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
182
186
|
|
|
183
187
|
// src/core/config.ts
|
|
184
188
|
var import_promises2 = require("fs/promises");
|
|
@@ -213,7 +217,7 @@ var defaultConfig = {
|
|
|
213
217
|
testFileGlobs: [],
|
|
214
218
|
testFileExcludeGlobs: [],
|
|
215
219
|
scNoTestSeverity: "error",
|
|
216
|
-
|
|
220
|
+
orphanContractsPolicy: "error",
|
|
217
221
|
unknownContractIdSeverity: "error"
|
|
218
222
|
}
|
|
219
223
|
},
|
|
@@ -224,6 +228,26 @@ var defaultConfig = {
|
|
|
224
228
|
function getConfigPath(root) {
|
|
225
229
|
return import_node_path4.default.join(root, "qfai.config.yaml");
|
|
226
230
|
}
|
|
231
|
+
async function findConfigRoot(startDir) {
|
|
232
|
+
const resolvedStart = import_node_path4.default.resolve(startDir);
|
|
233
|
+
let current = resolvedStart;
|
|
234
|
+
while (true) {
|
|
235
|
+
const configPath = getConfigPath(current);
|
|
236
|
+
if (await exists2(configPath)) {
|
|
237
|
+
return { root: current, configPath, found: true };
|
|
238
|
+
}
|
|
239
|
+
const parent = import_node_path4.default.dirname(current);
|
|
240
|
+
if (parent === current) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
current = parent;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
root: resolvedStart,
|
|
247
|
+
configPath: getConfigPath(resolvedStart),
|
|
248
|
+
found: false
|
|
249
|
+
};
|
|
250
|
+
}
|
|
227
251
|
async function loadConfig(root) {
|
|
228
252
|
const configPath = getConfigPath(root);
|
|
229
253
|
const issues = [];
|
|
@@ -413,10 +437,10 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
413
437
|
configPath,
|
|
414
438
|
issues
|
|
415
439
|
),
|
|
416
|
-
|
|
417
|
-
traceabilityRaw?.
|
|
418
|
-
base.traceability.
|
|
419
|
-
"validation.traceability.
|
|
440
|
+
orphanContractsPolicy: readOrphanContractsPolicy(
|
|
441
|
+
traceabilityRaw?.orphanContractsPolicy,
|
|
442
|
+
base.traceability.orphanContractsPolicy,
|
|
443
|
+
"validation.traceability.orphanContractsPolicy",
|
|
420
444
|
configPath,
|
|
421
445
|
issues
|
|
422
446
|
),
|
|
@@ -512,6 +536,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
512
536
|
}
|
|
513
537
|
return fallback;
|
|
514
538
|
}
|
|
539
|
+
function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
|
|
540
|
+
if (value === "error" || value === "warning" || value === "allow") {
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
if (value !== void 0) {
|
|
544
|
+
issues.push(
|
|
545
|
+
configIssue(
|
|
546
|
+
configPath,
|
|
547
|
+
`${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
548
|
+
)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return fallback;
|
|
552
|
+
}
|
|
515
553
|
function configIssue(file, message) {
|
|
516
554
|
return {
|
|
517
555
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -527,6 +565,14 @@ function isMissingFile(error2) {
|
|
|
527
565
|
}
|
|
528
566
|
return false;
|
|
529
567
|
}
|
|
568
|
+
async function exists2(target) {
|
|
569
|
+
try {
|
|
570
|
+
await (0, import_promises2.access)(target);
|
|
571
|
+
return true;
|
|
572
|
+
} catch {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
530
576
|
function formatError(error2) {
|
|
531
577
|
if (error2 instanceof Error) {
|
|
532
578
|
return error2.message;
|
|
@@ -537,20 +583,76 @@ function isRecord(value) {
|
|
|
537
583
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
538
584
|
}
|
|
539
585
|
|
|
586
|
+
// src/core/paths.ts
|
|
587
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
588
|
+
function toRelativePath(root, target) {
|
|
589
|
+
if (!target) {
|
|
590
|
+
return target;
|
|
591
|
+
}
|
|
592
|
+
if (!import_node_path5.default.isAbsolute(target)) {
|
|
593
|
+
return toPosixPath(target);
|
|
594
|
+
}
|
|
595
|
+
const relative = import_node_path5.default.relative(root, target);
|
|
596
|
+
if (!relative) {
|
|
597
|
+
return ".";
|
|
598
|
+
}
|
|
599
|
+
return toPosixPath(relative);
|
|
600
|
+
}
|
|
601
|
+
function toPosixPath(value) {
|
|
602
|
+
return value.replace(/\\/g, "/");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/core/normalize.ts
|
|
606
|
+
function normalizeIssuePaths(root, issues) {
|
|
607
|
+
return issues.map((issue7) => {
|
|
608
|
+
if (!issue7.file) {
|
|
609
|
+
return issue7;
|
|
610
|
+
}
|
|
611
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
612
|
+
if (normalized === issue7.file) {
|
|
613
|
+
return issue7;
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
...issue7,
|
|
617
|
+
file: normalized
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function normalizeScCoverage(root, sc) {
|
|
622
|
+
const refs = {};
|
|
623
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
624
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
...sc,
|
|
628
|
+
refs
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function normalizeValidationResult(root, result) {
|
|
632
|
+
return {
|
|
633
|
+
...result,
|
|
634
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
635
|
+
traceability: {
|
|
636
|
+
...result.traceability,
|
|
637
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
540
642
|
// src/core/report.ts
|
|
541
643
|
var import_promises15 = require("fs/promises");
|
|
542
|
-
var
|
|
644
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
543
645
|
|
|
544
646
|
// src/core/contractIndex.ts
|
|
545
647
|
var import_promises6 = require("fs/promises");
|
|
546
|
-
var
|
|
648
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
547
649
|
|
|
548
650
|
// src/core/discovery.ts
|
|
549
651
|
var import_promises5 = require("fs/promises");
|
|
550
652
|
|
|
551
653
|
// src/core/fs.ts
|
|
552
654
|
var import_promises3 = require("fs/promises");
|
|
553
|
-
var
|
|
655
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
554
656
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
555
657
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
556
658
|
"node_modules",
|
|
@@ -562,7 +664,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
562
664
|
]);
|
|
563
665
|
async function collectFiles(root, options = {}) {
|
|
564
666
|
const entries = [];
|
|
565
|
-
if (!await
|
|
667
|
+
if (!await exists3(root)) {
|
|
566
668
|
return entries;
|
|
567
669
|
}
|
|
568
670
|
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
@@ -588,7 +690,7 @@ async function collectFilesByGlobs(root, options) {
|
|
|
588
690
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
589
691
|
const items = await (0, import_promises3.readdir)(current, { withFileTypes: true });
|
|
590
692
|
for (const item of items) {
|
|
591
|
-
const fullPath =
|
|
693
|
+
const fullPath = import_node_path6.default.join(current, item.name);
|
|
592
694
|
if (item.isDirectory()) {
|
|
593
695
|
if (ignoreDirs.has(item.name)) {
|
|
594
696
|
continue;
|
|
@@ -598,7 +700,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
598
700
|
}
|
|
599
701
|
if (item.isFile()) {
|
|
600
702
|
if (extensions.length > 0) {
|
|
601
|
-
const ext =
|
|
703
|
+
const ext = import_node_path6.default.extname(item.name).toLowerCase();
|
|
602
704
|
if (!extensions.includes(ext)) {
|
|
603
705
|
continue;
|
|
604
706
|
}
|
|
@@ -607,7 +709,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
607
709
|
}
|
|
608
710
|
}
|
|
609
711
|
}
|
|
610
|
-
async function
|
|
712
|
+
async function exists3(target) {
|
|
611
713
|
try {
|
|
612
714
|
await (0, import_promises3.access)(target);
|
|
613
715
|
return true;
|
|
@@ -618,22 +720,22 @@ async function exists2(target) {
|
|
|
618
720
|
|
|
619
721
|
// src/core/specLayout.ts
|
|
620
722
|
var import_promises4 = require("fs/promises");
|
|
621
|
-
var
|
|
723
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
622
724
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
623
725
|
async function collectSpecEntries(specsRoot) {
|
|
624
726
|
const dirs = await listSpecDirs(specsRoot);
|
|
625
727
|
const entries = dirs.map((dir) => ({
|
|
626
728
|
dir,
|
|
627
|
-
specPath:
|
|
628
|
-
deltaPath:
|
|
629
|
-
scenarioPath:
|
|
729
|
+
specPath: import_node_path7.default.join(dir, "spec.md"),
|
|
730
|
+
deltaPath: import_node_path7.default.join(dir, "delta.md"),
|
|
731
|
+
scenarioPath: import_node_path7.default.join(dir, "scenario.md")
|
|
630
732
|
}));
|
|
631
733
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
632
734
|
}
|
|
633
735
|
async function listSpecDirs(specsRoot) {
|
|
634
736
|
try {
|
|
635
737
|
const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
|
|
636
|
-
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) =>
|
|
738
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path7.default.join(specsRoot, name));
|
|
637
739
|
} catch (error2) {
|
|
638
740
|
if (isMissingFileError(error2)) {
|
|
639
741
|
return [];
|
|
@@ -681,13 +783,13 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
|
681
783
|
async function filterExisting(files) {
|
|
682
784
|
const existing = [];
|
|
683
785
|
for (const file of files) {
|
|
684
|
-
if (await
|
|
786
|
+
if (await exists4(file)) {
|
|
685
787
|
existing.push(file);
|
|
686
788
|
}
|
|
687
789
|
}
|
|
688
790
|
return existing;
|
|
689
791
|
}
|
|
690
|
-
async function
|
|
792
|
+
async function exists4(target) {
|
|
691
793
|
try {
|
|
692
794
|
await (0, import_promises5.access)(target);
|
|
693
795
|
return true;
|
|
@@ -716,9 +818,9 @@ function stripContractDeclarationLines(text) {
|
|
|
716
818
|
// src/core/contractIndex.ts
|
|
717
819
|
async function buildContractIndex(root, config) {
|
|
718
820
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
719
|
-
const uiRoot =
|
|
720
|
-
const apiRoot =
|
|
721
|
-
const dbRoot =
|
|
821
|
+
const uiRoot = import_node_path8.default.join(contractsRoot, "ui");
|
|
822
|
+
const apiRoot = import_node_path8.default.join(contractsRoot, "api");
|
|
823
|
+
const dbRoot = import_node_path8.default.join(contractsRoot, "db");
|
|
722
824
|
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
723
825
|
collectUiContractFiles(uiRoot),
|
|
724
826
|
collectApiContractFiles(apiRoot),
|
|
@@ -800,6 +902,57 @@ function isValidId(value, prefix) {
|
|
|
800
902
|
return strict.test(value);
|
|
801
903
|
}
|
|
802
904
|
|
|
905
|
+
// src/core/parse/contractRefs.ts
|
|
906
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
907
|
+
function parseContractRefs(text, options = {}) {
|
|
908
|
+
const linePattern = buildLinePattern(options);
|
|
909
|
+
const lines = [];
|
|
910
|
+
for (const match of text.matchAll(linePattern)) {
|
|
911
|
+
lines.push((match[1] ?? "").trim());
|
|
912
|
+
}
|
|
913
|
+
const ids = [];
|
|
914
|
+
const invalidTokens = [];
|
|
915
|
+
let hasNone = false;
|
|
916
|
+
for (const line of lines) {
|
|
917
|
+
if (line.length === 0) {
|
|
918
|
+
invalidTokens.push("(empty)");
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
922
|
+
for (const token of tokens) {
|
|
923
|
+
if (token.length === 0) {
|
|
924
|
+
invalidTokens.push("(empty)");
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (token === "none") {
|
|
928
|
+
hasNone = true;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
932
|
+
ids.push(token);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
invalidTokens.push(token);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
lines,
|
|
940
|
+
ids: unique2(ids),
|
|
941
|
+
invalidTokens: unique2(invalidTokens),
|
|
942
|
+
hasNone
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function buildLinePattern(options) {
|
|
946
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
947
|
+
return new RegExp(
|
|
948
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
949
|
+
"gm"
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
function unique2(values) {
|
|
953
|
+
return Array.from(new Set(values));
|
|
954
|
+
}
|
|
955
|
+
|
|
803
956
|
// src/core/parse/markdown.ts
|
|
804
957
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
805
958
|
function parseHeadings(md) {
|
|
@@ -846,8 +999,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
|
846
999
|
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
847
1000
|
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
848
1001
|
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
849
|
-
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
850
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
851
1002
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
852
1003
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
853
1004
|
function parseSpec(md, file) {
|
|
@@ -920,50 +1071,10 @@ function parseSpec(md, file) {
|
|
|
920
1071
|
}
|
|
921
1072
|
return parsed;
|
|
922
1073
|
}
|
|
923
|
-
function parseContractRefs(md) {
|
|
924
|
-
const lines = [];
|
|
925
|
-
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
926
|
-
lines.push((match[1] ?? "").trim());
|
|
927
|
-
}
|
|
928
|
-
const ids = [];
|
|
929
|
-
const invalidTokens = [];
|
|
930
|
-
let hasNone = false;
|
|
931
|
-
for (const line of lines) {
|
|
932
|
-
if (line.length === 0) {
|
|
933
|
-
invalidTokens.push("(empty)");
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
937
|
-
for (const token of tokens) {
|
|
938
|
-
if (token.length === 0) {
|
|
939
|
-
invalidTokens.push("(empty)");
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
if (token === "none") {
|
|
943
|
-
hasNone = true;
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
947
|
-
ids.push(token);
|
|
948
|
-
continue;
|
|
949
|
-
}
|
|
950
|
-
invalidTokens.push(token);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return {
|
|
954
|
-
lines,
|
|
955
|
-
ids: unique2(ids),
|
|
956
|
-
invalidTokens: unique2(invalidTokens),
|
|
957
|
-
hasNone
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
function unique2(values) {
|
|
961
|
-
return Array.from(new Set(values));
|
|
962
|
-
}
|
|
963
1074
|
|
|
964
1075
|
// src/core/traceability.ts
|
|
965
1076
|
var import_promises7 = require("fs/promises");
|
|
966
|
-
var
|
|
1077
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
967
1078
|
|
|
968
1079
|
// src/core/gherkin/parse.ts
|
|
969
1080
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -994,9 +1105,6 @@ function formatError2(error2) {
|
|
|
994
1105
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
995
1106
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
996
1107
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
997
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
998
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
999
|
-
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
1000
1108
|
function parseScenarioDocument(text, uri) {
|
|
1001
1109
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1002
1110
|
if (!gherkinDocument) {
|
|
@@ -1021,31 +1129,21 @@ function parseScenarioDocument(text, uri) {
|
|
|
1021
1129
|
errors
|
|
1022
1130
|
};
|
|
1023
1131
|
}
|
|
1024
|
-
function buildScenarioAtoms(document) {
|
|
1132
|
+
function buildScenarioAtoms(document, contractIds = []) {
|
|
1133
|
+
const uniqueContractIds = unique3(contractIds).sort(
|
|
1134
|
+
(a, b) => a.localeCompare(b)
|
|
1135
|
+
);
|
|
1025
1136
|
return document.scenarios.map((scenario) => {
|
|
1026
1137
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1027
1138
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1028
1139
|
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1029
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1030
|
-
scenario.tags.forEach((tag) => {
|
|
1031
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
1032
|
-
contractIds.add(tag);
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
for (const step of scenario.steps) {
|
|
1036
|
-
for (const text of collectStepTexts(step)) {
|
|
1037
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1038
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1039
|
-
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
1140
|
const atom = {
|
|
1043
1141
|
uri: document.uri,
|
|
1044
1142
|
featureName: document.featureName ?? "",
|
|
1045
1143
|
scenarioName: scenario.name,
|
|
1046
1144
|
kind: scenario.kind,
|
|
1047
1145
|
brIds,
|
|
1048
|
-
contractIds:
|
|
1146
|
+
contractIds: uniqueContractIds
|
|
1049
1147
|
};
|
|
1050
1148
|
if (scenario.line !== void 0) {
|
|
1051
1149
|
atom.line = scenario.line;
|
|
@@ -1098,23 +1196,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
1098
1196
|
function collectTagNames(tags) {
|
|
1099
1197
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1100
1198
|
}
|
|
1101
|
-
function collectStepTexts(step) {
|
|
1102
|
-
const texts = [];
|
|
1103
|
-
if (step.text) {
|
|
1104
|
-
texts.push(step.text);
|
|
1105
|
-
}
|
|
1106
|
-
if (step.docString?.content) {
|
|
1107
|
-
texts.push(step.docString.content);
|
|
1108
|
-
}
|
|
1109
|
-
if (step.dataTable?.rows) {
|
|
1110
|
-
for (const row of step.dataTable.rows) {
|
|
1111
|
-
for (const cell of row.cells) {
|
|
1112
|
-
texts.push(cell.value);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
return texts;
|
|
1117
|
-
}
|
|
1118
1199
|
function unique3(values) {
|
|
1119
1200
|
return Array.from(new Set(values));
|
|
1120
1201
|
}
|
|
@@ -1216,7 +1297,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1216
1297
|
};
|
|
1217
1298
|
}
|
|
1218
1299
|
const normalizedFiles = Array.from(
|
|
1219
|
-
new Set(files.map((file) =>
|
|
1300
|
+
new Set(files.map((file) => import_node_path9.default.normalize(file)))
|
|
1220
1301
|
);
|
|
1221
1302
|
for (const file of normalizedFiles) {
|
|
1222
1303
|
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
@@ -1277,11 +1358,11 @@ function formatError3(error2) {
|
|
|
1277
1358
|
|
|
1278
1359
|
// src/core/version.ts
|
|
1279
1360
|
var import_promises8 = require("fs/promises");
|
|
1280
|
-
var
|
|
1361
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1281
1362
|
var import_node_url2 = require("url");
|
|
1282
1363
|
async function resolveToolVersion() {
|
|
1283
|
-
if ("0.
|
|
1284
|
-
return "0.
|
|
1364
|
+
if ("0.5.2".length > 0) {
|
|
1365
|
+
return "0.5.2";
|
|
1285
1366
|
}
|
|
1286
1367
|
try {
|
|
1287
1368
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1296,18 +1377,18 @@ async function resolveToolVersion() {
|
|
|
1296
1377
|
function resolvePackageJsonPath() {
|
|
1297
1378
|
const base = __filename;
|
|
1298
1379
|
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
1299
|
-
return
|
|
1380
|
+
return import_node_path10.default.resolve(import_node_path10.default.dirname(basePath), "../../package.json");
|
|
1300
1381
|
}
|
|
1301
1382
|
|
|
1302
1383
|
// src/core/validators/contracts.ts
|
|
1303
1384
|
var import_promises9 = require("fs/promises");
|
|
1304
|
-
var
|
|
1385
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1305
1386
|
|
|
1306
1387
|
// src/core/contracts.ts
|
|
1307
|
-
var
|
|
1388
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1308
1389
|
var import_yaml2 = require("yaml");
|
|
1309
1390
|
function parseStructuredContract(file, text) {
|
|
1310
|
-
const ext =
|
|
1391
|
+
const ext = import_node_path11.default.extname(file).toLowerCase();
|
|
1311
1392
|
if (ext === ".json") {
|
|
1312
1393
|
return JSON.parse(text);
|
|
1313
1394
|
}
|
|
@@ -1327,9 +1408,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1327
1408
|
async function validateContracts(root, config) {
|
|
1328
1409
|
const issues = [];
|
|
1329
1410
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1330
|
-
issues.push(...await validateUiContracts(
|
|
1331
|
-
issues.push(...await validateApiContracts(
|
|
1332
|
-
issues.push(...await validateDbContracts(
|
|
1411
|
+
issues.push(...await validateUiContracts(import_node_path12.default.join(contractsRoot, "ui")));
|
|
1412
|
+
issues.push(...await validateApiContracts(import_node_path12.default.join(contractsRoot, "api")));
|
|
1413
|
+
issues.push(...await validateDbContracts(import_node_path12.default.join(contractsRoot, "db")));
|
|
1333
1414
|
const contractIndex = await buildContractIndex(root, config);
|
|
1334
1415
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1335
1416
|
return issues;
|
|
@@ -1612,7 +1693,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1612
1693
|
|
|
1613
1694
|
// src/core/validators/delta.ts
|
|
1614
1695
|
var import_promises10 = require("fs/promises");
|
|
1615
|
-
var
|
|
1696
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1616
1697
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1617
1698
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1618
1699
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1626,7 +1707,7 @@ async function validateDeltas(root, config) {
|
|
|
1626
1707
|
}
|
|
1627
1708
|
const issues = [];
|
|
1628
1709
|
for (const pack of packs) {
|
|
1629
|
-
const deltaPath =
|
|
1710
|
+
const deltaPath = import_node_path13.default.join(pack, "delta.md");
|
|
1630
1711
|
let text;
|
|
1631
1712
|
try {
|
|
1632
1713
|
text = await (0, import_promises10.readFile)(deltaPath, "utf-8");
|
|
@@ -1702,7 +1783,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1702
1783
|
|
|
1703
1784
|
// src/core/validators/ids.ts
|
|
1704
1785
|
var import_promises11 = require("fs/promises");
|
|
1705
|
-
var
|
|
1786
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
1706
1787
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1707
1788
|
async function validateDefinedIds(root, config) {
|
|
1708
1789
|
const issues = [];
|
|
@@ -1768,7 +1849,7 @@ function recordId(out, id, file) {
|
|
|
1768
1849
|
}
|
|
1769
1850
|
function formatFileList(files, root) {
|
|
1770
1851
|
return files.map((file) => {
|
|
1771
|
-
const relative =
|
|
1852
|
+
const relative = import_node_path14.default.relative(root, file);
|
|
1772
1853
|
return relative.length > 0 ? relative : file;
|
|
1773
1854
|
}).join(", ");
|
|
1774
1855
|
}
|
|
@@ -2205,7 +2286,7 @@ async function validateTraceability(root, config) {
|
|
|
2205
2286
|
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2206
2287
|
issues.push(
|
|
2207
2288
|
issue6(
|
|
2208
|
-
"QFAI-TRACE-
|
|
2289
|
+
"QFAI-TRACE-023",
|
|
2209
2290
|
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2210
2291
|
"error",
|
|
2211
2292
|
file,
|
|
@@ -2237,7 +2318,7 @@ async function validateTraceability(root, config) {
|
|
|
2237
2318
|
if (unknownContractIds.length > 0) {
|
|
2238
2319
|
issues.push(
|
|
2239
2320
|
issue6(
|
|
2240
|
-
"QFAI-TRACE-
|
|
2321
|
+
"QFAI-TRACE-024",
|
|
2241
2322
|
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2242
2323
|
", "
|
|
2243
2324
|
)}`,
|
|
@@ -2252,11 +2333,62 @@ async function validateTraceability(root, config) {
|
|
|
2252
2333
|
for (const file of scenarioFiles) {
|
|
2253
2334
|
const text = await (0, import_promises14.readFile)(file, "utf-8");
|
|
2254
2335
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2336
|
+
const scenarioContractRefs = parseContractRefs(text, {
|
|
2337
|
+
allowCommentPrefix: true
|
|
2338
|
+
});
|
|
2339
|
+
if (scenarioContractRefs.lines.length === 0) {
|
|
2340
|
+
issues.push(
|
|
2341
|
+
issue6(
|
|
2342
|
+
"QFAI-TRACE-031",
|
|
2343
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2344
|
+
"error",
|
|
2345
|
+
file,
|
|
2346
|
+
"traceability.scenarioContractRefRequired"
|
|
2347
|
+
)
|
|
2348
|
+
);
|
|
2349
|
+
} else {
|
|
2350
|
+
if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
2351
|
+
issues.push(
|
|
2352
|
+
issue6(
|
|
2353
|
+
"QFAI-TRACE-033",
|
|
2354
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2355
|
+
"error",
|
|
2356
|
+
file,
|
|
2357
|
+
"traceability.scenarioContractRefFormat"
|
|
2358
|
+
)
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
if (scenarioContractRefs.invalidTokens.length > 0) {
|
|
2362
|
+
issues.push(
|
|
2363
|
+
issue6(
|
|
2364
|
+
"QFAI-TRACE-032",
|
|
2365
|
+
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2366
|
+
", "
|
|
2367
|
+
)}`,
|
|
2368
|
+
"error",
|
|
2369
|
+
file,
|
|
2370
|
+
"traceability.scenarioContractRefFormat",
|
|
2371
|
+
scenarioContractRefs.invalidTokens
|
|
2372
|
+
)
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2255
2376
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2256
2377
|
if (!document || errors.length > 0) {
|
|
2257
2378
|
continue;
|
|
2258
2379
|
}
|
|
2259
|
-
|
|
2380
|
+
if (document.scenarios.length !== 1) {
|
|
2381
|
+
issues.push(
|
|
2382
|
+
issue6(
|
|
2383
|
+
"QFAI-TRACE-030",
|
|
2384
|
+
`Scenario \u30D5\u30A1\u30A4\u30EB\u306F 1\u30D5\u30A1\u30A4\u30EB=1\u30B7\u30CA\u30EA\u30AA\u3067\u3059\u3002\u73FE\u5728: ${document.scenarios.length}\u4EF6 (file=${file})`,
|
|
2385
|
+
"error",
|
|
2386
|
+
file,
|
|
2387
|
+
"traceability.scenarioOnePerFile"
|
|
2388
|
+
)
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
|
|
2260
2392
|
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2261
2393
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2262
2394
|
const atom = atoms[index];
|
|
@@ -2401,7 +2533,7 @@ async function validateTraceability(root, config) {
|
|
|
2401
2533
|
if (orphanBrIds.length > 0) {
|
|
2402
2534
|
issues.push(
|
|
2403
2535
|
issue6(
|
|
2404
|
-
"
|
|
2536
|
+
"QFAI-TRACE-009",
|
|
2405
2537
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
2406
2538
|
"error",
|
|
2407
2539
|
specsRoot,
|
|
@@ -2471,17 +2603,19 @@ async function validateTraceability(root, config) {
|
|
|
2471
2603
|
);
|
|
2472
2604
|
}
|
|
2473
2605
|
}
|
|
2474
|
-
|
|
2606
|
+
const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
|
|
2607
|
+
if (orphanPolicy !== "allow") {
|
|
2475
2608
|
if (contractIds.size > 0) {
|
|
2476
2609
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2477
2610
|
(id) => !specContractIds.has(id)
|
|
2478
2611
|
);
|
|
2479
2612
|
if (orphanContracts.length > 0) {
|
|
2613
|
+
const severity = orphanPolicy === "warning" ? "warning" : "error";
|
|
2480
2614
|
issues.push(
|
|
2481
2615
|
issue6(
|
|
2482
2616
|
"QFAI-TRACE-022",
|
|
2483
2617
|
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2484
|
-
|
|
2618
|
+
severity,
|
|
2485
2619
|
specsRoot,
|
|
2486
2620
|
"traceability.contractCoverage",
|
|
2487
2621
|
orphanContracts
|
|
@@ -2606,16 +2740,17 @@ function countIssues(issues) {
|
|
|
2606
2740
|
// src/core/report.ts
|
|
2607
2741
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2608
2742
|
async function createReportData(root, validation, configResult) {
|
|
2609
|
-
const
|
|
2743
|
+
const resolvedRoot = import_node_path15.default.resolve(root);
|
|
2744
|
+
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2610
2745
|
const config = resolved.config;
|
|
2611
2746
|
const configPath = resolved.configPath;
|
|
2612
|
-
const specsRoot = resolvePath(
|
|
2613
|
-
const contractsRoot = resolvePath(
|
|
2614
|
-
const apiRoot =
|
|
2615
|
-
const uiRoot =
|
|
2616
|
-
const dbRoot =
|
|
2617
|
-
const srcRoot = resolvePath(
|
|
2618
|
-
const testsRoot = resolvePath(
|
|
2747
|
+
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2748
|
+
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2749
|
+
const apiRoot = import_node_path15.default.join(contractsRoot, "api");
|
|
2750
|
+
const uiRoot = import_node_path15.default.join(contractsRoot, "ui");
|
|
2751
|
+
const dbRoot = import_node_path15.default.join(contractsRoot, "db");
|
|
2752
|
+
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2753
|
+
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2619
2754
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
2620
2755
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2621
2756
|
const {
|
|
@@ -2623,12 +2758,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2623
2758
|
ui: uiFiles,
|
|
2624
2759
|
db: dbFiles
|
|
2625
2760
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2626
|
-
const contractIndex = await buildContractIndex(
|
|
2627
|
-
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2761
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2628
2762
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2763
|
+
const specContractRefs = await collectSpecContractRefs(
|
|
2764
|
+
specFiles,
|
|
2765
|
+
contractIdList
|
|
2766
|
+
);
|
|
2629
2767
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2630
|
-
for (const
|
|
2631
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2768
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2769
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2632
2770
|
}
|
|
2633
2771
|
const referencedContractCount = contractIdList.filter(
|
|
2634
2772
|
(id) => referencedContracts.has(id)
|
|
@@ -2637,8 +2775,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2637
2775
|
(id) => !referencedContracts.has(id)
|
|
2638
2776
|
).length;
|
|
2639
2777
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2640
|
-
const
|
|
2641
|
-
specContractRefs.
|
|
2778
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2779
|
+
specContractRefs.specToContracts
|
|
2642
2780
|
);
|
|
2643
2781
|
const idsByPrefix = await collectIds([
|
|
2644
2782
|
...specFiles,
|
|
@@ -2656,24 +2794,26 @@ async function createReportData(root, validation, configResult) {
|
|
|
2656
2794
|
srcRoot,
|
|
2657
2795
|
testsRoot
|
|
2658
2796
|
);
|
|
2659
|
-
const
|
|
2660
|
-
const
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2797
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2798
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2799
|
+
resolvedRoot,
|
|
2800
|
+
resolvedValidationRaw
|
|
2664
2801
|
);
|
|
2665
|
-
const scCoverage =
|
|
2666
|
-
const testFiles =
|
|
2802
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2803
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2667
2804
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2668
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2669
|
-
|
|
2805
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2806
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2807
|
+
);
|
|
2670
2808
|
const version = await resolveToolVersion();
|
|
2809
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2810
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2671
2811
|
return {
|
|
2672
2812
|
tool: "qfai",
|
|
2673
2813
|
version,
|
|
2674
2814
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2675
|
-
root,
|
|
2676
|
-
configPath,
|
|
2815
|
+
root: displayRoot,
|
|
2816
|
+
configPath: displayConfigPath,
|
|
2677
2817
|
summary: {
|
|
2678
2818
|
specs: specFiles.length,
|
|
2679
2819
|
scenarios: scenarioFiles.length,
|
|
@@ -2682,7 +2822,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2682
2822
|
ui: uiFiles.length,
|
|
2683
2823
|
db: dbFiles.length
|
|
2684
2824
|
},
|
|
2685
|
-
counts:
|
|
2825
|
+
counts: normalizedValidation.counts
|
|
2686
2826
|
},
|
|
2687
2827
|
ids: {
|
|
2688
2828
|
spec: idsByPrefix.SPEC,
|
|
@@ -2706,21 +2846,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2706
2846
|
},
|
|
2707
2847
|
specs: {
|
|
2708
2848
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2709
|
-
|
|
2849
|
+
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2850
|
+
specToContracts: specToContractsRecord
|
|
2710
2851
|
}
|
|
2711
2852
|
},
|
|
2712
|
-
issues:
|
|
2853
|
+
issues: normalizedValidation.issues
|
|
2713
2854
|
};
|
|
2714
2855
|
}
|
|
2715
2856
|
function formatReportMarkdown(data) {
|
|
2716
2857
|
const lines = [];
|
|
2717
2858
|
lines.push("# QFAI Report");
|
|
2859
|
+
lines.push("");
|
|
2718
2860
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2719
2861
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2720
2862
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2721
2863
|
lines.push(`- \u7248: ${data.version}`);
|
|
2722
2864
|
lines.push("");
|
|
2723
2865
|
lines.push("## \u6982\u8981");
|
|
2866
|
+
lines.push("");
|
|
2724
2867
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2725
2868
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2726
2869
|
lines.push(
|
|
@@ -2731,6 +2874,7 @@ function formatReportMarkdown(data) {
|
|
|
2731
2874
|
);
|
|
2732
2875
|
lines.push("");
|
|
2733
2876
|
lines.push("## ID\u96C6\u8A08");
|
|
2877
|
+
lines.push("");
|
|
2734
2878
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2735
2879
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2736
2880
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2739,12 +2883,14 @@ function formatReportMarkdown(data) {
|
|
|
2739
2883
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2740
2884
|
lines.push("");
|
|
2741
2885
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2886
|
+
lines.push("");
|
|
2742
2887
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2743
2888
|
lines.push(
|
|
2744
2889
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2745
2890
|
);
|
|
2746
2891
|
lines.push("");
|
|
2747
2892
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2893
|
+
lines.push("");
|
|
2748
2894
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2749
2895
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2750
2896
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2753,6 +2899,7 @@ function formatReportMarkdown(data) {
|
|
|
2753
2899
|
);
|
|
2754
2900
|
lines.push("");
|
|
2755
2901
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2902
|
+
lines.push("");
|
|
2756
2903
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2757
2904
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2758
2905
|
(a, b) => a.localeCompare(b)
|
|
@@ -2771,24 +2918,36 @@ function formatReportMarkdown(data) {
|
|
|
2771
2918
|
}
|
|
2772
2919
|
lines.push("");
|
|
2773
2920
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2774
|
-
|
|
2921
|
+
lines.push("");
|
|
2922
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2775
2923
|
const specIds = Object.keys(specToContracts).sort(
|
|
2776
2924
|
(a, b) => a.localeCompare(b)
|
|
2777
2925
|
);
|
|
2778
2926
|
if (specIds.length === 0) {
|
|
2779
2927
|
lines.push("- (none)");
|
|
2780
2928
|
} else {
|
|
2781
|
-
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2929
|
+
const rows = specIds.map((specId) => {
|
|
2930
|
+
const entry = specToContracts[specId];
|
|
2931
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
2932
|
+
const status = entry?.status ?? "missing";
|
|
2933
|
+
return [specId, status, contracts];
|
|
2934
|
+
});
|
|
2935
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2936
|
+
}
|
|
2937
|
+
lines.push("");
|
|
2938
|
+
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2939
|
+
lines.push("");
|
|
2940
|
+
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2941
|
+
if (missingRefSpecs.length === 0) {
|
|
2942
|
+
lines.push("- (none)");
|
|
2943
|
+
} else {
|
|
2944
|
+
for (const specId of missingRefSpecs) {
|
|
2945
|
+
lines.push(`- ${specId}`);
|
|
2788
2946
|
}
|
|
2789
2947
|
}
|
|
2790
2948
|
lines.push("");
|
|
2791
2949
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2950
|
+
lines.push("");
|
|
2792
2951
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2793
2952
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2794
2953
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2818,6 +2977,7 @@ function formatReportMarkdown(data) {
|
|
|
2818
2977
|
}
|
|
2819
2978
|
lines.push("");
|
|
2820
2979
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2980
|
+
lines.push("");
|
|
2821
2981
|
const scRefs = data.traceability.sc.refs;
|
|
2822
2982
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2823
2983
|
if (scIds.length === 0) {
|
|
@@ -2834,6 +2994,7 @@ function formatReportMarkdown(data) {
|
|
|
2834
2994
|
}
|
|
2835
2995
|
lines.push("");
|
|
2836
2996
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2997
|
+
lines.push("");
|
|
2837
2998
|
const specScIssues = data.issues.filter(
|
|
2838
2999
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2839
3000
|
);
|
|
@@ -2848,6 +3009,7 @@ function formatReportMarkdown(data) {
|
|
|
2848
3009
|
}
|
|
2849
3010
|
lines.push("");
|
|
2850
3011
|
lines.push("## Hotspots");
|
|
3012
|
+
lines.push("");
|
|
2851
3013
|
const hotspots = buildHotspots(data.issues);
|
|
2852
3014
|
if (hotspots.length === 0) {
|
|
2853
3015
|
lines.push("- (none)");
|
|
@@ -2860,6 +3022,7 @@ function formatReportMarkdown(data) {
|
|
|
2860
3022
|
}
|
|
2861
3023
|
lines.push("");
|
|
2862
3024
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
3025
|
+
lines.push("");
|
|
2863
3026
|
const traceIssues = data.issues.filter(
|
|
2864
3027
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2865
3028
|
);
|
|
@@ -2875,6 +3038,7 @@ function formatReportMarkdown(data) {
|
|
|
2875
3038
|
}
|
|
2876
3039
|
lines.push("");
|
|
2877
3040
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
3041
|
+
lines.push("");
|
|
2878
3042
|
if (data.issues.length === 0) {
|
|
2879
3043
|
lines.push("- (none)");
|
|
2880
3044
|
} else {
|
|
@@ -2891,29 +3055,41 @@ function formatReportMarkdown(data) {
|
|
|
2891
3055
|
function formatReportJson(data) {
|
|
2892
3056
|
return JSON.stringify(data, null, 2);
|
|
2893
3057
|
}
|
|
2894
|
-
async function collectSpecContractRefs(specFiles) {
|
|
2895
|
-
const
|
|
3058
|
+
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
3059
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2896
3060
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2897
3061
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
3062
|
+
for (const contractId of contractIdList) {
|
|
3063
|
+
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
3064
|
+
}
|
|
2898
3065
|
for (const file of specFiles) {
|
|
2899
3066
|
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2900
3067
|
const parsed = parseSpec(text, file);
|
|
2901
|
-
const specKey = parsed.specId
|
|
3068
|
+
const specKey = parsed.specId;
|
|
3069
|
+
if (!specKey) {
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
2902
3072
|
const refs = parsed.contractRefs;
|
|
2903
3073
|
if (refs.lines.length === 0) {
|
|
2904
3074
|
missingRefSpecs.add(specKey);
|
|
3075
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
3076
|
+
continue;
|
|
2905
3077
|
}
|
|
2906
|
-
const
|
|
3078
|
+
const current = specToContracts.get(specKey) ?? {
|
|
3079
|
+
status: "declared",
|
|
3080
|
+
ids: /* @__PURE__ */ new Set()
|
|
3081
|
+
};
|
|
2907
3082
|
for (const id of refs.ids) {
|
|
2908
|
-
|
|
2909
|
-
const specs = idToSpecs.get(id)
|
|
2910
|
-
specs
|
|
2911
|
-
|
|
3083
|
+
current.ids.add(id);
|
|
3084
|
+
const specs = idToSpecs.get(id);
|
|
3085
|
+
if (specs) {
|
|
3086
|
+
specs.add(specKey);
|
|
3087
|
+
}
|
|
2912
3088
|
}
|
|
2913
|
-
|
|
3089
|
+
specToContracts.set(specKey, current);
|
|
2914
3090
|
}
|
|
2915
3091
|
return {
|
|
2916
|
-
|
|
3092
|
+
specToContracts,
|
|
2917
3093
|
idToSpecs,
|
|
2918
3094
|
missingRefSpecs
|
|
2919
3095
|
};
|
|
@@ -2990,6 +3166,20 @@ function formatList(values) {
|
|
|
2990
3166
|
}
|
|
2991
3167
|
return values.join(", ");
|
|
2992
3168
|
}
|
|
3169
|
+
function formatMarkdownTable(headers, rows) {
|
|
3170
|
+
const widths = headers.map((header, index) => {
|
|
3171
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
3172
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
3173
|
+
});
|
|
3174
|
+
const formatRow = (cells) => {
|
|
3175
|
+
const padded = cells.map(
|
|
3176
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
3177
|
+
);
|
|
3178
|
+
return `| ${padded.join(" | ")} |`;
|
|
3179
|
+
};
|
|
3180
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
3181
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3182
|
+
}
|
|
2993
3183
|
function toSortedArray2(values) {
|
|
2994
3184
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2995
3185
|
}
|
|
@@ -3000,6 +3190,27 @@ function mapToSortedRecord(values) {
|
|
|
3000
3190
|
}
|
|
3001
3191
|
return record2;
|
|
3002
3192
|
}
|
|
3193
|
+
function mapToSpecContractRecord(values) {
|
|
3194
|
+
const record2 = {};
|
|
3195
|
+
for (const [key, entry] of values.entries()) {
|
|
3196
|
+
record2[key] = {
|
|
3197
|
+
status: entry.status,
|
|
3198
|
+
ids: toSortedArray2(entry.ids)
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
return record2;
|
|
3202
|
+
}
|
|
3203
|
+
function normalizeScSources(root, sources) {
|
|
3204
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3205
|
+
for (const [id, files] of sources.entries()) {
|
|
3206
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3207
|
+
for (const file of files) {
|
|
3208
|
+
mapped.add(toRelativePath(root, file));
|
|
3209
|
+
}
|
|
3210
|
+
normalized.set(id, mapped);
|
|
3211
|
+
}
|
|
3212
|
+
return normalized;
|
|
3213
|
+
}
|
|
3003
3214
|
function buildHotspots(issues) {
|
|
3004
3215
|
const map = /* @__PURE__ */ new Map();
|
|
3005
3216
|
for (const issue7 of issues) {
|
|
@@ -3024,38 +3235,53 @@ function buildHotspots(issues) {
|
|
|
3024
3235
|
|
|
3025
3236
|
// src/cli/commands/report.ts
|
|
3026
3237
|
async function runReport(options) {
|
|
3027
|
-
const root =
|
|
3238
|
+
const root = import_node_path16.default.resolve(options.root);
|
|
3028
3239
|
const configResult = await loadConfig(root);
|
|
3029
|
-
const input = configResult.config.output.validateJsonPath;
|
|
3030
|
-
const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
|
|
3031
3240
|
let validation;
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3241
|
+
if (options.runValidate) {
|
|
3242
|
+
if (options.inputPath) {
|
|
3243
|
+
warn("report: --run-validate \u304C\u6307\u5B9A\u3055\u308C\u305F\u305F\u3081 --in \u306F\u7121\u8996\u3057\u307E\u3059\u3002");
|
|
3244
|
+
}
|
|
3245
|
+
const result = await validateProject(root, configResult);
|
|
3246
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3247
|
+
await writeValidationResult(
|
|
3248
|
+
root,
|
|
3249
|
+
configResult.config.output.validateJsonPath,
|
|
3250
|
+
normalized
|
|
3251
|
+
);
|
|
3252
|
+
validation = normalized;
|
|
3253
|
+
} else {
|
|
3254
|
+
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3255
|
+
const inputPath = import_node_path16.default.isAbsolute(input) ? input : import_node_path16.default.resolve(root, input);
|
|
3256
|
+
try {
|
|
3257
|
+
validation = await readValidationResult(inputPath);
|
|
3258
|
+
} catch (err) {
|
|
3259
|
+
if (isMissingFileError5(err)) {
|
|
3260
|
+
error(
|
|
3261
|
+
[
|
|
3262
|
+
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
3263
|
+
"",
|
|
3264
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
3265
|
+
" qfai validate",
|
|
3266
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
3267
|
+
"",
|
|
3268
|
+
"\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3269
|
+
"GitHub Actions \u30C6\u30F3\u30D7\u30EC\u3092\u4F7F\u3063\u3066\u3044\u308B\u5834\u5408\u306F\u3001workflow \u306E validate \u30B8\u30E7\u30D6\u3092\u5148\u306B\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3270
|
+
].join("\n")
|
|
3271
|
+
);
|
|
3272
|
+
process.exitCode = 2;
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
throw err;
|
|
3049
3276
|
}
|
|
3050
|
-
throw err;
|
|
3051
3277
|
}
|
|
3052
3278
|
const data = await createReportData(root, validation, configResult);
|
|
3053
3279
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3054
3280
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3055
|
-
const defaultOut = options.format === "json" ?
|
|
3281
|
+
const defaultOut = options.format === "json" ? import_node_path16.default.join(outRoot, "report.json") : import_node_path16.default.join(outRoot, "report.md");
|
|
3056
3282
|
const out = options.outPath ?? defaultOut;
|
|
3057
|
-
const outPath =
|
|
3058
|
-
await (0, import_promises16.mkdir)(
|
|
3283
|
+
const outPath = import_node_path16.default.isAbsolute(out) ? out : import_node_path16.default.resolve(root, out);
|
|
3284
|
+
await (0, import_promises16.mkdir)(import_node_path16.default.dirname(outPath), { recursive: true });
|
|
3059
3285
|
await (0, import_promises16.writeFile)(outPath, `${output}
|
|
3060
3286
|
`, "utf-8");
|
|
3061
3287
|
info(
|
|
@@ -3119,10 +3345,16 @@ function isMissingFileError5(error2) {
|
|
|
3119
3345
|
const record2 = error2;
|
|
3120
3346
|
return record2.code === "ENOENT";
|
|
3121
3347
|
}
|
|
3348
|
+
async function writeValidationResult(root, outputPath, result) {
|
|
3349
|
+
const abs = import_node_path16.default.isAbsolute(outputPath) ? outputPath : import_node_path16.default.resolve(root, outputPath);
|
|
3350
|
+
await (0, import_promises16.mkdir)(import_node_path16.default.dirname(abs), { recursive: true });
|
|
3351
|
+
await (0, import_promises16.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3352
|
+
`, "utf-8");
|
|
3353
|
+
}
|
|
3122
3354
|
|
|
3123
3355
|
// src/cli/commands/validate.ts
|
|
3124
3356
|
var import_promises17 = require("fs/promises");
|
|
3125
|
-
var
|
|
3357
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
3126
3358
|
|
|
3127
3359
|
// src/cli/lib/failOn.ts
|
|
3128
3360
|
function shouldFail(result, failOn) {
|
|
@@ -3137,19 +3369,24 @@ function shouldFail(result, failOn) {
|
|
|
3137
3369
|
|
|
3138
3370
|
// src/cli/commands/validate.ts
|
|
3139
3371
|
async function runValidate(options) {
|
|
3140
|
-
const root =
|
|
3372
|
+
const root = import_node_path17.default.resolve(options.root);
|
|
3141
3373
|
const configResult = await loadConfig(root);
|
|
3142
3374
|
const result = await validateProject(root, configResult);
|
|
3375
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3143
3376
|
const format = options.format ?? "text";
|
|
3144
3377
|
if (format === "text") {
|
|
3145
|
-
emitText(
|
|
3378
|
+
emitText(normalized);
|
|
3146
3379
|
}
|
|
3147
3380
|
if (format === "github") {
|
|
3148
|
-
|
|
3381
|
+
const jsonPath = resolveJsonPath(
|
|
3382
|
+
root,
|
|
3383
|
+
configResult.config.output.validateJsonPath
|
|
3384
|
+
);
|
|
3385
|
+
emitGitHubOutput(normalized, root, jsonPath);
|
|
3149
3386
|
}
|
|
3150
|
-
await emitJson(
|
|
3387
|
+
await emitJson(normalized, root, configResult.config.output.validateJsonPath);
|
|
3151
3388
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
3152
|
-
return shouldFail(
|
|
3389
|
+
return shouldFail(normalized, failOn) ? 1 : 0;
|
|
3153
3390
|
}
|
|
3154
3391
|
function resolveFailOn(options, fallback) {
|
|
3155
3392
|
if (options.failOn) {
|
|
@@ -3174,6 +3411,22 @@ function emitText(result) {
|
|
|
3174
3411
|
`
|
|
3175
3412
|
);
|
|
3176
3413
|
}
|
|
3414
|
+
function emitGitHubOutput(result, root, jsonPath) {
|
|
3415
|
+
const deduped = dedupeIssues(result.issues);
|
|
3416
|
+
const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
|
|
3417
|
+
const dropped = Math.max(result.issues.length - deduped.length, 0);
|
|
3418
|
+
emitGitHubSummary(result, {
|
|
3419
|
+
total: deduped.length,
|
|
3420
|
+
omitted,
|
|
3421
|
+
dropped,
|
|
3422
|
+
jsonPath,
|
|
3423
|
+
root
|
|
3424
|
+
});
|
|
3425
|
+
const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
|
|
3426
|
+
for (const issue7 of issues) {
|
|
3427
|
+
emitGitHub(issue7);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3177
3430
|
function emitGitHub(issue7) {
|
|
3178
3431
|
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
3179
3432
|
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
@@ -3185,22 +3438,74 @@ function emitGitHub(issue7) {
|
|
|
3185
3438
|
`
|
|
3186
3439
|
);
|
|
3187
3440
|
}
|
|
3441
|
+
function emitGitHubSummary(result, options) {
|
|
3442
|
+
const summary = [
|
|
3443
|
+
"qfai validate summary:",
|
|
3444
|
+
`error=${result.counts.error}`,
|
|
3445
|
+
`warning=${result.counts.warning}`,
|
|
3446
|
+
`info=${result.counts.info}`,
|
|
3447
|
+
`annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`
|
|
3448
|
+
].join(" ");
|
|
3449
|
+
process.stdout.write(`${summary}
|
|
3450
|
+
`);
|
|
3451
|
+
if (options.dropped > 0 || options.omitted > 0) {
|
|
3452
|
+
const details = [
|
|
3453
|
+
"qfai validate note:",
|
|
3454
|
+
options.dropped > 0 ? `\u91CD\u8907\u9664\u5916=${options.dropped}` : null,
|
|
3455
|
+
options.omitted > 0 ? `\u4E0A\u9650\u7701\u7565=${options.omitted}` : null
|
|
3456
|
+
].filter(Boolean).join(" ");
|
|
3457
|
+
process.stdout.write(`${details}
|
|
3458
|
+
`);
|
|
3459
|
+
}
|
|
3460
|
+
const relative = toRelativePath(options.root, options.jsonPath);
|
|
3461
|
+
process.stdout.write(
|
|
3462
|
+
`qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
3463
|
+
`
|
|
3464
|
+
);
|
|
3465
|
+
}
|
|
3466
|
+
function dedupeIssues(issues) {
|
|
3467
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3468
|
+
const deduped = [];
|
|
3469
|
+
for (const issue7 of issues) {
|
|
3470
|
+
const key = issueKey(issue7);
|
|
3471
|
+
if (seen.has(key)) {
|
|
3472
|
+
continue;
|
|
3473
|
+
}
|
|
3474
|
+
seen.add(key);
|
|
3475
|
+
deduped.push(issue7);
|
|
3476
|
+
}
|
|
3477
|
+
return deduped;
|
|
3478
|
+
}
|
|
3479
|
+
function issueKey(issue7) {
|
|
3480
|
+
const file = issue7.file ?? "";
|
|
3481
|
+
const line = issue7.loc?.line ?? "";
|
|
3482
|
+
const column = issue7.loc?.column ?? "";
|
|
3483
|
+
return [issue7.code, issue7.severity, issue7.message, file, line, column].join(
|
|
3484
|
+
"|"
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3188
3487
|
async function emitJson(result, root, jsonPath) {
|
|
3189
|
-
const abs =
|
|
3190
|
-
await (0, import_promises17.mkdir)(
|
|
3488
|
+
const abs = resolveJsonPath(root, jsonPath);
|
|
3489
|
+
await (0, import_promises17.mkdir)(import_node_path17.default.dirname(abs), { recursive: true });
|
|
3191
3490
|
await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3192
3491
|
`, "utf-8");
|
|
3193
3492
|
}
|
|
3493
|
+
function resolveJsonPath(root, jsonPath) {
|
|
3494
|
+
return import_node_path17.default.isAbsolute(jsonPath) ? jsonPath : import_node_path17.default.resolve(root, jsonPath);
|
|
3495
|
+
}
|
|
3496
|
+
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3194
3497
|
|
|
3195
3498
|
// src/cli/lib/args.ts
|
|
3196
3499
|
function parseArgs(argv, cwd) {
|
|
3197
3500
|
const options = {
|
|
3198
3501
|
root: cwd,
|
|
3502
|
+
rootExplicit: false,
|
|
3199
3503
|
dir: cwd,
|
|
3200
3504
|
force: false,
|
|
3201
3505
|
yes: false,
|
|
3202
3506
|
dryRun: false,
|
|
3203
3507
|
reportFormat: "md",
|
|
3508
|
+
reportRunValidate: false,
|
|
3204
3509
|
validateFormat: "text",
|
|
3205
3510
|
strict: false,
|
|
3206
3511
|
help: false
|
|
@@ -3216,6 +3521,7 @@ function parseArgs(argv, cwd) {
|
|
|
3216
3521
|
switch (arg) {
|
|
3217
3522
|
case "--root":
|
|
3218
3523
|
options.root = args[i + 1] ?? options.root;
|
|
3524
|
+
options.rootExplicit = true;
|
|
3219
3525
|
i += 1;
|
|
3220
3526
|
break;
|
|
3221
3527
|
case "--dir":
|
|
@@ -3257,6 +3563,18 @@ function parseArgs(argv, cwd) {
|
|
|
3257
3563
|
}
|
|
3258
3564
|
i += 1;
|
|
3259
3565
|
break;
|
|
3566
|
+
case "--in":
|
|
3567
|
+
{
|
|
3568
|
+
const next = args[i + 1];
|
|
3569
|
+
if (next) {
|
|
3570
|
+
options.reportIn = next;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
i += 1;
|
|
3574
|
+
break;
|
|
3575
|
+
case "--run-validate":
|
|
3576
|
+
options.reportRunValidate = true;
|
|
3577
|
+
break;
|
|
3260
3578
|
case "--help":
|
|
3261
3579
|
case "-h":
|
|
3262
3580
|
options.help = true;
|
|
@@ -3308,19 +3626,27 @@ async function run(argv, cwd) {
|
|
|
3308
3626
|
});
|
|
3309
3627
|
return;
|
|
3310
3628
|
case "validate":
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3629
|
+
{
|
|
3630
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3631
|
+
process.exitCode = await runValidate({
|
|
3632
|
+
root: resolvedRoot,
|
|
3633
|
+
strict: options.strict,
|
|
3634
|
+
format: options.validateFormat,
|
|
3635
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3317
3638
|
return;
|
|
3318
3639
|
case "report":
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3640
|
+
{
|
|
3641
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3642
|
+
await runReport({
|
|
3643
|
+
root: resolvedRoot,
|
|
3644
|
+
format: options.reportFormat,
|
|
3645
|
+
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
|
|
3646
|
+
...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
|
|
3647
|
+
...options.reportRunValidate ? { runValidate: true } : {}
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3324
3650
|
return;
|
|
3325
3651
|
default:
|
|
3326
3652
|
error(`Unknown command: ${command}`);
|
|
@@ -3347,9 +3673,23 @@ Options:
|
|
|
3347
3673
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3348
3674
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3349
3675
|
--out <path> report: \u51FA\u529B\u5148
|
|
3676
|
+
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3677
|
+
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3350
3678
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
3351
3679
|
`;
|
|
3352
3680
|
}
|
|
3681
|
+
async function resolveRoot(options) {
|
|
3682
|
+
if (options.rootExplicit) {
|
|
3683
|
+
return options.root;
|
|
3684
|
+
}
|
|
3685
|
+
const search = await findConfigRoot(options.root);
|
|
3686
|
+
if (!search.found) {
|
|
3687
|
+
warn(
|
|
3688
|
+
`qfai: qfai.config.yaml \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081 defaultConfig \u3092\u4F7F\u7528\u3057\u307E\u3059 (root=${search.root})`
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
return search.root;
|
|
3692
|
+
}
|
|
3353
3693
|
|
|
3354
3694
|
// src/cli/index.ts
|
|
3355
3695
|
run(process.argv.slice(2), process.cwd()).catch((err) => {
|