qfai 1.2.13 → 1.3.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 +4 -4
- package/assets/init/.qfai/assistant/agents/atdd-api-implementer.md +47 -0
- package/assets/init/.qfai/assistant/agents/atdd-e2e-implementer.md +46 -0
- package/assets/init/.qfai/assistant/agents/atdd-integration-implementer.md +47 -0
- package/assets/init/.qfai/assistant/agents/doc-steward.md +38 -0
- package/assets/init/.qfai/assistant/agents/orchestrator.md +45 -0
- package/assets/init/.qfai/assistant/agents/researcher.md +1 -0
- package/assets/init/.qfai/assistant/agents/reviewer.md +43 -0
- package/assets/init/.qfai/assistant/agents/runtime-gatekeeper.md +2 -1
- package/assets/init/.qfai/assistant/agents/test-engineer.md +1 -1
- package/assets/init/.qfai/assistant/agents/test-volume-estimator.md +46 -0
- package/assets/init/.qfai/assistant/instructions/agent-selection.md +6 -0
- package/assets/init/.qfai/assistant/instructions/workflow.md +16 -0
- package/assets/init/.qfai/assistant/prompts/qfai-atdd.md +112 -20
- package/assets/init/.qfai/assistant/prompts/qfai-discuss.md +25 -0
- package/assets/init/.qfai/assistant/prompts/qfai-prototyping.md +30 -1
- package/assets/init/.qfai/assistant/prompts/qfai-require.md +24 -0
- package/assets/init/.qfai/assistant/prompts/qfai-spec.md +41 -0
- package/assets/init/.qfai/assistant/prompts/qfai-tdd-green.md +48 -12
- package/assets/init/.qfai/assistant/prompts/qfai-tdd-red.md +38 -2
- package/assets/init/.qfai/assistant/prompts/qfai-tdd-refactor.md +40 -3
- package/assets/init/.qfai/evidence/README.md +2 -1
- package/assets/init/.qfai/specs/README.md +17 -5
- package/assets/init/root/.claude/agents/atdd-api-implementer.md +17 -0
- package/assets/init/root/.claude/agents/atdd-e2e-implementer.md +17 -0
- package/assets/init/root/.claude/agents/atdd-integration-implementer.md +17 -0
- package/assets/init/root/.claude/agents/doc-steward.md +17 -0
- package/assets/init/root/.claude/agents/orchestrator.md +17 -0
- package/assets/init/root/.claude/agents/researcher.md +17 -0
- package/assets/init/root/.claude/agents/reviewer.md +17 -0
- package/assets/init/root/.claude/agents/test-volume-estimator.md +17 -0
- package/dist/cli/index.cjs +465 -55
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +442 -32
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +415 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.mjs +398 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1217,8 +1217,8 @@ function isValidId(value, prefix) {
|
|
|
1217
1217
|
}
|
|
1218
1218
|
|
|
1219
1219
|
// src/core/report.ts
|
|
1220
|
-
var
|
|
1221
|
-
var
|
|
1220
|
+
var import_promises19 = require("fs/promises");
|
|
1221
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
1222
1222
|
|
|
1223
1223
|
// src/core/contractIndex.ts
|
|
1224
1224
|
var import_promises6 = require("fs/promises");
|
|
@@ -1952,8 +1952,8 @@ var import_promises8 = require("fs/promises");
|
|
|
1952
1952
|
var import_node_path9 = __toESM(require("path"), 1);
|
|
1953
1953
|
var import_node_url = require("url");
|
|
1954
1954
|
async function resolveToolVersion() {
|
|
1955
|
-
if ("1.
|
|
1956
|
-
return "1.
|
|
1955
|
+
if ("1.3.0".length > 0) {
|
|
1956
|
+
return "1.3.0";
|
|
1957
1957
|
}
|
|
1958
1958
|
try {
|
|
1959
1959
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -2781,6 +2781,24 @@ async function validateCaseCatalogues(root, config) {
|
|
|
2781
2781
|
// src/core/validators/delta.ts
|
|
2782
2782
|
var import_promises11 = require("fs/promises");
|
|
2783
2783
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
2784
|
+
var CHANGE_TYPE_PRIMARY_RE = /^\s*[-*]?\s*change_type_primary\s*:\s*(.+)\s*$/im;
|
|
2785
|
+
var CHANGE_TYPE_TAGS_RE = /^\s*[-*]?\s*change_type_tags\s*:\s*(.*)\s*$/im;
|
|
2786
|
+
var DO_NOT_RE = /^\s*[-*]?\s*do_not\s*:/im;
|
|
2787
|
+
var TEMPTATION_RE = /^\s*[-*]?\s*temptation\s*:/im;
|
|
2788
|
+
var ALLOWED_CHANGE_TYPE_PRIMARY = /* @__PURE__ */ new Set([
|
|
2789
|
+
"initial",
|
|
2790
|
+
"behavior",
|
|
2791
|
+
"structural",
|
|
2792
|
+
"ops"
|
|
2793
|
+
]);
|
|
2794
|
+
var ALLOWED_CHANGE_TYPE_TAGS = /* @__PURE__ */ new Set([
|
|
2795
|
+
"@ui",
|
|
2796
|
+
"@api",
|
|
2797
|
+
"@db",
|
|
2798
|
+
"@nfr",
|
|
2799
|
+
"@docs",
|
|
2800
|
+
"@test"
|
|
2801
|
+
]);
|
|
2784
2802
|
async function validateDeltas(root, config) {
|
|
2785
2803
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2786
2804
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
@@ -2842,10 +2860,8 @@ async function validateDeltas(root, config) {
|
|
|
2842
2860
|
)
|
|
2843
2861
|
);
|
|
2844
2862
|
} else {
|
|
2845
|
-
const
|
|
2846
|
-
|
|
2847
|
-
);
|
|
2848
|
-
if (!hasRejected) {
|
|
2863
|
+
const rejectedBlocks = extractRejectedBlocks(decisionRecords.body);
|
|
2864
|
+
if (rejectedBlocks.length === 0) {
|
|
2849
2865
|
issues.push(
|
|
2850
2866
|
issue(
|
|
2851
2867
|
"QFAI-DELTA-101",
|
|
@@ -2858,6 +2874,28 @@ async function validateDeltas(root, config) {
|
|
|
2858
2874
|
"rejected \u3092\u6700\u4F4E1\u4EF6\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2859
2875
|
)
|
|
2860
2876
|
);
|
|
2877
|
+
} else {
|
|
2878
|
+
const hasDoNot = rejectedBlocks.some((block) => DO_NOT_RE.test(block));
|
|
2879
|
+
const hasTemptation = rejectedBlocks.some(
|
|
2880
|
+
(block) => TEMPTATION_RE.test(block)
|
|
2881
|
+
);
|
|
2882
|
+
if (!hasDoNot || !hasTemptation) {
|
|
2883
|
+
const missing = [];
|
|
2884
|
+
if (!hasDoNot) missing.push("do_not");
|
|
2885
|
+
if (!hasTemptation) missing.push("temptation");
|
|
2886
|
+
issues.push(
|
|
2887
|
+
issue(
|
|
2888
|
+
"QFAI-DELTA-204",
|
|
2889
|
+
`Decision Records \u306B ${missing.join(" / ")} \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`,
|
|
2890
|
+
"warning",
|
|
2891
|
+
deltaPath,
|
|
2892
|
+
"delta.rejectedDetails",
|
|
2893
|
+
void 0,
|
|
2894
|
+
"change",
|
|
2895
|
+
"rejected \u306B\u306F do_not / temptation \u3092\u6700\u4F4E1\u4EF6\u542B\u3081\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2896
|
+
)
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2861
2899
|
}
|
|
2862
2900
|
}
|
|
2863
2901
|
if (changeLog && decisionRecords) {
|
|
@@ -2876,6 +2914,54 @@ async function validateDeltas(root, config) {
|
|
|
2876
2914
|
);
|
|
2877
2915
|
}
|
|
2878
2916
|
}
|
|
2917
|
+
if (changeLog) {
|
|
2918
|
+
const blocks = extractChangeLogBlocks(text, changeLog);
|
|
2919
|
+
for (const block of blocks) {
|
|
2920
|
+
const primary = extractChangeTypePrimary(block);
|
|
2921
|
+
if (!primary) {
|
|
2922
|
+
issues.push(
|
|
2923
|
+
issue(
|
|
2924
|
+
"QFAI-DELTA-201",
|
|
2925
|
+
"Change Log \u306E CL \u30D6\u30ED\u30C3\u30AF\u306B change_type_primary \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
2926
|
+
"warning",
|
|
2927
|
+
deltaPath,
|
|
2928
|
+
"delta.changeTypePrimary",
|
|
2929
|
+
void 0,
|
|
2930
|
+
"change",
|
|
2931
|
+
"change_type_primary \u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2932
|
+
)
|
|
2933
|
+
);
|
|
2934
|
+
} else if (!isAllowedChangeTypePrimary(primary)) {
|
|
2935
|
+
issues.push(
|
|
2936
|
+
issue(
|
|
2937
|
+
"QFAI-DELTA-202",
|
|
2938
|
+
`change_type_primary \u304C\u4E0D\u6B63\u3067\u3059: ${primary}`,
|
|
2939
|
+
"warning",
|
|
2940
|
+
deltaPath,
|
|
2941
|
+
"delta.changeTypePrimary",
|
|
2942
|
+
void 0,
|
|
2943
|
+
"change",
|
|
2944
|
+
"change_type_primary \u306F Initial | Behavior | Structural | Ops \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2945
|
+
)
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
const invalidTags = extractInvalidChangeTypeTags(block);
|
|
2949
|
+
if (invalidTags && invalidTags.length > 0) {
|
|
2950
|
+
issues.push(
|
|
2951
|
+
issue(
|
|
2952
|
+
"QFAI-DELTA-203",
|
|
2953
|
+
`change_type_tags \u306B\u7121\u52B9\u306A\u30BF\u30B0\u304C\u3042\u308A\u307E\u3059: ${invalidTags.join(", ")}`,
|
|
2954
|
+
"warning",
|
|
2955
|
+
deltaPath,
|
|
2956
|
+
"delta.changeTypeTags",
|
|
2957
|
+
void 0,
|
|
2958
|
+
"change",
|
|
2959
|
+
"change_type_tags \u306F @ui @api @db @nfr @docs @test \u306E\u307F\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2960
|
+
)
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2879
2965
|
}
|
|
2880
2966
|
return issues;
|
|
2881
2967
|
}
|
|
@@ -2888,6 +2974,98 @@ function findSection(sections, title) {
|
|
|
2888
2974
|
}
|
|
2889
2975
|
return null;
|
|
2890
2976
|
}
|
|
2977
|
+
function extractChangeLogBlocks(text, changeLog) {
|
|
2978
|
+
const lines = text.split(/\r?\n/);
|
|
2979
|
+
const headings = parseHeadings(text).filter(
|
|
2980
|
+
(heading) => heading.level === 3 && heading.line >= changeLog.startLine && heading.line <= changeLog.endLine
|
|
2981
|
+
);
|
|
2982
|
+
if (headings.length === 0) {
|
|
2983
|
+
return [changeLog.body];
|
|
2984
|
+
}
|
|
2985
|
+
const blocks = [];
|
|
2986
|
+
for (let i = 0; i < headings.length; i += 1) {
|
|
2987
|
+
const current = headings[i];
|
|
2988
|
+
if (!current) continue;
|
|
2989
|
+
const next = headings[i + 1];
|
|
2990
|
+
const startLine = current.line + 1;
|
|
2991
|
+
const endLine = Math.min(
|
|
2992
|
+
changeLog.endLine,
|
|
2993
|
+
(next?.line ?? changeLog.endLine + 1) - 1
|
|
2994
|
+
);
|
|
2995
|
+
if (startLine > endLine) {
|
|
2996
|
+
blocks.push("");
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
blocks.push(lines.slice(startLine - 1, endLine).join("\n"));
|
|
3000
|
+
}
|
|
3001
|
+
return blocks;
|
|
3002
|
+
}
|
|
3003
|
+
function extractChangeTypePrimary(block) {
|
|
3004
|
+
const match = block.match(CHANGE_TYPE_PRIMARY_RE);
|
|
3005
|
+
const value = match?.[1]?.trim() ?? "";
|
|
3006
|
+
return value.length > 0 ? value : null;
|
|
3007
|
+
}
|
|
3008
|
+
function extractRejectedBlocks(sectionBody) {
|
|
3009
|
+
const lines = sectionBody.split(/\r?\n/);
|
|
3010
|
+
const blocks = [];
|
|
3011
|
+
let currentIndent = null;
|
|
3012
|
+
let buffer = [];
|
|
3013
|
+
const flush = () => {
|
|
3014
|
+
if (currentIndent === null) {
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
blocks.push(buffer.join("\n"));
|
|
3018
|
+
buffer = [];
|
|
3019
|
+
currentIndent = null;
|
|
3020
|
+
};
|
|
3021
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3022
|
+
const line = lines[i] ?? "";
|
|
3023
|
+
const match = line.match(/^(\s*)(?:[-*]\s*)?rejected\s*:(.*)$/i);
|
|
3024
|
+
if (match) {
|
|
3025
|
+
flush();
|
|
3026
|
+
const inlineValue = (match[2] ?? "").trim();
|
|
3027
|
+
if (inlineValue.length > 0) {
|
|
3028
|
+
blocks.push(inlineValue);
|
|
3029
|
+
currentIndent = null;
|
|
3030
|
+
} else {
|
|
3031
|
+
currentIndent = (match[1] ?? "").length;
|
|
3032
|
+
}
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
3035
|
+
if (currentIndent === null) {
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
3039
|
+
const indent = (indentMatch?.[1] ?? "").length;
|
|
3040
|
+
if (line.trim().length > 0 && indent <= currentIndent) {
|
|
3041
|
+
flush();
|
|
3042
|
+
i -= 1;
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
buffer.push(line);
|
|
3046
|
+
}
|
|
3047
|
+
flush();
|
|
3048
|
+
return blocks;
|
|
3049
|
+
}
|
|
3050
|
+
function extractInvalidChangeTypeTags(block) {
|
|
3051
|
+
const match = block.match(CHANGE_TYPE_TAGS_RE);
|
|
3052
|
+
if (!match) {
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
const raw = match[1]?.trim() ?? "";
|
|
3056
|
+
if (raw.length === 0) {
|
|
3057
|
+
return [];
|
|
3058
|
+
}
|
|
3059
|
+
const tags = raw.split(/[\s,]+/).map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
3060
|
+
const invalid = tags.filter((tag) => !isAllowedChangeTypeTag(tag));
|
|
3061
|
+
return invalid;
|
|
3062
|
+
}
|
|
3063
|
+
function isAllowedChangeTypePrimary(value) {
|
|
3064
|
+
return ALLOWED_CHANGE_TYPE_PRIMARY.has(value.trim().toLowerCase());
|
|
3065
|
+
}
|
|
3066
|
+
function isAllowedChangeTypeTag(value) {
|
|
3067
|
+
return ALLOWED_CHANGE_TYPE_TAGS.has(value.trim().toLowerCase());
|
|
3068
|
+
}
|
|
2891
3069
|
|
|
2892
3070
|
// src/core/validators/ids.ts
|
|
2893
3071
|
var import_promises12 = require("fs/promises");
|
|
@@ -3709,12 +3887,45 @@ function validateSpecContent(text, file, requiredSections) {
|
|
|
3709
3887
|
return issues;
|
|
3710
3888
|
}
|
|
3711
3889
|
|
|
3712
|
-
// src/core/validators/
|
|
3890
|
+
// src/core/validators/atddLedger.ts
|
|
3713
3891
|
var import_promises16 = require("fs/promises");
|
|
3892
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
3893
|
+
async function validateAtddCoverageLedgers(root, config) {
|
|
3894
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
3895
|
+
const entries = await collectSpecEntries(specsRoot);
|
|
3896
|
+
if (entries.length === 0) {
|
|
3897
|
+
return [];
|
|
3898
|
+
}
|
|
3899
|
+
const issues = [];
|
|
3900
|
+
for (const entry of entries) {
|
|
3901
|
+
const ledgerPath = import_node_path17.default.join(entry.dir, "atdd", "coverage-ledger.md");
|
|
3902
|
+
try {
|
|
3903
|
+
await (0, import_promises16.access)(ledgerPath);
|
|
3904
|
+
} catch (error) {
|
|
3905
|
+
if (isMissingFileError2(error)) {
|
|
3906
|
+
issues.push(
|
|
3907
|
+
issue(
|
|
3908
|
+
"QFAI-ATDD-001",
|
|
3909
|
+
"ATDD coverage-ledger.md \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
3910
|
+
"warning",
|
|
3911
|
+
ledgerPath,
|
|
3912
|
+
"atddLedger.exists"
|
|
3913
|
+
)
|
|
3914
|
+
);
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
throw error;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
return issues;
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
// src/core/validators/traceability.ts
|
|
3924
|
+
var import_promises17 = require("fs/promises");
|
|
3714
3925
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
3715
3926
|
var BR_TAG_RE2 = /^BR-\d{4}-\d{4}$/;
|
|
3716
3927
|
var SAMPLE_LIMIT = 20;
|
|
3717
|
-
async function validateTraceability(root, config) {
|
|
3928
|
+
async function validateTraceability(root, config, phase) {
|
|
3718
3929
|
const issues = [];
|
|
3719
3930
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
3720
3931
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
@@ -3735,7 +3946,7 @@ async function validateTraceability(root, config) {
|
|
|
3735
3946
|
const contractIndex = await buildContractIndex(root, config);
|
|
3736
3947
|
const contractIds = contractIndex.ids;
|
|
3737
3948
|
for (const file of specFiles) {
|
|
3738
|
-
const text = await (0,
|
|
3949
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3739
3950
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
3740
3951
|
const parsed = parseSpec(text, file);
|
|
3741
3952
|
if (parsed.specId) {
|
|
@@ -3808,7 +4019,7 @@ async function validateTraceability(root, config) {
|
|
|
3808
4019
|
}
|
|
3809
4020
|
}
|
|
3810
4021
|
for (const file of scenarioFiles) {
|
|
3811
|
-
const text = await (0,
|
|
4022
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
3812
4023
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
3813
4024
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
3814
4025
|
allowCommentPrefix: true
|
|
@@ -4144,18 +4355,22 @@ async function validateTraceability(root, config) {
|
|
|
4144
4355
|
);
|
|
4145
4356
|
} else {
|
|
4146
4357
|
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
4358
|
+
const ignoredLayers = phase === "atdd" ? /* @__PURE__ */ new Set(["unit", "component"]) : /* @__PURE__ */ new Set();
|
|
4147
4359
|
const enforcedLayers = /* @__PURE__ */ new Set();
|
|
4148
4360
|
for (const [scId, refs] of scTestRefs.entries()) {
|
|
4149
4361
|
if (!refs || refs.size === 0) {
|
|
4150
4362
|
continue;
|
|
4151
4363
|
}
|
|
4152
4364
|
const layer = scIdToLayer.get(scId);
|
|
4153
|
-
if (layer) {
|
|
4365
|
+
if (layer && !ignoredLayers.has(layer)) {
|
|
4154
4366
|
enforcedLayers.add(layer);
|
|
4155
4367
|
}
|
|
4156
4368
|
}
|
|
4157
4369
|
const deferredLayers = [];
|
|
4158
4370
|
for (const [layer, scIds] of layerToScIds.entries()) {
|
|
4371
|
+
if (ignoredLayers.has(layer)) {
|
|
4372
|
+
continue;
|
|
4373
|
+
}
|
|
4159
4374
|
const missing = Array.from(scIds).filter((id) => {
|
|
4160
4375
|
const refs = scTestRefs.get(id);
|
|
4161
4376
|
return !refs || refs.size === 0;
|
|
@@ -4244,7 +4459,7 @@ async function collectScenarioSpecInfo(entries) {
|
|
|
4244
4459
|
for (const entry of entries) {
|
|
4245
4460
|
let specText = "";
|
|
4246
4461
|
try {
|
|
4247
|
-
specText = await (0,
|
|
4462
|
+
specText = await (0, import_promises17.readFile)(entry.specPath, "utf-8");
|
|
4248
4463
|
} catch {
|
|
4249
4464
|
specText = "";
|
|
4250
4465
|
}
|
|
@@ -4289,7 +4504,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
4289
4504
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
4290
4505
|
let found = false;
|
|
4291
4506
|
for (const file of targetFiles) {
|
|
4292
|
-
const text = await (0,
|
|
4507
|
+
const text = await (0, import_promises17.readFile)(file, "utf-8");
|
|
4293
4508
|
if (pattern.test(text)) {
|
|
4294
4509
|
found = true;
|
|
4295
4510
|
break;
|
|
@@ -4314,9 +4529,139 @@ function buildIdPattern(ids) {
|
|
|
4314
4529
|
}
|
|
4315
4530
|
|
|
4316
4531
|
// src/core/validators/traceabilityMatrix.ts
|
|
4317
|
-
var
|
|
4532
|
+
var import_promises18 = require("fs/promises");
|
|
4533
|
+
|
|
4534
|
+
// src/core/traceabilityMatrix.ts
|
|
4535
|
+
var SC_ID_RE = /SC-\d{4}-\d{4}/g;
|
|
4536
|
+
function parseTraceabilityMatrixStatus(matrixText) {
|
|
4537
|
+
const lines = matrixText.split(/\r?\n/);
|
|
4538
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
4539
|
+
const headerLine = lines[i];
|
|
4540
|
+
const separatorLine = lines[i + 1];
|
|
4541
|
+
if (!headerLine || !separatorLine) {
|
|
4542
|
+
continue;
|
|
4543
|
+
}
|
|
4544
|
+
if (!headerLine.includes("|") || !separatorLine.includes("|")) {
|
|
4545
|
+
continue;
|
|
4546
|
+
}
|
|
4547
|
+
if (!isSeparatorRow(separatorLine)) {
|
|
4548
|
+
continue;
|
|
4549
|
+
}
|
|
4550
|
+
const headers = splitTableRow(headerLine);
|
|
4551
|
+
if (headers.length === 0) {
|
|
4552
|
+
continue;
|
|
4553
|
+
}
|
|
4554
|
+
const scIndex = findHeaderIndex(headers, ["SC", "Scenario"]);
|
|
4555
|
+
const statusIndex = findHeaderIndex(headers, ["Status", "status"]);
|
|
4556
|
+
if (scIndex === -1) {
|
|
4557
|
+
continue;
|
|
4558
|
+
}
|
|
4559
|
+
const statusBySc = /* @__PURE__ */ new Map();
|
|
4560
|
+
const invalidStatusValues = [];
|
|
4561
|
+
const hasStatusColumn = statusIndex !== -1;
|
|
4562
|
+
for (let row = i + 2; row < lines.length; row += 1) {
|
|
4563
|
+
const line = lines[row];
|
|
4564
|
+
if (!line || !line.includes("|")) {
|
|
4565
|
+
break;
|
|
4566
|
+
}
|
|
4567
|
+
if (isSeparatorRow(line)) {
|
|
4568
|
+
continue;
|
|
4569
|
+
}
|
|
4570
|
+
const cells = splitTableRow(line);
|
|
4571
|
+
const scCell = cells[scIndex] ?? "";
|
|
4572
|
+
const scIds = extractScIds(scCell);
|
|
4573
|
+
if (scIds.length === 0) {
|
|
4574
|
+
continue;
|
|
4575
|
+
}
|
|
4576
|
+
const rawStatus = hasStatusColumn ? cells[statusIndex] ?? "" : "";
|
|
4577
|
+
const normalizedStatus = normalizeStatus(rawStatus);
|
|
4578
|
+
if (rawStatus && !normalizedStatus) {
|
|
4579
|
+
invalidStatusValues.push(rawStatus);
|
|
4580
|
+
continue;
|
|
4581
|
+
}
|
|
4582
|
+
for (const scId of scIds) {
|
|
4583
|
+
const nextStatus = normalizedStatus ?? "implemented";
|
|
4584
|
+
const current = statusBySc.get(scId);
|
|
4585
|
+
if (current === "implemented") {
|
|
4586
|
+
continue;
|
|
4587
|
+
}
|
|
4588
|
+
if (nextStatus === "implemented") {
|
|
4589
|
+
statusBySc.set(scId, "implemented");
|
|
4590
|
+
} else if (!current) {
|
|
4591
|
+
statusBySc.set(scId, "planned");
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
return { statusBySc, invalidStatusValues, hasStatusColumn };
|
|
4596
|
+
}
|
|
4597
|
+
return {
|
|
4598
|
+
statusBySc: /* @__PURE__ */ new Map(),
|
|
4599
|
+
invalidStatusValues: [],
|
|
4600
|
+
hasStatusColumn: false
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
function splitTableRow(line) {
|
|
4604
|
+
const normalized = normalizeTableRow(line);
|
|
4605
|
+
if (!normalized) {
|
|
4606
|
+
return [];
|
|
4607
|
+
}
|
|
4608
|
+
return normalized.split("|").map((cell) => cell.trim());
|
|
4609
|
+
}
|
|
4610
|
+
function findHeaderIndex(headers, targets) {
|
|
4611
|
+
const normalized = headers.map((header) => header.toLowerCase());
|
|
4612
|
+
for (const target of targets) {
|
|
4613
|
+
const index = normalized.indexOf(target.toLowerCase());
|
|
4614
|
+
if (index !== -1) {
|
|
4615
|
+
return index;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
return -1;
|
|
4619
|
+
}
|
|
4620
|
+
function extractScIds(text) {
|
|
4621
|
+
const matches = text.match(SC_ID_RE);
|
|
4622
|
+
if (!matches) {
|
|
4623
|
+
return [];
|
|
4624
|
+
}
|
|
4625
|
+
return Array.from(new Set(matches));
|
|
4626
|
+
}
|
|
4627
|
+
function normalizeStatus(value) {
|
|
4628
|
+
const normalized = value.trim().toLowerCase();
|
|
4629
|
+
if (!normalized) {
|
|
4630
|
+
return null;
|
|
4631
|
+
}
|
|
4632
|
+
if (normalized === "implemented") {
|
|
4633
|
+
return "implemented";
|
|
4634
|
+
}
|
|
4635
|
+
if (normalized === "planned") {
|
|
4636
|
+
return "planned";
|
|
4637
|
+
}
|
|
4638
|
+
return null;
|
|
4639
|
+
}
|
|
4640
|
+
function isSeparatorRow(line) {
|
|
4641
|
+
const normalized = normalizeTableRow(line);
|
|
4642
|
+
if (!normalized) {
|
|
4643
|
+
return false;
|
|
4644
|
+
}
|
|
4645
|
+
return /^[-\s:|]+$/.test(normalized.trim());
|
|
4646
|
+
}
|
|
4647
|
+
function normalizeTableRow(line) {
|
|
4648
|
+
const trimmed = line.trim();
|
|
4649
|
+
if (!trimmed.includes("|")) {
|
|
4650
|
+
return null;
|
|
4651
|
+
}
|
|
4652
|
+
let normalized = trimmed;
|
|
4653
|
+
if (normalized.startsWith("|")) {
|
|
4654
|
+
normalized = normalized.slice(1);
|
|
4655
|
+
}
|
|
4656
|
+
if (normalized.endsWith("|")) {
|
|
4657
|
+
normalized = normalized.slice(0, -1);
|
|
4658
|
+
}
|
|
4659
|
+
return normalized;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
// src/core/validators/traceabilityMatrix.ts
|
|
4318
4663
|
var SC_TAG_RE5 = /^SC-\d{4}-\d{4}$/;
|
|
4319
|
-
async function validateTraceabilityMatrices(root, config) {
|
|
4664
|
+
async function validateTraceabilityMatrices(root, config, phase) {
|
|
4320
4665
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
4321
4666
|
const entries = await collectSpecEntries(specsRoot);
|
|
4322
4667
|
if (entries.length === 0) {
|
|
@@ -4326,7 +4671,7 @@ async function validateTraceabilityMatrices(root, config) {
|
|
|
4326
4671
|
for (const entry of entries) {
|
|
4327
4672
|
let matrixText;
|
|
4328
4673
|
try {
|
|
4329
|
-
matrixText = await (0,
|
|
4674
|
+
matrixText = await (0, import_promises18.readFile)(entry.traceabilityMatrixPath, "utf-8");
|
|
4330
4675
|
} catch (error) {
|
|
4331
4676
|
if (isMissingFileError2(error)) {
|
|
4332
4677
|
issues.push(
|
|
@@ -4361,9 +4706,42 @@ async function validateTraceabilityMatrices(root, config) {
|
|
|
4361
4706
|
)
|
|
4362
4707
|
);
|
|
4363
4708
|
}
|
|
4709
|
+
const statusResult = parseTraceabilityMatrixStatus(matrixText);
|
|
4710
|
+
if (statusResult.hasStatusColumn && statusResult.invalidStatusValues.length > 0) {
|
|
4711
|
+
const invalid = Array.from(new Set(statusResult.invalidStatusValues));
|
|
4712
|
+
issues.push(
|
|
4713
|
+
issue(
|
|
4714
|
+
"QFAI-RTM-004",
|
|
4715
|
+
`traceability-matrix \u306E status \u304C\u4E0D\u6B63\u3067\u3059: ${invalid.join(", ")}`,
|
|
4716
|
+
"error",
|
|
4717
|
+
entry.traceabilityMatrixPath,
|
|
4718
|
+
"traceabilityMatrix.status",
|
|
4719
|
+
invalid
|
|
4720
|
+
)
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
const isStrictPhase = phase === "full" || phase === "tdd";
|
|
4724
|
+
if (statusResult.hasStatusColumn && isStrictPhase) {
|
|
4725
|
+
const planned = Array.from(statusResult.statusBySc.entries()).filter(([, status]) => status === "planned").map(([scId]) => scId);
|
|
4726
|
+
if (planned.length > 0) {
|
|
4727
|
+
const uniquePlanned = Array.from(new Set(planned));
|
|
4728
|
+
issues.push(
|
|
4729
|
+
issue(
|
|
4730
|
+
"QFAI-RTM-005",
|
|
4731
|
+
`traceability-matrix \u306B planned \u304C\u6B8B\u3063\u3066\u3044\u307E\u3059: ${uniquePlanned.join(
|
|
4732
|
+
", "
|
|
4733
|
+
)} (phase=${phase})`,
|
|
4734
|
+
"warning",
|
|
4735
|
+
entry.traceabilityMatrixPath,
|
|
4736
|
+
"traceabilityMatrix.statusPlanned",
|
|
4737
|
+
uniquePlanned
|
|
4738
|
+
)
|
|
4739
|
+
);
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4364
4742
|
let specText = "";
|
|
4365
4743
|
try {
|
|
4366
|
-
specText = await (0,
|
|
4744
|
+
specText = await (0, import_promises18.readFile)(entry.specPath, "utf-8");
|
|
4367
4745
|
} catch {
|
|
4368
4746
|
specText = "";
|
|
4369
4747
|
}
|
|
@@ -4374,13 +4752,13 @@ async function validateTraceabilityMatrices(root, config) {
|
|
|
4374
4752
|
const acIds = new Set(specText ? extractIds(specText, "AC") : []);
|
|
4375
4753
|
const caseIds = new Set(specText ? extractIds(specText, "CASE") : []);
|
|
4376
4754
|
try {
|
|
4377
|
-
const caseText = await (0,
|
|
4755
|
+
const caseText = await (0, import_promises18.readFile)(entry.caseCataloguePath, "utf-8");
|
|
4378
4756
|
extractIds(caseText, "CASE").forEach((id) => caseIds.add(id));
|
|
4379
4757
|
} catch {
|
|
4380
4758
|
}
|
|
4381
4759
|
let scenarioText = "";
|
|
4382
4760
|
try {
|
|
4383
|
-
scenarioText = await (0,
|
|
4761
|
+
scenarioText = await (0, import_promises18.readFile)(entry.scenarioPath, "utf-8");
|
|
4384
4762
|
} catch {
|
|
4385
4763
|
scenarioText = "";
|
|
4386
4764
|
}
|
|
@@ -4451,20 +4829,22 @@ async function validateTraceabilityMatrices(root, config) {
|
|
|
4451
4829
|
}
|
|
4452
4830
|
|
|
4453
4831
|
// src/core/validate.ts
|
|
4454
|
-
async function validateProject(root, configResult) {
|
|
4832
|
+
async function validateProject(root, configResult, options = {}) {
|
|
4455
4833
|
const resolved = configResult ?? await loadConfig(root);
|
|
4456
4834
|
const { config, issues: configIssues } = resolved;
|
|
4835
|
+
const phase = options.phase ?? "full";
|
|
4457
4836
|
const issues = [
|
|
4458
4837
|
...configIssues,
|
|
4459
4838
|
...await validatePromptsIntegrity(root, config),
|
|
4460
4839
|
...await validateSpecs(root, config),
|
|
4461
4840
|
...await validateDeltas(root, config),
|
|
4462
4841
|
...await validateScenarios(root, config),
|
|
4842
|
+
...phase === "atdd" ? await validateAtddCoverageLedgers(root, config) : [],
|
|
4463
4843
|
...await validateCaseCatalogues(root, config),
|
|
4464
4844
|
...await validateContracts(root, config),
|
|
4465
|
-
...await validateTraceabilityMatrices(root, config),
|
|
4845
|
+
...await validateTraceabilityMatrices(root, config, phase),
|
|
4466
4846
|
...await validateDefinedIds(root, config),
|
|
4467
|
-
...await validateTraceability(root, config)
|
|
4847
|
+
...await validateTraceability(root, config, phase)
|
|
4468
4848
|
];
|
|
4469
4849
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
4470
4850
|
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
@@ -4478,6 +4858,7 @@ async function validateProject(root, configResult) {
|
|
|
4478
4858
|
const toolVersion = await resolveToolVersion();
|
|
4479
4859
|
return {
|
|
4480
4860
|
toolVersion,
|
|
4861
|
+
phase,
|
|
4481
4862
|
issues,
|
|
4482
4863
|
counts: countIssues(issues),
|
|
4483
4864
|
traceability: {
|
|
@@ -4512,15 +4893,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
4512
4893
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
4513
4894
|
var SC_TAG_RE6 = /^SC-\d{4}-\d{4}$/;
|
|
4514
4895
|
async function createReportData(root, validation, configResult) {
|
|
4515
|
-
const resolvedRoot =
|
|
4896
|
+
const resolvedRoot = import_node_path18.default.resolve(root);
|
|
4516
4897
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
4517
4898
|
const config = resolved.config;
|
|
4518
4899
|
const configPath = resolved.configPath;
|
|
4519
4900
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
4520
4901
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
4521
|
-
const apiRoot =
|
|
4522
|
-
const uiRoot =
|
|
4523
|
-
const dbRoot =
|
|
4902
|
+
const apiRoot = import_node_path18.default.join(contractsRoot, "api");
|
|
4903
|
+
const uiRoot = import_node_path18.default.join(contractsRoot, "ui");
|
|
4904
|
+
const dbRoot = import_node_path18.default.join(contractsRoot, "db");
|
|
4524
4905
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
4525
4906
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
4526
4907
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -5174,7 +5555,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
5174
5555
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
5175
5556
|
}
|
|
5176
5557
|
for (const file of specFiles) {
|
|
5177
|
-
const text = await (0,
|
|
5558
|
+
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
5178
5559
|
const parsed = parseSpec(text, file);
|
|
5179
5560
|
const specKey = parsed.specId;
|
|
5180
5561
|
if (!specKey) {
|
|
@@ -5218,7 +5599,7 @@ async function collectIds(files) {
|
|
|
5218
5599
|
THEMA: /* @__PURE__ */ new Set()
|
|
5219
5600
|
};
|
|
5220
5601
|
for (const file of files) {
|
|
5221
|
-
const text = await (0,
|
|
5602
|
+
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
5222
5603
|
for (const prefix of ID_PREFIXES2) {
|
|
5223
5604
|
const ids = extractIds(text, prefix);
|
|
5224
5605
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -5239,7 +5620,7 @@ async function collectIds(files) {
|
|
|
5239
5620
|
async function collectUpstreamIds(files) {
|
|
5240
5621
|
const ids = /* @__PURE__ */ new Set();
|
|
5241
5622
|
for (const file of files) {
|
|
5242
|
-
const text = await (0,
|
|
5623
|
+
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
5243
5624
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
5244
5625
|
}
|
|
5245
5626
|
return ids;
|
|
@@ -5260,7 +5641,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
5260
5641
|
}
|
|
5261
5642
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
5262
5643
|
for (const file of targetFiles) {
|
|
5263
|
-
const text = await (0,
|
|
5644
|
+
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
5264
5645
|
if (pattern.test(text)) {
|
|
5265
5646
|
return true;
|
|
5266
5647
|
}
|
|
@@ -5381,7 +5762,7 @@ function normalizeScSources(root, sources) {
|
|
|
5381
5762
|
async function countScenarios(scenarioFiles) {
|
|
5382
5763
|
let total = 0;
|
|
5383
5764
|
for (const file of scenarioFiles) {
|
|
5384
|
-
const text = await (0,
|
|
5765
|
+
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
5385
5766
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
5386
5767
|
if (!document || errors.length > 0) {
|
|
5387
5768
|
continue;
|
|
@@ -5412,7 +5793,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
5412
5793
|
let totalScenarios = 0;
|
|
5413
5794
|
let e2eCount = 0;
|
|
5414
5795
|
for (const file of scenarioFiles) {
|
|
5415
|
-
const text = await (0,
|
|
5796
|
+
const text = await (0, import_promises19.readFile)(file, "utf-8");
|
|
5416
5797
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
5417
5798
|
if (!document || errors.length > 0) {
|
|
5418
5799
|
continue;
|