qfai 0.5.0 → 0.6.0
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 +6 -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 +1263 -704
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +1246 -687
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +343 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +347 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/core/config.ts
|
|
2
|
-
import { readFile } from "fs/promises";
|
|
2
|
+
import { access, readFile } from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
var defaultConfig = {
|
|
@@ -31,7 +31,7 @@ var defaultConfig = {
|
|
|
31
31
|
testFileGlobs: [],
|
|
32
32
|
testFileExcludeGlobs: [],
|
|
33
33
|
scNoTestSeverity: "error",
|
|
34
|
-
|
|
34
|
+
orphanContractsPolicy: "error",
|
|
35
35
|
unknownContractIdSeverity: "error"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
@@ -42,6 +42,26 @@ var defaultConfig = {
|
|
|
42
42
|
function getConfigPath(root) {
|
|
43
43
|
return path.join(root, "qfai.config.yaml");
|
|
44
44
|
}
|
|
45
|
+
async function findConfigRoot(startDir) {
|
|
46
|
+
const resolvedStart = path.resolve(startDir);
|
|
47
|
+
let current = resolvedStart;
|
|
48
|
+
while (true) {
|
|
49
|
+
const configPath = getConfigPath(current);
|
|
50
|
+
if (await exists(configPath)) {
|
|
51
|
+
return { root: current, configPath, found: true };
|
|
52
|
+
}
|
|
53
|
+
const parent = path.dirname(current);
|
|
54
|
+
if (parent === current) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
root: resolvedStart,
|
|
61
|
+
configPath: getConfigPath(resolvedStart),
|
|
62
|
+
found: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
45
65
|
async function loadConfig(root) {
|
|
46
66
|
const configPath = getConfigPath(root);
|
|
47
67
|
const issues = [];
|
|
@@ -231,10 +251,10 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
231
251
|
configPath,
|
|
232
252
|
issues
|
|
233
253
|
),
|
|
234
|
-
|
|
235
|
-
traceabilityRaw?.
|
|
236
|
-
base.traceability.
|
|
237
|
-
"validation.traceability.
|
|
254
|
+
orphanContractsPolicy: readOrphanContractsPolicy(
|
|
255
|
+
traceabilityRaw?.orphanContractsPolicy,
|
|
256
|
+
base.traceability.orphanContractsPolicy,
|
|
257
|
+
"validation.traceability.orphanContractsPolicy",
|
|
238
258
|
configPath,
|
|
239
259
|
issues
|
|
240
260
|
),
|
|
@@ -330,6 +350,20 @@ function readTraceabilitySeverity(value, fallback, label, configPath, issues) {
|
|
|
330
350
|
}
|
|
331
351
|
return fallback;
|
|
332
352
|
}
|
|
353
|
+
function readOrphanContractsPolicy(value, fallback, label, configPath, issues) {
|
|
354
|
+
if (value === "error" || value === "warning" || value === "allow") {
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
if (value !== void 0) {
|
|
358
|
+
issues.push(
|
|
359
|
+
configIssue(
|
|
360
|
+
configPath,
|
|
361
|
+
`${label} \u306F error|warning|allow \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return fallback;
|
|
366
|
+
}
|
|
333
367
|
function configIssue(file, message) {
|
|
334
368
|
return {
|
|
335
369
|
code: "QFAI_CONFIG_INVALID",
|
|
@@ -345,6 +379,14 @@ function isMissingFile(error) {
|
|
|
345
379
|
}
|
|
346
380
|
return false;
|
|
347
381
|
}
|
|
382
|
+
async function exists(target) {
|
|
383
|
+
try {
|
|
384
|
+
await access(target);
|
|
385
|
+
return true;
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
348
390
|
function formatError(error) {
|
|
349
391
|
if (error instanceof Error) {
|
|
350
392
|
return error.message;
|
|
@@ -410,17 +452,17 @@ function isValidId(value, prefix) {
|
|
|
410
452
|
|
|
411
453
|
// src/core/report.ts
|
|
412
454
|
import { readFile as readFile11 } from "fs/promises";
|
|
413
|
-
import
|
|
455
|
+
import path12 from "path";
|
|
414
456
|
|
|
415
457
|
// src/core/contractIndex.ts
|
|
416
458
|
import { readFile as readFile2 } from "fs/promises";
|
|
417
459
|
import path4 from "path";
|
|
418
460
|
|
|
419
461
|
// src/core/discovery.ts
|
|
420
|
-
import { access as
|
|
462
|
+
import { access as access3 } from "fs/promises";
|
|
421
463
|
|
|
422
464
|
// src/core/fs.ts
|
|
423
|
-
import { access, readdir } from "fs/promises";
|
|
465
|
+
import { access as access2, readdir } from "fs/promises";
|
|
424
466
|
import path2 from "path";
|
|
425
467
|
import fg from "fast-glob";
|
|
426
468
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -433,7 +475,7 @@ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
433
475
|
]);
|
|
434
476
|
async function collectFiles(root, options = {}) {
|
|
435
477
|
const entries = [];
|
|
436
|
-
if (!await
|
|
478
|
+
if (!await exists2(root)) {
|
|
437
479
|
return entries;
|
|
438
480
|
}
|
|
439
481
|
const ignoreDirs = /* @__PURE__ */ new Set([
|
|
@@ -478,9 +520,9 @@ async function walk(base, current, ignoreDirs, extensions, out) {
|
|
|
478
520
|
}
|
|
479
521
|
}
|
|
480
522
|
}
|
|
481
|
-
async function
|
|
523
|
+
async function exists2(target) {
|
|
482
524
|
try {
|
|
483
|
-
await
|
|
525
|
+
await access2(target);
|
|
484
526
|
return true;
|
|
485
527
|
} catch {
|
|
486
528
|
return false;
|
|
@@ -552,15 +594,15 @@ async function collectContractFiles(uiRoot, apiRoot, dbRoot) {
|
|
|
552
594
|
async function filterExisting(files) {
|
|
553
595
|
const existing = [];
|
|
554
596
|
for (const file of files) {
|
|
555
|
-
if (await
|
|
597
|
+
if (await exists3(file)) {
|
|
556
598
|
existing.push(file);
|
|
557
599
|
}
|
|
558
600
|
}
|
|
559
601
|
return existing;
|
|
560
602
|
}
|
|
561
|
-
async function
|
|
603
|
+
async function exists3(target) {
|
|
562
604
|
try {
|
|
563
|
-
await
|
|
605
|
+
await access3(target);
|
|
564
606
|
return true;
|
|
565
607
|
} catch {
|
|
566
608
|
return false;
|
|
@@ -618,6 +660,113 @@ function record(index, id, file) {
|
|
|
618
660
|
index.idToFiles.set(id, current);
|
|
619
661
|
}
|
|
620
662
|
|
|
663
|
+
// src/core/paths.ts
|
|
664
|
+
import path5 from "path";
|
|
665
|
+
function toRelativePath(root, target) {
|
|
666
|
+
if (!target) {
|
|
667
|
+
return target;
|
|
668
|
+
}
|
|
669
|
+
if (!path5.isAbsolute(target)) {
|
|
670
|
+
return toPosixPath(target);
|
|
671
|
+
}
|
|
672
|
+
const relative = path5.relative(root, target);
|
|
673
|
+
if (!relative) {
|
|
674
|
+
return ".";
|
|
675
|
+
}
|
|
676
|
+
return toPosixPath(relative);
|
|
677
|
+
}
|
|
678
|
+
function toPosixPath(value) {
|
|
679
|
+
return value.replace(/\\/g, "/");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/core/normalize.ts
|
|
683
|
+
function normalizeIssuePaths(root, issues) {
|
|
684
|
+
return issues.map((issue7) => {
|
|
685
|
+
if (!issue7.file) {
|
|
686
|
+
return issue7;
|
|
687
|
+
}
|
|
688
|
+
const normalized = toRelativePath(root, issue7.file);
|
|
689
|
+
if (normalized === issue7.file) {
|
|
690
|
+
return issue7;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
...issue7,
|
|
694
|
+
file: normalized
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function normalizeScCoverage(root, sc) {
|
|
699
|
+
const refs = {};
|
|
700
|
+
for (const [scId, files] of Object.entries(sc.refs)) {
|
|
701
|
+
refs[scId] = files.map((file) => toRelativePath(root, file));
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
...sc,
|
|
705
|
+
refs
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function normalizeValidationResult(root, result) {
|
|
709
|
+
return {
|
|
710
|
+
...result,
|
|
711
|
+
issues: normalizeIssuePaths(root, result.issues),
|
|
712
|
+
traceability: {
|
|
713
|
+
...result.traceability,
|
|
714
|
+
sc: normalizeScCoverage(root, result.traceability.sc)
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/core/parse/contractRefs.ts
|
|
720
|
+
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
721
|
+
function parseContractRefs(text, options = {}) {
|
|
722
|
+
const linePattern = buildLinePattern(options);
|
|
723
|
+
const lines = [];
|
|
724
|
+
for (const match of text.matchAll(linePattern)) {
|
|
725
|
+
lines.push((match[1] ?? "").trim());
|
|
726
|
+
}
|
|
727
|
+
const ids = [];
|
|
728
|
+
const invalidTokens = [];
|
|
729
|
+
let hasNone = false;
|
|
730
|
+
for (const line of lines) {
|
|
731
|
+
if (line.length === 0) {
|
|
732
|
+
invalidTokens.push("(empty)");
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const tokens = line.split(",").map((token) => token.trim());
|
|
736
|
+
for (const token of tokens) {
|
|
737
|
+
if (token.length === 0) {
|
|
738
|
+
invalidTokens.push("(empty)");
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (token === "none") {
|
|
742
|
+
hasNone = true;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
746
|
+
ids.push(token);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
invalidTokens.push(token);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
lines,
|
|
754
|
+
ids: unique2(ids),
|
|
755
|
+
invalidTokens: unique2(invalidTokens),
|
|
756
|
+
hasNone
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function buildLinePattern(options) {
|
|
760
|
+
const prefix = options.allowCommentPrefix ? "#" : "";
|
|
761
|
+
return new RegExp(
|
|
762
|
+
`^[ \\t]*${prefix}[ \\t]*QFAI-CONTRACT-REF:[ \\t]*([^\\r\\n]*)[ \\t]*$`,
|
|
763
|
+
"gm"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
function unique2(values) {
|
|
767
|
+
return Array.from(new Set(values));
|
|
768
|
+
}
|
|
769
|
+
|
|
621
770
|
// src/core/parse/markdown.ts
|
|
622
771
|
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
623
772
|
function parseHeadings(md) {
|
|
@@ -664,8 +813,6 @@ var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
|
664
813
|
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
665
814
|
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
666
815
|
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
667
|
-
var CONTRACT_REF_LINE_RE = /^[ \t]*QFAI-CONTRACT-REF:[ \t]*([^\r\n]*)[ \t]*$/gm;
|
|
668
|
-
var CONTRACT_REF_ID_RE = /^(?:API|UI|DB)-\d{4}$/;
|
|
669
816
|
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
670
817
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
671
818
|
function parseSpec(md, file) {
|
|
@@ -738,50 +885,10 @@ function parseSpec(md, file) {
|
|
|
738
885
|
}
|
|
739
886
|
return parsed;
|
|
740
887
|
}
|
|
741
|
-
function parseContractRefs(md) {
|
|
742
|
-
const lines = [];
|
|
743
|
-
for (const match of md.matchAll(CONTRACT_REF_LINE_RE)) {
|
|
744
|
-
lines.push((match[1] ?? "").trim());
|
|
745
|
-
}
|
|
746
|
-
const ids = [];
|
|
747
|
-
const invalidTokens = [];
|
|
748
|
-
let hasNone = false;
|
|
749
|
-
for (const line of lines) {
|
|
750
|
-
if (line.length === 0) {
|
|
751
|
-
invalidTokens.push("(empty)");
|
|
752
|
-
continue;
|
|
753
|
-
}
|
|
754
|
-
const tokens = line.split(",").map((token) => token.trim());
|
|
755
|
-
for (const token of tokens) {
|
|
756
|
-
if (token.length === 0) {
|
|
757
|
-
invalidTokens.push("(empty)");
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
if (token === "none") {
|
|
761
|
-
hasNone = true;
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (CONTRACT_REF_ID_RE.test(token)) {
|
|
765
|
-
ids.push(token);
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
768
|
-
invalidTokens.push(token);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
return {
|
|
772
|
-
lines,
|
|
773
|
-
ids: unique2(ids),
|
|
774
|
-
invalidTokens: unique2(invalidTokens),
|
|
775
|
-
hasNone
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
function unique2(values) {
|
|
779
|
-
return Array.from(new Set(values));
|
|
780
|
-
}
|
|
781
888
|
|
|
782
889
|
// src/core/traceability.ts
|
|
783
890
|
import { readFile as readFile3 } from "fs/promises";
|
|
784
|
-
import
|
|
891
|
+
import path6 from "path";
|
|
785
892
|
|
|
786
893
|
// src/core/gherkin/parse.ts
|
|
787
894
|
import {
|
|
@@ -816,9 +923,6 @@ function formatError2(error) {
|
|
|
816
923
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
817
924
|
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
818
925
|
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
819
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
820
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
821
|
-
var DB_TAG_RE = /^DB-\d{4}$/;
|
|
822
926
|
function parseScenarioDocument(text, uri) {
|
|
823
927
|
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
824
928
|
if (!gherkinDocument) {
|
|
@@ -843,31 +947,21 @@ function parseScenarioDocument(text, uri) {
|
|
|
843
947
|
errors
|
|
844
948
|
};
|
|
845
949
|
}
|
|
846
|
-
function buildScenarioAtoms(document) {
|
|
950
|
+
function buildScenarioAtoms(document, contractIds = []) {
|
|
951
|
+
const uniqueContractIds = unique3(contractIds).sort(
|
|
952
|
+
(a, b) => a.localeCompare(b)
|
|
953
|
+
);
|
|
847
954
|
return document.scenarios.map((scenario) => {
|
|
848
955
|
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
849
956
|
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
850
957
|
const brIds = unique3(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
851
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
852
|
-
scenario.tags.forEach((tag) => {
|
|
853
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DB_TAG_RE.test(tag)) {
|
|
854
|
-
contractIds.add(tag);
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
for (const step of scenario.steps) {
|
|
858
|
-
for (const text of collectStepTexts(step)) {
|
|
859
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
860
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
861
|
-
extractIds(text, "DB").forEach((id) => contractIds.add(id));
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
958
|
const atom = {
|
|
865
959
|
uri: document.uri,
|
|
866
960
|
featureName: document.featureName ?? "",
|
|
867
961
|
scenarioName: scenario.name,
|
|
868
962
|
kind: scenario.kind,
|
|
869
963
|
brIds,
|
|
870
|
-
contractIds:
|
|
964
|
+
contractIds: uniqueContractIds
|
|
871
965
|
};
|
|
872
966
|
if (scenario.line !== void 0) {
|
|
873
967
|
atom.line = scenario.line;
|
|
@@ -920,23 +1014,6 @@ function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
|
920
1014
|
function collectTagNames(tags) {
|
|
921
1015
|
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
922
1016
|
}
|
|
923
|
-
function collectStepTexts(step) {
|
|
924
|
-
const texts = [];
|
|
925
|
-
if (step.text) {
|
|
926
|
-
texts.push(step.text);
|
|
927
|
-
}
|
|
928
|
-
if (step.docString?.content) {
|
|
929
|
-
texts.push(step.docString.content);
|
|
930
|
-
}
|
|
931
|
-
if (step.dataTable?.rows) {
|
|
932
|
-
for (const row of step.dataTable.rows) {
|
|
933
|
-
for (const cell of row.cells) {
|
|
934
|
-
texts.push(cell.value);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return texts;
|
|
939
|
-
}
|
|
940
1017
|
function unique3(values) {
|
|
941
1018
|
return Array.from(new Set(values));
|
|
942
1019
|
}
|
|
@@ -1038,7 +1115,7 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1038
1115
|
};
|
|
1039
1116
|
}
|
|
1040
1117
|
const normalizedFiles = Array.from(
|
|
1041
|
-
new Set(files.map((file) =>
|
|
1118
|
+
new Set(files.map((file) => path6.normalize(file)))
|
|
1042
1119
|
);
|
|
1043
1120
|
for (const file of normalizedFiles) {
|
|
1044
1121
|
const text = await readFile3(file, "utf-8");
|
|
@@ -1099,11 +1176,11 @@ function formatError3(error) {
|
|
|
1099
1176
|
|
|
1100
1177
|
// src/core/version.ts
|
|
1101
1178
|
import { readFile as readFile4 } from "fs/promises";
|
|
1102
|
-
import
|
|
1179
|
+
import path7 from "path";
|
|
1103
1180
|
import { fileURLToPath } from "url";
|
|
1104
1181
|
async function resolveToolVersion() {
|
|
1105
|
-
if ("0.
|
|
1106
|
-
return "0.
|
|
1182
|
+
if ("0.6.0".length > 0) {
|
|
1183
|
+
return "0.6.0";
|
|
1107
1184
|
}
|
|
1108
1185
|
try {
|
|
1109
1186
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -1118,18 +1195,18 @@ async function resolveToolVersion() {
|
|
|
1118
1195
|
function resolvePackageJsonPath() {
|
|
1119
1196
|
const base = import.meta.url;
|
|
1120
1197
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
1121
|
-
return
|
|
1198
|
+
return path7.resolve(path7.dirname(basePath), "../../package.json");
|
|
1122
1199
|
}
|
|
1123
1200
|
|
|
1124
1201
|
// src/core/validators/contracts.ts
|
|
1125
1202
|
import { readFile as readFile5 } from "fs/promises";
|
|
1126
|
-
import
|
|
1203
|
+
import path9 from "path";
|
|
1127
1204
|
|
|
1128
1205
|
// src/core/contracts.ts
|
|
1129
|
-
import
|
|
1206
|
+
import path8 from "path";
|
|
1130
1207
|
import { parse as parseYaml2 } from "yaml";
|
|
1131
1208
|
function parseStructuredContract(file, text) {
|
|
1132
|
-
const ext =
|
|
1209
|
+
const ext = path8.extname(file).toLowerCase();
|
|
1133
1210
|
if (ext === ".json") {
|
|
1134
1211
|
return JSON.parse(text);
|
|
1135
1212
|
}
|
|
@@ -1149,9 +1226,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
1149
1226
|
async function validateContracts(root, config) {
|
|
1150
1227
|
const issues = [];
|
|
1151
1228
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1152
|
-
issues.push(...await validateUiContracts(
|
|
1153
|
-
issues.push(...await validateApiContracts(
|
|
1154
|
-
issues.push(...await validateDbContracts(
|
|
1229
|
+
issues.push(...await validateUiContracts(path9.join(contractsRoot, "ui")));
|
|
1230
|
+
issues.push(...await validateApiContracts(path9.join(contractsRoot, "api")));
|
|
1231
|
+
issues.push(...await validateDbContracts(path9.join(contractsRoot, "db")));
|
|
1155
1232
|
const contractIndex = await buildContractIndex(root, config);
|
|
1156
1233
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1157
1234
|
return issues;
|
|
@@ -1434,7 +1511,7 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1434
1511
|
|
|
1435
1512
|
// src/core/validators/delta.ts
|
|
1436
1513
|
import { readFile as readFile6 } from "fs/promises";
|
|
1437
|
-
import
|
|
1514
|
+
import path10 from "path";
|
|
1438
1515
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1439
1516
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1440
1517
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1448,7 +1525,7 @@ async function validateDeltas(root, config) {
|
|
|
1448
1525
|
}
|
|
1449
1526
|
const issues = [];
|
|
1450
1527
|
for (const pack of packs) {
|
|
1451
|
-
const deltaPath =
|
|
1528
|
+
const deltaPath = path10.join(pack, "delta.md");
|
|
1452
1529
|
let text;
|
|
1453
1530
|
try {
|
|
1454
1531
|
text = await readFile6(deltaPath, "utf-8");
|
|
@@ -1524,7 +1601,7 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1524
1601
|
|
|
1525
1602
|
// src/core/validators/ids.ts
|
|
1526
1603
|
import { readFile as readFile7 } from "fs/promises";
|
|
1527
|
-
import
|
|
1604
|
+
import path11 from "path";
|
|
1528
1605
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1529
1606
|
async function validateDefinedIds(root, config) {
|
|
1530
1607
|
const issues = [];
|
|
@@ -1590,7 +1667,7 @@ function recordId(out, id, file) {
|
|
|
1590
1667
|
}
|
|
1591
1668
|
function formatFileList(files, root) {
|
|
1592
1669
|
return files.map((file) => {
|
|
1593
|
-
const relative =
|
|
1670
|
+
const relative = path11.relative(root, file);
|
|
1594
1671
|
return relative.length > 0 ? relative : file;
|
|
1595
1672
|
}).join(", ");
|
|
1596
1673
|
}
|
|
@@ -2027,7 +2104,7 @@ async function validateTraceability(root, config) {
|
|
|
2027
2104
|
if (contractRefs.hasNone && contractRefs.ids.length > 0) {
|
|
2028
2105
|
issues.push(
|
|
2029
2106
|
issue6(
|
|
2030
|
-
"QFAI-TRACE-
|
|
2107
|
+
"QFAI-TRACE-023",
|
|
2031
2108
|
"Spec \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2032
2109
|
"error",
|
|
2033
2110
|
file,
|
|
@@ -2059,7 +2136,7 @@ async function validateTraceability(root, config) {
|
|
|
2059
2136
|
if (unknownContractIds.length > 0) {
|
|
2060
2137
|
issues.push(
|
|
2061
2138
|
issue6(
|
|
2062
|
-
"QFAI-TRACE-
|
|
2139
|
+
"QFAI-TRACE-024",
|
|
2063
2140
|
`Spec \u304C\u672A\u77E5\u306E\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
2064
2141
|
", "
|
|
2065
2142
|
)}`,
|
|
@@ -2074,11 +2151,62 @@ async function validateTraceability(root, config) {
|
|
|
2074
2151
|
for (const file of scenarioFiles) {
|
|
2075
2152
|
const text = await readFile10(file, "utf-8");
|
|
2076
2153
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2154
|
+
const scenarioContractRefs = parseContractRefs(text, {
|
|
2155
|
+
allowCommentPrefix: true
|
|
2156
|
+
});
|
|
2157
|
+
if (scenarioContractRefs.lines.length === 0) {
|
|
2158
|
+
issues.push(
|
|
2159
|
+
issue6(
|
|
2160
|
+
"QFAI-TRACE-031",
|
|
2161
|
+
"Scenario \u306B QFAI-CONTRACT-REF \u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
2162
|
+
"error",
|
|
2163
|
+
file,
|
|
2164
|
+
"traceability.scenarioContractRefRequired"
|
|
2165
|
+
)
|
|
2166
|
+
);
|
|
2167
|
+
} else {
|
|
2168
|
+
if (scenarioContractRefs.hasNone && scenarioContractRefs.ids.length > 0) {
|
|
2169
|
+
issues.push(
|
|
2170
|
+
issue6(
|
|
2171
|
+
"QFAI-TRACE-033",
|
|
2172
|
+
"Scenario \u306E QFAI-CONTRACT-REF \u306B none \u3068\u5951\u7D04 ID \u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002",
|
|
2173
|
+
"error",
|
|
2174
|
+
file,
|
|
2175
|
+
"traceability.scenarioContractRefFormat"
|
|
2176
|
+
)
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
if (scenarioContractRefs.invalidTokens.length > 0) {
|
|
2180
|
+
issues.push(
|
|
2181
|
+
issue6(
|
|
2182
|
+
"QFAI-TRACE-032",
|
|
2183
|
+
`Scenario \u306E\u5951\u7D04 ID \u304C\u4E0D\u6B63\u3067\u3059: ${scenarioContractRefs.invalidTokens.join(
|
|
2184
|
+
", "
|
|
2185
|
+
)}`,
|
|
2186
|
+
"error",
|
|
2187
|
+
file,
|
|
2188
|
+
"traceability.scenarioContractRefFormat",
|
|
2189
|
+
scenarioContractRefs.invalidTokens
|
|
2190
|
+
)
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2077
2194
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2078
2195
|
if (!document || errors.length > 0) {
|
|
2079
2196
|
continue;
|
|
2080
2197
|
}
|
|
2081
|
-
|
|
2198
|
+
if (document.scenarios.length !== 1) {
|
|
2199
|
+
issues.push(
|
|
2200
|
+
issue6(
|
|
2201
|
+
"QFAI-TRACE-030",
|
|
2202
|
+
`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})`,
|
|
2203
|
+
"error",
|
|
2204
|
+
file,
|
|
2205
|
+
"traceability.scenarioOnePerFile"
|
|
2206
|
+
)
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
const atoms = buildScenarioAtoms(document, scenarioContractRefs.ids);
|
|
2082
2210
|
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
2083
2211
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
2084
2212
|
const atom = atoms[index];
|
|
@@ -2223,7 +2351,7 @@ async function validateTraceability(root, config) {
|
|
|
2223
2351
|
if (orphanBrIds.length > 0) {
|
|
2224
2352
|
issues.push(
|
|
2225
2353
|
issue6(
|
|
2226
|
-
"
|
|
2354
|
+
"QFAI-TRACE-009",
|
|
2227
2355
|
`BR \u304C SC \u306B\u7D10\u3065\u3044\u3066\u3044\u307E\u305B\u3093: ${orphanBrIds.join(", ")}`,
|
|
2228
2356
|
"error",
|
|
2229
2357
|
specsRoot,
|
|
@@ -2293,17 +2421,19 @@ async function validateTraceability(root, config) {
|
|
|
2293
2421
|
);
|
|
2294
2422
|
}
|
|
2295
2423
|
}
|
|
2296
|
-
|
|
2424
|
+
const orphanPolicy = config.validation.traceability.orphanContractsPolicy;
|
|
2425
|
+
if (orphanPolicy !== "allow") {
|
|
2297
2426
|
if (contractIds.size > 0) {
|
|
2298
2427
|
const orphanContracts = Array.from(contractIds).filter(
|
|
2299
2428
|
(id) => !specContractIds.has(id)
|
|
2300
2429
|
);
|
|
2301
2430
|
if (orphanContracts.length > 0) {
|
|
2431
|
+
const severity = orphanPolicy === "warning" ? "warning" : "error";
|
|
2302
2432
|
issues.push(
|
|
2303
2433
|
issue6(
|
|
2304
2434
|
"QFAI-TRACE-022",
|
|
2305
2435
|
`\u5951\u7D04\u304C Spec \u304B\u3089\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${orphanContracts.join(", ")}`,
|
|
2306
|
-
|
|
2436
|
+
severity,
|
|
2307
2437
|
specsRoot,
|
|
2308
2438
|
"traceability.contractCoverage",
|
|
2309
2439
|
orphanContracts
|
|
@@ -2428,16 +2558,17 @@ function countIssues(issues) {
|
|
|
2428
2558
|
// src/core/report.ts
|
|
2429
2559
|
var ID_PREFIXES2 = ["SPEC", "BR", "SC", "UI", "API", "DB"];
|
|
2430
2560
|
async function createReportData(root, validation, configResult) {
|
|
2431
|
-
const
|
|
2561
|
+
const resolvedRoot = path12.resolve(root);
|
|
2562
|
+
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
2432
2563
|
const config = resolved.config;
|
|
2433
2564
|
const configPath = resolved.configPath;
|
|
2434
|
-
const specsRoot = resolvePath(
|
|
2435
|
-
const contractsRoot = resolvePath(
|
|
2436
|
-
const apiRoot =
|
|
2437
|
-
const uiRoot =
|
|
2438
|
-
const dbRoot =
|
|
2439
|
-
const srcRoot = resolvePath(
|
|
2440
|
-
const testsRoot = resolvePath(
|
|
2565
|
+
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
2566
|
+
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
2567
|
+
const apiRoot = path12.join(contractsRoot, "api");
|
|
2568
|
+
const uiRoot = path12.join(contractsRoot, "ui");
|
|
2569
|
+
const dbRoot = path12.join(contractsRoot, "db");
|
|
2570
|
+
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
2571
|
+
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
2441
2572
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
2442
2573
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2443
2574
|
const {
|
|
@@ -2445,15 +2576,15 @@ async function createReportData(root, validation, configResult) {
|
|
|
2445
2576
|
ui: uiFiles,
|
|
2446
2577
|
db: dbFiles
|
|
2447
2578
|
} = await collectContractFiles(uiRoot, apiRoot, dbRoot);
|
|
2448
|
-
const contractIndex = await buildContractIndex(
|
|
2579
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2449
2580
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2450
2581
|
const specContractRefs = await collectSpecContractRefs(
|
|
2451
2582
|
specFiles,
|
|
2452
2583
|
contractIdList
|
|
2453
2584
|
);
|
|
2454
2585
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2455
|
-
for (const
|
|
2456
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2586
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2587
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2457
2588
|
}
|
|
2458
2589
|
const referencedContractCount = contractIdList.filter(
|
|
2459
2590
|
(id) => referencedContracts.has(id)
|
|
@@ -2462,8 +2593,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2462
2593
|
(id) => !referencedContracts.has(id)
|
|
2463
2594
|
).length;
|
|
2464
2595
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2465
|
-
const
|
|
2466
|
-
specContractRefs.
|
|
2596
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2597
|
+
specContractRefs.specToContracts
|
|
2467
2598
|
);
|
|
2468
2599
|
const idsByPrefix = await collectIds([
|
|
2469
2600
|
...specFiles,
|
|
@@ -2481,24 +2612,28 @@ async function createReportData(root, validation, configResult) {
|
|
|
2481
2612
|
srcRoot,
|
|
2482
2613
|
testsRoot
|
|
2483
2614
|
);
|
|
2484
|
-
const
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2615
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2616
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2617
|
+
resolvedRoot,
|
|
2618
|
+
resolvedValidationRaw
|
|
2489
2619
|
);
|
|
2490
|
-
const scCoverage =
|
|
2491
|
-
const testFiles =
|
|
2620
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2621
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2492
2622
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2493
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2494
|
-
|
|
2623
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2624
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2625
|
+
);
|
|
2495
2626
|
const version = await resolveToolVersion();
|
|
2627
|
+
const reportFormatVersion = 1;
|
|
2628
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2629
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2496
2630
|
return {
|
|
2497
2631
|
tool: "qfai",
|
|
2498
2632
|
version,
|
|
2633
|
+
reportFormatVersion,
|
|
2499
2634
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2500
|
-
root,
|
|
2501
|
-
configPath,
|
|
2635
|
+
root: displayRoot,
|
|
2636
|
+
configPath: displayConfigPath,
|
|
2502
2637
|
summary: {
|
|
2503
2638
|
specs: specFiles.length,
|
|
2504
2639
|
scenarios: scenarioFiles.length,
|
|
@@ -2507,7 +2642,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2507
2642
|
ui: uiFiles.length,
|
|
2508
2643
|
db: dbFiles.length
|
|
2509
2644
|
},
|
|
2510
|
-
counts:
|
|
2645
|
+
counts: normalizedValidation.counts
|
|
2511
2646
|
},
|
|
2512
2647
|
ids: {
|
|
2513
2648
|
spec: idsByPrefix.SPEC,
|
|
@@ -2532,21 +2667,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2532
2667
|
specs: {
|
|
2533
2668
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2534
2669
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2535
|
-
|
|
2670
|
+
specToContracts: specToContractsRecord
|
|
2536
2671
|
}
|
|
2537
2672
|
},
|
|
2538
|
-
issues:
|
|
2673
|
+
issues: normalizedValidation.issues
|
|
2539
2674
|
};
|
|
2540
2675
|
}
|
|
2541
2676
|
function formatReportMarkdown(data) {
|
|
2542
2677
|
const lines = [];
|
|
2543
2678
|
lines.push("# QFAI Report");
|
|
2679
|
+
lines.push("");
|
|
2544
2680
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2545
2681
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2546
2682
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2547
2683
|
lines.push(`- \u7248: ${data.version}`);
|
|
2548
2684
|
lines.push("");
|
|
2549
2685
|
lines.push("## \u6982\u8981");
|
|
2686
|
+
lines.push("");
|
|
2550
2687
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2551
2688
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2552
2689
|
lines.push(
|
|
@@ -2557,6 +2694,7 @@ function formatReportMarkdown(data) {
|
|
|
2557
2694
|
);
|
|
2558
2695
|
lines.push("");
|
|
2559
2696
|
lines.push("## ID\u96C6\u8A08");
|
|
2697
|
+
lines.push("");
|
|
2560
2698
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2561
2699
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2562
2700
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2565,12 +2703,14 @@ function formatReportMarkdown(data) {
|
|
|
2565
2703
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2566
2704
|
lines.push("");
|
|
2567
2705
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2706
|
+
lines.push("");
|
|
2568
2707
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2569
2708
|
lines.push(
|
|
2570
2709
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2571
2710
|
);
|
|
2572
2711
|
lines.push("");
|
|
2573
2712
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2713
|
+
lines.push("");
|
|
2574
2714
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2575
2715
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2576
2716
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2579,6 +2719,7 @@ function formatReportMarkdown(data) {
|
|
|
2579
2719
|
);
|
|
2580
2720
|
lines.push("");
|
|
2581
2721
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2722
|
+
lines.push("");
|
|
2582
2723
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2583
2724
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2584
2725
|
(a, b) => a.localeCompare(b)
|
|
@@ -2597,24 +2738,25 @@ function formatReportMarkdown(data) {
|
|
|
2597
2738
|
}
|
|
2598
2739
|
lines.push("");
|
|
2599
2740
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2600
|
-
|
|
2741
|
+
lines.push("");
|
|
2742
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2601
2743
|
const specIds = Object.keys(specToContracts).sort(
|
|
2602
2744
|
(a, b) => a.localeCompare(b)
|
|
2603
2745
|
);
|
|
2604
2746
|
if (specIds.length === 0) {
|
|
2605
2747
|
lines.push("- (none)");
|
|
2606
2748
|
} else {
|
|
2607
|
-
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
}
|
|
2749
|
+
const rows = specIds.map((specId) => {
|
|
2750
|
+
const entry = specToContracts[specId];
|
|
2751
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
2752
|
+
const status = entry?.status ?? "missing";
|
|
2753
|
+
return [specId, status, contracts];
|
|
2754
|
+
});
|
|
2755
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2615
2756
|
}
|
|
2616
2757
|
lines.push("");
|
|
2617
2758
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2759
|
+
lines.push("");
|
|
2618
2760
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2619
2761
|
if (missingRefSpecs.length === 0) {
|
|
2620
2762
|
lines.push("- (none)");
|
|
@@ -2625,6 +2767,7 @@ function formatReportMarkdown(data) {
|
|
|
2625
2767
|
}
|
|
2626
2768
|
lines.push("");
|
|
2627
2769
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2770
|
+
lines.push("");
|
|
2628
2771
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2629
2772
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2630
2773
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2654,6 +2797,7 @@ function formatReportMarkdown(data) {
|
|
|
2654
2797
|
}
|
|
2655
2798
|
lines.push("");
|
|
2656
2799
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2800
|
+
lines.push("");
|
|
2657
2801
|
const scRefs = data.traceability.sc.refs;
|
|
2658
2802
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2659
2803
|
if (scIds.length === 0) {
|
|
@@ -2670,6 +2814,7 @@ function formatReportMarkdown(data) {
|
|
|
2670
2814
|
}
|
|
2671
2815
|
lines.push("");
|
|
2672
2816
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2817
|
+
lines.push("");
|
|
2673
2818
|
const specScIssues = data.issues.filter(
|
|
2674
2819
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2675
2820
|
);
|
|
@@ -2684,6 +2829,7 @@ function formatReportMarkdown(data) {
|
|
|
2684
2829
|
}
|
|
2685
2830
|
lines.push("");
|
|
2686
2831
|
lines.push("## Hotspots");
|
|
2832
|
+
lines.push("");
|
|
2687
2833
|
const hotspots = buildHotspots(data.issues);
|
|
2688
2834
|
if (hotspots.length === 0) {
|
|
2689
2835
|
lines.push("- (none)");
|
|
@@ -2696,6 +2842,7 @@ function formatReportMarkdown(data) {
|
|
|
2696
2842
|
}
|
|
2697
2843
|
lines.push("");
|
|
2698
2844
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2845
|
+
lines.push("");
|
|
2699
2846
|
const traceIssues = data.issues.filter(
|
|
2700
2847
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2701
2848
|
);
|
|
@@ -2711,6 +2858,7 @@ function formatReportMarkdown(data) {
|
|
|
2711
2858
|
}
|
|
2712
2859
|
lines.push("");
|
|
2713
2860
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
2861
|
+
lines.push("");
|
|
2714
2862
|
if (data.issues.length === 0) {
|
|
2715
2863
|
lines.push("- (none)");
|
|
2716
2864
|
} else {
|
|
@@ -2728,7 +2876,7 @@ function formatReportJson(data) {
|
|
|
2728
2876
|
return JSON.stringify(data, null, 2);
|
|
2729
2877
|
}
|
|
2730
2878
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2731
|
-
const
|
|
2879
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2732
2880
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2733
2881
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2734
2882
|
for (const contractId of contractIdList) {
|
|
@@ -2737,24 +2885,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2737
2885
|
for (const file of specFiles) {
|
|
2738
2886
|
const text = await readFile11(file, "utf-8");
|
|
2739
2887
|
const parsed = parseSpec(text, file);
|
|
2740
|
-
const specKey = parsed.specId
|
|
2888
|
+
const specKey = parsed.specId;
|
|
2889
|
+
if (!specKey) {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2741
2892
|
const refs = parsed.contractRefs;
|
|
2742
2893
|
if (refs.lines.length === 0) {
|
|
2743
2894
|
missingRefSpecs.add(specKey);
|
|
2895
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2744
2896
|
continue;
|
|
2745
2897
|
}
|
|
2746
|
-
const
|
|
2898
|
+
const current = specToContracts.get(specKey) ?? {
|
|
2899
|
+
status: "declared",
|
|
2900
|
+
ids: /* @__PURE__ */ new Set()
|
|
2901
|
+
};
|
|
2747
2902
|
for (const id of refs.ids) {
|
|
2748
|
-
|
|
2903
|
+
current.ids.add(id);
|
|
2749
2904
|
const specs = idToSpecs.get(id);
|
|
2750
2905
|
if (specs) {
|
|
2751
2906
|
specs.add(specKey);
|
|
2752
2907
|
}
|
|
2753
2908
|
}
|
|
2754
|
-
|
|
2909
|
+
specToContracts.set(specKey, current);
|
|
2755
2910
|
}
|
|
2756
2911
|
return {
|
|
2757
|
-
|
|
2912
|
+
specToContracts,
|
|
2758
2913
|
idToSpecs,
|
|
2759
2914
|
missingRefSpecs
|
|
2760
2915
|
};
|
|
@@ -2831,6 +2986,20 @@ function formatList(values) {
|
|
|
2831
2986
|
}
|
|
2832
2987
|
return values.join(", ");
|
|
2833
2988
|
}
|
|
2989
|
+
function formatMarkdownTable(headers, rows) {
|
|
2990
|
+
const widths = headers.map((header, index) => {
|
|
2991
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
2992
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
2993
|
+
});
|
|
2994
|
+
const formatRow = (cells) => {
|
|
2995
|
+
const padded = cells.map(
|
|
2996
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
2997
|
+
);
|
|
2998
|
+
return `| ${padded.join(" | ")} |`;
|
|
2999
|
+
};
|
|
3000
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
3001
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3002
|
+
}
|
|
2834
3003
|
function toSortedArray2(values) {
|
|
2835
3004
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2836
3005
|
}
|
|
@@ -2841,6 +3010,27 @@ function mapToSortedRecord(values) {
|
|
|
2841
3010
|
}
|
|
2842
3011
|
return record2;
|
|
2843
3012
|
}
|
|
3013
|
+
function mapToSpecContractRecord(values) {
|
|
3014
|
+
const record2 = {};
|
|
3015
|
+
for (const [key, entry] of values.entries()) {
|
|
3016
|
+
record2[key] = {
|
|
3017
|
+
status: entry.status,
|
|
3018
|
+
ids: toSortedArray2(entry.ids)
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
return record2;
|
|
3022
|
+
}
|
|
3023
|
+
function normalizeScSources(root, sources) {
|
|
3024
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3025
|
+
for (const [id, files] of sources.entries()) {
|
|
3026
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3027
|
+
for (const file of files) {
|
|
3028
|
+
mapped.add(toRelativePath(root, file));
|
|
3029
|
+
}
|
|
3030
|
+
normalized.set(id, mapped);
|
|
3031
|
+
}
|
|
3032
|
+
return normalized;
|
|
3033
|
+
}
|
|
2844
3034
|
function buildHotspots(issues) {
|
|
2845
3035
|
const map = /* @__PURE__ */ new Map();
|
|
2846
3036
|
for (const issue7 of issues) {
|
|
@@ -2868,6 +3058,7 @@ export {
|
|
|
2868
3058
|
extractAllIds,
|
|
2869
3059
|
extractIds,
|
|
2870
3060
|
extractInvalidIds,
|
|
3061
|
+
findConfigRoot,
|
|
2871
3062
|
formatReportJson,
|
|
2872
3063
|
formatReportMarkdown,
|
|
2873
3064
|
getConfigPath,
|