codec-cortex 0.3.2__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.
- codec_cortex-0.3.2.dist-info/METADATA +183 -0
- codec_cortex-0.3.2.dist-info/RECORD +81 -0
- codec_cortex-0.3.2.dist-info/WHEEL +5 -0
- codec_cortex-0.3.2.dist-info/entry_points.txt +2 -0
- codec_cortex-0.3.2.dist-info/licenses/LICENSE +21 -0
- codec_cortex-0.3.2.dist-info/scm_file_list.json +124 -0
- codec_cortex-0.3.2.dist-info/scm_version.json +8 -0
- codec_cortex-0.3.2.dist-info/top_level.txt +1 -0
- cortex/__init__.py +22 -0
- cortex/__main__.py +7 -0
- cortex/_version.py +24 -0
- cortex/cli/__init__.py +5 -0
- cortex/cli/commands/__init__.py +150 -0
- cortex/cli/commands/add.py +89 -0
- cortex/cli/commands/compile.py +31 -0
- cortex/cli/commands/delete.py +44 -0
- cortex/cli/commands/diagram.py +130 -0
- cortex/cli/commands/diff.py +135 -0
- cortex/cli/commands/doctor.py +74 -0
- cortex/cli/commands/format.py +29 -0
- cortex/cli/commands/get.py +45 -0
- cortex/cli/commands/glossary.py +98 -0
- cortex/cli/commands/list.py +44 -0
- cortex/cli/commands/micro.py +79 -0
- cortex/cli/commands/move.py +45 -0
- cortex/cli/commands/new.py +80 -0
- cortex/cli/commands/recover.py +90 -0
- cortex/cli/commands/render.py +93 -0
- cortex/cli/commands/update.py +81 -0
- cortex/cli/commands/v2_canonicalize.py +162 -0
- cortex/cli/commands/v2_compare.py +74 -0
- cortex/cli/commands/v2_convert.py +183 -0
- cortex/cli/commands/v2_explain_loss.py +97 -0
- cortex/cli/commands/v2_inspect.py +113 -0
- cortex/cli/commands/v2_roundtrip.py +104 -0
- cortex/cli/commands/v2_roundtrip_bidir.py +128 -0
- cortex/cli/commands/v2_verify_view.py +65 -0
- cortex/cli/commands/verify.py +119 -0
- cortex/cli/main.py +636 -0
- cortex/core/__init__.py +85 -0
- cortex/core/ast.py +313 -0
- cortex/core/compare.py +180 -0
- cortex/core/document_kind.py +525 -0
- cortex/core/errors.py +399 -0
- cortex/core/lexer.py +267 -0
- cortex/core/parser.py +671 -0
- cortex/core/validator.py +268 -0
- cortex/core/writer.py +249 -0
- cortex/crud/__init__.py +17 -0
- cortex/crud/mutations.py +342 -0
- cortex/crud/selectors.py +95 -0
- cortex/crud/transactions.py +213 -0
- cortex/glossary/__init__.py +21 -0
- cortex/glossary/contracts.py +37 -0
- cortex/glossary/minimal.py +94 -0
- cortex/glossary/model.py +77 -0
- cortex/glossary/resolver.py +96 -0
- cortex/hcortex/__init__.py +35 -0
- cortex/hcortex/edit_parser.py +489 -0
- cortex/hcortex/edit_renderer.py +158 -0
- cortex/hcortex/markdown_model.py +81 -0
- cortex/hcortex/profiles.py +166 -0
- cortex/hcortex/read_renderer.py +342 -0
- cortex/hcortex/recovery.py +782 -0
- cortex/py.typed +0 -0
- cortex/templates/__init__.py +8 -0
- cortex/templates/brain.py +118 -0
- cortex/templates/minimal_glossary.py +42 -0
- cortex/templates/package.py +91 -0
- cortex/templates/skill.py +91 -0
- cortex/v2/__init__.py +30 -0
- cortex/v2/diagnostics.py +57 -0
- cortex/v2/encoder.py +1106 -0
- cortex/v2/equivalence.py +323 -0
- cortex/v2/hcortex_parser.py +615 -0
- cortex/v2/hcortex_renderer.py +450 -0
- cortex/v2/ir.py +223 -0
- cortex/v2/parser.py +655 -0
- cortex/v2/view.py +425 -0
- cortex/v2/view_renderer.py +474 -0
- cortex/v2/writer.py +301 -0
cortex/v2/equivalence.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Equivalence engine v2.3.0 — compare CORTEX and HCORTEX artefacts.
|
|
2
|
+
|
|
3
|
+
Implements F-16..F-22 from v2.3.0 spec:
|
|
4
|
+
F-16: byte-identical comparison (CORTEX→CORTEX, HCORTEX→HCORTEX)
|
|
5
|
+
F-17: AST-equivalent (same logical structure)
|
|
6
|
+
F-18: semantic-equivalent (same operational meaning)
|
|
7
|
+
F-19: content-equivalent (HCORTEX regenerado conserva contenido)
|
|
8
|
+
F-20: diff by sigil
|
|
9
|
+
F-21: diff by section
|
|
10
|
+
F-22: diff by VIEW
|
|
11
|
+
|
|
12
|
+
Architecture:
|
|
13
|
+
Two CortexV2Documents → compare → EquivalenceResult
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from .parser import CortexV2Document, V2Entry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Result types
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Diff:
|
|
30
|
+
"""A single difference between two documents."""
|
|
31
|
+
kind: str # added | removed | modified | moved
|
|
32
|
+
location: str # e.g. "$0/IDN:project" or "VIEW:foo"
|
|
33
|
+
field: Optional[str] = None # specific field if modified
|
|
34
|
+
left: Any = None
|
|
35
|
+
right: Any = None
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"kind": self.kind,
|
|
40
|
+
"location": self.location,
|
|
41
|
+
"field": self.field,
|
|
42
|
+
"left": str(self.left)[:200],
|
|
43
|
+
"right": str(self.right)[:200],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class EquivalenceResult:
|
|
49
|
+
"""Result of an equivalence comparison."""
|
|
50
|
+
byte_identical: bool = False
|
|
51
|
+
ast_equivalent: bool = False
|
|
52
|
+
semantic_equivalent: bool = False
|
|
53
|
+
content_equivalent: bool = False
|
|
54
|
+
diffs: List[Diff] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def equivalent(self) -> bool:
|
|
58
|
+
"""Equivalence at the AST level (the canonical notion)."""
|
|
59
|
+
return self.ast_equivalent
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
return {
|
|
63
|
+
"byte_identical": self.byte_identical,
|
|
64
|
+
"ast_equivalent": self.ast_equivalent,
|
|
65
|
+
"semantic_equivalent": self.semantic_equivalent,
|
|
66
|
+
"content_equivalent": self.content_equivalent,
|
|
67
|
+
"diffs": [d.to_dict() for d in self.diffs],
|
|
68
|
+
"diff_count": len(self.diffs),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# F-16: byte-identical comparison
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def compare_byte_identical(left_bytes: bytes, right_bytes: bytes) -> bool:
|
|
77
|
+
"""F-16: Pure byte comparison."""
|
|
78
|
+
return left_bytes == right_bytes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# F-17: AST-equivalent comparison
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def compare_ast_equivalent(left: CortexV2Document, right: CortexV2Document) -> Tuple[bool, List[Diff]]:
|
|
86
|
+
"""F-17: Compare two CortexV2Documents at the AST level.
|
|
87
|
+
|
|
88
|
+
AST-equivalent means same sections, same entries (by sigil:name),
|
|
89
|
+
same entry types, same values (modulo key ordering in attrs).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
diffs: List[Diff] = []
|
|
93
|
+
|
|
94
|
+
# Compare sections
|
|
95
|
+
left_sections = {s.id: s for s in left.sections}
|
|
96
|
+
right_sections = {s.id: s for s in right.sections}
|
|
97
|
+
|
|
98
|
+
for sec_id in left_sections:
|
|
99
|
+
if sec_id not in right_sections:
|
|
100
|
+
diffs.append(Diff("removed", sec_id, left=left_sections[sec_id]))
|
|
101
|
+
for sec_id in right_sections:
|
|
102
|
+
if sec_id not in left_sections:
|
|
103
|
+
diffs.append(Diff("added", sec_id, right=right_sections[sec_id]))
|
|
104
|
+
|
|
105
|
+
# Compare entries within shared sections
|
|
106
|
+
for sec_id in left_sections:
|
|
107
|
+
if sec_id not in right_sections:
|
|
108
|
+
continue
|
|
109
|
+
left_entries = {f"{e.sigil}:{e.name}": e for e in left_sections[sec_id].entries}
|
|
110
|
+
right_entries = {f"{e.sigil}:{e.name}": e for e in right_sections[sec_id].entries}
|
|
111
|
+
|
|
112
|
+
for key in left_entries:
|
|
113
|
+
if key not in right_entries:
|
|
114
|
+
diffs.append(Diff("removed", f"{sec_id}/{key}", left=left_entries[key]))
|
|
115
|
+
for key in right_entries:
|
|
116
|
+
if key not in left_entries:
|
|
117
|
+
diffs.append(Diff("added", f"{sec_id}/{key}", right=right_entries[key]))
|
|
118
|
+
|
|
119
|
+
for key in left_entries:
|
|
120
|
+
if key not in right_entries:
|
|
121
|
+
continue
|
|
122
|
+
le = left_entries[key]
|
|
123
|
+
re_ = right_entries[key]
|
|
124
|
+
entry_diffs = _compare_entry(le, re_, f"{sec_id}/{key}")
|
|
125
|
+
diffs.extend(entry_diffs)
|
|
126
|
+
|
|
127
|
+
return len(diffs) == 0, diffs
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _compare_entry(left: V2Entry, right: V2Entry, location: str) -> List[Diff]:
|
|
131
|
+
"""Compare two entries for AST equivalence."""
|
|
132
|
+
|
|
133
|
+
diffs: List[Diff] = []
|
|
134
|
+
|
|
135
|
+
if left.entry_type != right.entry_type:
|
|
136
|
+
diffs.append(Diff("modified", location, "entry_type", left.entry_type, right.entry_type))
|
|
137
|
+
return diffs
|
|
138
|
+
|
|
139
|
+
if left.entry_type in ("cuerpo", "bloque"):
|
|
140
|
+
if left.value != right.value:
|
|
141
|
+
diffs.append(Diff("modified", location, "value", left.value, right.value))
|
|
142
|
+
elif left.entry_type == "attrs":
|
|
143
|
+
left_attrs = dict(left.value) if isinstance(left.value, dict) else {}
|
|
144
|
+
right_attrs = dict(right.value) if isinstance(right.value, dict) else {}
|
|
145
|
+
for k in left_attrs:
|
|
146
|
+
if k not in right_attrs:
|
|
147
|
+
diffs.append(Diff("modified", location, k, left_attrs[k], None))
|
|
148
|
+
elif left_attrs[k] != right_attrs[k]:
|
|
149
|
+
diffs.append(Diff("modified", location, k, left_attrs[k], right_attrs[k]))
|
|
150
|
+
for k in right_attrs:
|
|
151
|
+
if k not in left_attrs:
|
|
152
|
+
diffs.append(Diff("modified", location, k, None, right_attrs[k]))
|
|
153
|
+
elif left.entry_type == "attrs-pos":
|
|
154
|
+
left_list = list(left.value) if isinstance(left.value, list) else []
|
|
155
|
+
right_list = list(right.value) if isinstance(right.value, list) else []
|
|
156
|
+
if left_list != right_list:
|
|
157
|
+
diffs.append(Diff("modified", location, "value", left_list, right_list))
|
|
158
|
+
|
|
159
|
+
return diffs
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# F-18: semantic-equivalent comparison
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
def compare_semantic_equivalent(left: CortexV2Document, right: CortexV2Document) -> Tuple[bool, List[Diff]]:
|
|
167
|
+
"""F-18: Same operational meaning with tolerable differences.
|
|
168
|
+
|
|
169
|
+
Semantic equivalence is AST-equivalent +:
|
|
170
|
+
- Same critical sigils present (FCS, OBJ, CNST)
|
|
171
|
+
- Same P0/P1 entries (blocking constraints)
|
|
172
|
+
- Tolerable differences in non-critical attrs (e.g., status fields)
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
ast_eq, ast_diffs = compare_ast_equivalent(left, right)
|
|
176
|
+
if ast_eq:
|
|
177
|
+
return True, []
|
|
178
|
+
|
|
179
|
+
# Filter diffs to only critical ones
|
|
180
|
+
critical_sigils = {"FCS", "OBJ", "CNST", "AXM", "LIM"}
|
|
181
|
+
critical_diffs: List[Diff] = []
|
|
182
|
+
for d in ast_diffs:
|
|
183
|
+
# Parse location: $N/SIGIL:name[/field]
|
|
184
|
+
parts = d.location.split("/")
|
|
185
|
+
if len(parts) >= 2:
|
|
186
|
+
sigil_part = parts[1].split(":")[0]
|
|
187
|
+
if sigil_part in critical_sigils:
|
|
188
|
+
critical_diffs.append(d)
|
|
189
|
+
|
|
190
|
+
return len(critical_diffs) == 0, critical_diffs
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# F-19: content-equivalent (HCORTEX regenerado)
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def compare_content_equivalent(left_hcortex: str, right_hcortex: str) -> Tuple[bool, List[Diff]]:
|
|
198
|
+
"""F-19: HCORTEX regenerado conserva contenido humano equivalente.
|
|
199
|
+
|
|
200
|
+
Content-equivalent means same VIEW blocks, same table rows (modulo
|
|
201
|
+
whitespace), same list items, same verbatim blocks.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
diffs: List[Diff] = []
|
|
205
|
+
|
|
206
|
+
# Extract VIEW blocks from each
|
|
207
|
+
left_blocks = _extract_view_blocks(left_hcortex)
|
|
208
|
+
right_blocks = _extract_view_blocks(right_hcortex)
|
|
209
|
+
|
|
210
|
+
left_names = {b[0] for b in left_blocks}
|
|
211
|
+
right_names = {b[0] for b in right_blocks}
|
|
212
|
+
|
|
213
|
+
for name in left_names - right_names:
|
|
214
|
+
diffs.append(Diff("removed", f"VIEW:{name}"))
|
|
215
|
+
for name in right_names - left_names:
|
|
216
|
+
diffs.append(Diff("added", f"VIEW:{name}"))
|
|
217
|
+
|
|
218
|
+
for name in left_names & right_names:
|
|
219
|
+
left_content = next((c for n, c in left_blocks if n == name), "")
|
|
220
|
+
right_content = next((c for n, c in right_blocks if n == name), "")
|
|
221
|
+
# Normalize whitespace
|
|
222
|
+
left_norm = _normalize_whitespace(left_content)
|
|
223
|
+
right_norm = _normalize_whitespace(right_content)
|
|
224
|
+
if left_norm != right_norm:
|
|
225
|
+
diffs.append(Diff("modified", f"VIEW:{name}", "content", left_norm[:200], right_norm[:200]))
|
|
226
|
+
|
|
227
|
+
return len(diffs) == 0, diffs
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _extract_view_blocks(text: str) -> List[Tuple[str, str]]:
|
|
231
|
+
"""Extract (view_name, content) pairs from HCORTEX text."""
|
|
232
|
+
|
|
233
|
+
import re
|
|
234
|
+
blocks: List[Tuple[str, str]] = []
|
|
235
|
+
pattern = re.compile(r'<!-- VIEW:(\w+)\s+.*?-->(.*?)<!-- /VIEW:\1 -->', re.DOTALL)
|
|
236
|
+
for m in pattern.finditer(text):
|
|
237
|
+
blocks.append((m.group(1), m.group(2)))
|
|
238
|
+
return blocks
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _normalize_whitespace(text: str) -> str:
|
|
242
|
+
"""Normalize whitespace for content comparison."""
|
|
243
|
+
import re
|
|
244
|
+
# Collapse multiple spaces to one
|
|
245
|
+
text = re.sub(r'[ \t]+', ' ', text)
|
|
246
|
+
# Collapse multiple blank lines to one
|
|
247
|
+
text = re.sub(r'\n\s*\n+', '\n\n', text)
|
|
248
|
+
return text.strip()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# F-20/F-21/F-22: Diffs by sigil / section / VIEW
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def diff_by_sigil(diffs: List[Diff]) -> Dict[str, List[Diff]]:
|
|
256
|
+
"""F-20: Group diffs by sigil."""
|
|
257
|
+
grouped: Dict[str, List[Diff]] = {}
|
|
258
|
+
for d in diffs:
|
|
259
|
+
parts = d.location.split("/")
|
|
260
|
+
if len(parts) >= 2:
|
|
261
|
+
sigil = parts[1].split(":")[0] if ":" in parts[1] else parts[1]
|
|
262
|
+
else:
|
|
263
|
+
sigil = "unknown"
|
|
264
|
+
grouped.setdefault(sigil, []).append(d)
|
|
265
|
+
return grouped
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def diff_by_section(diffs: List[Diff]) -> Dict[str, List[Diff]]:
|
|
269
|
+
"""F-21: Group diffs by section."""
|
|
270
|
+
grouped: Dict[str, List[Diff]] = {}
|
|
271
|
+
for d in diffs:
|
|
272
|
+
parts = d.location.split("/")
|
|
273
|
+
section = parts[0] if parts else "unknown"
|
|
274
|
+
grouped.setdefault(section, []).append(d)
|
|
275
|
+
return grouped
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def diff_by_view(diffs: List[Diff]) -> Dict[str, List[Diff]]:
|
|
279
|
+
"""F-22: Group diffs by VIEW."""
|
|
280
|
+
grouped: Dict[str, List[Diff]] = {}
|
|
281
|
+
for d in diffs:
|
|
282
|
+
if d.location.startswith("VIEW:"):
|
|
283
|
+
view_name = d.location.split(":")[1].split("/")[0]
|
|
284
|
+
grouped.setdefault(f"VIEW:{view_name}", []).append(d)
|
|
285
|
+
else:
|
|
286
|
+
grouped.setdefault("non-VIEW", []).append(d)
|
|
287
|
+
return grouped
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# Full comparison
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
def compare_documents(
|
|
295
|
+
left: CortexV2Document,
|
|
296
|
+
right: CortexV2Document,
|
|
297
|
+
left_bytes: Optional[bytes] = None,
|
|
298
|
+
right_bytes: Optional[bytes] = None,
|
|
299
|
+
left_hcortex: Optional[str] = None,
|
|
300
|
+
right_hcortex: Optional[str] = None,
|
|
301
|
+
) -> EquivalenceResult:
|
|
302
|
+
"""Full comparison between two documents across all equivalence levels."""
|
|
303
|
+
|
|
304
|
+
result = EquivalenceResult()
|
|
305
|
+
|
|
306
|
+
# F-16: byte-identical
|
|
307
|
+
if left_bytes is not None and right_bytes is not None:
|
|
308
|
+
result.byte_identical = compare_byte_identical(left_bytes, right_bytes)
|
|
309
|
+
|
|
310
|
+
# F-17: AST-equivalent
|
|
311
|
+
result.ast_equivalent, ast_diffs = compare_ast_equivalent(left, right)
|
|
312
|
+
result.diffs.extend(ast_diffs)
|
|
313
|
+
|
|
314
|
+
# F-18: semantic-equivalent
|
|
315
|
+
result.semantic_equivalent, sem_diffs = compare_semantic_equivalent(left, right)
|
|
316
|
+
# Don't add sem_diffs to result.diffs (they're a subset of ast_diffs)
|
|
317
|
+
|
|
318
|
+
# F-19: content-equivalent
|
|
319
|
+
if left_hcortex is not None and right_hcortex is not None:
|
|
320
|
+
result.content_equivalent, content_diffs = compare_content_equivalent(left_hcortex, right_hcortex)
|
|
321
|
+
result.diffs.extend(content_diffs)
|
|
322
|
+
|
|
323
|
+
return result
|