qfai 0.5.0 → 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 +3 -2
- package/assets/init/.qfai/README.md +6 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/steering/traceability.md +2 -1
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +1 -1
- 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 +528 -207
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +533 -212
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +341 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.mjs +345 -156
- 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.5.
|
|
1284
|
-
return "0.5.
|
|
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,15 +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(
|
|
2761
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2627
2762
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2628
2763
|
const specContractRefs = await collectSpecContractRefs(
|
|
2629
2764
|
specFiles,
|
|
2630
2765
|
contractIdList
|
|
2631
2766
|
);
|
|
2632
2767
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2633
|
-
for (const
|
|
2634
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2768
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2769
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2635
2770
|
}
|
|
2636
2771
|
const referencedContractCount = contractIdList.filter(
|
|
2637
2772
|
(id) => referencedContracts.has(id)
|
|
@@ -2640,8 +2775,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2640
2775
|
(id) => !referencedContracts.has(id)
|
|
2641
2776
|
).length;
|
|
2642
2777
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2643
|
-
const
|
|
2644
|
-
specContractRefs.
|
|
2778
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2779
|
+
specContractRefs.specToContracts
|
|
2645
2780
|
);
|
|
2646
2781
|
const idsByPrefix = await collectIds([
|
|
2647
2782
|
...specFiles,
|
|
@@ -2659,24 +2794,26 @@ async function createReportData(root, validation, configResult) {
|
|
|
2659
2794
|
srcRoot,
|
|
2660
2795
|
testsRoot
|
|
2661
2796
|
);
|
|
2662
|
-
const
|
|
2663
|
-
const
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2797
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2798
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2799
|
+
resolvedRoot,
|
|
2800
|
+
resolvedValidationRaw
|
|
2667
2801
|
);
|
|
2668
|
-
const scCoverage =
|
|
2669
|
-
const testFiles =
|
|
2802
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2803
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2670
2804
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2671
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2672
|
-
|
|
2805
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2806
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2807
|
+
);
|
|
2673
2808
|
const version = await resolveToolVersion();
|
|
2809
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2810
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2674
2811
|
return {
|
|
2675
2812
|
tool: "qfai",
|
|
2676
2813
|
version,
|
|
2677
2814
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2678
|
-
root,
|
|
2679
|
-
configPath,
|
|
2815
|
+
root: displayRoot,
|
|
2816
|
+
configPath: displayConfigPath,
|
|
2680
2817
|
summary: {
|
|
2681
2818
|
specs: specFiles.length,
|
|
2682
2819
|
scenarios: scenarioFiles.length,
|
|
@@ -2685,7 +2822,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2685
2822
|
ui: uiFiles.length,
|
|
2686
2823
|
db: dbFiles.length
|
|
2687
2824
|
},
|
|
2688
|
-
counts:
|
|
2825
|
+
counts: normalizedValidation.counts
|
|
2689
2826
|
},
|
|
2690
2827
|
ids: {
|
|
2691
2828
|
spec: idsByPrefix.SPEC,
|
|
@@ -2710,21 +2847,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2710
2847
|
specs: {
|
|
2711
2848
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2712
2849
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2713
|
-
|
|
2850
|
+
specToContracts: specToContractsRecord
|
|
2714
2851
|
}
|
|
2715
2852
|
},
|
|
2716
|
-
issues:
|
|
2853
|
+
issues: normalizedValidation.issues
|
|
2717
2854
|
};
|
|
2718
2855
|
}
|
|
2719
2856
|
function formatReportMarkdown(data) {
|
|
2720
2857
|
const lines = [];
|
|
2721
2858
|
lines.push("# QFAI Report");
|
|
2859
|
+
lines.push("");
|
|
2722
2860
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2723
2861
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2724
2862
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2725
2863
|
lines.push(`- \u7248: ${data.version}`);
|
|
2726
2864
|
lines.push("");
|
|
2727
2865
|
lines.push("## \u6982\u8981");
|
|
2866
|
+
lines.push("");
|
|
2728
2867
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2729
2868
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2730
2869
|
lines.push(
|
|
@@ -2735,6 +2874,7 @@ function formatReportMarkdown(data) {
|
|
|
2735
2874
|
);
|
|
2736
2875
|
lines.push("");
|
|
2737
2876
|
lines.push("## ID\u96C6\u8A08");
|
|
2877
|
+
lines.push("");
|
|
2738
2878
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2739
2879
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2740
2880
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2743,12 +2883,14 @@ function formatReportMarkdown(data) {
|
|
|
2743
2883
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2744
2884
|
lines.push("");
|
|
2745
2885
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2886
|
+
lines.push("");
|
|
2746
2887
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2747
2888
|
lines.push(
|
|
2748
2889
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2749
2890
|
);
|
|
2750
2891
|
lines.push("");
|
|
2751
2892
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2893
|
+
lines.push("");
|
|
2752
2894
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2753
2895
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2754
2896
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2757,6 +2899,7 @@ function formatReportMarkdown(data) {
|
|
|
2757
2899
|
);
|
|
2758
2900
|
lines.push("");
|
|
2759
2901
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2902
|
+
lines.push("");
|
|
2760
2903
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2761
2904
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2762
2905
|
(a, b) => a.localeCompare(b)
|
|
@@ -2775,24 +2918,25 @@ function formatReportMarkdown(data) {
|
|
|
2775
2918
|
}
|
|
2776
2919
|
lines.push("");
|
|
2777
2920
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2778
|
-
|
|
2921
|
+
lines.push("");
|
|
2922
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2779
2923
|
const specIds = Object.keys(specToContracts).sort(
|
|
2780
2924
|
(a, b) => a.localeCompare(b)
|
|
2781
2925
|
);
|
|
2782
2926
|
if (specIds.length === 0) {
|
|
2783
2927
|
lines.push("- (none)");
|
|
2784
2928
|
} else {
|
|
2785
|
-
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
}
|
|
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));
|
|
2793
2936
|
}
|
|
2794
2937
|
lines.push("");
|
|
2795
2938
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2939
|
+
lines.push("");
|
|
2796
2940
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2797
2941
|
if (missingRefSpecs.length === 0) {
|
|
2798
2942
|
lines.push("- (none)");
|
|
@@ -2803,6 +2947,7 @@ function formatReportMarkdown(data) {
|
|
|
2803
2947
|
}
|
|
2804
2948
|
lines.push("");
|
|
2805
2949
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2950
|
+
lines.push("");
|
|
2806
2951
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2807
2952
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2808
2953
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2832,6 +2977,7 @@ function formatReportMarkdown(data) {
|
|
|
2832
2977
|
}
|
|
2833
2978
|
lines.push("");
|
|
2834
2979
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2980
|
+
lines.push("");
|
|
2835
2981
|
const scRefs = data.traceability.sc.refs;
|
|
2836
2982
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2837
2983
|
if (scIds.length === 0) {
|
|
@@ -2848,6 +2994,7 @@ function formatReportMarkdown(data) {
|
|
|
2848
2994
|
}
|
|
2849
2995
|
lines.push("");
|
|
2850
2996
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2997
|
+
lines.push("");
|
|
2851
2998
|
const specScIssues = data.issues.filter(
|
|
2852
2999
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2853
3000
|
);
|
|
@@ -2862,6 +3009,7 @@ function formatReportMarkdown(data) {
|
|
|
2862
3009
|
}
|
|
2863
3010
|
lines.push("");
|
|
2864
3011
|
lines.push("## Hotspots");
|
|
3012
|
+
lines.push("");
|
|
2865
3013
|
const hotspots = buildHotspots(data.issues);
|
|
2866
3014
|
if (hotspots.length === 0) {
|
|
2867
3015
|
lines.push("- (none)");
|
|
@@ -2874,6 +3022,7 @@ function formatReportMarkdown(data) {
|
|
|
2874
3022
|
}
|
|
2875
3023
|
lines.push("");
|
|
2876
3024
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
3025
|
+
lines.push("");
|
|
2877
3026
|
const traceIssues = data.issues.filter(
|
|
2878
3027
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2879
3028
|
);
|
|
@@ -2889,6 +3038,7 @@ function formatReportMarkdown(data) {
|
|
|
2889
3038
|
}
|
|
2890
3039
|
lines.push("");
|
|
2891
3040
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
3041
|
+
lines.push("");
|
|
2892
3042
|
if (data.issues.length === 0) {
|
|
2893
3043
|
lines.push("- (none)");
|
|
2894
3044
|
} else {
|
|
@@ -2906,7 +3056,7 @@ function formatReportJson(data) {
|
|
|
2906
3056
|
return JSON.stringify(data, null, 2);
|
|
2907
3057
|
}
|
|
2908
3058
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2909
|
-
const
|
|
3059
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2910
3060
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2911
3061
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2912
3062
|
for (const contractId of contractIdList) {
|
|
@@ -2915,24 +3065,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2915
3065
|
for (const file of specFiles) {
|
|
2916
3066
|
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2917
3067
|
const parsed = parseSpec(text, file);
|
|
2918
|
-
const specKey = parsed.specId
|
|
3068
|
+
const specKey = parsed.specId;
|
|
3069
|
+
if (!specKey) {
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
2919
3072
|
const refs = parsed.contractRefs;
|
|
2920
3073
|
if (refs.lines.length === 0) {
|
|
2921
3074
|
missingRefSpecs.add(specKey);
|
|
3075
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2922
3076
|
continue;
|
|
2923
3077
|
}
|
|
2924
|
-
const
|
|
3078
|
+
const current = specToContracts.get(specKey) ?? {
|
|
3079
|
+
status: "declared",
|
|
3080
|
+
ids: /* @__PURE__ */ new Set()
|
|
3081
|
+
};
|
|
2925
3082
|
for (const id of refs.ids) {
|
|
2926
|
-
|
|
3083
|
+
current.ids.add(id);
|
|
2927
3084
|
const specs = idToSpecs.get(id);
|
|
2928
3085
|
if (specs) {
|
|
2929
3086
|
specs.add(specKey);
|
|
2930
3087
|
}
|
|
2931
3088
|
}
|
|
2932
|
-
|
|
3089
|
+
specToContracts.set(specKey, current);
|
|
2933
3090
|
}
|
|
2934
3091
|
return {
|
|
2935
|
-
|
|
3092
|
+
specToContracts,
|
|
2936
3093
|
idToSpecs,
|
|
2937
3094
|
missingRefSpecs
|
|
2938
3095
|
};
|
|
@@ -3009,6 +3166,20 @@ function formatList(values) {
|
|
|
3009
3166
|
}
|
|
3010
3167
|
return values.join(", ");
|
|
3011
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
|
+
}
|
|
3012
3183
|
function toSortedArray2(values) {
|
|
3013
3184
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
3014
3185
|
}
|
|
@@ -3019,6 +3190,27 @@ function mapToSortedRecord(values) {
|
|
|
3019
3190
|
}
|
|
3020
3191
|
return record2;
|
|
3021
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
|
+
}
|
|
3022
3214
|
function buildHotspots(issues) {
|
|
3023
3215
|
const map = /* @__PURE__ */ new Map();
|
|
3024
3216
|
for (const issue7 of issues) {
|
|
@@ -3043,38 +3235,53 @@ function buildHotspots(issues) {
|
|
|
3043
3235
|
|
|
3044
3236
|
// src/cli/commands/report.ts
|
|
3045
3237
|
async function runReport(options) {
|
|
3046
|
-
const root =
|
|
3238
|
+
const root = import_node_path16.default.resolve(options.root);
|
|
3047
3239
|
const configResult = await loadConfig(root);
|
|
3048
|
-
const input = configResult.config.output.validateJsonPath;
|
|
3049
|
-
const inputPath = import_node_path15.default.isAbsolute(input) ? input : import_node_path15.default.resolve(root, input);
|
|
3050
3240
|
let validation;
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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;
|
|
3068
3276
|
}
|
|
3069
|
-
throw err;
|
|
3070
3277
|
}
|
|
3071
3278
|
const data = await createReportData(root, validation, configResult);
|
|
3072
3279
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3073
3280
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3074
|
-
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");
|
|
3075
3282
|
const out = options.outPath ?? defaultOut;
|
|
3076
|
-
const outPath =
|
|
3077
|
-
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 });
|
|
3078
3285
|
await (0, import_promises16.writeFile)(outPath, `${output}
|
|
3079
3286
|
`, "utf-8");
|
|
3080
3287
|
info(
|
|
@@ -3138,10 +3345,16 @@ function isMissingFileError5(error2) {
|
|
|
3138
3345
|
const record2 = error2;
|
|
3139
3346
|
return record2.code === "ENOENT";
|
|
3140
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
|
+
}
|
|
3141
3354
|
|
|
3142
3355
|
// src/cli/commands/validate.ts
|
|
3143
3356
|
var import_promises17 = require("fs/promises");
|
|
3144
|
-
var
|
|
3357
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
3145
3358
|
|
|
3146
3359
|
// src/cli/lib/failOn.ts
|
|
3147
3360
|
function shouldFail(result, failOn) {
|
|
@@ -3156,19 +3369,24 @@ function shouldFail(result, failOn) {
|
|
|
3156
3369
|
|
|
3157
3370
|
// src/cli/commands/validate.ts
|
|
3158
3371
|
async function runValidate(options) {
|
|
3159
|
-
const root =
|
|
3372
|
+
const root = import_node_path17.default.resolve(options.root);
|
|
3160
3373
|
const configResult = await loadConfig(root);
|
|
3161
3374
|
const result = await validateProject(root, configResult);
|
|
3375
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3162
3376
|
const format = options.format ?? "text";
|
|
3163
3377
|
if (format === "text") {
|
|
3164
|
-
emitText(
|
|
3378
|
+
emitText(normalized);
|
|
3165
3379
|
}
|
|
3166
3380
|
if (format === "github") {
|
|
3167
|
-
|
|
3381
|
+
const jsonPath = resolveJsonPath(
|
|
3382
|
+
root,
|
|
3383
|
+
configResult.config.output.validateJsonPath
|
|
3384
|
+
);
|
|
3385
|
+
emitGitHubOutput(normalized, root, jsonPath);
|
|
3168
3386
|
}
|
|
3169
|
-
await emitJson(
|
|
3387
|
+
await emitJson(normalized, root, configResult.config.output.validateJsonPath);
|
|
3170
3388
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
3171
|
-
return shouldFail(
|
|
3389
|
+
return shouldFail(normalized, failOn) ? 1 : 0;
|
|
3172
3390
|
}
|
|
3173
3391
|
function resolveFailOn(options, fallback) {
|
|
3174
3392
|
if (options.failOn) {
|
|
@@ -3193,6 +3411,22 @@ function emitText(result) {
|
|
|
3193
3411
|
`
|
|
3194
3412
|
);
|
|
3195
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
|
+
}
|
|
3196
3430
|
function emitGitHub(issue7) {
|
|
3197
3431
|
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
3198
3432
|
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
@@ -3204,22 +3438,74 @@ function emitGitHub(issue7) {
|
|
|
3204
3438
|
`
|
|
3205
3439
|
);
|
|
3206
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
|
+
}
|
|
3207
3487
|
async function emitJson(result, root, jsonPath) {
|
|
3208
|
-
const abs =
|
|
3209
|
-
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 });
|
|
3210
3490
|
await (0, import_promises17.writeFile)(abs, `${JSON.stringify(result, null, 2)}
|
|
3211
3491
|
`, "utf-8");
|
|
3212
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;
|
|
3213
3497
|
|
|
3214
3498
|
// src/cli/lib/args.ts
|
|
3215
3499
|
function parseArgs(argv, cwd) {
|
|
3216
3500
|
const options = {
|
|
3217
3501
|
root: cwd,
|
|
3502
|
+
rootExplicit: false,
|
|
3218
3503
|
dir: cwd,
|
|
3219
3504
|
force: false,
|
|
3220
3505
|
yes: false,
|
|
3221
3506
|
dryRun: false,
|
|
3222
3507
|
reportFormat: "md",
|
|
3508
|
+
reportRunValidate: false,
|
|
3223
3509
|
validateFormat: "text",
|
|
3224
3510
|
strict: false,
|
|
3225
3511
|
help: false
|
|
@@ -3235,6 +3521,7 @@ function parseArgs(argv, cwd) {
|
|
|
3235
3521
|
switch (arg) {
|
|
3236
3522
|
case "--root":
|
|
3237
3523
|
options.root = args[i + 1] ?? options.root;
|
|
3524
|
+
options.rootExplicit = true;
|
|
3238
3525
|
i += 1;
|
|
3239
3526
|
break;
|
|
3240
3527
|
case "--dir":
|
|
@@ -3276,6 +3563,18 @@ function parseArgs(argv, cwd) {
|
|
|
3276
3563
|
}
|
|
3277
3564
|
i += 1;
|
|
3278
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;
|
|
3279
3578
|
case "--help":
|
|
3280
3579
|
case "-h":
|
|
3281
3580
|
options.help = true;
|
|
@@ -3327,19 +3626,27 @@ async function run(argv, cwd) {
|
|
|
3327
3626
|
});
|
|
3328
3627
|
return;
|
|
3329
3628
|
case "validate":
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
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
|
+
}
|
|
3336
3638
|
return;
|
|
3337
3639
|
case "report":
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
+
}
|
|
3343
3650
|
return;
|
|
3344
3651
|
default:
|
|
3345
3652
|
error(`Unknown command: ${command}`);
|
|
@@ -3366,9 +3673,23 @@ Options:
|
|
|
3366
3673
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3367
3674
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3368
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
|
|
3369
3678
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
3370
3679
|
`;
|
|
3371
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
|
+
}
|
|
3372
3693
|
|
|
3373
3694
|
// src/cli/index.ts
|
|
3374
3695
|
run(process.argv.slice(2), process.cwd()).catch((err) => {
|