@taodigital/agents 0.0.0-stage → 1.2.1
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 +99 -2
- package/dist/api-test-agent.d.ts +126 -0
- package/dist/api-test-agent.js +267 -0
- package/dist/artifact-intel-agent.d.ts +11 -0
- package/dist/artifact-intel-agent.js +152 -0
- package/dist/coverage-gap-agent.d.ts +25 -0
- package/dist/coverage-gap-agent.js +269 -0
- package/dist/coverage-map-agent.d.ts +15 -0
- package/dist/coverage-map-agent.js +255 -0
- package/dist/crawl-diff-agent.d.ts +27 -0
- package/dist/crawl-diff-agent.js +412 -0
- package/dist/data-agent.d.ts +101 -0
- package/dist/data-agent.js +2284 -0
- package/dist/document-agent.d.ts +10 -0
- package/dist/document-agent.js +32 -0
- package/dist/enhancement-agent.d.ts +48 -0
- package/dist/enhancement-agent.js +342 -0
- package/dist/field-detector-agent.d.ts +35 -0
- package/dist/field-detector-agent.js +220 -0
- package/dist/flow-agent.d.ts +130 -0
- package/dist/flow-agent.js +169 -0
- package/dist/github-agent.d.ts +18 -0
- package/dist/github-agent.js +39 -0
- package/dist/github-write-agent.d.ts +67 -0
- package/dist/github-write-agent.js +249 -0
- package/dist/grouping-agent.d.ts +18 -0
- package/dist/grouping-agent.js +338 -0
- package/dist/healing-agent.d.ts +73 -0
- package/dist/healing-agent.js +137 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +67 -0
- package/dist/integration-test-agent.d.ts +118 -0
- package/dist/integration-test-agent.js +165 -0
- package/dist/label-assignment-agent.d.ts +15 -0
- package/dist/label-assignment-agent.js +97 -0
- package/dist/llm-config.d.ts +38 -0
- package/dist/llm-config.js +287 -0
- package/dist/llm-mock.d.ts +77 -0
- package/dist/llm-mock.js +192 -0
- package/dist/llm.d.ts +58 -0
- package/dist/llm.js +438 -0
- package/dist/nlp-agent.d.ts +208 -0
- package/dist/nlp-agent.js +340 -0
- package/dist/orchestrator.d.ts +285 -0
- package/dist/orchestrator.js +141 -0
- package/dist/plan-agent.d.ts +4 -0
- package/dist/plan-agent.js +200 -0
- package/dist/repo-scan-agent.d.ts +189 -0
- package/dist/repo-scan-agent.js +2032 -0
- package/dist/requirements-agent.d.ts +59 -0
- package/dist/requirements-agent.js +641 -0
- package/dist/scenario-agent.d.ts +20 -0
- package/dist/scenario-agent.js +123 -0
- package/dist/script-agent.d.ts +447 -0
- package/dist/script-agent.js +11813 -0
- package/dist/security-test-agent.d.ts +111 -0
- package/dist/security-test-agent.js +141 -0
- package/dist/skill-file-agent.d.ts +264 -0
- package/dist/skill-file-agent.js +3470 -0
- package/dist/step-coverage.d.ts +46 -0
- package/dist/step-coverage.js +288 -0
- package/dist/step-utils.d.ts +2 -0
- package/dist/step-utils.js +62 -0
- package/dist/test-case-agent.d.ts +206 -0
- package/dist/test-case-agent.js +2141 -0
- package/dist/test-import-agent.d.ts +148 -0
- package/dist/test-import-agent.js +3235 -0
- package/dist/test-script-agent.d.ts +16 -0
- package/dist/test-script-agent.js +48 -0
- package/dist/token-pricing.d.ts +14 -0
- package/dist/token-pricing.js +62 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.js +7 -0
- package/dist/visual-agent.d.ts +114 -0
- package/dist/visual-agent.js +261 -0
- package/package.json +26 -4
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZeTa-AI Requirements Agent
|
|
4
|
+
*
|
|
5
|
+
* Converts product specs (PRDs, user stories, acceptance criteria, release notes)
|
|
6
|
+
* into structured, testable Requirement objects stored in the Requirements store.
|
|
7
|
+
*
|
|
8
|
+
* Each Requirement has:
|
|
9
|
+
* - reqId: REQ-{AREA}-{NNN} — stable, sequential per area
|
|
10
|
+
* - acceptanceCriteria[]: Given/When/Then deterministic statements
|
|
11
|
+
* - priority: CRITICAL | HIGH | MEDIUM | LOW (risk-derived)
|
|
12
|
+
* - status: pending (until matchCasesToRequirements() runs)
|
|
13
|
+
*
|
|
14
|
+
* Also provides:
|
|
15
|
+
* - matchCasesToRequirements(): fuzzy matching of test cases → reqIds
|
|
16
|
+
* - generateCoverageGapReport(): Prompt 2 pre-plan gap analysis
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.extractRequirements = extractRequirements;
|
|
20
|
+
exports.generateCoverageGapReport = generateCoverageGapReport;
|
|
21
|
+
exports.buildRequirementsBlock = buildRequirementsBlock;
|
|
22
|
+
const app_brain_1 = require("@taodigital/app-brain");
|
|
23
|
+
const llm_js_1 = require("./llm.js");
|
|
24
|
+
const llm_config_js_1 = require("./llm-config.js");
|
|
25
|
+
const core_1 = require("@taodigital/core");
|
|
26
|
+
// ─── System prompt (Prompt 1 from the spec) ─────────────────────────────────
|
|
27
|
+
const EXTRACTION_SYSTEM = `You are a QA architect converting product specifications into structured, testable requirements.
|
|
28
|
+
|
|
29
|
+
OUTPUT FORMAT — JSON only, no preamble, no markdown fences:
|
|
30
|
+
{ "requirements": Requirement[] }
|
|
31
|
+
|
|
32
|
+
Where Requirement = {
|
|
33
|
+
"reqId": string, // REQ-{AREA}-{NNN} — e.g. REQ-AUTH-014. Area = 2-5 char uppercase noun from the feature.
|
|
34
|
+
"title": string, // Concise, action-oriented. No pronouns.
|
|
35
|
+
"actor": string | null, // Who initiates: "user" | "admin" | "system" | "guest" | null
|
|
36
|
+
"type": string, // functional | nonfunctional | security | accessibility | data | performance
|
|
37
|
+
"priority": string, // CRITICAL | HIGH | MEDIUM | LOW (see rules below)
|
|
38
|
+
"acceptanceCriteria": string[], // Given/When/Then strings — at least one per requirement
|
|
39
|
+
"businessRules": string[], // Thresholds, role limits, constraints not in acceptance criteria
|
|
40
|
+
"relatedScreenHints": string[], // Screen name nouns/routes (hints only — do not assert they exist)
|
|
41
|
+
"sourceSection": string | null, // Heading, feature, epic, story, or uploaded-section name from the source input.
|
|
42
|
+
"derived": boolean // true if you inferred criterion (not explicitly stated in source)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
RULES:
|
|
46
|
+
1. One requirement = one verifiable behaviour. Split compound sentences — never combine two behaviours in one object.
|
|
47
|
+
2. Every requirement MUST have at least one acceptanceCriteria written as deterministic Given/When/Then. If the source lacks one, derive the minimal criterion implied and set derived=true.
|
|
48
|
+
3. Priority assignment:
|
|
49
|
+
- CRITICAL: auth, authorization, payment, data deletion, PII access, security gates — words "must", "required", "shall"
|
|
50
|
+
- HIGH: core happy paths, primary CRUD, navigation, data integrity
|
|
51
|
+
- MEDIUM: secondary flows, validation, edge conditions, warnings
|
|
52
|
+
- LOW: cosmetic, low-frequency, nice-to-have
|
|
53
|
+
4. No pronouns in title or criteria. No hedging words (possibly, may, might, should probably).
|
|
54
|
+
5. Every acceptanceCriteria string must start with "Given", "When", or "Then" or "And".
|
|
55
|
+
6. reqId must be stable and sequential: REQ-AUTH-001, REQ-AUTH-002, ... per area. Areas derive from the feature noun in the title.
|
|
56
|
+
7. businessRules: only explicit constraints (e.g. "Password must be 8-128 chars", "Max 5 login attempts before lockout").
|
|
57
|
+
8. relatedScreenHints: nouns/routes that hint at screen implementation. Do NOT assert the screen exists.
|
|
58
|
+
9. Preserve the source grouping. If the input has headings, epics, modules, numbered sections, or user-story labels, put the closest group name in sourceSection for every requirement. The CLOSEST means the nearest heading ABOVE that requirement, not the document title. If the document has one top-level title and several sub-sections below it, use the sub-section name (not the top-level title). Example: document title "Title Registration Workflow" with sub-sections "Vehicle Identification", "Fee Calculation", "Owner Validation" → each requirement's sourceSection is the sub-section it came from, NOT "Title Registration Workflow". Different requirements MUST have different sourceSection values if they come from different sections. Never assign the same sourceSection to all requirements unless the document truly has only one section.
|
|
59
|
+
10. Output valid JSON only.
|
|
60
|
+
11. Be EXHAUSTIVE. For a business requirements document (BRD), extract EVERY testable item including:
|
|
61
|
+
- All business rules with specific numeric constraints (e.g., "Password 3-8 chars" → separate requirement per rule)
|
|
62
|
+
- All non-functional requirements (performance thresholds, availability, data retention)
|
|
63
|
+
- All validation rules from tables (field length limits, format constraints, allowed values)
|
|
64
|
+
- All exception/error scenarios (what happens when X fails)
|
|
65
|
+
- All integration requirements (external system timeouts, API responses)
|
|
66
|
+
- Each row in a business rules table = a separate requirement
|
|
67
|
+
A 40-page BRD should produce 80-150 requirements, not 10.
|
|
68
|
+
12. For business rules tables: each row is a separate requirement. reqId format: REQ-{AREA}-{NNN} where AREA reflects the rule category (AUTH, VIN, FEE, INV, OWN, etc.)`;
|
|
69
|
+
// ─── Prompt 2: coverage gap analysis (pre-plan) ──────────────────────────────
|
|
70
|
+
const GAP_ANALYSIS_SYSTEM = `You are a QA coverage analyst auditing test coverage against requirements.
|
|
71
|
+
|
|
72
|
+
OUTPUT FORMAT — JSON only:
|
|
73
|
+
{
|
|
74
|
+
"covered": [{ "reqId": string, "caseRefs": string[] }],
|
|
75
|
+
"untested": string[],
|
|
76
|
+
"unbuilt": string[],
|
|
77
|
+
"byPriority": { "CRITICAL": { "covered": N, "total": N }, "HIGH": { ... }, "MEDIUM": { ... }, "LOW": { ... } },
|
|
78
|
+
"topGaps": string[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
RULES:
|
|
82
|
+
1. covered: reqId has ≥1 test case whose traceTo == reqId OR whose title/steps clearly satisfy an acceptance criterion. List all matching case refs.
|
|
83
|
+
2. untested: requirement maps to a plausible screen (relatedScreenHints match a crawled screen name) but NO satisfying test case found.
|
|
84
|
+
3. unbuilt: requirement has explicit screen hints, but NONE plausibly match crawled screens. This means "not mapped to crawl", not proof that the app is missing the feature.
|
|
85
|
+
4. topGaps: 5 plain-English gap sentences, highest priority first. Use "not mapped to a crawled screen" instead of "unimplemented" unless the source explicitly says the feature exists and no crawled route can satisfy it.
|
|
86
|
+
5. Never mark covered on weak title-only guess for CRITICAL or SECURITY requirements — require explicit traceTo or criterion-level match.
|
|
87
|
+
6. byPriority: count covered vs total per priority tier.
|
|
88
|
+
7. Output valid JSON only. No preamble.`;
|
|
89
|
+
// ─── Main extraction function ────────────────────────────────────────────────
|
|
90
|
+
async function extractRequirements(opts) {
|
|
91
|
+
const { tenantId, projectId, sourceText } = opts;
|
|
92
|
+
if (!sourceText?.trim()) {
|
|
93
|
+
throw new Error('sourceText is required for requirement extraction.');
|
|
94
|
+
}
|
|
95
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'testPlan');
|
|
96
|
+
if (!llmCfg) {
|
|
97
|
+
throw new Error('LLM provider not configured. Add an AI provider in Workspace → Settings → AI Provider.');
|
|
98
|
+
}
|
|
99
|
+
const CHAR_LIMIT = parseInt(process.env.REQUIREMENTS_CHAR_LIMIT ?? '80000', 10);
|
|
100
|
+
const domainCtx = await (0, llm_config_js_1.getWorkspaceDomainContext)(tenantId, projectId);
|
|
101
|
+
const sourceSections = detectSourceSections(sourceText);
|
|
102
|
+
const user = [
|
|
103
|
+
'Extract structured requirements from the following specification.',
|
|
104
|
+
'Assign sequential reqIds per area (REQ-AUTH-001, REQ-AUTH-002, REQ-CHECKOUT-001, etc.).',
|
|
105
|
+
'Preserve source grouping by setting sourceSection from the closest heading, epic, feature, module, story, or numbered section in the input.',
|
|
106
|
+
sourceSections.length
|
|
107
|
+
? `Detected source sections to preserve:\n${sourceSections.map((s) => `- ${s}`).join('\n')}`
|
|
108
|
+
: '',
|
|
109
|
+
domainCtx,
|
|
110
|
+
'',
|
|
111
|
+
'## Source Document',
|
|
112
|
+
sourceText.slice(0, CHAR_LIMIT), // cap to avoid context overflow
|
|
113
|
+
].filter(Boolean).join('\n');
|
|
114
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:${projectId}:extract-reqs:${sourceText.slice(0, 3000)}`);
|
|
115
|
+
const t0 = Date.now();
|
|
116
|
+
const extractionSystem = await (0, core_1.getAgentPrompt)('requirements-extraction', undefined, tenantId) ?? EXTRACTION_SYSTEM;
|
|
117
|
+
const { text: raw, usage } = await (0, llm_js_1.askTracked)(extractionSystem, user, llmCfg, undefined, undefined, { temperature: 0, seed });
|
|
118
|
+
const latencyMs = Date.now() - t0;
|
|
119
|
+
// Parse JSON
|
|
120
|
+
const jsonStart = raw.indexOf('{');
|
|
121
|
+
const jsonEnd = raw.lastIndexOf('}');
|
|
122
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
123
|
+
throw new Error('LLM did not return valid JSON for requirement extraction.');
|
|
124
|
+
}
|
|
125
|
+
const parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
126
|
+
const rawReqs = parsed.requirements ?? [];
|
|
127
|
+
const mapRawReqs = (rawList) => rawList.map((r) => ({
|
|
128
|
+
reqId: String(r.reqId ?? '').toUpperCase(),
|
|
129
|
+
title: String(r.title ?? ''),
|
|
130
|
+
actor: r.actor ?? null,
|
|
131
|
+
type: (['functional', 'nonfunctional', 'security', 'accessibility', 'data', 'performance'].includes(r.type)
|
|
132
|
+
? r.type : 'functional'),
|
|
133
|
+
priority: (['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].includes(r.priority)
|
|
134
|
+
? r.priority : 'HIGH'),
|
|
135
|
+
acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria : [],
|
|
136
|
+
businessRules: Array.isArray(r.businessRules) ? r.businessRules : [],
|
|
137
|
+
relatedScreenHints: Array.isArray(r.relatedScreenHints) ? r.relatedScreenHints : [],
|
|
138
|
+
sourceSection: normalizeSourceSection(r.sourceSection),
|
|
139
|
+
derived: r.derived === true,
|
|
140
|
+
})).filter(r => r.reqId && r.title && r.acceptanceCriteria.length > 0);
|
|
141
|
+
let requirements = mapRawReqs(rawReqs);
|
|
142
|
+
let droppedCount = rawReqs.length - requirements.length;
|
|
143
|
+
// Chunked processing for large documents (> CHAR_LIMIT chars)
|
|
144
|
+
const CHUNK_SIZE = 70000;
|
|
145
|
+
const CHUNK_OVERLAP = 5000;
|
|
146
|
+
if (sourceText.length > CHAR_LIMIT) {
|
|
147
|
+
const chunks = [];
|
|
148
|
+
let offset = 0;
|
|
149
|
+
while (offset < sourceText.length) {
|
|
150
|
+
chunks.push(sourceText.slice(offset, offset + CHUNK_SIZE));
|
|
151
|
+
offset += CHUNK_SIZE - CHUNK_OVERLAP;
|
|
152
|
+
}
|
|
153
|
+
// Skip chunk 0 — already processed above as the first CHAR_LIMIT chars
|
|
154
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
155
|
+
const chunkUser = [
|
|
156
|
+
'Extract structured requirements from the following specification.',
|
|
157
|
+
'Assign sequential reqIds per area (REQ-AUTH-001, REQ-AUTH-002, REQ-CHECKOUT-001, etc.).',
|
|
158
|
+
'Preserve source grouping by setting sourceSection from the closest heading, epic, feature, module, story, or numbered section in the input.',
|
|
159
|
+
sourceSections.length
|
|
160
|
+
? `Detected source sections to preserve:\n${sourceSections.map((s) => `- ${s}`).join('\n')}`
|
|
161
|
+
: '',
|
|
162
|
+
domainCtx,
|
|
163
|
+
'',
|
|
164
|
+
'## Source Document',
|
|
165
|
+
chunks[i],
|
|
166
|
+
].filter(Boolean).join('\n');
|
|
167
|
+
const chunkSeed = (0, llm_js_1.deterministicSeed)(`${tenantId}:${projectId}:extract-reqs-chunk-${i}:${chunks[i].slice(0, 1000)}`);
|
|
168
|
+
try {
|
|
169
|
+
const { text: chunkRaw, usage: chunkUsage } = await (0, llm_js_1.askTracked)(extractionSystem, chunkUser, llmCfg, undefined, undefined, { temperature: 0, seed: chunkSeed });
|
|
170
|
+
const cjStart = chunkRaw.indexOf('{');
|
|
171
|
+
const cjEnd = chunkRaw.lastIndexOf('}');
|
|
172
|
+
if (cjStart !== -1 && cjEnd !== -1) {
|
|
173
|
+
const chunkParsed = JSON.parse(chunkRaw.slice(cjStart, cjEnd + 1));
|
|
174
|
+
const chunkReqs = mapRawReqs(chunkParsed.requirements ?? []);
|
|
175
|
+
droppedCount += (chunkParsed.requirements?.length ?? 0) - chunkReqs.length;
|
|
176
|
+
requirements = requirements.concat(chunkReqs);
|
|
177
|
+
}
|
|
178
|
+
(0, app_brain_1.recordLLMCall)({
|
|
179
|
+
tenantId, projectId,
|
|
180
|
+
agentType: 'requirements',
|
|
181
|
+
label: `requirement_extraction_chunk_${i}`,
|
|
182
|
+
entityType: 'requirement',
|
|
183
|
+
usage: chunkUsage,
|
|
184
|
+
latencyMs: 0,
|
|
185
|
+
}).catch(console.error);
|
|
186
|
+
}
|
|
187
|
+
catch (chunkErr) {
|
|
188
|
+
console.warn(`[requirements-agent] chunk ${i} extraction failed:`, chunkErr instanceof Error ? chunkErr.message : chunkErr);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Deduplicate by reqId: keep first occurrence, but prefer entry with more acceptanceCriteria on collision
|
|
192
|
+
const seen = new Map();
|
|
193
|
+
for (const req of requirements) {
|
|
194
|
+
const existing = seen.get(req.reqId);
|
|
195
|
+
if (!existing || req.acceptanceCriteria.length > existing.acceptanceCriteria.length) {
|
|
196
|
+
seen.set(req.reqId, req);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
requirements = [...seen.values()];
|
|
200
|
+
}
|
|
201
|
+
(0, app_brain_1.recordLLMCall)({
|
|
202
|
+
tenantId,
|
|
203
|
+
projectId,
|
|
204
|
+
agentType: 'requirements',
|
|
205
|
+
label: 'requirement_extraction',
|
|
206
|
+
entityType: 'requirement',
|
|
207
|
+
usage,
|
|
208
|
+
latencyMs,
|
|
209
|
+
}).catch(console.error);
|
|
210
|
+
const saved = await app_brain_1.AppBrain.upsertRequirements(tenantId, projectId, requirements);
|
|
211
|
+
if (requirements.length > 0) {
|
|
212
|
+
await refreshRequirementSkillFiles(tenantId, projectId, requirements);
|
|
213
|
+
}
|
|
214
|
+
return { requirements, saved: saved.length, dropped: droppedCount };
|
|
215
|
+
}
|
|
216
|
+
function mdCell(value) {
|
|
217
|
+
return String(value ?? '')
|
|
218
|
+
.replace(/\|/g, '\\|')
|
|
219
|
+
.replace(/\r?\n/g, '<br/>')
|
|
220
|
+
.trim();
|
|
221
|
+
}
|
|
222
|
+
function criteriaList(value) {
|
|
223
|
+
return Array.isArray(value) ? value.map((v) => String(v ?? '').trim()).filter(Boolean) : [];
|
|
224
|
+
}
|
|
225
|
+
function requirementHaystack(requirement) {
|
|
226
|
+
return [
|
|
227
|
+
requirement.reqId,
|
|
228
|
+
requirement.title,
|
|
229
|
+
requirement.actor,
|
|
230
|
+
requirement.type,
|
|
231
|
+
requirement.priority,
|
|
232
|
+
requirement.sourceSection,
|
|
233
|
+
...criteriaList(requirement.acceptanceCriteria),
|
|
234
|
+
...criteriaList(requirement.businessRules),
|
|
235
|
+
...criteriaList(requirement.relatedScreenHints),
|
|
236
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
237
|
+
}
|
|
238
|
+
function requirementMatches(requirement, words) {
|
|
239
|
+
const text = requirementHaystack(requirement);
|
|
240
|
+
return words.some((word) => text.includes(word));
|
|
241
|
+
}
|
|
242
|
+
function requirementTable(requirements, emptyMessage) {
|
|
243
|
+
if (requirements.length === 0)
|
|
244
|
+
return emptyMessage;
|
|
245
|
+
return [
|
|
246
|
+
`| ID | Group | Priority | Type | Title | Criteria |`,
|
|
247
|
+
`|---|---|---|---|---|---|`,
|
|
248
|
+
...requirements.map((r) => {
|
|
249
|
+
const criteria = criteriaList(r.acceptanceCriteria).map((c) => `- ${mdCell(c)}`).join('<br/>');
|
|
250
|
+
return `| ${mdCell(r.reqId)} | ${mdCell(r.sourceSection ?? '')} | ${mdCell(r.priority)} | ${mdCell(r.type)} | **${mdCell(r.title)}** | ${criteria} |`;
|
|
251
|
+
}),
|
|
252
|
+
].join('\n');
|
|
253
|
+
}
|
|
254
|
+
async function upsertRequirementSkillFile(tenantId, projectId, kind, content, summary, tags) {
|
|
255
|
+
await app_brain_1.AppBrain.upsertSkillFile(tenantId, projectId, kind, content, {
|
|
256
|
+
summary,
|
|
257
|
+
tags: ['requirements', 'prd', ...tags],
|
|
258
|
+
source: 'AUTO',
|
|
259
|
+
}).catch((e) => console.warn(`[requirements-agent] failed to upsert ${kind} skill file:`, e));
|
|
260
|
+
}
|
|
261
|
+
async function refreshRequirementSkillFiles(tenantId, projectId, requirements) {
|
|
262
|
+
const byType = (types) => {
|
|
263
|
+
const set = new Set(types.map((t) => t.toLowerCase()));
|
|
264
|
+
return requirements.filter((r) => set.has(String(r.type ?? '').toLowerCase()));
|
|
265
|
+
};
|
|
266
|
+
const functional = byType(['functional']);
|
|
267
|
+
const nonfunctional = requirements.filter((r) => ['nonfunctional', 'accessibility', 'data', 'performance'].includes(String(r.type ?? '').toLowerCase()));
|
|
268
|
+
const security = byType(['security']);
|
|
269
|
+
const withRules = requirements.filter((r) => criteriaList(r.businessRules).length > 0);
|
|
270
|
+
const accessibility = byType(['accessibility']);
|
|
271
|
+
const performance = byType(['performance']);
|
|
272
|
+
const dataRules = requirements.filter((r) => String(r.type ?? '').toLowerCase() === 'data' || requirementMatches(r, [
|
|
273
|
+
'data', 'field', 'vin', 'plate', 'inventory', 'expiration', 'date', 'fee', 'amount', 'number', 'code', 'class', 'default', 'value', 'validation',
|
|
274
|
+
]));
|
|
275
|
+
const workflows = requirements.filter((r) => requirementMatches(r, [
|
|
276
|
+
'workflow', 'flow', 'journey', 'transaction', 'submit', 'submission', 'registration', 'renewal', 'reversal', 'batch', 'queue', 'print', 'order', 'shipment', 'report',
|
|
277
|
+
]));
|
|
278
|
+
const roles = requirements.filter((r) => r.actor || requirementMatches(r, [
|
|
279
|
+
'role', 'permission', 'operator', 'admin', 'access', 'authorization', 'authorized', 'unauthorized', 'allow', 'deny', 'denied', 'rbac',
|
|
280
|
+
]));
|
|
281
|
+
const integrations = requirements.filter((r) => requirementMatches(r, [
|
|
282
|
+
'api', 'host', 'dmv', 'external', 'integration', 'transmit', 'transmission', 'response', 'connection', 'secure connection', 'batch', 'webhook',
|
|
283
|
+
]));
|
|
284
|
+
const compliance = requirements.filter((r) => requirementMatches(r, [
|
|
285
|
+
'compliance', 'agreement', 'audit', 'log', 'retention', 'state agreement', 'privacy', 'pii', 'regulation', 'regulatory', 'vsa', 'security event',
|
|
286
|
+
]));
|
|
287
|
+
await Promise.all([
|
|
288
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS', [
|
|
289
|
+
'# Product Requirements',
|
|
290
|
+
'',
|
|
291
|
+
'Purpose: compact index of all extracted intended behavior. Test generation uses this for traceability only after crawl evidence confirms the UI path exists.',
|
|
292
|
+
'',
|
|
293
|
+
requirementTable(requirements, 'No product requirements were extracted.'),
|
|
294
|
+
].join('\n'), 'Structured product requirements extracted from uploaded specifications.', ['index', 'traceability']),
|
|
295
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_FUNCTIONAL', [
|
|
296
|
+
'# Functional Requirements',
|
|
297
|
+
'',
|
|
298
|
+
'Purpose: user-visible behaviors, workflows, permissions, and form rules that may become executable UI tests when matched to crawled evidence.',
|
|
299
|
+
'',
|
|
300
|
+
requirementTable(functional, 'No functional requirements were extracted.'),
|
|
301
|
+
].join('\n'), 'Functional requirements that can drive executable UI and flow test generation.', ['functional', 'ui-tests']),
|
|
302
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_NONFUNCTIONAL', [
|
|
303
|
+
'# Nonfunctional Requirements',
|
|
304
|
+
'',
|
|
305
|
+
'Purpose: quality attributes such as accessibility, data, performance, reliability, and compliance that influence specialized test passes.',
|
|
306
|
+
'',
|
|
307
|
+
requirementTable(nonfunctional, 'No nonfunctional requirements were extracted.'),
|
|
308
|
+
].join('\n'), 'Nonfunctional requirements for specialized quality and compliance passes.', ['nonfunctional', 'quality']),
|
|
309
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_SECURITY', [
|
|
310
|
+
'# Security Requirements',
|
|
311
|
+
'',
|
|
312
|
+
'Purpose: authorization, authentication, privacy, audit, and abuse-prevention expectations used by security and negative test passes.',
|
|
313
|
+
'',
|
|
314
|
+
requirementTable(security, 'No security requirements were extracted.'),
|
|
315
|
+
].join('\n'), 'Security-focused requirements used by authorization and negative test generation.', ['security', 'authorization']),
|
|
316
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_BUSINESS_RULES', [
|
|
317
|
+
'# Business Rules',
|
|
318
|
+
'',
|
|
319
|
+
'Purpose: constraints, thresholds, role rules, dates, and validation limits that should appear in assertions and edge tests.',
|
|
320
|
+
'',
|
|
321
|
+
withRules.length
|
|
322
|
+
? withRules.map((r) => [
|
|
323
|
+
`## ${r.reqId}: ${r.title}`,
|
|
324
|
+
`Priority: ${r.priority} | Type: ${r.type} | Group: ${r.sourceSection ?? 'General'}`,
|
|
325
|
+
...criteriaList(r.businessRules).map((rule) => `- ${rule}`),
|
|
326
|
+
].join('\n')).join('\n\n')
|
|
327
|
+
: 'No explicit business rules were extracted.',
|
|
328
|
+
].join('\n'), 'Business rules extracted from requirements for assertions, boundaries, and edge cases.', ['business-rules', 'assertions']),
|
|
329
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_ACCEPTANCE_CRITERIA', [
|
|
330
|
+
'# Acceptance Criteria',
|
|
331
|
+
'',
|
|
332
|
+
'Purpose: Given/When/Then expectations used to shape expected results without inventing missing UI steps.',
|
|
333
|
+
'',
|
|
334
|
+
requirements.map((r) => [
|
|
335
|
+
`## ${r.reqId}: ${r.title}`,
|
|
336
|
+
...criteriaList(r.acceptanceCriteria).map((criterion) => `- ${criterion}`),
|
|
337
|
+
].join('\n')).join('\n\n'),
|
|
338
|
+
].join('\n'), 'Acceptance criteria normalized for expected-result generation.', ['acceptance-criteria', 'expected-results']),
|
|
339
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_TRACEABILITY', [
|
|
340
|
+
'# Requirement Traceability Hints',
|
|
341
|
+
'',
|
|
342
|
+
'Purpose: source sections and screen hints used to compare uploaded intent with crawled implementation evidence.',
|
|
343
|
+
'',
|
|
344
|
+
`| ID | Group | Screen Hints | Derived |`,
|
|
345
|
+
`|---|---|---|---|`,
|
|
346
|
+
...requirements.map((r) => `| ${mdCell(r.reqId)} | ${mdCell(r.sourceSection ?? '')} | ${mdCell(criteriaList(r.relatedScreenHints).join(', '))} | ${r.derived ? 'yes' : 'no'} |`),
|
|
347
|
+
].join('\n'), 'Source-section and screen-hint map used for truth comparison and coverage review.', ['traceability', 'truth-diff']),
|
|
348
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_WORKFLOWS', [
|
|
349
|
+
'# Requirement Workflows',
|
|
350
|
+
'',
|
|
351
|
+
'Purpose: end-to-end flows, transaction lifecycles, queues, batch jobs, printing, ordering, and operational journeys used by E2E and state-transition passes.',
|
|
352
|
+
'',
|
|
353
|
+
requirementTable(workflows, 'No workflow-oriented requirements were extracted.'),
|
|
354
|
+
].join('\n'), 'Workflow and lifecycle requirements used for E2E, state-transition, and operational test generation.', ['workflows', 'e2e', 'state-transition']),
|
|
355
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_ROLES_PERMISSIONS', [
|
|
356
|
+
'# Roles And Permissions Requirements',
|
|
357
|
+
'',
|
|
358
|
+
'Purpose: actors, roles, permission gates, access restrictions, and authorization outcomes used by RBAC and negative/security test passes.',
|
|
359
|
+
'',
|
|
360
|
+
requirementTable(roles, 'No role or permission requirements were extracted.'),
|
|
361
|
+
].join('\n'), 'Role, actor, and permission requirements used for RBAC and authorization testing.', ['roles', 'permissions', 'rbac']),
|
|
362
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_DATA_RULES', [
|
|
363
|
+
'# Data And Validation Rules',
|
|
364
|
+
'',
|
|
365
|
+
'Purpose: field constraints, identifiers, inventory values, dates, fees, defaults, and validation rules used by data, edge, and boundary-value tests.',
|
|
366
|
+
'',
|
|
367
|
+
requirementTable(dataRules, 'No data or validation requirements were extracted.'),
|
|
368
|
+
].join('\n'), 'Data rules and validation constraints for boundary, edge, and data-driven tests.', ['data', 'validation', 'boundary-values']),
|
|
369
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_INTEGRATIONS', [
|
|
370
|
+
'# Integration Requirements',
|
|
371
|
+
'',
|
|
372
|
+
'Purpose: external systems, host communication, transmissions, API behavior, response handling, and batch/integration contracts used by API and integration tests.',
|
|
373
|
+
'',
|
|
374
|
+
requirementTable(integrations, 'No integration requirements were extracted.'),
|
|
375
|
+
].join('\n'), 'Integration and external-system requirements for API, batch, and host-communication tests.', ['integrations', 'api', 'external-systems']),
|
|
376
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_COMPLIANCE_AUDIT', [
|
|
377
|
+
'# Compliance And Audit Requirements',
|
|
378
|
+
'',
|
|
379
|
+
'Purpose: agreements, audit logs, compliance gates, privacy/security evidence, and regulatory obligations used by audit and governance tests.',
|
|
380
|
+
'',
|
|
381
|
+
requirementTable(compliance, 'No compliance or audit requirements were extracted.'),
|
|
382
|
+
].join('\n'), 'Compliance, agreement, audit, and regulatory requirements for governance-focused tests.', ['compliance', 'audit', 'governance']),
|
|
383
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_ACCESSIBILITY', [
|
|
384
|
+
'# Accessibility Requirements',
|
|
385
|
+
'',
|
|
386
|
+
'Purpose: explicit accessibility expectations from requirements used by WCAG and assistive-technology test passes.',
|
|
387
|
+
'',
|
|
388
|
+
requirementTable(accessibility, 'No accessibility requirements were extracted.'),
|
|
389
|
+
].join('\n'), 'Accessibility requirements for WCAG and assistive-technology test generation.', ['accessibility', 'wcag', 'a11y']),
|
|
390
|
+
upsertRequirementSkillFile(tenantId, projectId, 'REQUIREMENTS_PERFORMANCE', [
|
|
391
|
+
'# Performance Requirements',
|
|
392
|
+
'',
|
|
393
|
+
'Purpose: timing, batch, responsiveness, stability, and load expectations used by performance and load-oriented passes.',
|
|
394
|
+
'',
|
|
395
|
+
requirementTable(performance, 'No performance requirements were extracted.'),
|
|
396
|
+
].join('\n'), 'Performance and load expectations extracted from requirement documents.', ['performance', 'load', 'sla']),
|
|
397
|
+
]);
|
|
398
|
+
}
|
|
399
|
+
function detectSourceSections(sourceText) {
|
|
400
|
+
const sections = [];
|
|
401
|
+
const seen = new Set();
|
|
402
|
+
const lines = sourceText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
403
|
+
for (const line of lines) {
|
|
404
|
+
const cleaned = line
|
|
405
|
+
.replace(/^#{1,6}\s+/, '')
|
|
406
|
+
.replace(/^\d+(?:\.\d+)*[\).:-]?\s+/, '')
|
|
407
|
+
.replace(/^[-*]\s+/, '')
|
|
408
|
+
.trim();
|
|
409
|
+
const isLikelyHeading = /^#{1,6}\s+/.test(line) ||
|
|
410
|
+
/^(feature|module|epic|story|user story|section|requirement group|screen|page|flow)\s*[:\-]/i.test(line) ||
|
|
411
|
+
// Allow parens and dots in heading text (e.g. "Title Registration (1 Step) Workflow")
|
|
412
|
+
(/^[A-Z][A-Za-z0-9 &/_().:-]{2,80}$/.test(cleaned) && cleaned.split(/\s+/).length <= 10 && !/[!?]$/.test(cleaned) && cleaned.split(/\s+/).length >= 2);
|
|
413
|
+
if (!isLikelyHeading || cleaned.length < 3 || cleaned.length > 100)
|
|
414
|
+
continue;
|
|
415
|
+
const key = cleaned.toLowerCase();
|
|
416
|
+
if (seen.has(key))
|
|
417
|
+
continue;
|
|
418
|
+
seen.add(key);
|
|
419
|
+
sections.push(cleaned);
|
|
420
|
+
if (sections.length >= 50)
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
return sections;
|
|
424
|
+
}
|
|
425
|
+
function normalizeSourceSection(value) {
|
|
426
|
+
const section = String(value ?? '').trim();
|
|
427
|
+
if (!section || section.toLowerCase() === 'null' || section.toLowerCase() === 'undefined')
|
|
428
|
+
return null;
|
|
429
|
+
return section.slice(0, 120);
|
|
430
|
+
}
|
|
431
|
+
// ─── Coverage gap analysis (Prompt 2) ───────────────────────────────────────
|
|
432
|
+
async function generateCoverageGapReport(opts) {
|
|
433
|
+
const { tenantId, projectId } = opts;
|
|
434
|
+
const [requirements, testCases, screens] = await Promise.all([
|
|
435
|
+
app_brain_1.AppBrain.listRequirements(tenantId, projectId),
|
|
436
|
+
app_brain_1.AppBrain.listTestCases(tenantId, projectId, { status: 'APPROVED' }),
|
|
437
|
+
app_brain_1.AppBrain.getProjectMap(tenantId, projectId).then(pm => (pm?.screens ?? []).map((s) => s.name)),
|
|
438
|
+
]);
|
|
439
|
+
if (requirements.length === 0) {
|
|
440
|
+
return { covered: [], untested: [], unbuilt: [], byPriority: {}, topGaps: ['No requirements extracted yet. Upload a PRD or user stories first.'] };
|
|
441
|
+
}
|
|
442
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'testPlan');
|
|
443
|
+
if (!llmCfg) {
|
|
444
|
+
// Fallback: do simple traceTo matching without LLM
|
|
445
|
+
return await matchWithoutLLM(requirements, testCases, screens);
|
|
446
|
+
}
|
|
447
|
+
const requirementsSummary = requirements.map((r) => ({
|
|
448
|
+
reqId: r.reqId,
|
|
449
|
+
title: r.title,
|
|
450
|
+
priority: r.priority,
|
|
451
|
+
type: r.type,
|
|
452
|
+
relatedScreenHints: r.relatedScreenHints,
|
|
453
|
+
acceptanceCriteria: r.acceptanceCriteria.slice(0, 2), // first 2 criteria only
|
|
454
|
+
}));
|
|
455
|
+
const casesSummary = testCases.map((tc) => ({
|
|
456
|
+
ref: tc.ref,
|
|
457
|
+
title: tc.title,
|
|
458
|
+
traceTo: tc.traceTo,
|
|
459
|
+
groundedOn: tc.groundedOn.slice(0, 3),
|
|
460
|
+
kind: tc.kind,
|
|
461
|
+
priority: tc.priority,
|
|
462
|
+
}));
|
|
463
|
+
const user = [
|
|
464
|
+
`REQUIREMENTS (${requirements.length} total):`,
|
|
465
|
+
JSON.stringify(requirementsSummary, null, 2).slice(0, 6000),
|
|
466
|
+
'',
|
|
467
|
+
`TEST CASES (${testCases.length} approved):`,
|
|
468
|
+
JSON.stringify(casesSummary, null, 2).slice(0, 4000),
|
|
469
|
+
'',
|
|
470
|
+
`CRAWLED SCREENS: ${screens.join(', ')}`,
|
|
471
|
+
].join('\n');
|
|
472
|
+
try {
|
|
473
|
+
const gapSeed = (0, llm_js_1.deterministicSeed)(`${tenantId}:${projectId}:coverage-gap:${requirements.map((r) => r.reqId).sort().join('|').slice(0, 300)}`);
|
|
474
|
+
const t0 = Date.now();
|
|
475
|
+
const gapSystem = await (0, core_1.getAgentPrompt)('requirements-gap-analysis', undefined, tenantId) ?? GAP_ANALYSIS_SYSTEM;
|
|
476
|
+
const { text: raw, usage } = await (0, llm_js_1.askTracked)(gapSystem, user, llmCfg, undefined, undefined, { temperature: 0, seed: gapSeed });
|
|
477
|
+
const latencyMs = Date.now() - t0;
|
|
478
|
+
const jsonStart = raw.indexOf('{');
|
|
479
|
+
const jsonEnd = raw.lastIndexOf('}');
|
|
480
|
+
const result = await normalizeCoverageResult(JSON.parse(raw.slice(jsonStart, jsonEnd + 1)), requirements, screens);
|
|
481
|
+
(0, app_brain_1.recordLLMCall)({
|
|
482
|
+
tenantId, projectId,
|
|
483
|
+
agentType: 'requirements',
|
|
484
|
+
label: 'coverage_gap_analysis',
|
|
485
|
+
entityType: 'requirement',
|
|
486
|
+
usage, latencyMs,
|
|
487
|
+
}).catch(console.error);
|
|
488
|
+
// Persist coverage status back to requirements
|
|
489
|
+
await app_brain_1.AppBrain.updateRequirementStatuses(tenantId, projectId, {
|
|
490
|
+
covered: result.covered.map(c => c.reqId),
|
|
491
|
+
untested: result.untested,
|
|
492
|
+
unbuilt: result.unbuilt,
|
|
493
|
+
});
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
console.error('[requirements-agent] gap analysis LLM failed:', err instanceof Error ? err.message : err);
|
|
498
|
+
return await matchWithoutLLM(requirements, testCases, screens);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// ─── Deterministic fallback (no LLM) ────────────────────────────────────────
|
|
502
|
+
function normalizeForMatch(s) {
|
|
503
|
+
return String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
504
|
+
}
|
|
505
|
+
function requirementMatchesScreen(req, screens) {
|
|
506
|
+
const screenText = screens.map(normalizeForMatch).join(' ');
|
|
507
|
+
if (!screenText)
|
|
508
|
+
return false;
|
|
509
|
+
const hints = Array.isArray(req.relatedScreenHints) ? req.relatedScreenHints : [];
|
|
510
|
+
if (hints.some((h) => {
|
|
511
|
+
const n = normalizeForMatch(h);
|
|
512
|
+
return n && screenText.includes(n);
|
|
513
|
+
}))
|
|
514
|
+
return true;
|
|
515
|
+
const stop = new Set(['the', 'a', 'an', 'to', 'and', 'or', 'for', 'with', 'can', 'be', 'is', 'are', 'user', 'system', 'requirement']);
|
|
516
|
+
const tokens = normalizeForMatch(req.title)
|
|
517
|
+
.split(/\s+/)
|
|
518
|
+
.filter((t) => t.length >= 4 && !stop.has(t));
|
|
519
|
+
return tokens.some((t) => screenText.includes(t));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Pre-compute semantic screen matches for all requirements in one pass.
|
|
523
|
+
* Returns a Set of reqIds where at least one screen is semantically relevant.
|
|
524
|
+
* Falls back gracefully if reranker is unavailable.
|
|
525
|
+
*/
|
|
526
|
+
async function buildSemanticScreenMatchSet(requirements, screens) {
|
|
527
|
+
if (screens.length === 0)
|
|
528
|
+
return new Set();
|
|
529
|
+
const matched = new Set();
|
|
530
|
+
try {
|
|
531
|
+
await Promise.all(requirements.map(async (req) => {
|
|
532
|
+
const hints = Array.isArray(req.relatedScreenHints) ? req.relatedScreenHints.filter(Boolean) : [];
|
|
533
|
+
const query = [req.title, ...hints].join(' ');
|
|
534
|
+
const ranked = await (0, app_brain_1.rerankStrings)(query, screens, 1, 0);
|
|
535
|
+
if (ranked[0] && ranked[0].score >= 0.55)
|
|
536
|
+
matched.add(req.reqId);
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// reranker unavailable — return empty set (caller falls back to sync check)
|
|
541
|
+
}
|
|
542
|
+
return matched;
|
|
543
|
+
}
|
|
544
|
+
function priorityRank(p) {
|
|
545
|
+
return { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }[p] ?? 9;
|
|
546
|
+
}
|
|
547
|
+
async function normalizeCoverageResult(result, requirements, screens) {
|
|
548
|
+
const coveredIds = new Set((result.covered ?? []).map((c) => c.reqId));
|
|
549
|
+
const untested = new Set(result.untested ?? []);
|
|
550
|
+
const unbuilt = new Set(result.unbuilt ?? []);
|
|
551
|
+
const semanticMatches = await buildSemanticScreenMatchSet(requirements, screens);
|
|
552
|
+
for (const req of requirements) {
|
|
553
|
+
if (coveredIds.has(req.reqId)) {
|
|
554
|
+
untested.delete(req.reqId);
|
|
555
|
+
unbuilt.delete(req.reqId);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const hints = Array.isArray(req.relatedScreenHints) ? req.relatedScreenHints.filter(Boolean) : [];
|
|
559
|
+
const hasScreenMatch = requirementMatchesScreen(req, screens) || semanticMatches.has(req.reqId);
|
|
560
|
+
if (unbuilt.has(req.reqId) && (hints.length === 0 || hasScreenMatch)) {
|
|
561
|
+
unbuilt.delete(req.reqId);
|
|
562
|
+
untested.add(req.reqId);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const byPriority = {};
|
|
566
|
+
for (const req of requirements) {
|
|
567
|
+
const p = req.priority;
|
|
568
|
+
if (!byPriority[p])
|
|
569
|
+
byPriority[p] = { covered: 0, total: 0 };
|
|
570
|
+
byPriority[p].total++;
|
|
571
|
+
if (coveredIds.has(req.reqId))
|
|
572
|
+
byPriority[p].covered++;
|
|
573
|
+
}
|
|
574
|
+
const topGaps = [...new Set([...unbuilt, ...untested])]
|
|
575
|
+
.map((reqId) => requirements.find((r) => r.reqId === reqId))
|
|
576
|
+
.filter(Boolean)
|
|
577
|
+
.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority))
|
|
578
|
+
.slice(0, 5)
|
|
579
|
+
.map((r) => {
|
|
580
|
+
if (unbuilt.has(r.reqId)) {
|
|
581
|
+
return `${r.priority}: Requirement ${r.reqId} is not mapped to any crawled screen; add screen hints or crawl the relevant route before coverage can be verified.`;
|
|
582
|
+
}
|
|
583
|
+
return `${r.priority}: Requirement ${r.reqId} has no approved test case linked by traceTo or matching acceptance criteria.`;
|
|
584
|
+
});
|
|
585
|
+
return {
|
|
586
|
+
covered: result.covered ?? [],
|
|
587
|
+
untested: [...untested],
|
|
588
|
+
unbuilt: [...unbuilt],
|
|
589
|
+
byPriority,
|
|
590
|
+
topGaps,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async function matchWithoutLLM(requirements, testCases, screens = []) {
|
|
594
|
+
const covered = [];
|
|
595
|
+
const untested = [];
|
|
596
|
+
const unbuilt = [];
|
|
597
|
+
const byPriority = {};
|
|
598
|
+
const semanticMatches = await buildSemanticScreenMatchSet(requirements, screens);
|
|
599
|
+
for (const req of requirements) {
|
|
600
|
+
const p = req.priority;
|
|
601
|
+
if (!byPriority[p])
|
|
602
|
+
byPriority[p] = { covered: 0, total: 0 };
|
|
603
|
+
byPriority[p].total++;
|
|
604
|
+
// Match by traceTo exact match
|
|
605
|
+
const matching = testCases.filter((tc) => tc.traceTo === req.reqId);
|
|
606
|
+
if (matching.length > 0) {
|
|
607
|
+
covered.push({ reqId: req.reqId, caseRefs: matching.map((tc) => tc.ref ?? tc.id) });
|
|
608
|
+
byPriority[p].covered++;
|
|
609
|
+
}
|
|
610
|
+
else if (requirementMatchesScreen(req, screens) || semanticMatches.has(req.reqId) || !req.relatedScreenHints?.length) {
|
|
611
|
+
untested.push(req.reqId);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
unbuilt.push(req.reqId);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const topGaps = [...unbuilt, ...untested]
|
|
618
|
+
.map(reqId => requirements.find(r => r.reqId === reqId))
|
|
619
|
+
.filter(Boolean)
|
|
620
|
+
.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority))
|
|
621
|
+
.slice(0, 5)
|
|
622
|
+
.map((r) => unbuilt.includes(r.reqId)
|
|
623
|
+
? `${r.priority}: Requirement ${r.reqId} is not mapped to any crawled screen; add screen hints or crawl the relevant route before coverage can be verified.`
|
|
624
|
+
: `${r.priority}: Requirement ${r.reqId} has no approved test case linked by traceTo or matching acceptance criteria.`);
|
|
625
|
+
return { covered, untested, unbuilt, byPriority, topGaps };
|
|
626
|
+
}
|
|
627
|
+
// ─── Requirement summary block for LLM injection ────────────────────────────
|
|
628
|
+
/**
|
|
629
|
+
* Build a compact requirements block to inject into test generation prompts.
|
|
630
|
+
* Used by test-case-agent.ts to populate traceTo with real reqIds.
|
|
631
|
+
*/
|
|
632
|
+
function buildRequirementsBlock(requirements) {
|
|
633
|
+
if (!requirements.length)
|
|
634
|
+
return '';
|
|
635
|
+
const lines = requirements.slice(0, 30).map((r) => ` ${r.reqId} [${r.priority}] ${r.title}${r.relatedScreenHints?.length ? ` (hints: ${r.relatedScreenHints.join(', ')})` : ''}`);
|
|
636
|
+
return [
|
|
637
|
+
'REQUIREMENTS (use reqId for traceTo when this test case satisfies an acceptance criterion):',
|
|
638
|
+
...lines,
|
|
639
|
+
requirements.length > 30 ? ` ... and ${requirements.length - 30} more` : '',
|
|
640
|
+
].filter(Boolean).join('\n');
|
|
641
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ScenarioOutput {
|
|
2
|
+
title: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
gherkin: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function generateScenarios(opts: {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
requirement: {
|
|
10
|
+
reqId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
actor?: string;
|
|
13
|
+
type?: string;
|
|
14
|
+
priority?: string;
|
|
15
|
+
acceptanceCriteria?: string[];
|
|
16
|
+
businessRules?: string[];
|
|
17
|
+
};
|
|
18
|
+
/** Number of scenarios to generate. Default 4, min 1, max 10. */
|
|
19
|
+
count?: number;
|
|
20
|
+
}): Promise<ScenarioOutput[]>;
|