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.mjs
CHANGED
|
@@ -116,6 +116,10 @@ function info(message) {
|
|
|
116
116
|
process.stdout.write(`${message}
|
|
117
117
|
`);
|
|
118
118
|
}
|
|
119
|
+
function warn(message) {
|
|
120
|
+
process.stdout.write(`${message}
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
119
123
|
function error(message) {
|
|
120
124
|
process.stderr.write(`${message}
|
|
121
125
|
`);
|
|
@@ -155,10 +159,10 @@ function report(copied, skipped, dryRun, label) {
|
|
|
155
159
|
|
|
156
160
|
// src/cli/commands/report.ts
|
|
157
161
|
import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
|
|
158
|
-
import
|
|
162
|
+
import path16 from "path";
|
|
159
163
|
|
|
160
164
|
// src/core/config.ts
|
|
161
|
-
import { readFile } from "fs/promises";
|
|
165
|
+
import { access as access2, readFile } from "fs/promises";
|
|
162
166
|
import path4 from "path";
|
|
163
167
|
import { parse as parseYaml } from "yaml";
|
|
164
168
|
var defaultConfig = {
|
|
@@ -190,7 +194,7 @@ var defaultConfig = {
|
|
|
190
194
|
testFileGlobs: [],
|
|
191
195
|
testFileExcludeGlobs: [],
|
|
192
196
|
scNoTestSeverity: "error",
|
|
193
|
-
|
|
197
|
+
orphanContractsPolicy: "error",
|
|
194
198
|
unknownContractIdSeverity: "error"
|
|
195
199
|
}
|
|
196
200
|
},
|
|
@@ -201,6 +205,26 @@ var defaultConfig = {
|
|
|
201
205
|
function getConfigPath(root) {
|
|
202
206
|
return path4.join(root, "qfai.config.yaml");
|
|
203
207
|
}
|
|
208
|
+
async function findConfigRoot(startDir) {
|
|
209
|
+
const resolvedStart = path4.resolve(startDir);
|
|
210
|
+
let current = resolvedStart;
|
|
211
|
+
while (true) {
|
|
212
|
+
const configPath = getConfigPath(current);
|
|
213
|
+
if (await exists2(configPath)) {
|
|
214
|
+
return { root: current, configPath, found: true };
|
|
215
|
+
}
|
|
216
|
+
const parent = path4.dirname(current);
|
|
217
|
+
if (parent === current) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
current = parent;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
root: resolvedStart,
|
|
224
|
+
configPath: getConfigPath(resolvedStart),
|
|
225
|
+
found: false
|
|
226
|
+
};
|
|
227
|
+
}
|
|
204
228
|
async function loadConfig(root) {
|
|
205
229
|
const configPath = getConfigPath(root);
|
|
206
230
|
const issues = [];
|
|
@@ -390,10 +414,10 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
390
414
|
configPath,
|
|
391
415
|
issues
|
|
392
416
|
),
|
|
393
|
-
|
|
394
|
-
traceabilityRaw?.
|
|
395
|
-
base.traceability.
|
|
396
|
-
"validation.traceability.
|
|
417
|
+
orphanContractsPolicy: readOrphanContractsPolicy(
|
|
418
|
+
traceabilityRaw?.orphanContractsPolicy,
|
|
419
|
+
base.traceability.orphanContractsPolicy,
|
|
420
|
+
"validation.traceability.orphanContractsPolicy",
|
|
397
421
|
configPath,
|
|
398
422
|
issues
|
|
399
423
|
),
|
|
@@ -489,6 +513,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
489
513
|
}
|
|
490
514
|
return fallback;
|
|
491
515
|
}
|
|
516
|
+
function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
|
|
517
|
+
if (value === "error" || value === "warning" || value === "allow") {
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
if (value !== void 0) {
|
|
521
|
+
issues.push(
|
|
522
|
+
configIssue(
|
|
523
|
+
configPath,
|
|
524
|
+
`${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
525
|
+
)
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return fallback;
|
|
529
|
+
}
|
|
492
530
|
function configIssue(file, message) {
|
|
493
531
|
return {
|
|
494
532
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -504,6 +542,14 @@ function isMissingFile(error2) {
|
|
|
504
542
|
}
|
|
505
543
|
return false;
|
|
506
544
|
}
|
|
545
|
+
async function exists2(target) {
|
|
546
|
+
try {
|
|
547
|
+
await access2(target);
|
|
548
|
+
return true;
|
|
549
|
+
} catch {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
507
553
|
function formatError(error2) {
|
|
508
554
|
if (error2 instanceof Error) {
|
|
509
555
|
return error2.message;
|
|
@@ -514,20 +560,76 @@ function isRecord(value) {
|
|
|
514
560
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
515
561
|
}
|
|
516
562
|
|
|
563
|
+
// src/core/paths.ts
|
|
564
|
+
import path5 from "path";
|
|
565
|
+
function toRelativePath(root, target) {
|
|
566
|
+
if (!target) {
|
|
567
|
+
return target;
|
|
568
|
+
}
|
|
569
|
+
if (!path5.isAbsolute(target)) {
|
|
570
|
+
return toPosixPath(target);
|
|
571
|
+
}
|
|
572
|
+
const relative = path5.relative(root, target);
|
|
573
|
+
if (!relative) {
|
|
574
|
+
return ".";
|
|
575
|
+
}
|
|
576
|
+
return toPosixPath(relative);
|
|
577
|
+
}
|
|
578
|
+
function toPosixPath(value) {
|
|
579
|
+
return value.replace(/\\/g, "/");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/core/normalize.ts
|
|
583
|
+
function normalizeIssuePaths(root, issues) {
|
|
584
|
+
return issues.map((issue7) => {
|
|
585
|
+
if (!issue7.file) {
|
|
586
|
+
return issue7;
|
|
587
|
+
}
|
|
588
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
589
|
+
if (normalized === issue7.file) {
|
|
590
|
+
return issue7;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
...issue7,
|
|
594
|
+
file: normalized
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function normalizeScCoverage(root, sc) {
|
|
599
|
+
const refs = {};
|
|
600
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
601
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
...sc,
|
|
605
|
+
refs
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function normalizeValidationResult(root, result) {
|
|
609
|
+
return {
|
|
610
|
+
...result,
|
|
611
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
612
|
+
traceability: {
|
|
613
|
+
...result.traceability,
|
|
614
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
517
619
|
// src/core/report.ts
|
|
518
620
|
import { readFile as readFile11 } from "fs/promises";
|
|
519
|
-
import
|
|
621
|
+
import path15 from "path";
|
|
520
622
|
|
|
521
623
|
// src/core/contractIndex.ts
|
|
522
624
|
import { readFile as readFile2 } from "fs/promises";
|
|
523
|
-
import
|
|
625
|
+
import path8 from "path";
|
|
524
626
|
|
|
525
627
|
// src/core/discovery.ts
|
|
526
|
-
import { access as
|
|
628
|
+
import { access as access4 } from "fs/promises";
|
|
527
629
|
|
|
528
630
|
// src/core/fs.ts
|
|
529
|
-
import { access as
|
|
530
|
-
import
|
|
631
|
+
import { access as access3, readdir as readdir2 } from "fs/promises";
|
|
632
|
+
import path6 from "path";
|
|
531
633
|
import fg from "fast-glob";
|
|
532
634
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
533
635
|
"node_modules",
|
|
@@ -539,7 +641,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
539
641
|
]);
|
|
540
642
|
async function collectFiles(root, options = {}) {
|
|
541
643
|
const entries = [];
|
|
542
|
-
if (!await
|
|
644
|
+
if (!await exists3(root)) {
|
|
543
645
|
return entries;
|
|
544
646
|
}
|
|
545
647
|
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
@@ -565,7 +667,7 @@ async function collectFilesByGlobs(root, options) {
|
|
|
565
667
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
566
668
|
const items = await readdir2(current, { withFileTypes: true });
|
|
567
669
|
for (const item of items) {
|
|
568
|
-
const fullPath =
|
|
670
|
+
const fullPath = path6.join(current, item.name);
|
|
569
671
|
if (item.isDirectory()) {
|
|
570
672
|
if (ignoreDirs.has(item.name)) {
|
|
571
673
|
continue;
|
|
@@ -575,7 +677,7 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
575
677
|
}
|
|
576
678
|
if (item.isFile()) {
|
|
577
679
|
if (extensions.length > 0) {
|
|
578
|
-
const ext =
|
|
680
|
+
const ext = path6.extname(item.name).toLowerCase();
|
|
579
681
|
if (!extensions.includes(ext)) {
|
|
580
682
|
continue;
|
|
581
683
|
}
|
|
@@ -584,9 +686,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
584
686
|
}
|
|
585
687
|
}
|
|
586
688
|
}
|
|
587
|
-
async function
|
|
689
|
+
async function exists3(target) {
|
|
588
690
|
try {
|
|
589
|
-
await
|
|
691
|
+
await access3(target);
|
|
590
692
|
return true;
|
|
591
693
|
} catch {
|
|
592
694
|
return false;
|
|
@@ -595,22 +697,22 @@ async function exists2(target) {
|
|
|
595
697
|
|
|
596
698
|
// src/core/specLayout.ts
|
|
597
699
|
import { readdir as readdir3 } from "fs/promises";
|
|
598
|
-
import
|
|
700
|
+
import path7 from "path";
|
|
599
701
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
600
702
|
async function collectSpecEntries(specsRoot) {
|
|
601
703
|
const dirs = await listSpecDirs(specsRoot);
|
|
602
704
|
const entries = dirs.map((dir) => ({
|
|
603
705
|
dir,
|
|
604
|
-
specPath:
|
|
605
|
-
deltaPath:
|
|
606
|
-
scenarioPath:
|
|
706
|
+
specPath: path7.join(dir, "spec.md"),
|
|
707
|
+
deltaPath: path7.join(dir, "delta.md"),
|
|
708
|
+
scenarioPath: path7.join(dir, "scenario.md")
|
|
607
709
|
}));
|
|
608
710
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
609
711
|
}
|
|
610
712
|
async function listSpecDirs(specsRoot) {
|
|
611
713
|
try {
|
|
612
714
|
const items = await readdir3(specsRoot, { withFileTypes: true });
|
|
613
|
-
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) =>
|
|
715
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path7.join(specsRoot, name));
|
|
614
716
|
} catch (error2) {
|
|
615
717
|
if (isMissingFileError(error2)) {
|
|
616
718
|
return [];
|
|
@@ -658,15 +760,15 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
|
658
760
|
async function filterExisting(files) {
|
|
659
761
|
const existing = [];
|
|
660
762
|
for (const file of files) {
|
|
661
|
-
if (await
|
|
763
|
+
if (await exists4(file)) {
|
|
662
764
|
existing.push(file);
|
|
663
765
|
}
|
|
664
766
|
}
|
|
665
767
|
return existing;
|
|
666
768
|
}
|
|
667
|
-
async function
|
|
769
|
+
async function exists4(target) {
|
|
668
770
|
try {
|
|
669
|
-
await
|
|
771
|
+
await access4(target);
|
|
670
772
|
return true;
|
|
671
773
|
} catch {
|
|
672
774
|
return false;
|
|
@@ -693,9 +795,9 @@ function stripContractDeclarationLines(text) {
|
|
|
693
795
|
// src/core/contractIndex.ts
|
|
694
796
|
async function buildContractIndex(root, config) {
|
|
695
797
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
696
|
-
const uiRoot =
|
|
697
|
-
const apiRoot =
|
|
698
|
-
const dbRoot =
|
|
798
|
+
const uiRoot = path8.join(contractsRoot, "ui");
|
|
799
|
+
const apiRoot = path8.join(contractsRoot, "api");
|
|
800
|
+
const dbRoot = path8.join(contractsRoot, "db");
|
|
699
801
|
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
700
802
|
collectUiContractFiles(uiRoot),
|
|
701
803
|
collectApiContractFiles(apiRoot),
|
|
@@ -777,6 +879,57 @@ function isValidId(value, prefix) {
|
|
|
777
879
|
return strict.test(value);
|
|
778
880
|
}
|
|
779
881
|
|
|
882
|
+
// src/core/parse/contractRefs.ts
|
|
883
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
884
|
+
function parseContractRefs(text, options = {}) {
|
|
885
|
+
const linePattern = buildLinePattern(options);
|
|
886
|
+
const lines = [];
|
|
887
|
+
for (const match of text.matchAll(linePattern)) {
|
|
888
|
+
lines.push((match[1] ?? "").trim());
|
|
889
|
+
}
|
|
890
|
+
const ids = [];
|
|
891
|
+
const invalidTokens = [];
|
|
892
|
+
let hasNone = false;
|
|
893
|
+
for (const line of lines) {
|
|
894
|
+
if (line.length === 0) {
|
|
895
|
+
invalidTokens.push("(empty)");
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
899
|
+
for (const token of tokens) {
|
|
900
|
+
if (token.length === 0) {
|
|
901
|
+
invalidTokens.push("(empty)");
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (token === "none") {
|
|
905
|
+
hasNone = true;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
909
|
+
ids.push(token);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
invalidTokens.push(token);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
lines,
|
|
917
|
+
ids: unique2(ids),
|
|
918
|
+
invalidTokens: unique2(invalidTokens),
|
|
919
|
+
hasNone
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function buildLinePattern(options) {
|
|
923
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
924
|
+
return new RegExp(
|
|
925
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
926
|
+
"gm"
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
function unique2(values) {
|
|
930
|
+
return Array.from(new Set(values));
|
|
931
|
+
}
|
|
932
|
+
|
|
780
933
|
// src/core/parse/markdown.ts
|
|
781
934
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
782
935
|
function parseHeadings(md) {
|
|
@@ -823,8 +976,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
|
823
976
|
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
824
977
|
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
825
978
|
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
826
|
-
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
827
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
828
979
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
829
980
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
830
981
|
function parseSpec(md, file) {
|
|
@@ -897,50 +1048,10 @@ function parseSpec(md, file) {
|
|
|
897
1048
|
}
|
|
898
1049
|
return parsed;
|
|
899
1050
|
}
|
|
900
|
-
function parseContractRefs(md) {
|
|
901
|
-
const lines = [];
|
|
902
|
-
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
903
|
-
lines.push((match[1] ?? "").trim());
|
|
904
|
-
}
|
|
905
|
-
const ids = [];
|
|
906
|
-
const invalidTokens = [];
|
|
907
|
-
let hasNone = false;
|
|
908
|
-
for (const line of lines) {
|
|
909
|
-
if (line.length === 0) {
|
|
910
|
-
invalidTokens.push("(empty)");
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
914
|
-
for (const token of tokens) {
|
|
915
|
-
if (token.length === 0) {
|
|
916
|
-
invalidTokens.push("(empty)");
|
|
917
|
-
continue;
|
|
918
|
-
}
|
|
919
|
-
if (token === "none") {
|
|
920
|
-
hasNone = true;
|
|
921
|
-
continue;
|
|
922
|
-
}
|
|
923
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
924
|
-
ids.push(token);
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
invalidTokens.push(token);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return {
|
|
931
|
-
lines,
|
|
932
|
-
ids: unique2(ids),
|
|
933
|
-
invalidTokens: unique2(invalidTokens),
|
|
934
|
-
hasNone
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
function unique2(values) {
|
|
938
|
-
return Array.from(new Set(values));
|
|
939
|
-
}
|
|
940
1051
|
|
|
941
1052
|
// src/core/traceability.ts
|
|
942
1053
|
import { readFile as readFile3 } from "fs/promises";
|
|
943
|
-
import
|
|
1054
|
+
import path9 from "path";
|
|
944
1055
|
|
|
945
1056
|
// src/core/gherkin/parse.ts
|
|
946
1057
|
import {
|
|
@@ -975,9 +1086,6 @@ function formatError2(error2) {
|
|
|
975
1086
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
976
1087
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
977
1088
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
978
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
979
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
980
|
-
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
981
1089
|
function parseScenarioDocument(text, uri) {
|
|
982
1090
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
983
1091
|
if (!gherkinDocument) {
|
|
@@ -1002,31 +1110,21 @@ function parseScenarioDocument(text, uri) {
|
|
|
1002
1110
|
errors
|
|
1003
1111
|
};
|
|
1004
1112
|
}
|
|
1005
|
-
function buildScenarioAtoms(document) {
|
|
1113
|
+
function buildScenarioAtoms(document, contractIds = []) {
|
|
1114
|
+
const uniqueContractIds = unique3(contractIds).sort(
|
|
1115
|
+
(a, b) => a.localeCompare(b)
|
|
1116
|
+
);
|
|
1006
1117
|
return document.scenarios.map((scenario) => {
|
|
1007
1118
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1008
1119
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1009
1120
|
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1010
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1011
|
-
scenario.tags.forEach((tag) => {
|
|
1012
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
1013
|
-
contractIds.add(tag);
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
for (const step of scenario.steps) {
|
|
1017
|
-
for (const text of collectStepTexts(step)) {
|
|
1018
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1019
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1020
|
-
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
1121
|
const atom = {
|
|
1024
1122
|
uri: document.uri,
|
|
1025
1123
|
featureName: document.featureName ?? "",
|
|
1026
1124
|
scenarioName: scenario.name,
|
|
1027
1125
|
kind: scenario.kind,
|
|
1028
1126
|
brIds,
|
|
1029
|
-
contractIds:
|
|
1127
|
+
contractIds: uniqueContractIds
|
|
1030
1128
|
};
|
|
1031
1129
|
if (scenario.line !== void 0) {
|
|
1032
1130
|
atom.line = scenario.line;
|
|
@@ -1079,23 +1177,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
1079
1177
|
function collectTagNames(tags) {
|
|
1080
1178
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1081
1179
|
}
|
|
1082
|
-
function collectStepTexts(step) {
|
|
1083
|
-
const texts = [];
|
|
1084
|
-
if (step.text) {
|
|
1085
|
-
texts.push(step.text);
|
|
1086
|
-
}
|
|
1087
|
-
if (step.docString?.content) {
|
|
1088
|
-
texts.push(step.docString.content);
|
|
1089
|
-
}
|
|
1090
|
-
if (step.dataTable?.rows) {
|
|
1091
|
-
for (const row of step.dataTable.rows) {
|
|
1092
|
-
for (const cell of row.cells) {
|
|
1093
|
-
texts.push(cell.value);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return texts;
|
|
1098
|
-
}
|
|
1099
1180
|
function unique3(values) {
|
|
1100
1181
|
return Array.from(new Set(values));
|
|
1101
1182
|
}
|
|
@@ -1197,7 +1278,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1197
1278
|
};
|
|
1198
1279
|
}
|
|
1199
1280
|
const normalizedFiles = Array.from(
|
|
1200
|
-
new Set(files.map((file) =>
|
|
1281
|
+
new Set(files.map((file) => path9.normalize(file)))
|
|
1201
1282
|
);
|
|
1202
1283
|
for (const file of normalizedFiles) {
|
|
1203
1284
|
const text = await readFile3(file, "utf-8");
|
|
@@ -1258,11 +1339,11 @@ function formatError3(error2) {
|
|
|
1258
1339
|
|
|
1259
1340
|
// src/core/version.ts
|
|
1260
1341
|
import { readFile as readFile4 } from "fs/promises";
|
|
1261
|
-
import
|
|
1342
|
+
import path10 from "path";
|
|
1262
1343
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1263
1344
|
async function resolveToolVersion() {
|
|
1264
|
-
if ("0.5.
|
|
1265
|
-
return "0.5.
|
|
1345
|
+
if ("0.5.2".length > 0) {
|
|
1346
|
+
return "0.5.2";
|
|
1266
1347
|
}
|
|
1267
1348
|
try {
|
|
1268
1349
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1277,18 +1358,18 @@ async function resolveToolVersion() {
|
|
|
1277
1358
|
function resolvePackageJsonPath() {
|
|
1278
1359
|
const base = import.meta.url;
|
|
1279
1360
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
1280
|
-
return
|
|
1361
|
+
return path10.resolve(path10.dirname(basePath), "../../package.json");
|
|
1281
1362
|
}
|
|
1282
1363
|
|
|
1283
1364
|
// src/core/validators/contracts.ts
|
|
1284
1365
|
import { readFile as readFile5 } from "fs/promises";
|
|
1285
|
-
import
|
|
1366
|
+
import path12 from "path";
|
|
1286
1367
|
|
|
1287
1368
|
// src/core/contracts.ts
|
|
1288
|
-
import
|
|
1369
|
+
import path11 from "path";
|
|
1289
1370
|
import { parse as parseYaml2 } from "yaml";
|
|
1290
1371
|
function parseStructuredContract(file, text) {
|
|
1291
|
-
const ext =
|
|
1372
|
+
const ext = path11.extname(file).toLowerCase();
|
|
1292
1373
|
if (ext === ".json") {
|
|
1293
1374
|
return JSON.parse(text);
|
|
1294
1375
|
}
|
|
@@ -1308,9 +1389,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1308
1389
|
async function validateContracts(root, config) {
|
|
1309
1390
|
const issues = [];
|
|
1310
1391
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1311
|
-
issues.push(...await validateUiContracts(
|
|
1312
|
-
issues.push(...await validateApiContracts(
|
|
1313
|
-
issues.push(...await validateDbContracts(
|
|
1392
|
+
issues.push(...await validateUiContracts(path12.join(contractsRoot, "ui")));
|
|
1393
|
+
issues.push(...await validateApiContracts(path12.join(contractsRoot, "api")));
|
|
1394
|
+
issues.push(...await validateDbContracts(path12.join(contractsRoot, "db")));
|
|
1314
1395
|
const contractIndex = await buildContractIndex(root, config);
|
|
1315
1396
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1316
1397
|
return issues;
|
|
@@ -1593,7 +1674,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1593
1674
|
|
|
1594
1675
|
// src/core/validators/delta.ts
|
|
1595
1676
|
import { readFile as readFile6 } from "fs/promises";
|
|
1596
|
-
import
|
|
1677
|
+
import path13 from "path";
|
|
1597
1678
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1598
1679
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1599
1680
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1607,7 +1688,7 @@ async function validateDeltas(root, config) {
|
|
|
1607
1688
|
}
|
|
1608
1689
|
const issues = [];
|
|
1609
1690
|
for (const pack of packs) {
|
|
1610
|
-
const deltaPath =
|
|
1691
|
+
const deltaPath = path13.join(pack, "delta.md");
|
|
1611
1692
|
let text;
|
|
1612
1693
|
try {
|
|
1613
1694
|
text = await readFile6(deltaPath, "utf-8");
|
|
@@ -1683,7 +1764,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1683
1764
|
|
|
1684
1765
|
// src/core/validators/ids.ts
|
|
1685
1766
|
import { readFile as readFile7 } from "fs/promises";
|
|
1686
|
-
import
|
|
1767
|
+
import path14 from "path";
|
|
1687
1768
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1688
1769
|
async function validateDefinedIds(root, config) {
|
|
1689
1770
|
const issues = [];
|
|
@@ -1749,7 +1830,7 @@ function recordId(out, id, file) {
|
|
|
1749
1830
|
}
|
|
1750
1831
|
function formatFileList(files, root) {
|
|
1751
1832
|
return files.map((file) => {
|
|
1752
|
-
const relative =
|
|
1833
|
+
const relative = path14.relative(root, file);
|
|
1753
1834
|
return relative.length > 0 ? relative : file;
|
|
1754
1835
|
}).join(", ");
|
|
1755
1836
|
}
|
|
@@ -2186,7 +2267,7 @@ async function validateTraceability(root, config) {
|
|
|
2186
2267
|
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2187
2268
|
issues.push(
|
|
2188
2269
|
issue6(
|
|
2189
|
-
"QFAI-TRACE-
|
|
2270
|
+
"QFAI-TRACE-023",
|
|
2190
2271
|
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2191
2272
|
"error",
|
|
2192
2273
|
file,
|
|
@@ -2218,7 +2299,7 @@ async function validateTraceability(root, config) {
|
|
|
2218
2299
|
if (unknownContractIds.length > 0) {
|
|
2219
2300
|
issues.push(
|
|
2220
2301
|
issue6(
|
|
2221
|
-
"QFAI-TRACE-
|
|
2302
|
+
"QFAI-TRACE-024",
|
|
2222
2303
|
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2223
2304
|
", "
|
|
2224
2305
|
)}`,
|
|
@@ -2233,11 +2314,62 @@ async function validateTraceability(root, config) {
|
|
|
2233
2314
|
for (const file of scenarioFiles) {
|
|
2234
2315
|
const text = await readFile10(file, "utf-8");
|
|
2235
2316
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2317
|
+
const scenarioContractRefs = parseContractRefs(text, {
|
|
2318
|
+
allowCommentPrefix: true
|
|
2319
|
+
});
|
|
2320
|
+
if (scenarioContractRefs.lines.length === 0) {
|
|
2321
|
+
issues.push(
|
|
2322
|
+
issue6(
|
|
2323
|
+
"QFAI-TRACE-031",
|
|
2324
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2325
|
+
"error",
|
|
2326
|
+
file,
|
|
2327
|
+
"traceability.scenarioContractRefRequired"
|
|
2328
|
+
)
|
|
2329
|
+
);
|
|
2330
|
+
} else {
|
|
2331
|
+
if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
2332
|
+
issues.push(
|
|
2333
|
+
issue6(
|
|
2334
|
+
"QFAI-TRACE-033",
|
|
2335
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2336
|
+
"error",
|
|
2337
|
+
file,
|
|
2338
|
+
"traceability.scenarioContractRefFormat"
|
|
2339
|
+
)
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
if (scenarioContractRefs.invalidTokens.length > 0) {
|
|
2343
|
+
issues.push(
|
|
2344
|
+
issue6(
|
|
2345
|
+
"QFAI-TRACE-032",
|
|
2346
|
+
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2347
|
+
", "
|
|
2348
|
+
)}`,
|
|
2349
|
+
"error",
|
|
2350
|
+
file,
|
|
2351
|
+
"traceability.scenarioContractRefFormat",
|
|
2352
|
+
scenarioContractRefs.invalidTokens
|
|
2353
|
+
)
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2236
2357
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2237
2358
|
if (!document || errors.length > 0) {
|
|
2238
2359
|
continue;
|
|
2239
2360
|
}
|
|
2240
|
-
|
|
2361
|
+
if (document.scenarios.length !== 1) {
|
|
2362
|
+
issues.push(
|
|
2363
|
+
issue6(
|
|
2364
|
+
"QFAI-TRACE-030",
|
|
2365
|
+
`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})`,
|
|
2366
|
+
"error",
|
|
2367
|
+
file,
|
|
2368
|
+
"traceability.scenarioOnePerFile"
|
|
2369
|
+
)
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
|
|
2241
2373
|
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2242
2374
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2243
2375
|
const atom = atoms[index];
|
|
@@ -2382,7 +2514,7 @@ async function validateTraceability(root, config) {
|
|
|
2382
2514
|
if (orphanBrIds.length > 0) {
|
|
2383
2515
|
issues.push(
|
|
2384
2516
|
issue6(
|
|
2385
|
-
"
|
|
2517
|
+
"QFAI-TRACE-009",
|
|
2386
2518
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
2387
2519
|
"error",
|
|
2388
2520
|
specsRoot,
|
|
@@ -2452,17 +2584,19 @@ async function validateTraceability(root, config) {
|
|
|
2452
2584
|
);
|
|
2453
2585
|
}
|
|
2454
2586
|
}
|
|
2455
|
-
|
|
2587
|
+
const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
|
|
2588
|
+
if (orphanPolicy !== "allow") {
|
|
2456
2589
|
if (contractIds.size > 0) {
|
|
2457
2590
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2458
2591
|
(id) => !specContractIds.has(id)
|
|
2459
2592
|
);
|
|
2460
2593
|
if (orphanContracts.length > 0) {
|
|
2594
|
+
const severity = orphanPolicy === "warning" ? "warning" : "error";
|
|
2461
2595
|
issues.push(
|
|
2462
2596
|
issue6(
|
|
2463
2597
|
"QFAI-TRACE-022",
|
|
2464
2598
|
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2465
|
-
|
|
2599
|
+
severity,
|
|
2466
2600
|
specsRoot,
|
|
2467
2601
|
"traceability.contractCoverage",
|
|
2468
2602
|
orphanContracts
|
|
@@ -2587,16 +2721,17 @@ function countIssues(issues) {
|
|
|
2587
2721
|
// src/core/report.ts
|
|
2588
2722
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2589
2723
|
async function createReportData(root, validation, configResult) {
|
|
2590
|
-
const
|
|
2724
|
+
const resolvedRoot = path15.resolve(root);
|
|
2725
|
+
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2591
2726
|
const config = resolved.config;
|
|
2592
2727
|
const configPath = resolved.configPath;
|
|
2593
|
-
const specsRoot = resolvePath(
|
|
2594
|
-
const contractsRoot = resolvePath(
|
|
2595
|
-
const apiRoot =
|
|
2596
|
-
const uiRoot =
|
|
2597
|
-
const dbRoot =
|
|
2598
|
-
const srcRoot = resolvePath(
|
|
2599
|
-
const testsRoot = resolvePath(
|
|
2728
|
+
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2729
|
+
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2730
|
+
const apiRoot = path15.join(contractsRoot, "api");
|
|
2731
|
+
const uiRoot = path15.join(contractsRoot, "ui");
|
|
2732
|
+
const dbRoot = path15.join(contractsRoot, "db");
|
|
2733
|
+
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2734
|
+
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2600
2735
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
2601
2736
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2602
2737
|
const {
|
|
@@ -2604,15 +2739,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2604
2739
|
ui: uiFiles,
|
|
2605
2740
|
db: dbFiles
|
|
2606
2741
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2607
|
-
const contractIndex = await buildContractIndex(
|
|
2742
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2608
2743
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2609
2744
|
const specContractRefs = await collectSpecContractRefs(
|
|
2610
2745
|
specFiles,
|
|
2611
2746
|
contractIdList
|
|
2612
2747
|
);
|
|
2613
2748
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2614
|
-
for (const
|
|
2615
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2749
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2750
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2616
2751
|
}
|
|
2617
2752
|
const referencedContractCount = contractIdList.filter(
|
|
2618
2753
|
(id) => referencedContracts.has(id)
|
|
@@ -2621,8 +2756,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2621
2756
|
(id) => !referencedContracts.has(id)
|
|
2622
2757
|
).length;
|
|
2623
2758
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2624
|
-
const
|
|
2625
|
-
specContractRefs.
|
|
2759
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2760
|
+
specContractRefs.specToContracts
|
|
2626
2761
|
);
|
|
2627
2762
|
const idsByPrefix = await collectIds([
|
|
2628
2763
|
...specFiles,
|
|
@@ -2640,24 +2775,26 @@ async function createReportData(root, validation, configResult) {
|
|
|
2640
2775
|
srcRoot,
|
|
2641
2776
|
testsRoot
|
|
2642
2777
|
);
|
|
2643
|
-
const
|
|
2644
|
-
const
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2778
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2779
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2780
|
+
resolvedRoot,
|
|
2781
|
+
resolvedValidationRaw
|
|
2648
2782
|
);
|
|
2649
|
-
const scCoverage =
|
|
2650
|
-
const testFiles =
|
|
2783
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2784
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2651
2785
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2652
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2653
|
-
|
|
2786
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2787
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2788
|
+
);
|
|
2654
2789
|
const version = await resolveToolVersion();
|
|
2790
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2791
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2655
2792
|
return {
|
|
2656
2793
|
tool: "qfai",
|
|
2657
2794
|
version,
|
|
2658
2795
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2659
|
-
root,
|
|
2660
|
-
configPath,
|
|
2796
|
+
root: displayRoot,
|
|
2797
|
+
configPath: displayConfigPath,
|
|
2661
2798
|
summary: {
|
|
2662
2799
|
specs: specFiles.length,
|
|
2663
2800
|
scenarios: scenarioFiles.length,
|
|
@@ -2666,7 +2803,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2666
2803
|
ui: uiFiles.length,
|
|
2667
2804
|
db: dbFiles.length
|
|
2668
2805
|
},
|
|
2669
|
-
counts:
|
|
2806
|
+
counts: normalizedValidation.counts
|
|
2670
2807
|
},
|
|
2671
2808
|
ids: {
|
|
2672
2809
|
spec: idsByPrefix.SPEC,
|
|
@@ -2691,21 +2828,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2691
2828
|
specs: {
|
|
2692
2829
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2693
2830
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2694
|
-
|
|
2831
|
+
specToContracts: specToContractsRecord
|
|
2695
2832
|
}
|
|
2696
2833
|
},
|
|
2697
|
-
issues:
|
|
2834
|
+
issues: normalizedValidation.issues
|
|
2698
2835
|
};
|
|
2699
2836
|
}
|
|
2700
2837
|
function formatReportMarkdown(data) {
|
|
2701
2838
|
const lines = [];
|
|
2702
2839
|
lines.push("# QFAI Report");
|
|
2840
|
+
lines.push("");
|
|
2703
2841
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2704
2842
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2705
2843
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2706
2844
|
lines.push(`- \u7248: ${data.version}`);
|
|
2707
2845
|
lines.push("");
|
|
2708
2846
|
lines.push("## \u6982\u8981");
|
|
2847
|
+
lines.push("");
|
|
2709
2848
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2710
2849
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2711
2850
|
lines.push(
|
|
@@ -2716,6 +2855,7 @@ function formatReportMarkdown(data) {
|
|
|
2716
2855
|
);
|
|
2717
2856
|
lines.push("");
|
|
2718
2857
|
lines.push("## ID\u96C6\u8A08");
|
|
2858
|
+
lines.push("");
|
|
2719
2859
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2720
2860
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2721
2861
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2724,12 +2864,14 @@ function formatReportMarkdown(data) {
|
|
|
2724
2864
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2725
2865
|
lines.push("");
|
|
2726
2866
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2867
|
+
lines.push("");
|
|
2727
2868
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2728
2869
|
lines.push(
|
|
2729
2870
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2730
2871
|
);
|
|
2731
2872
|
lines.push("");
|
|
2732
2873
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2874
|
+
lines.push("");
|
|
2733
2875
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2734
2876
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2735
2877
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2738,6 +2880,7 @@ function formatReportMarkdown(data) {
|
|
|
2738
2880
|
);
|
|
2739
2881
|
lines.push("");
|
|
2740
2882
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2883
|
+
lines.push("");
|
|
2741
2884
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2742
2885
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2743
2886
|
(a, b) => a.localeCompare(b)
|
|
@@ -2756,24 +2899,25 @@ function formatReportMarkdown(data) {
|
|
|
2756
2899
|
}
|
|
2757
2900
|
lines.push("");
|
|
2758
2901
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2759
|
-
|
|
2902
|
+
lines.push("");
|
|
2903
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2760
2904
|
const specIds = Object.keys(specToContracts).sort(
|
|
2761
2905
|
(a, b) => a.localeCompare(b)
|
|
2762
2906
|
);
|
|
2763
2907
|
if (specIds.length === 0) {
|
|
2764
2908
|
lines.push("- (none)");
|
|
2765
2909
|
} else {
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
}
|
|
2910
|
+
const rows = specIds.map((specId) => {
|
|
2911
|
+
const entry = specToContracts[specId];
|
|
2912
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
2913
|
+
const status = entry?.status ?? "missing";
|
|
2914
|
+
return [specId, status, contracts];
|
|
2915
|
+
});
|
|
2916
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2774
2917
|
}
|
|
2775
2918
|
lines.push("");
|
|
2776
2919
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2920
|
+
lines.push("");
|
|
2777
2921
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2778
2922
|
if (missingRefSpecs.length === 0) {
|
|
2779
2923
|
lines.push("- (none)");
|
|
@@ -2784,6 +2928,7 @@ function formatReportMarkdown(data) {
|
|
|
2784
2928
|
}
|
|
2785
2929
|
lines.push("");
|
|
2786
2930
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2931
|
+
lines.push("");
|
|
2787
2932
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2788
2933
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2789
2934
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2813,6 +2958,7 @@ function formatReportMarkdown(data) {
|
|
|
2813
2958
|
}
|
|
2814
2959
|
lines.push("");
|
|
2815
2960
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2961
|
+
lines.push("");
|
|
2816
2962
|
const scRefs = data.traceability.sc.refs;
|
|
2817
2963
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2818
2964
|
if (scIds.length === 0) {
|
|
@@ -2829,6 +2975,7 @@ function formatReportMarkdown(data) {
|
|
|
2829
2975
|
}
|
|
2830
2976
|
lines.push("");
|
|
2831
2977
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2978
|
+
lines.push("");
|
|
2832
2979
|
const specScIssues = data.issues.filter(
|
|
2833
2980
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2834
2981
|
);
|
|
@@ -2843,6 +2990,7 @@ function formatReportMarkdown(data) {
|
|
|
2843
2990
|
}
|
|
2844
2991
|
lines.push("");
|
|
2845
2992
|
lines.push("## Hotspots");
|
|
2993
|
+
lines.push("");
|
|
2846
2994
|
const hotspots = buildHotspots(data.issues);
|
|
2847
2995
|
if (hotspots.length === 0) {
|
|
2848
2996
|
lines.push("- (none)");
|
|
@@ -2855,6 +3003,7 @@ function formatReportMarkdown(data) {
|
|
|
2855
3003
|
}
|
|
2856
3004
|
lines.push("");
|
|
2857
3005
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
3006
|
+
lines.push("");
|
|
2858
3007
|
const traceIssues = data.issues.filter(
|
|
2859
3008
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2860
3009
|
);
|
|
@@ -2870,6 +3019,7 @@ function formatReportMarkdown(data) {
|
|
|
2870
3019
|
}
|
|
2871
3020
|
lines.push("");
|
|
2872
3021
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
3022
|
+
lines.push("");
|
|
2873
3023
|
if (data.issues.length === 0) {
|
|
2874
3024
|
lines.push("- (none)");
|
|
2875
3025
|
} else {
|
|
@@ -2887,7 +3037,7 @@ function formatReportJson(data) {
|
|
|
2887
3037
|
return JSON.stringify(data, null, 2);
|
|
2888
3038
|
}
|
|
2889
3039
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2890
|
-
const
|
|
3040
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2891
3041
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2892
3042
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2893
3043
|
for (const contractId of contractIdList) {
|
|
@@ -2896,24 +3046,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2896
3046
|
for (const file of specFiles) {
|
|
2897
3047
|
const text = await readFile11(file, "utf-8");
|
|
2898
3048
|
const parsed = parseSpec(text, file);
|
|
2899
|
-
const specKey = parsed.specId
|
|
3049
|
+
const specKey = parsed.specId;
|
|
3050
|
+
if (!specKey) {
|
|
3051
|
+
continue;
|
|
3052
|
+
}
|
|
2900
3053
|
const refs = parsed.contractRefs;
|
|
2901
3054
|
if (refs.lines.length === 0) {
|
|
2902
3055
|
missingRefSpecs.add(specKey);
|
|
3056
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2903
3057
|
continue;
|
|
2904
3058
|
}
|
|
2905
|
-
const
|
|
3059
|
+
const current = specToContracts.get(specKey) ?? {
|
|
3060
|
+
status: "declared",
|
|
3061
|
+
ids: /* @__PURE__ */ new Set()
|
|
3062
|
+
};
|
|
2906
3063
|
for (const id of refs.ids) {
|
|
2907
|
-
|
|
3064
|
+
current.ids.add(id);
|
|
2908
3065
|
const specs = idToSpecs.get(id);
|
|
2909
3066
|
if (specs) {
|
|
2910
3067
|
specs.add(specKey);
|
|
2911
3068
|
}
|
|
2912
3069
|
}
|
|
2913
|
-
|
|
3070
|
+
specToContracts.set(specKey, current);
|
|
2914
3071
|
}
|
|
2915
3072
|
return {
|
|
2916
|
-
|
|
3073
|
+
specToContracts,
|
|
2917
3074
|
idToSpecs,
|
|
2918
3075
|
missingRefSpecs
|
|
2919
3076
|
};
|
|
@@ -2990,6 +3147,20 @@ function formatList(values) {
|
|
|
2990
3147
|
}
|
|
2991
3148
|
return values.join(", ");
|
|
2992
3149
|
}
|
|
3150
|
+
function formatMarkdownTable(headers, rows) {
|
|
3151
|
+
const widths = headers.map((header, index) => {
|
|
3152
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
3153
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
3154
|
+
});
|
|
3155
|
+
const formatRow = (cells) => {
|
|
3156
|
+
const padded = cells.map(
|
|
3157
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
3158
|
+
);
|
|
3159
|
+
return `| ${padded.join(" | ")} |`;
|
|
3160
|
+
};
|
|
3161
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
3162
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3163
|
+
}
|
|
2993
3164
|
function toSortedArray2(values) {
|
|
2994
3165
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2995
3166
|
}
|
|
@@ -3000,6 +3171,27 @@ function mapToSortedRecord(values) {
|
|
|
3000
3171
|
}
|
|
3001
3172
|
return record2;
|
|
3002
3173
|
}
|
|
3174
|
+
function mapToSpecContractRecord(values) {
|
|
3175
|
+
const record2 = {};
|
|
3176
|
+
for (const [key, entry] of values.entries()) {
|
|
3177
|
+
record2[key] = {
|
|
3178
|
+
status: entry.status,
|
|
3179
|
+
ids: toSortedArray2(entry.ids)
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
return record2;
|
|
3183
|
+
}
|
|
3184
|
+
function normalizeScSources(root, sources) {
|
|
3185
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3186
|
+
for (const [id, files] of sources.entries()) {
|
|
3187
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3188
|
+
for (const file of files) {
|
|
3189
|
+
mapped.add(toRelativePath(root, file));
|
|
3190
|
+
}
|
|
3191
|
+
normalized.set(id, mapped);
|
|
3192
|
+
}
|
|
3193
|
+
return normalized;
|
|
3194
|
+
}
|
|
3003
3195
|
function buildHotspots(issues) {
|
|
3004
3196
|
const map = /* @__PURE__ */ new Map();
|
|
3005
3197
|
for (const issue7 of issues) {
|
|
@@ -3024,38 +3216,53 @@ function buildHotspots(issues) {
|
|
|
3024
3216
|
|
|
3025
3217
|
// src/cli/commands/report.ts
|
|
3026
3218
|
async function runReport(options) {
|
|
3027
|
-
const root =
|
|
3219
|
+
const root = path16.resolve(options.root);
|
|
3028
3220
|
const configResult = await loadConfig(root);
|
|
3029
|
-
const input = configResult.config.output.validateJsonPath;
|
|
3030
|
-
const inputPath = path15.isAbsolute(input) ? input : path15.resolve(root, input);
|
|
3031
3221
|
let validation;
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3222
|
+
if (options.runValidate) {
|
|
3223
|
+
if (options.inputPath) {
|
|
3224
|
+
warn("report: --run-validate \u304C\u6307\u5B9A\u3055\u308C\u305F\u305F\u3081 --in \u306F\u7121\u8996\u3057\u307E\u3059\u3002");
|
|
3225
|
+
}
|
|
3226
|
+
const result = await validateProject(root, configResult);
|
|
3227
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3228
|
+
await writeValidationResult(
|
|
3229
|
+
root,
|
|
3230
|
+
configResult.config.output.validateJsonPath,
|
|
3231
|
+
normalized
|
|
3232
|
+
);
|
|
3233
|
+
validation = normalized;
|
|
3234
|
+
} else {
|
|
3235
|
+
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
3236
|
+
const inputPath = path16.isAbsolute(input) ? input : path16.resolve(root, input);
|
|
3237
|
+
try {
|
|
3238
|
+
validation = await readValidationResult(inputPath);
|
|
3239
|
+
} catch (err) {
|
|
3240
|
+
if (isMissingFileError5(err)) {
|
|
3241
|
+
error(
|
|
3242
|
+
[
|
|
3243
|
+
`qfai report: \u5165\u529B\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${inputPath}`,
|
|
3244
|
+
"",
|
|
3245
|
+
"\u307E\u305A qfai validate \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B:",
|
|
3246
|
+
" qfai validate",
|
|
3247
|
+
"\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306E\u51FA\u529B\u5148: .qfai/out/validate.json\uFF09",
|
|
3248
|
+
"",
|
|
3249
|
+
"\u307E\u305F\u306F report \u306B --run-validate \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3250
|
+
"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"
|
|
3251
|
+
].join("\n")
|
|
3252
|
+
);
|
|
3253
|
+
process.exitCode = 2;
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
throw err;
|
|
3049
3257
|
}
|
|
3050
|
-
throw err;
|
|
3051
3258
|
}
|
|
3052
3259
|
const data = await createReportData(root, validation, configResult);
|
|
3053
3260
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
3054
3261
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
3055
|
-
const defaultOut = options.format === "json" ?
|
|
3262
|
+
const defaultOut = options.format === "json" ? path16.join(outRoot, "report.json") : path16.join(outRoot, "report.md");
|
|
3056
3263
|
const out = options.outPath ?? defaultOut;
|
|
3057
|
-
const outPath =
|
|
3058
|
-
await mkdir2(
|
|
3264
|
+
const outPath = path16.isAbsolute(out) ? out : path16.resolve(root, out);
|
|
3265
|
+
await mkdir2(path16.dirname(outPath), { recursive: true });
|
|
3059
3266
|
await writeFile(outPath, `${output}
|
|
3060
3267
|
`, "utf-8");
|
|
3061
3268
|
info(
|
|
@@ -3119,10 +3326,16 @@ function isMissingFileError5(error2) {
|
|
|
3119
3326
|
const record2 = error2;
|
|
3120
3327
|
return record2.code === "ENOENT";
|
|
3121
3328
|
}
|
|
3329
|
+
async function writeValidationResult(root, outputPath, result) {
|
|
3330
|
+
const abs = path16.isAbsolute(outputPath) ? outputPath : path16.resolve(root, outputPath);
|
|
3331
|
+
await mkdir2(path16.dirname(abs), { recursive: true });
|
|
3332
|
+
await writeFile(abs, `${JSON.stringify(result, null, 2)}
|
|
3333
|
+
`, "utf-8");
|
|
3334
|
+
}
|
|
3122
3335
|
|
|
3123
3336
|
// src/cli/commands/validate.ts
|
|
3124
3337
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
3125
|
-
import
|
|
3338
|
+
import path17 from "path";
|
|
3126
3339
|
|
|
3127
3340
|
// src/cli/lib/failOn.ts
|
|
3128
3341
|
function shouldFail(result, failOn) {
|
|
@@ -3137,19 +3350,24 @@ function shouldFail(result, failOn) {
|
|
|
3137
3350
|
|
|
3138
3351
|
// src/cli/commands/validate.ts
|
|
3139
3352
|
async function runValidate(options) {
|
|
3140
|
-
const root =
|
|
3353
|
+
const root = path17.resolve(options.root);
|
|
3141
3354
|
const configResult = await loadConfig(root);
|
|
3142
3355
|
const result = await validateProject(root, configResult);
|
|
3356
|
+
const normalized = normalizeValidationResult(root, result);
|
|
3143
3357
|
const format = options.format ?? "text";
|
|
3144
3358
|
if (format === "text") {
|
|
3145
|
-
emitText(
|
|
3359
|
+
emitText(normalized);
|
|
3146
3360
|
}
|
|
3147
3361
|
if (format === "github") {
|
|
3148
|
-
|
|
3362
|
+
const jsonPath = resolveJsonPath(
|
|
3363
|
+
root,
|
|
3364
|
+
configResult.config.output.validateJsonPath
|
|
3365
|
+
);
|
|
3366
|
+
emitGitHubOutput(normalized, root, jsonPath);
|
|
3149
3367
|
}
|
|
3150
|
-
await emitJson(
|
|
3368
|
+
await emitJson(normalized, root, configResult.config.output.validateJsonPath);
|
|
3151
3369
|
const failOn = resolveFailOn(options, configResult.config.validation.failOn);
|
|
3152
|
-
return shouldFail(
|
|
3370
|
+
return shouldFail(normalized, failOn) ? 1 : 0;
|
|
3153
3371
|
}
|
|
3154
3372
|
function resolveFailOn(options, fallback) {
|
|
3155
3373
|
if (options.failOn) {
|
|
@@ -3174,6 +3392,22 @@ function emitText(result) {
|
|
|
3174
3392
|
`
|
|
3175
3393
|
);
|
|
3176
3394
|
}
|
|
3395
|
+
function emitGitHubOutput(result, root, jsonPath) {
|
|
3396
|
+
const deduped = dedupeIssues(result.issues);
|
|
3397
|
+
const omitted = Math.max(deduped.length - GITHUB_ANNOTATION_LIMIT, 0);
|
|
3398
|
+
const dropped = Math.max(result.issues.length - deduped.length, 0);
|
|
3399
|
+
emitGitHubSummary(result, {
|
|
3400
|
+
total: deduped.length,
|
|
3401
|
+
omitted,
|
|
3402
|
+
dropped,
|
|
3403
|
+
jsonPath,
|
|
3404
|
+
root
|
|
3405
|
+
});
|
|
3406
|
+
const issues = deduped.slice(0, GITHUB_ANNOTATION_LIMIT);
|
|
3407
|
+
for (const issue7 of issues) {
|
|
3408
|
+
emitGitHub(issue7);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3177
3411
|
function emitGitHub(issue7) {
|
|
3178
3412
|
const level = issue7.severity === "error" ? "error" : issue7.severity === "warning" ? "warning" : "notice";
|
|
3179
3413
|
const file = issue7.file ? `file=${issue7.file}` : "";
|
|
@@ -3185,22 +3419,74 @@ function emitGitHub(issue7) {
|
|
|
3185
3419
|
`
|
|
3186
3420
|
);
|
|
3187
3421
|
}
|
|
3422
|
+
function emitGitHubSummary(result, options) {
|
|
3423
|
+
const summary = [
|
|
3424
|
+
"qfai validate summary:",
|
|
3425
|
+
`error=${result.counts.error}`,
|
|
3426
|
+
`warning=${result.counts.warning}`,
|
|
3427
|
+
`info=${result.counts.info}`,
|
|
3428
|
+
`annotations=${Math.min(options.total, GITHUB_ANNOTATION_LIMIT)}/${options.total}`
|
|
3429
|
+
].join(" ");
|
|
3430
|
+
process.stdout.write(`${summary}
|
|
3431
|
+
`);
|
|
3432
|
+
if (options.dropped > 0 || options.omitted > 0) {
|
|
3433
|
+
const details = [
|
|
3434
|
+
"qfai validate note:",
|
|
3435
|
+
options.dropped > 0 ? `\u91CD\u8907\u9664\u5916=${options.dropped}` : null,
|
|
3436
|
+
options.omitted > 0 ? `\u4E0A\u9650\u7701\u7565=${options.omitted}` : null
|
|
3437
|
+
].filter(Boolean).join(" ");
|
|
3438
|
+
process.stdout.write(`${details}
|
|
3439
|
+
`);
|
|
3440
|
+
}
|
|
3441
|
+
const relative = toRelativePath(options.root, options.jsonPath);
|
|
3442
|
+
process.stdout.write(
|
|
3443
|
+
`qfai validate note: \u8A73\u7D30\u306F ${relative} \u307E\u305F\u306F --format text \u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
3444
|
+
`
|
|
3445
|
+
);
|
|
3446
|
+
}
|
|
3447
|
+
function dedupeIssues(issues) {
|
|
3448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3449
|
+
const deduped = [];
|
|
3450
|
+
for (const issue7 of issues) {
|
|
3451
|
+
const key = issueKey(issue7);
|
|
3452
|
+
if (seen.has(key)) {
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
seen.add(key);
|
|
3456
|
+
deduped.push(issue7);
|
|
3457
|
+
}
|
|
3458
|
+
return deduped;
|
|
3459
|
+
}
|
|
3460
|
+
function issueKey(issue7) {
|
|
3461
|
+
const file = issue7.file ?? "";
|
|
3462
|
+
const line = issue7.loc?.line ?? "";
|
|
3463
|
+
const column = issue7.loc?.column ?? "";
|
|
3464
|
+
return [issue7.code, issue7.severity, issue7.message, file, line, column].join(
|
|
3465
|
+
"|"
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3188
3468
|
async function emitJson(result, root, jsonPath) {
|
|
3189
|
-
const abs =
|
|
3190
|
-
await mkdir3(
|
|
3469
|
+
const abs = resolveJsonPath(root, jsonPath);
|
|
3470
|
+
await mkdir3(path17.dirname(abs), { recursive: true });
|
|
3191
3471
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
3192
3472
|
`, "utf-8");
|
|
3193
3473
|
}
|
|
3474
|
+
function resolveJsonPath(root, jsonPath) {
|
|
3475
|
+
return path17.isAbsolute(jsonPath) ? jsonPath : path17.resolve(root, jsonPath);
|
|
3476
|
+
}
|
|
3477
|
+
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
3194
3478
|
|
|
3195
3479
|
// src/cli/lib/args.ts
|
|
3196
3480
|
function parseArgs(argv, cwd) {
|
|
3197
3481
|
const options = {
|
|
3198
3482
|
root: cwd,
|
|
3483
|
+
rootExplicit: false,
|
|
3199
3484
|
dir: cwd,
|
|
3200
3485
|
force: false,
|
|
3201
3486
|
yes: false,
|
|
3202
3487
|
dryRun: false,
|
|
3203
3488
|
reportFormat: "md",
|
|
3489
|
+
reportRunValidate: false,
|
|
3204
3490
|
validateFormat: "text",
|
|
3205
3491
|
strict: false,
|
|
3206
3492
|
help: false
|
|
@@ -3216,6 +3502,7 @@ function parseArgs(argv, cwd) {
|
|
|
3216
3502
|
switch (arg) {
|
|
3217
3503
|
case "--root":
|
|
3218
3504
|
options.root = args[i + 1] ?? options.root;
|
|
3505
|
+
options.rootExplicit = true;
|
|
3219
3506
|
i += 1;
|
|
3220
3507
|
break;
|
|
3221
3508
|
case "--dir":
|
|
@@ -3257,6 +3544,18 @@ function parseArgs(argv, cwd) {
|
|
|
3257
3544
|
}
|
|
3258
3545
|
i += 1;
|
|
3259
3546
|
break;
|
|
3547
|
+
case "--in":
|
|
3548
|
+
{
|
|
3549
|
+
const next = args[i + 1];
|
|
3550
|
+
if (next) {
|
|
3551
|
+
options.reportIn = next;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
i += 1;
|
|
3555
|
+
break;
|
|
3556
|
+
case "--run-validate":
|
|
3557
|
+
options.reportRunValidate = true;
|
|
3558
|
+
break;
|
|
3260
3559
|
case "--help":
|
|
3261
3560
|
case "-h":
|
|
3262
3561
|
options.help = true;
|
|
@@ -3308,19 +3607,27 @@ async function run(argv, cwd) {
|
|
|
3308
3607
|
});
|
|
3309
3608
|
return;
|
|
3310
3609
|
case "validate":
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3610
|
+
{
|
|
3611
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3612
|
+
process.exitCode = await runValidate({
|
|
3613
|
+
root: resolvedRoot,
|
|
3614
|
+
strict: options.strict,
|
|
3615
|
+
format: options.validateFormat,
|
|
3616
|
+
...options.failOn !== void 0 ? { failOn: options.failOn } : {}
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3317
3619
|
return;
|
|
3318
3620
|
case "report":
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3621
|
+
{
|
|
3622
|
+
const resolvedRoot = await resolveRoot(options);
|
|
3623
|
+
await runReport({
|
|
3624
|
+
root: resolvedRoot,
|
|
3625
|
+
format: options.reportFormat,
|
|
3626
|
+
...options.reportOut !== void 0 ? { outPath: options.reportOut } : {},
|
|
3627
|
+
...options.reportIn !== void 0 ? { inputPath: options.reportIn } : {},
|
|
3628
|
+
...options.reportRunValidate ? { runValidate: true } : {}
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3324
3631
|
return;
|
|
3325
3632
|
default:
|
|
3326
3633
|
error(`Unknown command: ${command}`);
|
|
@@ -3347,9 +3654,23 @@ Options:
|
|
|
3347
3654
|
--strict validate: warning \u4EE5\u4E0A\u3067 exit 1
|
|
3348
3655
|
--fail-on <error|warning|never> validate: \u5931\u6557\u6761\u4EF6
|
|
3349
3656
|
--out <path> report: \u51FA\u529B\u5148
|
|
3657
|
+
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
3658
|
+
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
3350
3659
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
3351
3660
|
`;
|
|
3352
3661
|
}
|
|
3662
|
+
async function resolveRoot(options) {
|
|
3663
|
+
if (options.rootExplicit) {
|
|
3664
|
+
return options.root;
|
|
3665
|
+
}
|
|
3666
|
+
const search = await findConfigRoot(options.root);
|
|
3667
|
+
if (!search.found) {
|
|
3668
|
+
warn(
|
|
3669
|
+
`qfai: qfai.config.yaml \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081 defaultConfig \u3092\u4F7F\u7528\u3057\u307E\u3059 (root=${search.root})`
|
|
3670
|
+
);
|
|
3671
|
+
}
|
|
3672
|
+
return search.root;
|
|
3673
|
+
}
|
|
3353
3674
|
|
|
3354
3675
|
// src/cli/index.ts
|
|
3355
3676
|
run(process.argv.slice(2), process.cwd()).catch((err) => {
|