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.
Files changed (61) hide show
  1. intake/__init__.py +6 -0
  2. intake/__main__.py +7 -0
  3. intake/analyze/__init__.py +8 -0
  4. intake/analyze/analyzer.py +224 -0
  5. intake/analyze/conflicts.py +63 -0
  6. intake/analyze/dedup.py +115 -0
  7. intake/analyze/design.py +136 -0
  8. intake/analyze/extraction.py +113 -0
  9. intake/analyze/models.py +152 -0
  10. intake/analyze/prompts.py +208 -0
  11. intake/analyze/questions.py +59 -0
  12. intake/analyze/risks.py +70 -0
  13. intake/cli.py +670 -0
  14. intake/config/__init__.py +8 -0
  15. intake/config/defaults.py +21 -0
  16. intake/config/loader.py +143 -0
  17. intake/config/presets.py +99 -0
  18. intake/config/schema.py +84 -0
  19. intake/diff/__init__.py +12 -0
  20. intake/diff/differ.py +314 -0
  21. intake/doctor/__init__.py +7 -0
  22. intake/doctor/checks.py +355 -0
  23. intake/export/__init__.py +17 -0
  24. intake/export/architect.py +212 -0
  25. intake/export/base.py +37 -0
  26. intake/export/generic.py +183 -0
  27. intake/export/registry.py +70 -0
  28. intake/generate/__init__.py +8 -0
  29. intake/generate/lock.py +154 -0
  30. intake/generate/spec_builder.py +193 -0
  31. intake/ingest/__init__.py +24 -0
  32. intake/ingest/base.py +233 -0
  33. intake/ingest/confluence.py +216 -0
  34. intake/ingest/docx.py +192 -0
  35. intake/ingest/image.py +125 -0
  36. intake/ingest/jira.py +231 -0
  37. intake/ingest/markdown.py +114 -0
  38. intake/ingest/pdf.py +165 -0
  39. intake/ingest/plaintext.py +95 -0
  40. intake/ingest/registry.py +207 -0
  41. intake/ingest/yaml_input.py +142 -0
  42. intake/llm/__init__.py +7 -0
  43. intake/llm/adapter.py +250 -0
  44. intake/templates/acceptance.yaml.j2 +35 -0
  45. intake/templates/context.md.j2 +43 -0
  46. intake/templates/design.md.j2 +51 -0
  47. intake/templates/requirements.md.j2 +68 -0
  48. intake/templates/sources.md.j2 +29 -0
  49. intake/templates/tasks.md.j2 +37 -0
  50. intake/utils/__init__.py +3 -0
  51. intake/utils/cost.py +109 -0
  52. intake/utils/file_detect.py +56 -0
  53. intake/utils/logging.py +34 -0
  54. intake/utils/project_detect.py +104 -0
  55. intake/verify/__init__.py +24 -0
  56. intake/verify/engine.py +363 -0
  57. intake/verify/reporter.py +206 -0
  58. intake_ai_cli-0.1.0.dist-info/METADATA +365 -0
  59. intake_ai_cli-0.1.0.dist-info/RECORD +61 -0
  60. intake_ai_cli-0.1.0.dist-info/WHEEL +4 -0
  61. intake_ai_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -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())
@@ -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)]