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/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.5.
|
|
1106
|
-
return "0.5.
|
|
1182
|
+
if ("0.5.2".length > 0) {
|
|
1183
|
+
return "0.5.2";
|
|
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,26 @@ 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 displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2628
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2496
2629
|
return {
|
|
2497
2630
|
tool: "qfai",
|
|
2498
2631
|
version,
|
|
2499
2632
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2500
|
-
root,
|
|
2501
|
-
configPath,
|
|
2633
|
+
root: displayRoot,
|
|
2634
|
+
configPath: displayConfigPath,
|
|
2502
2635
|
summary: {
|
|
2503
2636
|
specs: specFiles.length,
|
|
2504
2637
|
scenarios: scenarioFiles.length,
|
|
@@ -2507,7 +2640,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2507
2640
|
ui: uiFiles.length,
|
|
2508
2641
|
db: dbFiles.length
|
|
2509
2642
|
},
|
|
2510
|
-
counts:
|
|
2643
|
+
counts: normalizedValidation.counts
|
|
2511
2644
|
},
|
|
2512
2645
|
ids: {
|
|
2513
2646
|
spec: idsByPrefix.SPEC,
|
|
@@ -2532,21 +2665,23 @@ async function createReportData(root, validation, configResult) {
|
|
|
2532
2665
|
specs: {
|
|
2533
2666
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2534
2667
|
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2535
|
-
|
|
2668
|
+
specToContracts: specToContractsRecord
|
|
2536
2669
|
}
|
|
2537
2670
|
},
|
|
2538
|
-
issues:
|
|
2671
|
+
issues: normalizedValidation.issues
|
|
2539
2672
|
};
|
|
2540
2673
|
}
|
|
2541
2674
|
function formatReportMarkdown(data) {
|
|
2542
2675
|
const lines = [];
|
|
2543
2676
|
lines.push("# QFAI Report");
|
|
2677
|
+
lines.push("");
|
|
2544
2678
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2545
2679
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2546
2680
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2547
2681
|
lines.push(`- \u7248: ${data.version}`);
|
|
2548
2682
|
lines.push("");
|
|
2549
2683
|
lines.push("## \u6982\u8981");
|
|
2684
|
+
lines.push("");
|
|
2550
2685
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2551
2686
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2552
2687
|
lines.push(
|
|
@@ -2557,6 +2692,7 @@ function formatReportMarkdown(data) {
|
|
|
2557
2692
|
);
|
|
2558
2693
|
lines.push("");
|
|
2559
2694
|
lines.push("## ID\u96C6\u8A08");
|
|
2695
|
+
lines.push("");
|
|
2560
2696
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2561
2697
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2562
2698
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2565,12 +2701,14 @@ function formatReportMarkdown(data) {
|
|
|
2565
2701
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2566
2702
|
lines.push("");
|
|
2567
2703
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2704
|
+
lines.push("");
|
|
2568
2705
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2569
2706
|
lines.push(
|
|
2570
2707
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2571
2708
|
);
|
|
2572
2709
|
lines.push("");
|
|
2573
2710
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2711
|
+
lines.push("");
|
|
2574
2712
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2575
2713
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2576
2714
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2579,6 +2717,7 @@ function formatReportMarkdown(data) {
|
|
|
2579
2717
|
);
|
|
2580
2718
|
lines.push("");
|
|
2581
2719
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2720
|
+
lines.push("");
|
|
2582
2721
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2583
2722
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2584
2723
|
(a, b) => a.localeCompare(b)
|
|
@@ -2597,24 +2736,25 @@ function formatReportMarkdown(data) {
|
|
|
2597
2736
|
}
|
|
2598
2737
|
lines.push("");
|
|
2599
2738
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2600
|
-
|
|
2739
|
+
lines.push("");
|
|
2740
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2601
2741
|
const specIds = Object.keys(specToContracts).sort(
|
|
2602
2742
|
(a, b) => a.localeCompare(b)
|
|
2603
2743
|
);
|
|
2604
2744
|
if (specIds.length === 0) {
|
|
2605
2745
|
lines.push("- (none)");
|
|
2606
2746
|
} else {
|
|
2607
|
-
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
}
|
|
2747
|
+
const rows = specIds.map((specId) => {
|
|
2748
|
+
const entry = specToContracts[specId];
|
|
2749
|
+
const contracts = entry?.status === "missing" ? "(missing)" : entry && entry.ids.length > 0 ? entry.ids.join(", ") : "(none)";
|
|
2750
|
+
const status = entry?.status ?? "missing";
|
|
2751
|
+
return [specId, status, contracts];
|
|
2752
|
+
});
|
|
2753
|
+
lines.push(...formatMarkdownTable(["Spec", "Status", "Contracts"], rows));
|
|
2615
2754
|
}
|
|
2616
2755
|
lines.push("");
|
|
2617
2756
|
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2757
|
+
lines.push("");
|
|
2618
2758
|
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2619
2759
|
if (missingRefSpecs.length === 0) {
|
|
2620
2760
|
lines.push("- (none)");
|
|
@@ -2625,6 +2765,7 @@ function formatReportMarkdown(data) {
|
|
|
2625
2765
|
}
|
|
2626
2766
|
lines.push("");
|
|
2627
2767
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2768
|
+
lines.push("");
|
|
2628
2769
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2629
2770
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2630
2771
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2654,6 +2795,7 @@ function formatReportMarkdown(data) {
|
|
|
2654
2795
|
}
|
|
2655
2796
|
lines.push("");
|
|
2656
2797
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2798
|
+
lines.push("");
|
|
2657
2799
|
const scRefs = data.traceability.sc.refs;
|
|
2658
2800
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2659
2801
|
if (scIds.length === 0) {
|
|
@@ -2670,6 +2812,7 @@ function formatReportMarkdown(data) {
|
|
|
2670
2812
|
}
|
|
2671
2813
|
lines.push("");
|
|
2672
2814
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2815
|
+
lines.push("");
|
|
2673
2816
|
const specScIssues = data.issues.filter(
|
|
2674
2817
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2675
2818
|
);
|
|
@@ -2684,6 +2827,7 @@ function formatReportMarkdown(data) {
|
|
|
2684
2827
|
}
|
|
2685
2828
|
lines.push("");
|
|
2686
2829
|
lines.push("## Hotspots");
|
|
2830
|
+
lines.push("");
|
|
2687
2831
|
const hotspots = buildHotspots(data.issues);
|
|
2688
2832
|
if (hotspots.length === 0) {
|
|
2689
2833
|
lines.push("- (none)");
|
|
@@ -2696,6 +2840,7 @@ function formatReportMarkdown(data) {
|
|
|
2696
2840
|
}
|
|
2697
2841
|
lines.push("");
|
|
2698
2842
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2843
|
+
lines.push("");
|
|
2699
2844
|
const traceIssues = data.issues.filter(
|
|
2700
2845
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2701
2846
|
);
|
|
@@ -2711,6 +2856,7 @@ function formatReportMarkdown(data) {
|
|
|
2711
2856
|
}
|
|
2712
2857
|
lines.push("");
|
|
2713
2858
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
2859
|
+
lines.push("");
|
|
2714
2860
|
if (data.issues.length === 0) {
|
|
2715
2861
|
lines.push("- (none)");
|
|
2716
2862
|
} else {
|
|
@@ -2728,7 +2874,7 @@ function formatReportJson(data) {
|
|
|
2728
2874
|
return JSON.stringify(data, null, 2);
|
|
2729
2875
|
}
|
|
2730
2876
|
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2731
|
-
const
|
|
2877
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2732
2878
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2733
2879
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2734
2880
|
for (const contractId of contractIdList) {
|
|
@@ -2737,24 +2883,31 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
2737
2883
|
for (const file of specFiles) {
|
|
2738
2884
|
const text = await readFile11(file, "utf-8");
|
|
2739
2885
|
const parsed = parseSpec(text, file);
|
|
2740
|
-
const specKey = parsed.specId
|
|
2886
|
+
const specKey = parsed.specId;
|
|
2887
|
+
if (!specKey) {
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2741
2890
|
const refs = parsed.contractRefs;
|
|
2742
2891
|
if (refs.lines.length === 0) {
|
|
2743
2892
|
missingRefSpecs.add(specKey);
|
|
2893
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2744
2894
|
continue;
|
|
2745
2895
|
}
|
|
2746
|
-
const
|
|
2896
|
+
const current = specToContracts.get(specKey) ?? {
|
|
2897
|
+
status: "declared",
|
|
2898
|
+
ids: /* @__PURE__ */ new Set()
|
|
2899
|
+
};
|
|
2747
2900
|
for (const id of refs.ids) {
|
|
2748
|
-
|
|
2901
|
+
current.ids.add(id);
|
|
2749
2902
|
const specs = idToSpecs.get(id);
|
|
2750
2903
|
if (specs) {
|
|
2751
2904
|
specs.add(specKey);
|
|
2752
2905
|
}
|
|
2753
2906
|
}
|
|
2754
|
-
|
|
2907
|
+
specToContracts.set(specKey, current);
|
|
2755
2908
|
}
|
|
2756
2909
|
return {
|
|
2757
|
-
|
|
2910
|
+
specToContracts,
|
|
2758
2911
|
idToSpecs,
|
|
2759
2912
|
missingRefSpecs
|
|
2760
2913
|
};
|
|
@@ -2831,6 +2984,20 @@ function formatList(values) {
|
|
|
2831
2984
|
}
|
|
2832
2985
|
return values.join(", ");
|
|
2833
2986
|
}
|
|
2987
|
+
function formatMarkdownTable(headers, rows) {
|
|
2988
|
+
const widths = headers.map((header, index) => {
|
|
2989
|
+
const candidates = rows.map((row) => row[index] ?? "");
|
|
2990
|
+
return Math.max(header.length, ...candidates.map((item) => item.length));
|
|
2991
|
+
});
|
|
2992
|
+
const formatRow = (cells) => {
|
|
2993
|
+
const padded = cells.map(
|
|
2994
|
+
(cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)
|
|
2995
|
+
);
|
|
2996
|
+
return `| ${padded.join(" | ")} |`;
|
|
2997
|
+
};
|
|
2998
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
2999
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)];
|
|
3000
|
+
}
|
|
2834
3001
|
function toSortedArray2(values) {
|
|
2835
3002
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2836
3003
|
}
|
|
@@ -2841,6 +3008,27 @@ function mapToSortedRecord(values) {
|
|
|
2841
3008
|
}
|
|
2842
3009
|
return record2;
|
|
2843
3010
|
}
|
|
3011
|
+
function mapToSpecContractRecord(values) {
|
|
3012
|
+
const record2 = {};
|
|
3013
|
+
for (const [key, entry] of values.entries()) {
|
|
3014
|
+
record2[key] = {
|
|
3015
|
+
status: entry.status,
|
|
3016
|
+
ids: toSortedArray2(entry.ids)
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
return record2;
|
|
3020
|
+
}
|
|
3021
|
+
function normalizeScSources(root, sources) {
|
|
3022
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
3023
|
+
for (const [id, files] of sources.entries()) {
|
|
3024
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
3025
|
+
for (const file of files) {
|
|
3026
|
+
mapped.add(toRelativePath(root, file));
|
|
3027
|
+
}
|
|
3028
|
+
normalized.set(id, mapped);
|
|
3029
|
+
}
|
|
3030
|
+
return normalized;
|
|
3031
|
+
}
|
|
2844
3032
|
function buildHotspots(issues) {
|
|
2845
3033
|
const map = /* @__PURE__ */ new Map();
|
|
2846
3034
|
for (const issue7 of issues) {
|
|
@@ -2868,6 +3056,7 @@ export {
|
|
|
2868
3056
|
extractAllIds,
|
|
2869
3057
|
extractIds,
|
|
2870
3058
|
extractInvalidIds,
|
|
3059
|
+
findConfigRoot,
|
|
2871
3060
|
formatReportJson,
|
|
2872
3061
|
formatReportMarkdown,
|
|
2873
3062
|
getConfigPath,
|