qfai 0.4.9 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/assets/init/.qfai/README.md +9 -0
- package/assets/init/.qfai/contracts/README.md +2 -2
- package/assets/init/.qfai/promptpack/commands/implement.md +2 -0
- package/assets/init/.qfai/promptpack/commands/plan.md +2 -0
- package/assets/init/.qfai/promptpack/commands/review.md +1 -0
- package/assets/init/.qfai/promptpack/steering/traceability.md +8 -2
- package/assets/init/.qfai/prompts/README.md +16 -0
- package/assets/init/.qfai/prompts/qfai-classify-change.md +33 -0
- package/assets/init/.qfai/prompts/qfai-maintain-contracts.md +35 -0
- package/assets/init/.qfai/prompts/qfai-maintain-traceability.md +36 -0
- package/assets/init/.qfai/specs/README.md +3 -1
- package/assets/init/.qfai/specs/spec-0001/scenario.md +2 -1
- package/assets/init/root/qfai.config.yaml +1 -1
- package/dist/cli/index.cjs +551 -211
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +556 -216
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +364 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +368 -160
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/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.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,12 +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(
|
|
2449
|
-
const specContractRefs = await collectSpecContractRefs(specFiles);
|
|
2579
|
+
const contractIndex = await buildContractIndex(resolvedRoot, config);
|
|
2450
2580
|
const contractIdList = Array.from(contractIndex.ids);
|
|
2581
|
+
const specContractRefs = await collectSpecContractRefs(
|
|
2582
|
+
specFiles,
|
|
2583
|
+
contractIdList
|
|
2584
|
+
);
|
|
2451
2585
|
const referencedContracts = /* @__PURE__ */ new Set();
|
|
2452
|
-
for (const
|
|
2453
|
-
ids.forEach((id) => referencedContracts.add(id));
|
|
2586
|
+
for (const entry of specContractRefs.specToContracts.values()) {
|
|
2587
|
+
entry.ids.forEach((id) => referencedContracts.add(id));
|
|
2454
2588
|
}
|
|
2455
2589
|
const referencedContractCount = contractIdList.filter(
|
|
2456
2590
|
(id) => referencedContracts.has(id)
|
|
@@ -2459,8 +2593,8 @@ async function createReportData(root, validation, configResult) {
|
|
|
2459
2593
|
(id) => !referencedContracts.has(id)
|
|
2460
2594
|
).length;
|
|
2461
2595
|
const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
|
|
2462
|
-
const
|
|
2463
|
-
specContractRefs.
|
|
2596
|
+
const specToContractsRecord = mapToSpecContractRecord(
|
|
2597
|
+
specContractRefs.specToContracts
|
|
2464
2598
|
);
|
|
2465
2599
|
const idsByPrefix = await collectIds([
|
|
2466
2600
|
...specFiles,
|
|
@@ -2478,24 +2612,26 @@ async function createReportData(root, validation, configResult) {
|
|
|
2478
2612
|
srcRoot,
|
|
2479
2613
|
testsRoot
|
|
2480
2614
|
);
|
|
2481
|
-
const
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
2615
|
+
const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
|
|
2616
|
+
const normalizedValidation = normalizeValidationResult(
|
|
2617
|
+
resolvedRoot,
|
|
2618
|
+
resolvedValidationRaw
|
|
2486
2619
|
);
|
|
2487
|
-
const scCoverage =
|
|
2488
|
-
const testFiles =
|
|
2620
|
+
const scCoverage = normalizedValidation.traceability.sc;
|
|
2621
|
+
const testFiles = normalizedValidation.traceability.testFiles;
|
|
2489
2622
|
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2490
|
-
const scSourceRecord = mapToSortedRecord(
|
|
2491
|
-
|
|
2623
|
+
const scSourceRecord = mapToSortedRecord(
|
|
2624
|
+
normalizeScSources(resolvedRoot, scSources)
|
|
2625
|
+
);
|
|
2492
2626
|
const version = await resolveToolVersion();
|
|
2627
|
+
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
2628
|
+
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
2493
2629
|
return {
|
|
2494
2630
|
tool: "qfai",
|
|
2495
2631
|
version,
|
|
2496
2632
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2497
|
-
root,
|
|
2498
|
-
configPath,
|
|
2633
|
+
root: displayRoot,
|
|
2634
|
+
configPath: displayConfigPath,
|
|
2499
2635
|
summary: {
|
|
2500
2636
|
specs: specFiles.length,
|
|
2501
2637
|
scenarios: scenarioFiles.length,
|
|
@@ -2504,7 +2640,7 @@ async function createReportData(root, validation, configResult) {
|
|
|
2504
2640
|
ui: uiFiles.length,
|
|
2505
2641
|
db: dbFiles.length
|
|
2506
2642
|
},
|
|
2507
|
-
counts:
|
|
2643
|
+
counts: normalizedValidation.counts
|
|
2508
2644
|
},
|
|
2509
2645
|
ids: {
|
|
2510
2646
|
spec: idsByPrefix.SPEC,
|
|
@@ -2528,21 +2664,24 @@ async function createReportData(root, validation, configResult) {
|
|
|
2528
2664
|
},
|
|
2529
2665
|
specs: {
|
|
2530
2666
|
contractRefMissing: specContractRefs.missingRefSpecs.size,
|
|
2531
|
-
|
|
2667
|
+
missingRefSpecs: toSortedArray2(specContractRefs.missingRefSpecs),
|
|
2668
|
+
specToContracts: specToContractsRecord
|
|
2532
2669
|
}
|
|
2533
2670
|
},
|
|
2534
|
-
issues:
|
|
2671
|
+
issues: normalizedValidation.issues
|
|
2535
2672
|
};
|
|
2536
2673
|
}
|
|
2537
2674
|
function formatReportMarkdown(data) {
|
|
2538
2675
|
const lines = [];
|
|
2539
2676
|
lines.push("# QFAI Report");
|
|
2677
|
+
lines.push("");
|
|
2540
2678
|
lines.push(`- \u751F\u6210\u65E5\u6642: ${data.generatedAt}`);
|
|
2541
2679
|
lines.push(`- \u30EB\u30FC\u30C8: ${data.root}`);
|
|
2542
2680
|
lines.push(`- \u8A2D\u5B9A: ${data.configPath}`);
|
|
2543
2681
|
lines.push(`- \u7248: ${data.version}`);
|
|
2544
2682
|
lines.push("");
|
|
2545
2683
|
lines.push("## \u6982\u8981");
|
|
2684
|
+
lines.push("");
|
|
2546
2685
|
lines.push(`- specs: ${data.summary.specs}`);
|
|
2547
2686
|
lines.push(`- scenarios: ${data.summary.scenarios}`);
|
|
2548
2687
|
lines.push(
|
|
@@ -2553,6 +2692,7 @@ function formatReportMarkdown(data) {
|
|
|
2553
2692
|
);
|
|
2554
2693
|
lines.push("");
|
|
2555
2694
|
lines.push("## ID\u96C6\u8A08");
|
|
2695
|
+
lines.push("");
|
|
2556
2696
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
2557
2697
|
lines.push(formatIdLine("BR", data.ids.br));
|
|
2558
2698
|
lines.push(formatIdLine("SC", data.ids.sc));
|
|
@@ -2561,12 +2701,14 @@ function formatReportMarkdown(data) {
|
|
|
2561
2701
|
lines.push(formatIdLine("DB", data.ids.db));
|
|
2562
2702
|
lines.push("");
|
|
2563
2703
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3");
|
|
2704
|
+
lines.push("");
|
|
2564
2705
|
lines.push(`- \u4E0A\u6D41ID\u691C\u51FA\u6570: ${data.traceability.upstreamIdsFound}`);
|
|
2565
2706
|
lines.push(
|
|
2566
2707
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2567
2708
|
);
|
|
2568
2709
|
lines.push("");
|
|
2569
2710
|
lines.push("## \u5951\u7D04\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2711
|
+
lines.push("");
|
|
2570
2712
|
lines.push(`- total: ${data.traceability.contracts.total}`);
|
|
2571
2713
|
lines.push(`- referenced: ${data.traceability.contracts.referenced}`);
|
|
2572
2714
|
lines.push(`- orphan: ${data.traceability.contracts.orphan}`);
|
|
@@ -2575,6 +2717,7 @@ function formatReportMarkdown(data) {
|
|
|
2575
2717
|
);
|
|
2576
2718
|
lines.push("");
|
|
2577
2719
|
lines.push("## \u5951\u7D04\u2192Spec");
|
|
2720
|
+
lines.push("");
|
|
2578
2721
|
const contractToSpecs = data.traceability.contracts.idToSpecs;
|
|
2579
2722
|
const contractIds = Object.keys(contractToSpecs).sort(
|
|
2580
2723
|
(a, b) => a.localeCompare(b)
|
|
@@ -2593,24 +2736,36 @@ function formatReportMarkdown(data) {
|
|
|
2593
2736
|
}
|
|
2594
2737
|
lines.push("");
|
|
2595
2738
|
lines.push("## Spec\u2192\u5951\u7D04");
|
|
2596
|
-
|
|
2739
|
+
lines.push("");
|
|
2740
|
+
const specToContracts = data.traceability.specs.specToContracts;
|
|
2597
2741
|
const specIds = Object.keys(specToContracts).sort(
|
|
2598
2742
|
(a, b) => a.localeCompare(b)
|
|
2599
2743
|
);
|
|
2600
2744
|
if (specIds.length === 0) {
|
|
2601
2745
|
lines.push("- (none)");
|
|
2602
2746
|
} else {
|
|
2603
|
-
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
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));
|
|
2754
|
+
}
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
lines.push("## Spec\u3067 contract-ref \u672A\u5BA3\u8A00");
|
|
2757
|
+
lines.push("");
|
|
2758
|
+
const missingRefSpecs = data.traceability.specs.missingRefSpecs;
|
|
2759
|
+
if (missingRefSpecs.length === 0) {
|
|
2760
|
+
lines.push("- (none)");
|
|
2761
|
+
} else {
|
|
2762
|
+
for (const specId of missingRefSpecs) {
|
|
2763
|
+
lines.push(`- ${specId}`);
|
|
2610
2764
|
}
|
|
2611
2765
|
}
|
|
2612
2766
|
lines.push("");
|
|
2613
2767
|
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2768
|
+
lines.push("");
|
|
2614
2769
|
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2615
2770
|
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2616
2771
|
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
@@ -2640,6 +2795,7 @@ function formatReportMarkdown(data) {
|
|
|
2640
2795
|
}
|
|
2641
2796
|
lines.push("");
|
|
2642
2797
|
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2798
|
+
lines.push("");
|
|
2643
2799
|
const scRefs = data.traceability.sc.refs;
|
|
2644
2800
|
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2645
2801
|
if (scIds.length === 0) {
|
|
@@ -2656,6 +2812,7 @@ function formatReportMarkdown(data) {
|
|
|
2656
2812
|
}
|
|
2657
2813
|
lines.push("");
|
|
2658
2814
|
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2815
|
+
lines.push("");
|
|
2659
2816
|
const specScIssues = data.issues.filter(
|
|
2660
2817
|
(item) => item.code === "QFAI-TRACE-012"
|
|
2661
2818
|
);
|
|
@@ -2670,6 +2827,7 @@ function formatReportMarkdown(data) {
|
|
|
2670
2827
|
}
|
|
2671
2828
|
lines.push("");
|
|
2672
2829
|
lines.push("## Hotspots");
|
|
2830
|
+
lines.push("");
|
|
2673
2831
|
const hotspots = buildHotspots(data.issues);
|
|
2674
2832
|
if (hotspots.length === 0) {
|
|
2675
2833
|
lines.push("- (none)");
|
|
@@ -2682,6 +2840,7 @@ function formatReportMarkdown(data) {
|
|
|
2682
2840
|
}
|
|
2683
2841
|
lines.push("");
|
|
2684
2842
|
lines.push("## \u30C8\u30EC\u30FC\u30B5\u30D3\u30EA\u30C6\u30A3\uFF08\u691C\u8A3C\uFF09");
|
|
2843
|
+
lines.push("");
|
|
2685
2844
|
const traceIssues = data.issues.filter(
|
|
2686
2845
|
(item) => item.rule?.startsWith("traceability.") || item.code.startsWith("QFAI_TRACE") || item.code.startsWith("QFAI-TRACE-")
|
|
2687
2846
|
);
|
|
@@ -2697,6 +2856,7 @@ function formatReportMarkdown(data) {
|
|
|
2697
2856
|
}
|
|
2698
2857
|
lines.push("");
|
|
2699
2858
|
lines.push("## \u691C\u8A3C\u7D50\u679C");
|
|
2859
|
+
lines.push("");
|
|
2700
2860
|
if (data.issues.length === 0) {
|
|
2701
2861
|
lines.push("- (none)");
|
|
2702
2862
|
} else {
|
|
@@ -2713,29 +2873,41 @@ function formatReportMarkdown(data) {
|
|
|
2713
2873
|
function formatReportJson(data) {
|
|
2714
2874
|
return JSON.stringify(data, null, 2);
|
|
2715
2875
|
}
|
|
2716
|
-
async function collectSpecContractRefs(specFiles) {
|
|
2717
|
-
const
|
|
2876
|
+
async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
2877
|
+
const specToContracts = /* @__PURE__ */ new Map();
|
|
2718
2878
|
const idToSpecs = /* @__PURE__ */ new Map();
|
|
2719
2879
|
const missingRefSpecs = /* @__PURE__ */ new Set();
|
|
2880
|
+
for (const contractId of contractIdList) {
|
|
2881
|
+
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
2882
|
+
}
|
|
2720
2883
|
for (const file of specFiles) {
|
|
2721
2884
|
const text = await readFile11(file, "utf-8");
|
|
2722
2885
|
const parsed = parseSpec(text, file);
|
|
2723
|
-
const specKey = parsed.specId
|
|
2886
|
+
const specKey = parsed.specId;
|
|
2887
|
+
if (!specKey) {
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2724
2890
|
const refs = parsed.contractRefs;
|
|
2725
2891
|
if (refs.lines.length === 0) {
|
|
2726
2892
|
missingRefSpecs.add(specKey);
|
|
2893
|
+
specToContracts.set(specKey, { status: "missing", ids: /* @__PURE__ */ new Set() });
|
|
2894
|
+
continue;
|
|
2727
2895
|
}
|
|
2728
|
-
const
|
|
2896
|
+
const current = specToContracts.get(specKey) ?? {
|
|
2897
|
+
status: "declared",
|
|
2898
|
+
ids: /* @__PURE__ */ new Set()
|
|
2899
|
+
};
|
|
2729
2900
|
for (const id of refs.ids) {
|
|
2730
|
-
|
|
2731
|
-
const specs = idToSpecs.get(id)
|
|
2732
|
-
specs
|
|
2733
|
-
|
|
2901
|
+
current.ids.add(id);
|
|
2902
|
+
const specs = idToSpecs.get(id);
|
|
2903
|
+
if (specs) {
|
|
2904
|
+
specs.add(specKey);
|
|
2905
|
+
}
|
|
2734
2906
|
}
|
|
2735
|
-
|
|
2907
|
+
specToContracts.set(specKey, current);
|
|
2736
2908
|
}
|
|
2737
2909
|
return {
|
|
2738
|
-
|
|
2910
|
+
specToContracts,
|
|
2739
2911
|
idToSpecs,
|
|
2740
2912
|
missingRefSpecs
|
|
2741
2913
|
};
|
|
@@ -2812,6 +2984,20 @@ function formatList(values) {
|
|
|
2812
2984
|
}
|
|
2813
2985
|
return values.join(", ");
|
|
2814
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
|
+
}
|
|
2815
3001
|
function toSortedArray2(values) {
|
|
2816
3002
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2817
3003
|
}
|
|
@@ -2822,6 +3008,27 @@ function mapToSortedRecord(values) {
|
|
|
2822
3008
|
}
|
|
2823
3009
|
return record2;
|
|
2824
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
|
+
}
|
|
2825
3032
|
function buildHotspots(issues) {
|
|
2826
3033
|
const map = /* @__PURE__ */ new Map();
|
|
2827
3034
|
for (const issue7 of issues) {
|
|
@@ -2849,6 +3056,7 @@ export {
|
|
|
2849
3056
|
extractAllIds,
|
|
2850
3057
|
extractIds,
|
|
2851
3058
|
extractInvalidIds,
|
|
3059
|
+
findConfigRoot,
|
|
2852
3060
|
formatReportJson,
|
|
2853
3061
|
formatReportMarkdown,
|
|
2854
3062
|
getConfigPath,
|