raise-cli 2.2.1__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 (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,598 @@
1
+ """Writer module for appending memory entries to JSONL files.
2
+
3
+ This module provides functions to append new patterns, calibrations,
4
+ and sessions to the memory JSONL files, with auto-ID generation
5
+ and cache invalidation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ from dataclasses import dataclass
13
+ from datetime import date
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ from raise_cli.config.paths import get_global_rai_dir, get_memory_dir, get_personal_dir
20
+ from raise_cli.memory.models import MemoryScope, PatternSubType
21
+
22
+
23
+ @dataclass
24
+ class SessionIndexValidation:
25
+ """Result of session index validation.
26
+
27
+ Attributes:
28
+ is_valid: True if no issues found.
29
+ total_entries: Number of entries in index.
30
+ entries_without_id: Entries missing ID field.
31
+ non_standard_ids: IDs not matching SES-NNN format.
32
+ duplicate_ids: IDs that appear more than once.
33
+ max_id: Highest SES-NNN number found.
34
+ gaps: List of (start, end) tuples for gaps > 5 in sequence.
35
+ """
36
+
37
+ is_valid: bool
38
+ total_entries: int
39
+ entries_without_id: int
40
+ non_standard_ids: list[str]
41
+ duplicate_ids: list[str]
42
+ max_id: int
43
+ gaps: list[tuple[int, int]]
44
+
45
+ def summary(self) -> str:
46
+ """Generate human-readable summary of validation issues."""
47
+ if self.is_valid:
48
+ return f"Session index OK: {self.total_entries} entries, max ID: SES-{self.max_id:03d}"
49
+
50
+ issues: list[str] = []
51
+ if self.entries_without_id > 0:
52
+ issues.append(f"{self.entries_without_id} entries missing ID")
53
+ if self.non_standard_ids:
54
+ issues.append(f"{len(self.non_standard_ids)} non-standard IDs")
55
+ if self.duplicate_ids:
56
+ issues.append(f"duplicates: {', '.join(self.duplicate_ids)}")
57
+ if self.gaps:
58
+ gap_strs = [f"{s}-{e}" for s, e in self.gaps]
59
+ issues.append(f"gaps: {', '.join(gap_strs)}")
60
+
61
+ return f"Session index issues: {'; '.join(issues)}"
62
+
63
+
64
+ @dataclass
65
+ class _ParsedSessionEntries:
66
+ """Intermediate result from parsing session index entries."""
67
+
68
+ total_entries: int
69
+ entries_without_id: int
70
+ non_standard_ids: list[str]
71
+ id_counts: dict[str, int]
72
+ ses_numbers: list[int]
73
+
74
+
75
+ def _parse_session_entries(file_path: Path) -> _ParsedSessionEntries:
76
+ """Parse session index JSONL and collect statistics."""
77
+ ses_pattern = re.compile(r"^SES-(\d{3})$")
78
+ entries_without_id = 0
79
+ non_standard_ids: list[str] = []
80
+ id_counts: dict[str, int] = {}
81
+ ses_numbers: list[int] = []
82
+
83
+ with file_path.open("r", encoding="utf-8") as f:
84
+ total_entries = 0
85
+ for line in f:
86
+ line = line.strip()
87
+ if not line:
88
+ continue
89
+ total_entries += 1
90
+ try:
91
+ data = json.loads(line)
92
+ entry_id = data.get("id")
93
+
94
+ if entry_id is None:
95
+ entries_without_id += 1
96
+ continue
97
+
98
+ id_counts[entry_id] = id_counts.get(entry_id, 0) + 1
99
+
100
+ match = ses_pattern.match(entry_id)
101
+ if match:
102
+ ses_numbers.append(int(match.group(1)))
103
+ else:
104
+ non_standard_ids.append(entry_id)
105
+
106
+ except json.JSONDecodeError:
107
+ entries_without_id += 1
108
+
109
+ return _ParsedSessionEntries(
110
+ total_entries=total_entries,
111
+ entries_without_id=entries_without_id,
112
+ non_standard_ids=non_standard_ids,
113
+ id_counts=id_counts,
114
+ ses_numbers=ses_numbers,
115
+ )
116
+
117
+
118
+ def _find_sequence_gaps(
119
+ ses_numbers: list[int], gap_threshold: int = 5
120
+ ) -> tuple[int, list[tuple[int, int]]]:
121
+ """Find gaps in session number sequence.
122
+
123
+ Returns:
124
+ Tuple of (max_id, list of gap tuples).
125
+ """
126
+ if not ses_numbers:
127
+ return 0, []
128
+
129
+ sorted_nums = sorted(ses_numbers)
130
+ max_id = sorted_nums[-1]
131
+ gaps: list[tuple[int, int]] = []
132
+
133
+ for i in range(1, len(sorted_nums)):
134
+ gap = sorted_nums[i] - sorted_nums[i - 1]
135
+ if gap > gap_threshold:
136
+ gaps.append((sorted_nums[i - 1], sorted_nums[i]))
137
+
138
+ return max_id, gaps
139
+
140
+
141
+ def validate_session_index(memory_dir: Path) -> SessionIndexValidation:
142
+ """Validate session index for data quality issues.
143
+
144
+ Jidoka check: detect entries without IDs, non-standard formats,
145
+ duplicates, and large gaps in sequence.
146
+
147
+ Args:
148
+ memory_dir: Path to .raise/rai/memory/ directory.
149
+
150
+ Returns:
151
+ SessionIndexValidation with findings.
152
+ """
153
+ file_path = memory_dir / "sessions" / "index.jsonl"
154
+
155
+ if not file_path.exists():
156
+ return SessionIndexValidation(
157
+ is_valid=True,
158
+ total_entries=0,
159
+ entries_without_id=0,
160
+ non_standard_ids=[],
161
+ duplicate_ids=[],
162
+ max_id=0,
163
+ gaps=[],
164
+ )
165
+
166
+ parsed = _parse_session_entries(file_path)
167
+ duplicate_ids = [k for k, v in parsed.id_counts.items() if v > 1]
168
+ max_id, gaps = _find_sequence_gaps(parsed.ses_numbers)
169
+
170
+ is_valid = (
171
+ parsed.entries_without_id == 0
172
+ and len(parsed.non_standard_ids) == 0
173
+ and len(duplicate_ids) == 0
174
+ and len(gaps) == 0
175
+ )
176
+
177
+ return SessionIndexValidation(
178
+ is_valid=is_valid,
179
+ total_entries=parsed.total_entries,
180
+ entries_without_id=parsed.entries_without_id,
181
+ non_standard_ids=parsed.non_standard_ids,
182
+ duplicate_ids=duplicate_ids,
183
+ max_id=max_id,
184
+ gaps=gaps,
185
+ )
186
+
187
+
188
+ class PatternInput(BaseModel):
189
+ """Input for creating a new pattern entry.
190
+
191
+ Attributes:
192
+ content: Pattern description.
193
+ sub_type: Pattern sub-type (codebase, process, architecture, technical).
194
+ context: Context keywords for retrieval.
195
+ learned_from: Story/session where pattern was learned.
196
+ """
197
+
198
+ content: str = Field(..., description="Pattern description")
199
+ sub_type: PatternSubType = Field(
200
+ default=PatternSubType.PROCESS, description="Pattern sub-type"
201
+ )
202
+ context: list[str] = Field(default_factory=list, description="Context keywords")
203
+ learned_from: str | None = Field(
204
+ default=None, description="Story/session where learned"
205
+ )
206
+ base: bool = Field(default=False, description="Whether this is a base pattern")
207
+ version: int | None = Field(
208
+ default=None, description="Base pattern version (for update tracking)"
209
+ )
210
+
211
+
212
+ class CalibrationInput(BaseModel):
213
+ """Input for creating a new calibration entry.
214
+
215
+ Attributes:
216
+ story: Story ID (e.g., 'F3.5').
217
+ name: Story name.
218
+ size: T-shirt size (XS, S, M, L, XL).
219
+ sp: Story points.
220
+ estimated_min: Estimated minutes (if any).
221
+ actual_min: Actual minutes.
222
+ kata_cycle: Whether kata cycle was followed.
223
+ notes: Additional notes.
224
+ """
225
+
226
+ story: str = Field(..., description="Story ID (e.g., 'F3.5')")
227
+ name: str = Field(..., description="Story name")
228
+ size: str = Field(..., description="T-shirt size (XS, S, M, L, XL)")
229
+ sp: int | None = Field(default=None, description="Story points")
230
+ estimated_min: int | None = Field(default=None, description="Estimated minutes")
231
+ actual_min: int = Field(..., description="Actual minutes")
232
+ kata_cycle: bool = Field(default=True, description="Kata cycle followed")
233
+ notes: str | None = Field(default=None, description="Additional notes")
234
+
235
+
236
+ class SessionInput(BaseModel):
237
+ """Input for creating a new session entry.
238
+
239
+ Attributes:
240
+ topic: Session topic.
241
+ session_type: Session type (story, research, maintenance, etc.).
242
+ outcomes: List of session outcomes.
243
+ log_path: Path to session log file (if any).
244
+ """
245
+
246
+ topic: str = Field(..., description="Session topic")
247
+ session_type: str = Field(
248
+ default="story", description="Session type (story, research, etc.)"
249
+ )
250
+ outcomes: list[str] = Field(default_factory=list, description="Session outcomes")
251
+ log_path: str | None = Field(default=None, description="Path to session log")
252
+
253
+
254
+ class WriteResult(BaseModel):
255
+ """Result of a write operation.
256
+
257
+ Attributes:
258
+ success: Whether write succeeded.
259
+ id: Generated ID for the entry.
260
+ file_path: Path to the file written.
261
+ message: Status message.
262
+ """
263
+
264
+ success: bool = Field(..., description="Whether write succeeded")
265
+ id: str = Field(..., description="Generated ID")
266
+ file_path: str = Field(..., description="Path to file written")
267
+ message: str = Field(default="", description="Status message")
268
+
269
+
270
+ class ReinforceResult(BaseModel):
271
+ """Result of a pattern reinforcement operation.
272
+
273
+ Attributes:
274
+ pattern_id: ID of the reinforced pattern.
275
+ vote: Vote applied (+1, 0, -1).
276
+ positives: Updated positive evaluation count.
277
+ negatives: Updated negative evaluation count.
278
+ evaluations: Updated total evaluation count.
279
+ last_evaluated: ISO date of last evaluation (None if vote=0 and never evaluated).
280
+ was_updated: False when vote=0 (N/A — file not modified).
281
+ """
282
+
283
+ pattern_id: str = Field(..., description="ID of the reinforced pattern")
284
+ vote: int = Field(..., description="Vote applied (+1, 0, -1)")
285
+ positives: int = Field(..., description="Positive evaluation count")
286
+ negatives: int = Field(..., description="Negative evaluation count")
287
+ evaluations: int = Field(..., description="Total evaluation count")
288
+ last_evaluated: str | None = Field(
289
+ default=None, description="ISO date of last evaluation"
290
+ )
291
+ was_updated: bool = Field(..., description="False when vote=0 (file not modified)")
292
+
293
+
294
+ def get_memory_dir_for_scope(
295
+ scope: MemoryScope, project_root: Path | None = None
296
+ ) -> Path:
297
+ """Get the appropriate memory directory for a given scope.
298
+
299
+ Args:
300
+ scope: Memory scope (GLOBAL, PROJECT, or PERSONAL).
301
+ project_root: Project root path. Defaults to cwd.
302
+
303
+ Returns:
304
+ Path to the memory directory for that scope.
305
+
306
+ Example:
307
+ >>> dir_path = get_memory_dir_for_scope(MemoryScope.GLOBAL)
308
+ >>> # Returns ~/.rai/
309
+ >>> dir_path = get_memory_dir_for_scope(MemoryScope.PROJECT, Path("."))
310
+ >>> # Returns .raise/rai/memory/
311
+ """
312
+ if scope == MemoryScope.GLOBAL:
313
+ return get_global_rai_dir()
314
+ elif scope == MemoryScope.PERSONAL:
315
+ return get_personal_dir(project_root)
316
+ else: # PROJECT
317
+ return get_memory_dir(project_root)
318
+
319
+
320
+ def get_next_id(
321
+ file_path: Path, prefix: str, developer_prefix: str | None = None
322
+ ) -> str:
323
+ """Get next available ID for a JSONL file.
324
+
325
+ Scans existing entries to find max ID and increments.
326
+
327
+ Args:
328
+ file_path: Path to JSONL file.
329
+ prefix: ID prefix (e.g., 'PAT', 'CAL', 'SES').
330
+ developer_prefix: Optional developer prefix for multi-dev safety.
331
+ When provided, generates e.g. 'PAT-E-001' and only counts
332
+ IDs matching that developer's prefix.
333
+
334
+ Returns:
335
+ Next ID. With developer_prefix='E': 'PAT-E-001'.
336
+ Without: 'PAT-001' (backward compatible).
337
+ """
338
+ max_num = 0
339
+
340
+ # Build the full prefix to match against
341
+ full_prefix = f"{prefix}-{developer_prefix}-" if developer_prefix else f"{prefix}-"
342
+
343
+ if file_path.exists():
344
+ with file_path.open("r", encoding="utf-8") as f:
345
+ for line in f:
346
+ line = line.strip()
347
+ if not line:
348
+ continue
349
+ try:
350
+ data = json.loads(line)
351
+ entry_id = data.get("id", "")
352
+ if entry_id.startswith(full_prefix):
353
+ num_str = entry_id[len(full_prefix) :]
354
+ num = int(num_str)
355
+ max_num = max(max_num, num)
356
+ except (json.JSONDecodeError, ValueError):
357
+ continue
358
+
359
+ if developer_prefix:
360
+ return f"{prefix}-{developer_prefix}-{max_num + 1:03d}"
361
+ return f"{prefix}-{max_num + 1:03d}"
362
+
363
+
364
+ def _append_jsonl(file_path: Path, data: dict[str, Any]) -> None:
365
+ """Append a JSON object as a line to a JSONL file.
366
+
367
+ Args:
368
+ file_path: Path to JSONL file.
369
+ data: Dictionary to serialize as JSON line.
370
+ """
371
+ file_path.parent.mkdir(parents=True, exist_ok=True)
372
+ with file_path.open("a", encoding="utf-8") as f:
373
+ f.write(json.dumps(data) + "\n")
374
+
375
+
376
+ def append_pattern(
377
+ memory_dir: Path,
378
+ input_data: PatternInput,
379
+ created: date | None = None,
380
+ scope: MemoryScope = MemoryScope.PROJECT,
381
+ developer_prefix: str | None = None,
382
+ ) -> WriteResult:
383
+ """Append a new pattern to patterns.jsonl.
384
+
385
+ Args:
386
+ memory_dir: Path to memory directory (global, project, or personal).
387
+ input_data: Pattern input data.
388
+ created: Date created (defaults to today).
389
+ scope: Memory scope for this pattern (affects ID generation context).
390
+ developer_prefix: Optional developer prefix for multi-dev safety.
391
+ When provided, generates IDs like 'PAT-E-001' instead of 'PAT-001'.
392
+
393
+ Returns:
394
+ WriteResult with generated ID and status.
395
+ """
396
+ file_path = memory_dir / "patterns.jsonl"
397
+ pattern_id = get_next_id(file_path, "PAT", developer_prefix=developer_prefix)
398
+ created_date = created or date.today()
399
+
400
+ entry: dict[str, Any] = {
401
+ "id": pattern_id,
402
+ "type": input_data.sub_type.value,
403
+ "content": input_data.content,
404
+ "context": input_data.context,
405
+ "learned_from": input_data.learned_from,
406
+ "created": created_date.isoformat(),
407
+ }
408
+
409
+ # Include base/version only for base patterns (clean output for personal patterns)
410
+ if input_data.base:
411
+ entry["base"] = True
412
+ entry["version"] = input_data.version or 1
413
+
414
+ _append_jsonl(file_path, entry)
415
+
416
+ return WriteResult(
417
+ success=True,
418
+ id=pattern_id,
419
+ file_path=str(file_path),
420
+ message=f"Pattern {pattern_id} appended to {file_path.name} (scope: {scope.value})",
421
+ )
422
+
423
+
424
+ def reinforce_pattern(
425
+ file_path: Path,
426
+ pattern_id: str,
427
+ vote: int,
428
+ story_id: str | None = None, # noqa: ARG001 (traceability, not stored in v1)
429
+ ) -> ReinforceResult:
430
+ """Update reinforcement fields for a pattern in a JSONL file.
431
+
432
+ Vote semantics:
433
+ +1 = implementation followed the pattern (positives + evaluations++)
434
+ -1 = implementation contradicted the pattern (negatives + evaluations++)
435
+ 0 = not relevant to this story (N/A — file not modified)
436
+
437
+ Rewrites the JSONL file atomically (temp file + rename) to prevent
438
+ corruption on crash.
439
+
440
+ Args:
441
+ file_path: Path to the patterns.jsonl file.
442
+ pattern_id: ID of the pattern to reinforce (e.g., 'PAT-E-183').
443
+ vote: +1, 0, or -1.
444
+ story_id: Optional story ID for traceability (not stored in JSONL v1).
445
+
446
+ Returns:
447
+ ReinforceResult with updated counts.
448
+
449
+ Raises:
450
+ KeyError: If pattern_id is not found in the file.
451
+ """
452
+ lines = file_path.read_text(encoding="utf-8").splitlines()
453
+ records: list[dict[str, Any]] = []
454
+ found = False
455
+
456
+ for line in lines:
457
+ line = line.strip()
458
+ if not line:
459
+ continue
460
+ record: dict[str, Any] = json.loads(line)
461
+ if record.get("id") == pattern_id:
462
+ found = True
463
+ if vote != 0:
464
+ record["positives"] = (record.get("positives") or 0) + (
465
+ 1 if vote == 1 else 0
466
+ )
467
+ record["negatives"] = (record.get("negatives") or 0) + (
468
+ 1 if vote == -1 else 0
469
+ )
470
+ record["evaluations"] = (record.get("evaluations") or 0) + 1
471
+ record["last_evaluated"] = date.today().isoformat()
472
+ records.append(record)
473
+
474
+ if not found:
475
+ raise KeyError(f"Pattern '{pattern_id}' not found in {file_path}")
476
+
477
+ target = next(r for r in records if r.get("id") == pattern_id)
478
+ positives = target.get("positives") or 0
479
+ negatives = target.get("negatives") or 0
480
+ evaluations = target.get("evaluations") or 0
481
+ last_evaluated: str | None = target.get("last_evaluated")
482
+
483
+ if vote == 0:
484
+ return ReinforceResult(
485
+ pattern_id=pattern_id,
486
+ vote=0,
487
+ positives=positives,
488
+ negatives=negatives,
489
+ evaluations=evaluations,
490
+ last_evaluated=last_evaluated,
491
+ was_updated=False,
492
+ )
493
+
494
+ # Atomic rewrite: write to temp, then rename
495
+ tmp_path = file_path.with_suffix(".jsonl.tmp")
496
+ tmp_path.write_text(
497
+ "\n".join(json.dumps(r) for r in records) + "\n",
498
+ encoding="utf-8",
499
+ )
500
+ tmp_path.replace(file_path)
501
+
502
+ return ReinforceResult(
503
+ pattern_id=pattern_id,
504
+ vote=vote,
505
+ positives=positives,
506
+ negatives=negatives,
507
+ evaluations=evaluations,
508
+ last_evaluated=last_evaluated,
509
+ was_updated=True,
510
+ )
511
+
512
+
513
+ def append_calibration(
514
+ memory_dir: Path,
515
+ input_data: CalibrationInput,
516
+ created: date | None = None,
517
+ scope: MemoryScope = MemoryScope.PROJECT,
518
+ ) -> WriteResult:
519
+ """Append a new calibration to calibration.jsonl.
520
+
521
+ Args:
522
+ memory_dir: Path to memory directory (global, project, or personal).
523
+ input_data: Calibration input data.
524
+ created: Date created (defaults to today).
525
+ scope: Memory scope for this calibration (affects ID generation context).
526
+
527
+ Returns:
528
+ WriteResult with generated ID and status.
529
+ """
530
+ file_path = memory_dir / "calibration.jsonl"
531
+ cal_id = get_next_id(file_path, "CAL")
532
+ created_date = created or date.today()
533
+
534
+ # Calculate ratio if both estimated and actual present
535
+ ratio: float | None = None
536
+ if input_data.estimated_min and input_data.actual_min:
537
+ ratio = round(input_data.estimated_min / input_data.actual_min, 1)
538
+
539
+ entry = {
540
+ "id": cal_id,
541
+ "story": input_data.story,
542
+ "name": input_data.name,
543
+ "size": input_data.size,
544
+ "sp": input_data.sp,
545
+ "estimated_min": input_data.estimated_min,
546
+ "actual_min": input_data.actual_min,
547
+ "ratio": ratio,
548
+ "kata_cycle": input_data.kata_cycle,
549
+ "notes": input_data.notes,
550
+ "created": created_date.isoformat(),
551
+ }
552
+
553
+ _append_jsonl(file_path, entry)
554
+
555
+ return WriteResult(
556
+ success=True,
557
+ id=cal_id,
558
+ file_path=str(file_path),
559
+ message=f"Calibration {cal_id} appended to {file_path.name} (scope: {scope.value})",
560
+ )
561
+
562
+
563
+ def append_session(
564
+ memory_dir: Path,
565
+ input_data: SessionInput,
566
+ session_date: date | None = None,
567
+ ) -> WriteResult:
568
+ """Append a new session to sessions/index.jsonl.
569
+
570
+ Args:
571
+ memory_dir: Path to .raise/rai/memory/ directory.
572
+ input_data: Session input data.
573
+ session_date: Session date (defaults to today).
574
+
575
+ Returns:
576
+ WriteResult with generated ID and status.
577
+ """
578
+ file_path = memory_dir / "sessions" / "index.jsonl"
579
+ session_id = get_next_id(file_path, "SES")
580
+ session_date_val = session_date or date.today()
581
+
582
+ entry = {
583
+ "id": session_id,
584
+ "date": session_date_val.isoformat(),
585
+ "type": input_data.session_type,
586
+ "topic": input_data.topic,
587
+ "outcomes": input_data.outcomes,
588
+ "log_path": input_data.log_path,
589
+ }
590
+
591
+ _append_jsonl(file_path, entry)
592
+
593
+ return WriteResult(
594
+ success=True,
595
+ id=session_id,
596
+ file_path=str(file_path),
597
+ message=f"Session {session_id} appended to {file_path.name}",
598
+ )
@@ -0,0 +1,103 @@
1
+ """Onboarding module for RaiSE CLI.
2
+
3
+ Handles developer profile management, project initialization, and convention detection.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from raise_cli.onboarding.conventions import (
9
+ Confidence,
10
+ ConventionResult,
11
+ IndentationConvention,
12
+ LineLengthConvention,
13
+ NamingConvention,
14
+ NamingConventions,
15
+ QuoteConvention,
16
+ StructureConventions,
17
+ StyleConventions,
18
+ detect_conventions,
19
+ )
20
+ from raise_cli.onboarding.detection import (
21
+ CODE_EXTENSIONS,
22
+ DetectionResult,
23
+ ProjectType,
24
+ count_code_files,
25
+ detect_project_type,
26
+ )
27
+ from raise_cli.onboarding.governance import (
28
+ GeneratedGuardrail,
29
+ GuardrailGenerator,
30
+ GuardrailLevel,
31
+ generate_guardrails,
32
+ )
33
+ from raise_cli.onboarding.instructions import (
34
+ ClaudeMdGenerator,
35
+ InstructionsGenerator,
36
+ generate_claude_md,
37
+ generate_instructions,
38
+ )
39
+ from raise_cli.onboarding.manifest import (
40
+ BranchConfig,
41
+ ProjectInfo,
42
+ ProjectManifest,
43
+ load_manifest,
44
+ save_manifest,
45
+ )
46
+ from raise_cli.onboarding.migration import migrate_developer_profile
47
+ from raise_cli.onboarding.profile import (
48
+ CommunicationPreferences,
49
+ CommunicationStyle,
50
+ DeveloperProfile,
51
+ ExperienceLevel,
52
+ get_rai_home,
53
+ increment_session,
54
+ load_developer_profile,
55
+ save_developer_profile,
56
+ )
57
+
58
+ __all__ = [
59
+ # Conventions
60
+ "Confidence",
61
+ "ConventionResult",
62
+ "IndentationConvention",
63
+ "LineLengthConvention",
64
+ "NamingConvention",
65
+ "NamingConventions",
66
+ "QuoteConvention",
67
+ "StructureConventions",
68
+ "StyleConventions",
69
+ "detect_conventions",
70
+ # Instructions
71
+ "InstructionsGenerator",
72
+ "generate_instructions",
73
+ # Backward-compat
74
+ "ClaudeMdGenerator",
75
+ "generate_claude_md",
76
+ # Detection
77
+ "CODE_EXTENSIONS",
78
+ "DetectionResult",
79
+ "ProjectType",
80
+ "count_code_files",
81
+ "detect_project_type",
82
+ # Governance
83
+ "GeneratedGuardrail",
84
+ "GuardrailGenerator",
85
+ "GuardrailLevel",
86
+ "generate_guardrails",
87
+ # Manifest
88
+ "BranchConfig",
89
+ "ProjectInfo",
90
+ "ProjectManifest",
91
+ "load_manifest",
92
+ "save_manifest",
93
+ # Profile
94
+ "CommunicationPreferences",
95
+ "CommunicationStyle",
96
+ "DeveloperProfile",
97
+ "ExperienceLevel",
98
+ "get_rai_home",
99
+ "increment_session",
100
+ "load_developer_profile",
101
+ "migrate_developer_profile",
102
+ "save_developer_profile",
103
+ ]