intake-ai-cli 0.1.0__py3-none-any.whl
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.
- intake/__init__.py +6 -0
- intake/__main__.py +7 -0
- intake/analyze/__init__.py +8 -0
- intake/analyze/analyzer.py +224 -0
- intake/analyze/conflicts.py +63 -0
- intake/analyze/dedup.py +115 -0
- intake/analyze/design.py +136 -0
- intake/analyze/extraction.py +113 -0
- intake/analyze/models.py +152 -0
- intake/analyze/prompts.py +208 -0
- intake/analyze/questions.py +59 -0
- intake/analyze/risks.py +70 -0
- intake/cli.py +670 -0
- intake/config/__init__.py +8 -0
- intake/config/defaults.py +21 -0
- intake/config/loader.py +143 -0
- intake/config/presets.py +99 -0
- intake/config/schema.py +84 -0
- intake/diff/__init__.py +12 -0
- intake/diff/differ.py +314 -0
- intake/doctor/__init__.py +7 -0
- intake/doctor/checks.py +355 -0
- intake/export/__init__.py +17 -0
- intake/export/architect.py +212 -0
- intake/export/base.py +37 -0
- intake/export/generic.py +183 -0
- intake/export/registry.py +70 -0
- intake/generate/__init__.py +8 -0
- intake/generate/lock.py +154 -0
- intake/generate/spec_builder.py +193 -0
- intake/ingest/__init__.py +24 -0
- intake/ingest/base.py +233 -0
- intake/ingest/confluence.py +216 -0
- intake/ingest/docx.py +192 -0
- intake/ingest/image.py +125 -0
- intake/ingest/jira.py +231 -0
- intake/ingest/markdown.py +114 -0
- intake/ingest/pdf.py +165 -0
- intake/ingest/plaintext.py +95 -0
- intake/ingest/registry.py +207 -0
- intake/ingest/yaml_input.py +142 -0
- intake/llm/__init__.py +7 -0
- intake/llm/adapter.py +250 -0
- intake/templates/acceptance.yaml.j2 +35 -0
- intake/templates/context.md.j2 +43 -0
- intake/templates/design.md.j2 +51 -0
- intake/templates/requirements.md.j2 +68 -0
- intake/templates/sources.md.j2 +29 -0
- intake/templates/tasks.md.j2 +37 -0
- intake/utils/__init__.py +3 -0
- intake/utils/cost.py +109 -0
- intake/utils/file_detect.py +56 -0
- intake/utils/logging.py +34 -0
- intake/utils/project_detect.py +104 -0
- intake/verify/__init__.py +24 -0
- intake/verify/engine.py +363 -0
- intake/verify/reporter.py +206 -0
- intake_ai_cli-0.1.0.dist-info/METADATA +365 -0
- intake_ai_cli-0.1.0.dist-info/RECORD +61 -0
- intake_ai_cli-0.1.0.dist-info/WHEEL +4 -0
- intake_ai_cli-0.1.0.dist-info/entry_points.txt +2 -0
intake/analyze/models.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Data models for the analysis pipeline.
|
|
2
|
+
|
|
3
|
+
All models are dataclasses (lightweight, no validation overhead).
|
|
4
|
+
These flow between analyze sub-phases and into the generate module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Requirement:
|
|
14
|
+
"""A single extracted requirement (functional or non-functional)."""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
type: str # "functional" | "non_functional"
|
|
18
|
+
title: str
|
|
19
|
+
description: str
|
|
20
|
+
acceptance_criteria: list[str]
|
|
21
|
+
source: str
|
|
22
|
+
priority: str = "medium"
|
|
23
|
+
risk_score: float = 0.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Conflict:
|
|
28
|
+
"""A contradiction detected between two sources."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
description: str
|
|
32
|
+
source_a: dict[str, str]
|
|
33
|
+
source_b: dict[str, str]
|
|
34
|
+
recommendation: str
|
|
35
|
+
severity: str = "medium" # "low" | "medium" | "high"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class OpenQuestion:
|
|
40
|
+
"""An ambiguity that cannot be resolved from available sources."""
|
|
41
|
+
|
|
42
|
+
id: str
|
|
43
|
+
question: str
|
|
44
|
+
context: str
|
|
45
|
+
source: str
|
|
46
|
+
recommendation: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class RiskItem:
|
|
51
|
+
"""Risk assessment for a requirement or group of requirements."""
|
|
52
|
+
|
|
53
|
+
id: str
|
|
54
|
+
requirement_ids: list[str]
|
|
55
|
+
description: str
|
|
56
|
+
probability: str # "low" | "medium" | "high"
|
|
57
|
+
impact: str # "low" | "medium" | "high"
|
|
58
|
+
mitigation: str
|
|
59
|
+
category: str # "technical" | "scope" | "integration" | "security" | "performance"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class TechDecision:
|
|
64
|
+
"""A technical design decision linked to a requirement."""
|
|
65
|
+
|
|
66
|
+
decision: str
|
|
67
|
+
justification: str
|
|
68
|
+
requirement: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class TaskItem:
|
|
73
|
+
"""An atomic implementation task."""
|
|
74
|
+
|
|
75
|
+
id: int
|
|
76
|
+
title: str
|
|
77
|
+
description: str
|
|
78
|
+
files: list[str]
|
|
79
|
+
dependencies: list[int]
|
|
80
|
+
checks: list[str]
|
|
81
|
+
estimated_minutes: int = 15
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class FileAction:
|
|
86
|
+
"""A file to create or modify."""
|
|
87
|
+
|
|
88
|
+
path: str
|
|
89
|
+
description: str
|
|
90
|
+
action: str = "create" # "create" | "modify"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class AcceptanceCheck:
|
|
95
|
+
"""An executable acceptance check."""
|
|
96
|
+
|
|
97
|
+
id: str
|
|
98
|
+
name: str
|
|
99
|
+
type: str # "command" | "files_exist" | "pattern_present" | "pattern_absent"
|
|
100
|
+
required: bool = True
|
|
101
|
+
tags: list[str] = field(default_factory=list)
|
|
102
|
+
command: str = ""
|
|
103
|
+
paths: list[str] = field(default_factory=list)
|
|
104
|
+
glob: str = ""
|
|
105
|
+
patterns: list[str] = field(default_factory=list)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class DesignResult:
|
|
110
|
+
"""Output of the design analysis phase."""
|
|
111
|
+
|
|
112
|
+
components: list[str] = field(default_factory=list)
|
|
113
|
+
files_to_create: list[FileAction] = field(default_factory=list)
|
|
114
|
+
files_to_modify: list[FileAction] = field(default_factory=list)
|
|
115
|
+
tech_decisions: list[TechDecision] = field(default_factory=list)
|
|
116
|
+
tasks: list[TaskItem] = field(default_factory=list)
|
|
117
|
+
acceptance_checks: list[AcceptanceCheck] = field(default_factory=list)
|
|
118
|
+
dependencies: list[str] = field(default_factory=list)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class AnalysisResult:
|
|
123
|
+
"""Complete output of the analysis pipeline.
|
|
124
|
+
|
|
125
|
+
Aggregates requirements, conflicts, questions, risks, and design.
|
|
126
|
+
This is the primary data structure consumed by the generate module.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
functional_requirements: list[Requirement] = field(default_factory=list)
|
|
130
|
+
non_functional_requirements: list[Requirement] = field(default_factory=list)
|
|
131
|
+
open_questions: list[OpenQuestion] = field(default_factory=list)
|
|
132
|
+
conflicts: list[Conflict] = field(default_factory=list)
|
|
133
|
+
risks: list[RiskItem] = field(default_factory=list)
|
|
134
|
+
design: DesignResult = field(default_factory=DesignResult)
|
|
135
|
+
duplicates_removed: int = 0
|
|
136
|
+
total_cost: float = 0.0
|
|
137
|
+
model_used: str = ""
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def all_requirements(self) -> list[Requirement]:
|
|
141
|
+
"""All requirements (functional + non-functional)."""
|
|
142
|
+
return self.functional_requirements + self.non_functional_requirements
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def requirement_count(self) -> int:
|
|
146
|
+
"""Total number of requirements."""
|
|
147
|
+
return len(self.functional_requirements) + len(self.non_functional_requirements)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def task_count(self) -> int:
|
|
151
|
+
"""Total number of tasks."""
|
|
152
|
+
return len(self.design.tasks)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""System prompts for the LLM analysis pipeline.
|
|
2
|
+
|
|
3
|
+
Each prompt is a template string with named placeholders.
|
|
4
|
+
Never hardcode model-specific instructions — keep them provider-agnostic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
EXTRACTION_PROMPT = """You are a senior software requirements analyst.
|
|
10
|
+
|
|
11
|
+
You are given {n_sources} requirement sources for a software project.
|
|
12
|
+
Your job is to analyze ALL sources and produce a structured result.
|
|
13
|
+
|
|
14
|
+
OUTPUT LANGUAGE: {language}
|
|
15
|
+
REQUIREMENTS FORMAT: {requirements_format}
|
|
16
|
+
|
|
17
|
+
INSTRUCTIONS:
|
|
18
|
+
|
|
19
|
+
1. FUNCTIONAL REQUIREMENTS (FR-XX):
|
|
20
|
+
- Extract ALL functional requirements from all sources
|
|
21
|
+
- EARS format: "When [trigger], the system shall [behavior], so that [outcome]"
|
|
22
|
+
- For each requirement, list concrete and verifiable acceptance criteria
|
|
23
|
+
- Indicate which source each requirement comes from (source number)
|
|
24
|
+
|
|
25
|
+
2. NON-FUNCTIONAL REQUIREMENTS (NFR-XX):
|
|
26
|
+
- Performance, security, scalability, accessibility, etc.
|
|
27
|
+
- Each with concrete metrics when possible
|
|
28
|
+
|
|
29
|
+
3. CONFLICTS:
|
|
30
|
+
- If two sources contradict each other, document it
|
|
31
|
+
- Indicate what each source says and your recommendation
|
|
32
|
+
|
|
33
|
+
4. OPEN QUESTIONS:
|
|
34
|
+
- Ambiguities that cannot be resolved with the available information
|
|
35
|
+
- For each, indicate which source generates it and a recommendation
|
|
36
|
+
|
|
37
|
+
5. TECHNICAL DEPENDENCIES:
|
|
38
|
+
- Libraries, external services, APIs that will be needed
|
|
39
|
+
|
|
40
|
+
Respond ONLY with valid JSON, no markdown or additional text.
|
|
41
|
+
|
|
42
|
+
RESPONSE SCHEMA:
|
|
43
|
+
{{
|
|
44
|
+
"functional_requirements": [
|
|
45
|
+
{{
|
|
46
|
+
"id": "FR-01",
|
|
47
|
+
"title": "Short title",
|
|
48
|
+
"description": "When ..., the system shall ..., so that ...",
|
|
49
|
+
"acceptance_criteria": ["AC-01.1: ...", "AC-01.2: ..."],
|
|
50
|
+
"source": "Source 1",
|
|
51
|
+
"priority": "high|medium|low"
|
|
52
|
+
}}
|
|
53
|
+
],
|
|
54
|
+
"non_functional_requirements": [
|
|
55
|
+
{{
|
|
56
|
+
"id": "NFR-01",
|
|
57
|
+
"title": "Short title",
|
|
58
|
+
"description": "The system shall ...",
|
|
59
|
+
"acceptance_criteria": ["AC-NFR-01.1: ..."],
|
|
60
|
+
"source": "Source 1",
|
|
61
|
+
"priority": "high|medium|low"
|
|
62
|
+
}}
|
|
63
|
+
],
|
|
64
|
+
"conflicts": [
|
|
65
|
+
{{
|
|
66
|
+
"id": "CONFLICT-01",
|
|
67
|
+
"description": "Conflict description",
|
|
68
|
+
"source_a": {{"source": "Source 1", "says": "..."}},
|
|
69
|
+
"source_b": {{"source": "Source 2", "says": "..."}},
|
|
70
|
+
"recommendation": "...",
|
|
71
|
+
"severity": "high|medium|low"
|
|
72
|
+
}}
|
|
73
|
+
],
|
|
74
|
+
"open_questions": [
|
|
75
|
+
{{
|
|
76
|
+
"id": "Q-01",
|
|
77
|
+
"question": "...?",
|
|
78
|
+
"context": "Why this matters",
|
|
79
|
+
"source": "Source that generates the ambiguity",
|
|
80
|
+
"recommendation": "Suggestion"
|
|
81
|
+
}}
|
|
82
|
+
],
|
|
83
|
+
"dependencies": ["authlib", "postgresql", "redis"]
|
|
84
|
+
}}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
RISK_ASSESSMENT_PROMPT = """You are a senior software risk analyst.
|
|
89
|
+
|
|
90
|
+
Given these extracted requirements and detected conflicts, assess the
|
|
91
|
+
implementation risks.
|
|
92
|
+
|
|
93
|
+
For each risk, provide:
|
|
94
|
+
- Which requirements are affected
|
|
95
|
+
- Probability (low/medium/high)
|
|
96
|
+
- Impact (low/medium/high)
|
|
97
|
+
- Category (technical/scope/integration/security/performance)
|
|
98
|
+
- Concrete mitigation strategy
|
|
99
|
+
|
|
100
|
+
OUTPUT LANGUAGE: {language}
|
|
101
|
+
|
|
102
|
+
REQUIREMENTS:
|
|
103
|
+
{requirements_json}
|
|
104
|
+
|
|
105
|
+
CONFLICTS:
|
|
106
|
+
{conflicts_json}
|
|
107
|
+
|
|
108
|
+
Respond ONLY with valid JSON.
|
|
109
|
+
|
|
110
|
+
RESPONSE SCHEMA:
|
|
111
|
+
{{
|
|
112
|
+
"risks": [
|
|
113
|
+
{{
|
|
114
|
+
"id": "RISK-01",
|
|
115
|
+
"requirement_ids": ["FR-01", "NFR-03"],
|
|
116
|
+
"description": "...",
|
|
117
|
+
"probability": "medium",
|
|
118
|
+
"impact": "high",
|
|
119
|
+
"category": "technical",
|
|
120
|
+
"mitigation": "..."
|
|
121
|
+
}}
|
|
122
|
+
]
|
|
123
|
+
}}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
DESIGN_PROMPT = """You are a senior software architect.
|
|
128
|
+
|
|
129
|
+
You are given:
|
|
130
|
+
1. Extracted and structured requirements
|
|
131
|
+
2. Existing project structure (file tree)
|
|
132
|
+
3. Project tech stack
|
|
133
|
+
|
|
134
|
+
Your job is to produce:
|
|
135
|
+
|
|
136
|
+
1. TECHNICAL DESIGN:
|
|
137
|
+
- Architecture components
|
|
138
|
+
- Files to create and modify (real paths)
|
|
139
|
+
- Technical decisions with justification linked to requirements
|
|
140
|
+
|
|
141
|
+
2. TASK PLAN:
|
|
142
|
+
- Atomic tasks, each implementable in 5-30 minutes by an agent
|
|
143
|
+
- Ordered by dependencies (DAG)
|
|
144
|
+
- Each task with: affected files, verification checks
|
|
145
|
+
|
|
146
|
+
3. EXECUTABLE ACCEPTANCE CRITERIA:
|
|
147
|
+
- Shell commands that validate if a requirement is met
|
|
148
|
+
- Code patterns that must exist or not exist
|
|
149
|
+
- Files that must exist
|
|
150
|
+
|
|
151
|
+
OUTPUT LANGUAGE: {language}
|
|
152
|
+
DESIGN DEPTH: {design_depth}
|
|
153
|
+
TASK GRANULARITY: {task_granularity}
|
|
154
|
+
|
|
155
|
+
PROJECT STACK: {stack}
|
|
156
|
+
FILE TREE:
|
|
157
|
+
{file_tree}
|
|
158
|
+
|
|
159
|
+
REQUIREMENTS:
|
|
160
|
+
{requirements_json}
|
|
161
|
+
|
|
162
|
+
Respond ONLY with valid JSON.
|
|
163
|
+
|
|
164
|
+
RESPONSE SCHEMA:
|
|
165
|
+
{{
|
|
166
|
+
"components": ["component1", "component2"],
|
|
167
|
+
"files_to_create": [
|
|
168
|
+
{{"path": "src/auth/handler.py", "description": "Authentication handler"}}
|
|
169
|
+
],
|
|
170
|
+
"files_to_modify": [
|
|
171
|
+
{{"path": "src/main.py", "description": "Add auth middleware"}}
|
|
172
|
+
],
|
|
173
|
+
"tech_decisions": [
|
|
174
|
+
{{"decision": "Use JWT for auth",
|
|
175
|
+
"justification": "Stateless, scalable", "requirement": "FR-01"}}
|
|
176
|
+
],
|
|
177
|
+
"tasks": [
|
|
178
|
+
{{
|
|
179
|
+
"id": 1,
|
|
180
|
+
"title": "Set up auth module",
|
|
181
|
+
"description": "Create src/auth/ with handler and middleware",
|
|
182
|
+
"files": ["src/auth/handler.py", "src/auth/middleware.py"],
|
|
183
|
+
"dependencies": [],
|
|
184
|
+
"checks": ["pytest tests/test_auth.py -q"],
|
|
185
|
+
"estimated_minutes": 15
|
|
186
|
+
}}
|
|
187
|
+
],
|
|
188
|
+
"acceptance_checks": [
|
|
189
|
+
{{
|
|
190
|
+
"id": "unit-tests",
|
|
191
|
+
"name": "Unit tests pass",
|
|
192
|
+
"type": "command",
|
|
193
|
+
"command": "pytest tests/ -q",
|
|
194
|
+
"required": true,
|
|
195
|
+
"tags": ["test"]
|
|
196
|
+
}},
|
|
197
|
+
{{
|
|
198
|
+
"id": "auth-module-exists",
|
|
199
|
+
"name": "Auth module created",
|
|
200
|
+
"type": "files_exist",
|
|
201
|
+
"paths": ["src/auth/handler.py"],
|
|
202
|
+
"required": true,
|
|
203
|
+
"tags": ["structure"]
|
|
204
|
+
}}
|
|
205
|
+
],
|
|
206
|
+
"dependencies": ["pyjwt", "bcrypt"]
|
|
207
|
+
}}
|
|
208
|
+
"""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Open questions validation and enrichment.
|
|
2
|
+
|
|
3
|
+
Validates open questions extracted by the LLM and filters out
|
|
4
|
+
low-quality or incomplete entries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from intake.analyze.models import OpenQuestion
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_questions(questions: list[OpenQuestion]) -> list[OpenQuestion]:
|
|
20
|
+
"""Validate and filter extracted open questions.
|
|
21
|
+
|
|
22
|
+
Removes questions that lack essential fields (question text,
|
|
23
|
+
context, or recommendation).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
questions: Raw questions from the extraction phase.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Filtered list of valid open questions.
|
|
30
|
+
"""
|
|
31
|
+
valid: list[OpenQuestion] = []
|
|
32
|
+
|
|
33
|
+
for question in questions:
|
|
34
|
+
if _is_valid(question):
|
|
35
|
+
valid.append(question)
|
|
36
|
+
else:
|
|
37
|
+
logger.debug(
|
|
38
|
+
"question_filtered",
|
|
39
|
+
id=question.id,
|
|
40
|
+
reason="missing required fields",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
filtered = len(questions) - len(valid)
|
|
44
|
+
if filtered > 0:
|
|
45
|
+
logger.info(
|
|
46
|
+
"questions_validated",
|
|
47
|
+
total=len(questions),
|
|
48
|
+
valid=len(valid),
|
|
49
|
+
filtered=filtered,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return valid
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_valid(question: OpenQuestion) -> bool:
|
|
56
|
+
"""Check that a question has all required fields populated."""
|
|
57
|
+
if not question.question.strip():
|
|
58
|
+
return False
|
|
59
|
+
return bool(question.context.strip())
|
intake/analyze/risks.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Risk assessment — parses LLM risk analysis into typed models.
|
|
2
|
+
|
|
3
|
+
Called after extraction when config.spec.risk_assessment is True.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from intake.analyze.models import RiskItem
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_risks(raw: dict[str, object]) -> list[RiskItem]:
|
|
16
|
+
"""Parse the LLM risk assessment response into typed RiskItem list.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
raw: Parsed JSON dict from the LLM risk assessment call.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of validated RiskItem instances.
|
|
23
|
+
"""
|
|
24
|
+
items: list[RiskItem] = []
|
|
25
|
+
|
|
26
|
+
for entry in _get_list(raw, "risks"):
|
|
27
|
+
risk = _parse_risk_item(entry)
|
|
28
|
+
if _is_valid(risk):
|
|
29
|
+
items.append(risk)
|
|
30
|
+
else:
|
|
31
|
+
logger.debug(
|
|
32
|
+
"risk_filtered",
|
|
33
|
+
id=risk.id,
|
|
34
|
+
reason="missing required fields",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger.info("risks_parsed", count=len(items))
|
|
38
|
+
return items
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_risk_item(item: dict[str, object]) -> RiskItem:
|
|
42
|
+
"""Parse a single risk item from the LLM output."""
|
|
43
|
+
req_ids = item.get("requirement_ids", [])
|
|
44
|
+
if not isinstance(req_ids, list):
|
|
45
|
+
req_ids = []
|
|
46
|
+
|
|
47
|
+
return RiskItem(
|
|
48
|
+
id=str(item.get("id", "")),
|
|
49
|
+
requirement_ids=[str(r) for r in req_ids],
|
|
50
|
+
description=str(item.get("description", "")),
|
|
51
|
+
probability=str(item.get("probability", "medium")),
|
|
52
|
+
impact=str(item.get("impact", "medium")),
|
|
53
|
+
mitigation=str(item.get("mitigation", "")),
|
|
54
|
+
category=str(item.get("category", "technical")),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_valid(risk: RiskItem) -> bool:
|
|
59
|
+
"""Check that a risk has all required fields populated."""
|
|
60
|
+
if not risk.description.strip():
|
|
61
|
+
return False
|
|
62
|
+
return bool(risk.mitigation.strip())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_list(data: dict[str, object], key: str) -> list[dict[str, object]]:
|
|
66
|
+
"""Safely extract a list from a dict, returning empty list on failure."""
|
|
67
|
+
value = data.get(key, [])
|
|
68
|
+
if not isinstance(value, list):
|
|
69
|
+
return []
|
|
70
|
+
return [item for item in value if isinstance(item, dict)]
|