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.
Files changed (81) hide show
  1. codec_cortex-0.3.2.dist-info/METADATA +183 -0
  2. codec_cortex-0.3.2.dist-info/RECORD +81 -0
  3. codec_cortex-0.3.2.dist-info/WHEEL +5 -0
  4. codec_cortex-0.3.2.dist-info/entry_points.txt +2 -0
  5. codec_cortex-0.3.2.dist-info/licenses/LICENSE +21 -0
  6. codec_cortex-0.3.2.dist-info/scm_file_list.json +124 -0
  7. codec_cortex-0.3.2.dist-info/scm_version.json +8 -0
  8. codec_cortex-0.3.2.dist-info/top_level.txt +1 -0
  9. cortex/__init__.py +22 -0
  10. cortex/__main__.py +7 -0
  11. cortex/_version.py +24 -0
  12. cortex/cli/__init__.py +5 -0
  13. cortex/cli/commands/__init__.py +150 -0
  14. cortex/cli/commands/add.py +89 -0
  15. cortex/cli/commands/compile.py +31 -0
  16. cortex/cli/commands/delete.py +44 -0
  17. cortex/cli/commands/diagram.py +130 -0
  18. cortex/cli/commands/diff.py +135 -0
  19. cortex/cli/commands/doctor.py +74 -0
  20. cortex/cli/commands/format.py +29 -0
  21. cortex/cli/commands/get.py +45 -0
  22. cortex/cli/commands/glossary.py +98 -0
  23. cortex/cli/commands/list.py +44 -0
  24. cortex/cli/commands/micro.py +79 -0
  25. cortex/cli/commands/move.py +45 -0
  26. cortex/cli/commands/new.py +80 -0
  27. cortex/cli/commands/recover.py +90 -0
  28. cortex/cli/commands/render.py +93 -0
  29. cortex/cli/commands/update.py +81 -0
  30. cortex/cli/commands/v2_canonicalize.py +162 -0
  31. cortex/cli/commands/v2_compare.py +74 -0
  32. cortex/cli/commands/v2_convert.py +183 -0
  33. cortex/cli/commands/v2_explain_loss.py +97 -0
  34. cortex/cli/commands/v2_inspect.py +113 -0
  35. cortex/cli/commands/v2_roundtrip.py +104 -0
  36. cortex/cli/commands/v2_roundtrip_bidir.py +128 -0
  37. cortex/cli/commands/v2_verify_view.py +65 -0
  38. cortex/cli/commands/verify.py +119 -0
  39. cortex/cli/main.py +636 -0
  40. cortex/core/__init__.py +85 -0
  41. cortex/core/ast.py +313 -0
  42. cortex/core/compare.py +180 -0
  43. cortex/core/document_kind.py +525 -0
  44. cortex/core/errors.py +399 -0
  45. cortex/core/lexer.py +267 -0
  46. cortex/core/parser.py +671 -0
  47. cortex/core/validator.py +268 -0
  48. cortex/core/writer.py +249 -0
  49. cortex/crud/__init__.py +17 -0
  50. cortex/crud/mutations.py +342 -0
  51. cortex/crud/selectors.py +95 -0
  52. cortex/crud/transactions.py +213 -0
  53. cortex/glossary/__init__.py +21 -0
  54. cortex/glossary/contracts.py +37 -0
  55. cortex/glossary/minimal.py +94 -0
  56. cortex/glossary/model.py +77 -0
  57. cortex/glossary/resolver.py +96 -0
  58. cortex/hcortex/__init__.py +35 -0
  59. cortex/hcortex/edit_parser.py +489 -0
  60. cortex/hcortex/edit_renderer.py +158 -0
  61. cortex/hcortex/markdown_model.py +81 -0
  62. cortex/hcortex/profiles.py +166 -0
  63. cortex/hcortex/read_renderer.py +342 -0
  64. cortex/hcortex/recovery.py +782 -0
  65. cortex/py.typed +0 -0
  66. cortex/templates/__init__.py +8 -0
  67. cortex/templates/brain.py +118 -0
  68. cortex/templates/minimal_glossary.py +42 -0
  69. cortex/templates/package.py +91 -0
  70. cortex/templates/skill.py +91 -0
  71. cortex/v2/__init__.py +30 -0
  72. cortex/v2/diagnostics.py +57 -0
  73. cortex/v2/encoder.py +1106 -0
  74. cortex/v2/equivalence.py +323 -0
  75. cortex/v2/hcortex_parser.py +615 -0
  76. cortex/v2/hcortex_renderer.py +450 -0
  77. cortex/v2/ir.py +223 -0
  78. cortex/v2/parser.py +655 -0
  79. cortex/v2/view.py +425 -0
  80. cortex/v2/view_renderer.py +474 -0
  81. cortex/v2/writer.py +301 -0
@@ -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