its-magic 0.1.3-0 → 0.1.3-2
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/installer.ps1 +8 -0
- package/installer.py +8 -0
- package/installer.sh +1 -0
- package/package.json +1 -1
- package/scripts/check_intake_template_parity.py +62 -0
- package/template/.cursor/agents/curator.mdc +1 -0
- package/template/.cursor/agents/po.mdc +1 -0
- package/template/.cursor/agents/release.mdc +1 -0
- package/template/.cursor/commands/auto.md +90 -0
- package/template/.cursor/commands/execute.md +26 -0
- package/template/.cursor/commands/refresh-context.md +32 -0
- package/template/.cursor/commands/sovereign-critic.md +104 -0
- package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
- package/template/.cursor/model-catalog.local.example.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
- package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
- package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
- package/template/.cursor/rules/sovereign-role-manifest.mdc.example +27 -0
- package/template/.cursor/scratchpad.local.example.md +55 -0
- package/template/.cursor/scratchpad.md +203 -0
- package/template/.cursor/sovereign-role-manifest.yaml.example +53 -0
- package/template/decisions/DEC-0104.md +279 -0
- package/template/decisions/DEC-0105.md +231 -0
- package/template/decisions/DEC-0107.md +246 -0
- package/template/docs/engineering/architecture.md +78 -0
- package/template/docs/engineering/auto-orchestration-reference.md +7 -0
- package/template/docs/engineering/context/installer-owned-paths.manifest +8 -0
- package/template/docs/engineering/reason_codes.md +411 -0
- package/template/docs/engineering/runbook.md +1119 -0
- package/template/docs/engineering/sovereign-memory/.gitkeep +0 -0
- package/template/docs/engineering/sovereign-memory/retrospectives/.gitkeep +0 -0
- package/template/handoffs/sovereign_decisions/.gitkeep +0 -0
- package/template/handoffs/sovereign_deferrals/.gitkeep +0 -0
- package/template/handoffs/sovereign_role_reviews.jsonl +0 -0
- package/template/scripts/__pycache__/decision_ledger_lib.cpython-312.pyc +0 -0
- package/template/scripts/check_intake_template_parity.py +181 -0
- package/template/scripts/decision_ledger_lib.py +732 -0
- package/template/scripts/ledger_validate.py +153 -0
- package/template/scripts/model_tier_lib.py +680 -0
- package/template/scripts/model_tier_validate.py +368 -0
- package/template/scripts/parallel_dev_arbiter.py +923 -0
- package/template/scripts/release_trigger_adapters.py +843 -0
- package/template/scripts/self_healing_deploy_lib.py +463 -0
- package/template/scripts/self_healing_deploy_validate.py +78 -0
- package/template/scripts/sovereign_convergence_lib.py +994 -0
- package/template/scripts/sovereign_convergence_validate.py +206 -0
- package/template/scripts/sovereign_critic_lib.py +629 -0
- package/template/scripts/sovereign_critic_validate.py +131 -0
- package/template/scripts/sovereign_loop_lib.py +828 -0
- package/template/scripts/sovereign_loop_validate.py +122 -0
- package/template/scripts/sovereign_memory_lib.py +869 -0
- package/template/scripts/sovereign_memory_validate.py +153 -0
- package/template/scripts/sovereign_role_manifest_lib.py +547 -0
- package/template/scripts/sovereign_role_manifest_validate.py +105 -0
- package/template/tests/us0108_contract_test.py +207 -0
- package/template/tests/us0109_contract_test.py +209 -0
- package/template/tests/us0109_us0110_compose_test.py +34 -0
- package/template/tests/us0111_contract_test.py +345 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sovereign Memory helper library (US-0105 / DEC-0105).
|
|
4
|
+
|
|
5
|
+
Project-level institutional memory: bounded JSONL artifacts under
|
|
6
|
+
docs/engineering/sovereign-memory/ with phase-spawn injection digest.
|
|
7
|
+
|
|
8
|
+
Reason codes (DEC-0105 §9):
|
|
9
|
+
SOVEREIGN_MEMORY_DISABLED, SOVEREIGN_MEMORY_SCHEMA_INVALID,
|
|
10
|
+
SOVEREIGN_MEMORY_APPEND_FAILED, SOVEREIGN_MEMORY_DECISION_DUPLICATE,
|
|
11
|
+
SOVEREIGN_MEMORY_SECRET_DETECTED, SOVEREIGN_MEMORY_ARCHIVE_REQUIRED,
|
|
12
|
+
SOVEREIGN_MEMORY_READ_BOUND, SOVEREIGN_MEMORY_PROMOTION_SKIPPED
|
|
13
|
+
|
|
14
|
+
Default-off: SOVEREIGN_MEMORY=0 → zero overhead (no reads, no writes, no digest).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
|
30
|
+
|
|
31
|
+
SCHEMA_VERSION = 1
|
|
32
|
+
MEMORY_DIR_REL = "docs/engineering/sovereign-memory"
|
|
33
|
+
ARCHIVE_DIR_REL = "docs/engineering/sovereign-memory-archive"
|
|
34
|
+
RETROSPECTIVES_SUBDIR = "retrospectives"
|
|
35
|
+
|
|
36
|
+
SOVEREIGN_MEMORY_KEY = "SOVEREIGN_MEMORY"
|
|
37
|
+
SOVEREIGN_MEMORY_TOP_N_KEY = "SOVEREIGN_MEMORY_TOP_N"
|
|
38
|
+
SOVEREIGN_MEMORY_TOP_K_KEY = "SOVEREIGN_MEMORY_TOP_K"
|
|
39
|
+
SOVEREIGN_MEMORY_MAX_CHARS_KEY = "SOVEREIGN_MEMORY_MAX_CHARS"
|
|
40
|
+
SOVEREIGN_MEMORY_JSONL_MAX_LINES_KEY = "SOVEREIGN_MEMORY_JSONL_MAX_LINES"
|
|
41
|
+
|
|
42
|
+
SOVEREIGN_MEMORY_VALUES = frozenset({"0", "1"})
|
|
43
|
+
SOVEREIGN_MEMORY_DEFAULT = "0"
|
|
44
|
+
SOVEREIGN_MEMORY_TOP_N_DEFAULT = 5
|
|
45
|
+
SOVEREIGN_MEMORY_TOP_K_DEFAULT = 3
|
|
46
|
+
SOVEREIGN_MEMORY_MAX_CHARS_DEFAULT = 2048
|
|
47
|
+
SOVEREIGN_MEMORY_JSONL_MAX_LINES_DEFAULT = 500
|
|
48
|
+
SOVEREIGN_MEMORY_READ_TAIL_DEFAULT = 500
|
|
49
|
+
|
|
50
|
+
TEXT_MAX_CHARS = 2000
|
|
51
|
+
RATIONALE_MAX_CHARS = 2000
|
|
52
|
+
TAGS_MAX = 10
|
|
53
|
+
TAG_MAX_CHARS = 40
|
|
54
|
+
DECISION_KEY_HEX_LEN = 16
|
|
55
|
+
|
|
56
|
+
JSONL_FAMILIES = frozenset({"decisions", "mistakes", "patterns", "plan-drift"})
|
|
57
|
+
JSONL_FILENAMES = {
|
|
58
|
+
"decisions": "decisions-log.jsonl",
|
|
59
|
+
"mistakes": "mistakes.jsonl",
|
|
60
|
+
"patterns": "patterns.jsonl",
|
|
61
|
+
"plan-drift": "plan-drift-register.jsonl",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
STATUS_VALUES = frozenset({"active", "superseded", "archived"})
|
|
65
|
+
MISTAKE_TAG_VALUES = frozenset({
|
|
66
|
+
"fix_failed",
|
|
67
|
+
"revert_applied",
|
|
68
|
+
"plan_fidelity_violation",
|
|
69
|
+
"test_regression",
|
|
70
|
+
"scope_creep",
|
|
71
|
+
})
|
|
72
|
+
MISTAKE_HOOK_TABLE = {
|
|
73
|
+
"fix_failed": "FIX_FAILED",
|
|
74
|
+
"revert_applied": "REVERT_APPLIED",
|
|
75
|
+
"plan_fidelity_violation": "PLAN_FIDELITY_VIOLATION",
|
|
76
|
+
"test_regression": "TEST_REGRESSION",
|
|
77
|
+
"scope_creep": "PLAN_FIDELITY_SCOPE_GATE",
|
|
78
|
+
}
|
|
79
|
+
DRIFT_TYPE_VALUES = frozenset({
|
|
80
|
+
"ac_drop",
|
|
81
|
+
"ac_reorder",
|
|
82
|
+
"scope_add",
|
|
83
|
+
"plan_change",
|
|
84
|
+
"acceptance_drift",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
BASE_REQUIRED_FIELDS = frozenset({
|
|
88
|
+
"schema_version",
|
|
89
|
+
"ts",
|
|
90
|
+
"entry_id",
|
|
91
|
+
"impact_score",
|
|
92
|
+
"text",
|
|
93
|
+
"tags",
|
|
94
|
+
"status",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
DECISIONS_REQUIRED = BASE_REQUIRED_FIELDS | frozenset({
|
|
98
|
+
"decision_key",
|
|
99
|
+
"decision_text",
|
|
100
|
+
"rationale",
|
|
101
|
+
})
|
|
102
|
+
MISTAKES_REQUIRED = BASE_REQUIRED_FIELDS | frozenset({
|
|
103
|
+
"mistake_tag",
|
|
104
|
+
"failure_reason_code",
|
|
105
|
+
})
|
|
106
|
+
PATTERNS_REQUIRED = BASE_REQUIRED_FIELDS | frozenset({
|
|
107
|
+
"pattern_id",
|
|
108
|
+
"applies_to",
|
|
109
|
+
})
|
|
110
|
+
PLAN_DRIFT_REQUIRED = BASE_REQUIRED_FIELDS | frozenset({
|
|
111
|
+
"drift_type",
|
|
112
|
+
"from_artifact",
|
|
113
|
+
"to_artifact",
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
FAMILY_REQUIRED = {
|
|
117
|
+
"decisions": DECISIONS_REQUIRED,
|
|
118
|
+
"mistakes": MISTAKES_REQUIRED,
|
|
119
|
+
"patterns": PATTERNS_REQUIRED,
|
|
120
|
+
"plan-drift": PLAN_DRIFT_REQUIRED,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
SECRET_PATTERNS = (
|
|
124
|
+
re.compile(r"(?i)(api[_-]?key|secret|password|token|bearer)\s*[:=]\s*\S+"),
|
|
125
|
+
re.compile(r"(?i)-----BEGIN (RSA |EC )?PRIVATE KEY-----"),
|
|
126
|
+
re.compile(r"sk-[a-zA-Z0-9]{20,}"),
|
|
127
|
+
re.compile(r"ghp_[a-zA-Z0-9]{20,}"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ReasonCode(str, Enum):
|
|
132
|
+
SOVEREIGN_MEMORY_DISABLED = "SOVEREIGN_MEMORY_DISABLED"
|
|
133
|
+
SOVEREIGN_MEMORY_SCHEMA_INVALID = "SOVEREIGN_MEMORY_SCHEMA_INVALID"
|
|
134
|
+
SOVEREIGN_MEMORY_APPEND_FAILED = "SOVEREIGN_MEMORY_APPEND_FAILED"
|
|
135
|
+
SOVEREIGN_MEMORY_DECISION_DUPLICATE = "SOVEREIGN_MEMORY_DECISION_DUPLICATE"
|
|
136
|
+
SOVEREIGN_MEMORY_SECRET_DETECTED = "SOVEREIGN_MEMORY_SECRET_DETECTED"
|
|
137
|
+
SOVEREIGN_MEMORY_ARCHIVE_REQUIRED = "SOVEREIGN_MEMORY_ARCHIVE_REQUIRED"
|
|
138
|
+
SOVEREIGN_MEMORY_READ_BOUND = "SOVEREIGN_MEMORY_READ_BOUND"
|
|
139
|
+
SOVEREIGN_MEMORY_PROMOTION_SKIPPED = "SOVEREIGN_MEMORY_PROMOTION_SKIPPED"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class InjectionDigest:
|
|
144
|
+
digest_text: str
|
|
145
|
+
entry_ids: List[str] = field(default_factory=list)
|
|
146
|
+
char_count: int = 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _utc_now_iso() -> str:
|
|
150
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _archive_timestamp() -> str:
|
|
154
|
+
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _fsync_file(path: Path) -> None:
|
|
158
|
+
try:
|
|
159
|
+
fd = os.open(str(path), os.O_RDONLY)
|
|
160
|
+
try:
|
|
161
|
+
os.fsync(fd)
|
|
162
|
+
except OSError:
|
|
163
|
+
pass
|
|
164
|
+
finally:
|
|
165
|
+
os.close(fd)
|
|
166
|
+
except OSError:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_scratchpad_int(
|
|
171
|
+
scratchpad: Optional[Dict[str, str]],
|
|
172
|
+
key: str,
|
|
173
|
+
default: int,
|
|
174
|
+
*,
|
|
175
|
+
minimum: int = 0,
|
|
176
|
+
maximum: Optional[int] = None,
|
|
177
|
+
) -> int:
|
|
178
|
+
pad = scratchpad or {}
|
|
179
|
+
raw = pad.get(key, str(default))
|
|
180
|
+
try:
|
|
181
|
+
value = int(str(raw).strip())
|
|
182
|
+
except (TypeError, ValueError):
|
|
183
|
+
value = default
|
|
184
|
+
if maximum is not None:
|
|
185
|
+
value = min(maximum, value)
|
|
186
|
+
return max(minimum, value)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def is_sovereign_memory_enabled(scratchpad: Optional[Dict[str, str]]) -> bool:
|
|
190
|
+
if not scratchpad:
|
|
191
|
+
return False
|
|
192
|
+
return scratchpad.get(SOVEREIGN_MEMORY_KEY, SOVEREIGN_MEMORY_DEFAULT).strip() == "1"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def resolve_memory_dir(repo_root: Optional[Path] = None) -> Path:
|
|
196
|
+
root = Path(repo_root or ".").resolve()
|
|
197
|
+
return root / MEMORY_DIR_REL
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def resolve_archive_dir(repo_root: Optional[Path] = None) -> Path:
|
|
201
|
+
root = Path(repo_root or ".").resolve()
|
|
202
|
+
return root / ARCHIVE_DIR_REL
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def resolve_jsonl_path(family: str, repo_root: Optional[Path] = None) -> Path:
|
|
206
|
+
if family not in JSONL_FAMILIES:
|
|
207
|
+
raise ValueError(f"unknown family: {family}")
|
|
208
|
+
return resolve_memory_dir(repo_root) / JSONL_FILENAMES[family]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def resolve_retrospectives_dir(repo_root: Optional[Path] = None) -> Path:
|
|
212
|
+
return resolve_memory_dir(repo_root) / RETROSPECTIVES_SUBDIR
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def normalize_decision_text(text: str) -> str:
|
|
216
|
+
return " ".join(str(text).strip().lower().split())
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def compute_decision_key(decision_text: str) -> str:
|
|
220
|
+
normalized = normalize_decision_text(decision_text)
|
|
221
|
+
digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
222
|
+
return digest[:DECISION_KEY_HEX_LEN]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def scan_secrets(text: str) -> Optional[ReasonCode]:
|
|
226
|
+
for pattern in SECRET_PATTERNS:
|
|
227
|
+
if pattern.search(text or ""):
|
|
228
|
+
return ReasonCode.SOVEREIGN_MEMORY_SECRET_DETECTED
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def schema_check(entry: dict, family: str) -> Tuple[bool, Optional[str]]:
|
|
233
|
+
if family not in JSONL_FAMILIES:
|
|
234
|
+
return False, f"unknown family: {family}"
|
|
235
|
+
if not isinstance(entry, dict):
|
|
236
|
+
return False, "entry must be a dict"
|
|
237
|
+
|
|
238
|
+
required = FAMILY_REQUIRED[family]
|
|
239
|
+
missing = required - set(entry.keys())
|
|
240
|
+
if missing:
|
|
241
|
+
return False, f"missing fields: {sorted(missing)}"
|
|
242
|
+
|
|
243
|
+
if entry.get("schema_version") != SCHEMA_VERSION:
|
|
244
|
+
return False, "schema_version must be 1"
|
|
245
|
+
|
|
246
|
+
if entry.get("status") not in STATUS_VALUES:
|
|
247
|
+
return False, f"status must be one of {sorted(STATUS_VALUES)}"
|
|
248
|
+
|
|
249
|
+
impact = entry.get("impact_score")
|
|
250
|
+
if not isinstance(impact, int) or impact < 0 or impact > 100:
|
|
251
|
+
return False, "impact_score must be int 0..100"
|
|
252
|
+
|
|
253
|
+
tags = entry.get("tags")
|
|
254
|
+
if not isinstance(tags, list) or len(tags) > TAGS_MAX:
|
|
255
|
+
return False, f"tags must be list with at most {TAGS_MAX} items"
|
|
256
|
+
for tag in tags:
|
|
257
|
+
if not isinstance(tag, str) or len(tag) > TAG_MAX_CHARS:
|
|
258
|
+
return False, f"each tag must be string max {TAG_MAX_CHARS} chars"
|
|
259
|
+
|
|
260
|
+
text = entry.get("text", "")
|
|
261
|
+
if not isinstance(text, str) or not text.strip() or len(text) > TEXT_MAX_CHARS:
|
|
262
|
+
return False, f"text must be non-empty string max {TEXT_MAX_CHARS} chars"
|
|
263
|
+
|
|
264
|
+
secret = scan_secrets(text)
|
|
265
|
+
if secret:
|
|
266
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SECRET_DETECTED.value
|
|
267
|
+
|
|
268
|
+
if family == "decisions":
|
|
269
|
+
rationale = entry.get("rationale", "")
|
|
270
|
+
if not isinstance(rationale, str) or not rationale.strip() or len(rationale) > RATIONALE_MAX_CHARS:
|
|
271
|
+
return False, f"rationale must be non-empty max {RATIONALE_MAX_CHARS} chars"
|
|
272
|
+
if scan_secrets(rationale):
|
|
273
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SECRET_DETECTED.value
|
|
274
|
+
dt = entry.get("decision_text", "")
|
|
275
|
+
if scan_secrets(str(dt)):
|
|
276
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SECRET_DETECTED.value
|
|
277
|
+
expected_key = compute_decision_key(str(dt))
|
|
278
|
+
if entry.get("decision_key") != expected_key:
|
|
279
|
+
return False, "decision_key mismatch"
|
|
280
|
+
|
|
281
|
+
if family == "mistakes":
|
|
282
|
+
if entry.get("mistake_tag") not in MISTAKE_TAG_VALUES:
|
|
283
|
+
return False, f"mistake_tag must be one of {sorted(MISTAKE_TAG_VALUES)}"
|
|
284
|
+
|
|
285
|
+
if family == "patterns":
|
|
286
|
+
applies = entry.get("applies_to")
|
|
287
|
+
if not isinstance(applies, list):
|
|
288
|
+
return False, "applies_to must be a list"
|
|
289
|
+
|
|
290
|
+
if family == "plan-drift":
|
|
291
|
+
if entry.get("drift_type") not in DRIFT_TYPE_VALUES:
|
|
292
|
+
return False, f"drift_type must be one of {sorted(DRIFT_TYPE_VALUES)}"
|
|
293
|
+
|
|
294
|
+
return True, None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _count_nonempty_lines(path: Path) -> int:
|
|
298
|
+
if not path.is_file():
|
|
299
|
+
return 0
|
|
300
|
+
count = 0
|
|
301
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
302
|
+
for line in handle:
|
|
303
|
+
if line.strip():
|
|
304
|
+
count += 1
|
|
305
|
+
return count
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _load_existing_decision_keys(repo_root: Optional[Path] = None) -> Set[str]:
|
|
309
|
+
path = resolve_jsonl_path("decisions", repo_root)
|
|
310
|
+
if not path.is_file():
|
|
311
|
+
return set()
|
|
312
|
+
keys: Set[str] = set()
|
|
313
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
314
|
+
for line in handle:
|
|
315
|
+
if not line.strip():
|
|
316
|
+
continue
|
|
317
|
+
try:
|
|
318
|
+
obj = json.loads(line)
|
|
319
|
+
except json.JSONDecodeError:
|
|
320
|
+
continue
|
|
321
|
+
key = obj.get("decision_key")
|
|
322
|
+
if isinstance(key, str):
|
|
323
|
+
keys.add(key)
|
|
324
|
+
return keys
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _ensure_memory_dir(repo_root: Optional[Path] = None) -> Tuple[bool, Optional[ReasonCode]]:
|
|
328
|
+
try:
|
|
329
|
+
resolve_memory_dir(repo_root).mkdir(parents=True, exist_ok=True)
|
|
330
|
+
return True, None
|
|
331
|
+
except OSError:
|
|
332
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_APPEND_FAILED
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _append_jsonl_line(
|
|
336
|
+
family: str,
|
|
337
|
+
entry: dict,
|
|
338
|
+
repo_root: Optional[Path] = None,
|
|
339
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
340
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
341
|
+
ok_dir, rc_dir = _ensure_memory_dir(repo_root)
|
|
342
|
+
if not ok_dir:
|
|
343
|
+
return False, rc_dir
|
|
344
|
+
|
|
345
|
+
ok_archive, rc_archive = maybe_archive_jsonl(family, repo_root, scratchpad)
|
|
346
|
+
if not ok_archive:
|
|
347
|
+
return False, rc_archive
|
|
348
|
+
|
|
349
|
+
path = resolve_jsonl_path(family, repo_root)
|
|
350
|
+
try:
|
|
351
|
+
line = json.dumps(entry, separators=(",", ":"), ensure_ascii=False) + "\n"
|
|
352
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
353
|
+
handle.write(line)
|
|
354
|
+
handle.flush()
|
|
355
|
+
_fsync_file(path)
|
|
356
|
+
return True, None
|
|
357
|
+
except OSError:
|
|
358
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_APPEND_FAILED
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def read_entries(
|
|
362
|
+
family: str,
|
|
363
|
+
repo_root: Optional[Path] = None,
|
|
364
|
+
*,
|
|
365
|
+
tail_n: Optional[int] = None,
|
|
366
|
+
active_only: bool = True,
|
|
367
|
+
) -> Tuple[List[dict], Optional[ReasonCode]]:
|
|
368
|
+
path = resolve_jsonl_path(family, repo_root)
|
|
369
|
+
if not path.exists():
|
|
370
|
+
return [], None
|
|
371
|
+
|
|
372
|
+
bound = tail_n if tail_n is not None else SOVEREIGN_MEMORY_READ_TAIL_DEFAULT
|
|
373
|
+
lines: List[str] = []
|
|
374
|
+
truncated = False
|
|
375
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
376
|
+
for line in handle:
|
|
377
|
+
if line.strip():
|
|
378
|
+
lines.append(line)
|
|
379
|
+
if len(lines) > bound:
|
|
380
|
+
truncated = True
|
|
381
|
+
lines = lines[-bound:]
|
|
382
|
+
|
|
383
|
+
entries: List[dict] = []
|
|
384
|
+
for line in lines:
|
|
385
|
+
try:
|
|
386
|
+
obj = json.loads(line)
|
|
387
|
+
except json.JSONDecodeError:
|
|
388
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
389
|
+
ok, _ = schema_check(obj, family)
|
|
390
|
+
if not ok:
|
|
391
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
392
|
+
if active_only and obj.get("status") != "active":
|
|
393
|
+
continue
|
|
394
|
+
entries.append(obj)
|
|
395
|
+
|
|
396
|
+
if truncated:
|
|
397
|
+
return entries, ReasonCode.SOVEREIGN_MEMORY_READ_BOUND
|
|
398
|
+
return entries, None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _sort_recent(entries: Sequence[dict]) -> List[dict]:
|
|
402
|
+
by_id = sorted(entries, key=lambda item: str(item.get("entry_id", "")))
|
|
403
|
+
by_ts = sorted(by_id, key=lambda item: str(item.get("ts", "")), reverse=True)
|
|
404
|
+
return by_ts
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _sort_high_impact(entries: Sequence[dict]) -> List[dict]:
|
|
408
|
+
by_id = sorted(entries, key=lambda item: str(item.get("entry_id", "")))
|
|
409
|
+
by_ts = sorted(by_id, key=lambda item: str(item.get("ts", "")), reverse=True)
|
|
410
|
+
return sorted(by_ts, key=lambda item: -int(item.get("impact_score", 0)))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _format_digest_line(family: str, entry: dict) -> str:
|
|
414
|
+
label = JSONL_FILENAMES.get(family, family).replace(".jsonl", "")
|
|
415
|
+
text = str(entry.get("text", "")).strip()
|
|
416
|
+
if len(text) > 160:
|
|
417
|
+
text = text[:157] + "..."
|
|
418
|
+
return f"- [{label}] {entry.get('entry_id')}: {text}"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def build_injection_digest(
|
|
422
|
+
repo_root: Optional[Path] = None,
|
|
423
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
424
|
+
) -> InjectionDigest:
|
|
425
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
426
|
+
return InjectionDigest(digest_text="", entry_ids=[], char_count=0)
|
|
427
|
+
|
|
428
|
+
top_n = parse_scratchpad_int(scratchpad, SOVEREIGN_MEMORY_TOP_N_KEY, SOVEREIGN_MEMORY_TOP_N_DEFAULT, minimum=0)
|
|
429
|
+
top_k = parse_scratchpad_int(scratchpad, SOVEREIGN_MEMORY_TOP_K_KEY, SOVEREIGN_MEMORY_TOP_K_DEFAULT, minimum=0)
|
|
430
|
+
max_chars = parse_scratchpad_int(
|
|
431
|
+
scratchpad,
|
|
432
|
+
SOVEREIGN_MEMORY_MAX_CHARS_KEY,
|
|
433
|
+
SOVEREIGN_MEMORY_MAX_CHARS_DEFAULT,
|
|
434
|
+
minimum=0,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
all_recent: List[Tuple[str, dict]] = []
|
|
438
|
+
high_impact_pool: List[Tuple[str, dict]] = []
|
|
439
|
+
|
|
440
|
+
for family in JSONL_FAMILIES:
|
|
441
|
+
entries, _ = read_entries(family, repo_root, active_only=True)
|
|
442
|
+
for entry in entries:
|
|
443
|
+
all_recent.append((family, entry))
|
|
444
|
+
if family in ("patterns", "mistakes"):
|
|
445
|
+
high_impact_pool.append((family, entry))
|
|
446
|
+
|
|
447
|
+
if not all_recent:
|
|
448
|
+
placeholder = "(no sovereign memory entries)"
|
|
449
|
+
return InjectionDigest(
|
|
450
|
+
digest_text=placeholder,
|
|
451
|
+
entry_ids=[],
|
|
452
|
+
char_count=len(placeholder),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
recent_sorted = _sort_recent([entry for _, entry in all_recent])
|
|
456
|
+
recent_ids: List[str] = []
|
|
457
|
+
recent_lines: List[str] = []
|
|
458
|
+
for entry in recent_sorted:
|
|
459
|
+
entry_id = str(entry.get("entry_id"))
|
|
460
|
+
if entry_id in recent_ids:
|
|
461
|
+
continue
|
|
462
|
+
recent_ids.append(entry_id)
|
|
463
|
+
family = next(f for f, e in all_recent if e.get("entry_id") == entry_id)
|
|
464
|
+
recent_lines.append(_format_digest_line(family, entry))
|
|
465
|
+
if len(recent_ids) >= top_n:
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
high_sorted = _sort_high_impact([entry for _, entry in high_impact_pool])
|
|
469
|
+
high_lines: List[str] = []
|
|
470
|
+
high_ids: List[str] = []
|
|
471
|
+
selected = set(recent_ids)
|
|
472
|
+
for entry in high_sorted:
|
|
473
|
+
entry_id = str(entry.get("entry_id"))
|
|
474
|
+
if entry_id in selected:
|
|
475
|
+
continue
|
|
476
|
+
selected.add(entry_id)
|
|
477
|
+
high_ids.append(entry_id)
|
|
478
|
+
family = next(f for f, e in high_impact_pool if e.get("entry_id") == entry_id)
|
|
479
|
+
high_lines.append(_format_digest_line(family, entry))
|
|
480
|
+
if len(high_ids) >= top_k:
|
|
481
|
+
break
|
|
482
|
+
|
|
483
|
+
sections: List[str] = []
|
|
484
|
+
if recent_lines:
|
|
485
|
+
sections.append("## Recent learnings\n" + "\n".join(recent_lines))
|
|
486
|
+
if high_lines:
|
|
487
|
+
sections.append("## High-impact patterns\n" + "\n".join(high_lines))
|
|
488
|
+
|
|
489
|
+
digest_text = "\n\n".join(sections) if sections else "(no sovereign memory entries)"
|
|
490
|
+
if max_chars > 0 and len(digest_text) > max_chars:
|
|
491
|
+
digest_text = digest_text[: max_chars - 3].rstrip() + "..."
|
|
492
|
+
|
|
493
|
+
ordered_ids = recent_ids + [item for item in high_ids if item not in recent_ids]
|
|
494
|
+
return InjectionDigest(
|
|
495
|
+
digest_text=digest_text,
|
|
496
|
+
entry_ids=ordered_ids,
|
|
497
|
+
char_count=len(digest_text),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def build_injection_digest_block(
|
|
502
|
+
repo_root: Optional[Path] = None,
|
|
503
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
504
|
+
) -> Optional[str]:
|
|
505
|
+
"""Spawn assembler helper: fenced sovereign_memory_digest block or None when disabled."""
|
|
506
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
507
|
+
return None
|
|
508
|
+
digest = build_injection_digest(repo_root, scratchpad)
|
|
509
|
+
return (
|
|
510
|
+
"### sovereign_memory_digest\n\n"
|
|
511
|
+
"```\n"
|
|
512
|
+
f"{digest.digest_text}\n"
|
|
513
|
+
"```\n"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def dedupe_decision(
|
|
518
|
+
decision_text: str,
|
|
519
|
+
existing_keys: Iterable[str],
|
|
520
|
+
) -> Tuple[str, bool]:
|
|
521
|
+
key = compute_decision_key(decision_text)
|
|
522
|
+
return key, key in set(existing_keys)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def maybe_archive_jsonl(
|
|
526
|
+
family: str,
|
|
527
|
+
repo_root: Optional[Path] = None,
|
|
528
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
529
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
530
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
531
|
+
return True, None
|
|
532
|
+
|
|
533
|
+
path = resolve_jsonl_path(family, repo_root)
|
|
534
|
+
if not path.is_file():
|
|
535
|
+
return True, None
|
|
536
|
+
|
|
537
|
+
max_lines = parse_scratchpad_int(
|
|
538
|
+
scratchpad,
|
|
539
|
+
SOVEREIGN_MEMORY_JSONL_MAX_LINES_KEY,
|
|
540
|
+
SOVEREIGN_MEMORY_JSONL_MAX_LINES_DEFAULT,
|
|
541
|
+
minimum=1,
|
|
542
|
+
)
|
|
543
|
+
if _count_nonempty_lines(path) < max_lines:
|
|
544
|
+
return True, None
|
|
545
|
+
|
|
546
|
+
archive_dir = resolve_archive_dir(repo_root)
|
|
547
|
+
basename = path.stem
|
|
548
|
+
archive_path = archive_dir / f"{basename}-{_archive_timestamp()}.jsonl"
|
|
549
|
+
try:
|
|
550
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
551
|
+
if archive_path.exists():
|
|
552
|
+
return True, None
|
|
553
|
+
path.replace(archive_path)
|
|
554
|
+
path.write_text("", encoding="utf-8")
|
|
555
|
+
_fsync_file(path)
|
|
556
|
+
return True, None
|
|
557
|
+
except OSError:
|
|
558
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_ARCHIVE_REQUIRED
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def append_decision(
|
|
562
|
+
entry: dict,
|
|
563
|
+
repo_root: Optional[Path] = None,
|
|
564
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
565
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
566
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
567
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_DISABLED
|
|
568
|
+
ok, _ = schema_check(entry, "decisions")
|
|
569
|
+
if not ok:
|
|
570
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
571
|
+
|
|
572
|
+
existing = _load_existing_decision_keys(repo_root)
|
|
573
|
+
_, is_dup = dedupe_decision(str(entry.get("decision_text", "")), existing)
|
|
574
|
+
if is_dup:
|
|
575
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_DECISION_DUPLICATE
|
|
576
|
+
|
|
577
|
+
return _append_jsonl_line("decisions", entry, repo_root, scratchpad)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def append_mistake(
|
|
581
|
+
entry: dict,
|
|
582
|
+
repo_root: Optional[Path] = None,
|
|
583
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
584
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
585
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
586
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_DISABLED
|
|
587
|
+
ok, _ = schema_check(entry, "mistakes")
|
|
588
|
+
if not ok:
|
|
589
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
590
|
+
return _append_jsonl_line("mistakes", entry, repo_root, scratchpad)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def append_pattern(
|
|
594
|
+
entry: dict,
|
|
595
|
+
repo_root: Optional[Path] = None,
|
|
596
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
597
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
598
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
599
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_DISABLED
|
|
600
|
+
ok, _ = schema_check(entry, "patterns")
|
|
601
|
+
if not ok:
|
|
602
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
603
|
+
return _append_jsonl_line("patterns", entry, repo_root, scratchpad)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def append_drift(
|
|
607
|
+
entry: dict,
|
|
608
|
+
repo_root: Optional[Path] = None,
|
|
609
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
610
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
611
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
612
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_DISABLED
|
|
613
|
+
ok, _ = schema_check(entry, "plan-drift")
|
|
614
|
+
if not ok:
|
|
615
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
616
|
+
return _append_jsonl_line("plan-drift", entry, repo_root, scratchpad)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def build_mistake_entry(
|
|
620
|
+
*,
|
|
621
|
+
mistake_tag: str,
|
|
622
|
+
failure_reason_code: str,
|
|
623
|
+
text: str,
|
|
624
|
+
impact_score: int = 50,
|
|
625
|
+
source_orchestrator_run_id: Optional[str] = None,
|
|
626
|
+
source_story_id: Optional[str] = None,
|
|
627
|
+
phase_id: Optional[str] = None,
|
|
628
|
+
provenance_ref: Optional[str] = None,
|
|
629
|
+
tags: Optional[List[str]] = None,
|
|
630
|
+
) -> dict:
|
|
631
|
+
return {
|
|
632
|
+
"schema_version": SCHEMA_VERSION,
|
|
633
|
+
"ts": _utc_now_iso(),
|
|
634
|
+
"entry_id": str(uuid.uuid4()),
|
|
635
|
+
"source_orchestrator_run_id": source_orchestrator_run_id,
|
|
636
|
+
"source_story_id": source_story_id,
|
|
637
|
+
"phase_id": phase_id,
|
|
638
|
+
"impact_score": impact_score,
|
|
639
|
+
"text": text[:TEXT_MAX_CHARS],
|
|
640
|
+
"tags": tags or ["mistake"],
|
|
641
|
+
"status": "active",
|
|
642
|
+
"provenance_ref": provenance_ref,
|
|
643
|
+
"mistake_tag": mistake_tag,
|
|
644
|
+
"failure_reason_code": failure_reason_code,
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def record_mistake_hook(
|
|
649
|
+
mistake_tag: str,
|
|
650
|
+
*,
|
|
651
|
+
text: str,
|
|
652
|
+
repo_root: Optional[Path] = None,
|
|
653
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
654
|
+
source_orchestrator_run_id: Optional[str] = None,
|
|
655
|
+
source_story_id: Optional[str] = None,
|
|
656
|
+
phase_id: Optional[str] = None,
|
|
657
|
+
provenance_ref: Optional[str] = None,
|
|
658
|
+
) -> Tuple[bool, Optional[ReasonCode]]:
|
|
659
|
+
"""Orchestrator-detectable mistake hook — no-op when SOVEREIGN_MEMORY=0."""
|
|
660
|
+
if mistake_tag not in MISTAKE_TAG_VALUES:
|
|
661
|
+
return False, ReasonCode.SOVEREIGN_MEMORY_SCHEMA_INVALID
|
|
662
|
+
failure_reason_code = MISTAKE_HOOK_TABLE.get(mistake_tag, mistake_tag.upper())
|
|
663
|
+
entry = build_mistake_entry(
|
|
664
|
+
mistake_tag=mistake_tag,
|
|
665
|
+
failure_reason_code=failure_reason_code,
|
|
666
|
+
text=text,
|
|
667
|
+
source_orchestrator_run_id=source_orchestrator_run_id,
|
|
668
|
+
source_story_id=source_story_id,
|
|
669
|
+
phase_id=phase_id,
|
|
670
|
+
provenance_ref=provenance_ref,
|
|
671
|
+
)
|
|
672
|
+
return append_mistake(entry, repo_root, scratchpad)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def promote_from_ledger(
|
|
676
|
+
orchestrator_run_id: str,
|
|
677
|
+
repo_root: Optional[Path] = None,
|
|
678
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
679
|
+
*,
|
|
680
|
+
decision_types: Optional[Sequence[str]] = None,
|
|
681
|
+
) -> Tuple[List[str], Optional[ReasonCode]]:
|
|
682
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
683
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_DISABLED
|
|
684
|
+
|
|
685
|
+
pad = scratchpad or {}
|
|
686
|
+
if pad.get("AI_DECISION_LEDGER", "0").strip() != "1":
|
|
687
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_PROMOTION_SKIPPED
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
from decision_ledger_lib import ( # noqa: WPS433
|
|
691
|
+
ReasonCode as LedgerReasonCode,
|
|
692
|
+
read_entries as read_ledger_entries,
|
|
693
|
+
resolve_ledger_path,
|
|
694
|
+
)
|
|
695
|
+
except ImportError:
|
|
696
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_PROMOTION_SKIPPED
|
|
697
|
+
|
|
698
|
+
ledger_path = resolve_ledger_path(orchestrator_run_id, repo_root)
|
|
699
|
+
entries, rc, _ = read_ledger_entries(ledger_path)
|
|
700
|
+
if rc == LedgerReasonCode.LEDGER_FILE_MISSING or not entries:
|
|
701
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_PROMOTION_SKIPPED
|
|
702
|
+
|
|
703
|
+
types_filter = set(decision_types) if decision_types else None
|
|
704
|
+
promoted: List[str] = []
|
|
705
|
+
existing_keys = _load_existing_decision_keys(repo_root)
|
|
706
|
+
|
|
707
|
+
for ledger_entry in entries:
|
|
708
|
+
dt = str(ledger_entry.get("decision_type", ""))
|
|
709
|
+
if types_filter is not None and dt not in types_filter:
|
|
710
|
+
continue
|
|
711
|
+
decision_text = str(ledger_entry.get("rationale", "")).strip()
|
|
712
|
+
if not decision_text:
|
|
713
|
+
decision_text = (
|
|
714
|
+
f"{ledger_entry.get('from_artifact', '(none)')} -> "
|
|
715
|
+
f"{ledger_entry.get('to_artifact', '(none)')}"
|
|
716
|
+
)
|
|
717
|
+
key, is_dup = dedupe_decision(decision_text, existing_keys)
|
|
718
|
+
if is_dup:
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
entry_id = str(uuid.uuid4())
|
|
722
|
+
memory_entry = {
|
|
723
|
+
"schema_version": SCHEMA_VERSION,
|
|
724
|
+
"ts": _utc_now_iso(),
|
|
725
|
+
"entry_id": entry_id,
|
|
726
|
+
"source_orchestrator_run_id": orchestrator_run_id,
|
|
727
|
+
"source_story_id": None,
|
|
728
|
+
"phase_id": ledger_entry.get("phase_id"),
|
|
729
|
+
"impact_score": 60,
|
|
730
|
+
"text": decision_text[:TEXT_MAX_CHARS],
|
|
731
|
+
"tags": ["promoted", "ledger"],
|
|
732
|
+
"status": "active",
|
|
733
|
+
"provenance_ref": f"ledger:{ledger_entry.get('decision_id')}",
|
|
734
|
+
"decision_key": key,
|
|
735
|
+
"decision_text": decision_text[:TEXT_MAX_CHARS],
|
|
736
|
+
"rationale": f"Promoted from per-run ledger decision_type={dt}.",
|
|
737
|
+
}
|
|
738
|
+
ok, reason = append_decision(memory_entry, repo_root, scratchpad)
|
|
739
|
+
if ok:
|
|
740
|
+
promoted.append(entry_id)
|
|
741
|
+
existing_keys.add(key)
|
|
742
|
+
|
|
743
|
+
if not promoted:
|
|
744
|
+
return [], ReasonCode.SOVEREIGN_MEMORY_PROMOTION_SKIPPED
|
|
745
|
+
return promoted, None
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def write_retrospective(
|
|
749
|
+
sprint_id: str,
|
|
750
|
+
body: str,
|
|
751
|
+
repo_root: Optional[Path] = None,
|
|
752
|
+
scratchpad: Optional[Dict[str, str]] = None,
|
|
753
|
+
) -> Tuple[Optional[Path], Optional[ReasonCode]]:
|
|
754
|
+
if not is_sovereign_memory_enabled(scratchpad):
|
|
755
|
+
return None, ReasonCode.SOVEREIGN_MEMORY_DISABLED
|
|
756
|
+
|
|
757
|
+
ok_dir, rc_dir = _ensure_memory_dir(repo_root)
|
|
758
|
+
if not ok_dir:
|
|
759
|
+
return None, rc_dir
|
|
760
|
+
|
|
761
|
+
retro_dir = resolve_retrospectives_dir(repo_root)
|
|
762
|
+
try:
|
|
763
|
+
retro_dir.mkdir(parents=True, exist_ok=True)
|
|
764
|
+
path = retro_dir / f"{sprint_id}.md"
|
|
765
|
+
path.write_text(body, encoding="utf-8")
|
|
766
|
+
_fsync_file(path)
|
|
767
|
+
return path, None
|
|
768
|
+
except OSError:
|
|
769
|
+
return None, ReasonCode.SOVEREIGN_MEMORY_APPEND_FAILED
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def build_sample_decision() -> dict:
|
|
773
|
+
decision_text = "Prefer create-on-first-write for sovereign-memory JSONL files"
|
|
774
|
+
return {
|
|
775
|
+
"schema_version": SCHEMA_VERSION,
|
|
776
|
+
"ts": _utc_now_iso(),
|
|
777
|
+
"entry_id": str(uuid.uuid4()),
|
|
778
|
+
"source_orchestrator_run_id": "auto-research-stub",
|
|
779
|
+
"source_story_id": "US-0105",
|
|
780
|
+
"phase_id": "research",
|
|
781
|
+
"impact_score": 70,
|
|
782
|
+
"text": decision_text,
|
|
783
|
+
"tags": ["schema", "bootstrap"],
|
|
784
|
+
"status": "active",
|
|
785
|
+
"provenance_ref": "R-0093",
|
|
786
|
+
"decision_key": compute_decision_key(decision_text),
|
|
787
|
+
"decision_text": decision_text,
|
|
788
|
+
"rationale": "Avoids empty tracked JSONL clutter; directory .gitkeep suffices until first write.",
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def build_sample_mistake() -> dict:
|
|
793
|
+
return build_mistake_entry(
|
|
794
|
+
mistake_tag="fix_failed",
|
|
795
|
+
failure_reason_code="FIX_FAILED",
|
|
796
|
+
text="Auto-loop exhausted fix attempts without green QA.",
|
|
797
|
+
source_story_id="US-0105",
|
|
798
|
+
phase_id="execute",
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def self_test() -> bool:
|
|
803
|
+
errors: List[str] = []
|
|
804
|
+
|
|
805
|
+
if is_sovereign_memory_enabled({SOVEREIGN_MEMORY_KEY: "0"}):
|
|
806
|
+
errors.append("SOVEREIGN_MEMORY=0 must disable")
|
|
807
|
+
if not is_sovereign_memory_enabled({SOVEREIGN_MEMORY_KEY: "1"}):
|
|
808
|
+
errors.append("SOVEREIGN_MEMORY=1 must enable")
|
|
809
|
+
|
|
810
|
+
sample = build_sample_decision()
|
|
811
|
+
ok, err = schema_check(sample, "decisions")
|
|
812
|
+
if not ok:
|
|
813
|
+
errors.append(f"decisions schema_check failed: {err}")
|
|
814
|
+
|
|
815
|
+
key_a = compute_decision_key("Prefer create-on-first-write")
|
|
816
|
+
key_b = compute_decision_key("prefer create-on-first-write")
|
|
817
|
+
if key_a != key_b:
|
|
818
|
+
errors.append("decision_key normalization mismatch")
|
|
819
|
+
|
|
820
|
+
_, is_dup = dedupe_decision("Prefer create-on-first-write", {key_a})
|
|
821
|
+
if not is_dup:
|
|
822
|
+
errors.append("dedupe_decision should detect duplicate key")
|
|
823
|
+
|
|
824
|
+
disabled = build_injection_digest(scratchpad={SOVEREIGN_MEMORY_KEY: "0"})
|
|
825
|
+
if disabled.digest_text or disabled.entry_ids:
|
|
826
|
+
errors.append("disabled gate must return empty digest")
|
|
827
|
+
|
|
828
|
+
if build_injection_digest_block(scratchpad={SOVEREIGN_MEMORY_KEY: "0"}) is not None:
|
|
829
|
+
errors.append("digest block must be None when disabled")
|
|
830
|
+
|
|
831
|
+
enabled = build_injection_digest(scratchpad={
|
|
832
|
+
SOVEREIGN_MEMORY_KEY: "1",
|
|
833
|
+
SOVEREIGN_MEMORY_MAX_CHARS_KEY: "128",
|
|
834
|
+
})
|
|
835
|
+
if enabled.char_count > 128:
|
|
836
|
+
errors.append("digest must respect SOVEREIGN_MEMORY_MAX_CHARS")
|
|
837
|
+
|
|
838
|
+
secret_sample = build_sample_decision()
|
|
839
|
+
secret_sample["text"] = "api_key=supersecretvalue"
|
|
840
|
+
bad, bad_err = schema_check(secret_sample, "decisions")
|
|
841
|
+
if bad or bad_err != ReasonCode.SOVEREIGN_MEMORY_SECRET_DETECTED.value:
|
|
842
|
+
errors.append("secret scanner must reject api_key patterns")
|
|
843
|
+
|
|
844
|
+
if len(FAMILY_REQUIRED) != 4:
|
|
845
|
+
errors.append("expected four JSONL families")
|
|
846
|
+
|
|
847
|
+
if len(MISTAKE_HOOK_TABLE) != len(MISTAKE_TAG_VALUES):
|
|
848
|
+
errors.append("mistake hook table must cover all tags")
|
|
849
|
+
|
|
850
|
+
if errors:
|
|
851
|
+
for item in errors:
|
|
852
|
+
print(f" {item}", file=sys.stderr)
|
|
853
|
+
print("[SELF_TEST_FAILED]", file=sys.stderr)
|
|
854
|
+
return False
|
|
855
|
+
|
|
856
|
+
print("[SOVEREIGN_MEMORY_SELF_TEST_OK]")
|
|
857
|
+
return True
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
if __name__ == "__main__":
|
|
861
|
+
import argparse
|
|
862
|
+
|
|
863
|
+
parser = argparse.ArgumentParser(description="Sovereign memory library (US-0105 / DEC-0105)")
|
|
864
|
+
parser.add_argument("--self-test", action="store_true", help="Run self-test")
|
|
865
|
+
args = parser.parse_args()
|
|
866
|
+
if args.self_test:
|
|
867
|
+
sys.exit(0 if self_test() else 1)
|
|
868
|
+
parser.print_help()
|
|
869
|
+
sys.exit(2)
|