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
cortex/v2/writer.py ADDED
@@ -0,0 +1,301 @@
1
+ r"""CORTEX v2 writer — serializes a :class:`CortexV2Document` back to exact format.
2
+
3
+ Reproduces the exact byte layout of ``skill/cortex/SKILL.md``:
4
+ 1. ``\`\`\`markdown`` wrapper
5
+ 2. ``<!-- CODEC-CORTEX -->`` header
6
+ 3. ``$0`` section with sigil declarations and metadata
7
+ 4. Sections ``$1``–``$12`` with entries
8
+ 5. Closing ``\`\`\``
9
+
10
+ v0.3.2 additions:
11
+ - ``write_cortex_v2_preserve()``: structure-preserving serializer used by
12
+ ``cortex canonicalize --preserve`` and by the VIEW-aware fallback path
13
+ of ``cortex canonicalize`` when no VIEW directives are present. It only
14
+ normalizes whitespace and section ordering, leaving entries in their
15
+ original form. This keeps v1-render compatibility (B-01/B-05 fix).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from typing import Any, Dict, List
22
+
23
+ from .parser import CortexV2Document, V2Entry, V2Section
24
+
25
+
26
+ # Keys that are always bare (enum/micro values)
27
+ _BARE_KEYS = frozenset({
28
+ 'type', 'risk', 'cortex', 'status', 'survive', 'severity',
29
+ 'encoding', 'license', 'priority', 'expand', 'pos',
30
+ })
31
+
32
+ # Keys that should use escaped quotes inside strings
33
+ _NEEDS_ESCAPE = re.compile(r'[\\"]')
34
+
35
+
36
+ def write_cortex_v2(doc: CortexV2Document) -> str:
37
+ """Serialize a :class:`CortexV2Document` to CORTEX v2 format.
38
+
39
+ Produces output byte-identical to the original when the document
40
+ was parsed from a well-formed CORTEX v2 file.
41
+ """
42
+
43
+ parts: List[str] = []
44
+
45
+ # 1. Opening wrapper
46
+ parts.append('```markdown')
47
+
48
+ # 2. CODEC-CORTEX header
49
+ if doc.header:
50
+ parts.append('<!-- CODEC-CORTEX')
51
+ for k, v in doc.header.items():
52
+ # v2.0.1 M-01: quote values that contain spaces, commas, or special chars
53
+ if isinstance(v, str) and (' ' in v or ',' in v or '"' in v or '—' in v):
54
+ # Escape inner quotes
55
+ escaped = v.replace('"', '\\"')
56
+ parts.append(f'{k}: "{escaped}"')
57
+ else:
58
+ parts.append(f'{k}: {v}')
59
+ parts.append('-->')
60
+ parts.append('') # blank line after header
61
+
62
+ # 3. Sections
63
+ for sec in doc.sections:
64
+ parts.append(sec.id)
65
+ for entry in sec.entries:
66
+ parts.append(_serialize_entry(entry))
67
+ # Add blank line between sections (but not after last entry of last section)
68
+ # Actually, looking at the original, there are blank lines between sections
69
+ parts.append('') # blank line after each section's entries
70
+
71
+ # 4. Remove trailing blank lines and add closing wrapper
72
+ # The original ends with: last_entry\n```
73
+ # So we need to join, strip trailing whitespace, add \n```
74
+ text = '\n'.join(parts)
75
+ # Remove trailing blank lines
76
+ text = text.rstrip('\n')
77
+ text += '\n```'
78
+
79
+ return text
80
+
81
+
82
+ def _serialize_entry(entry: V2Entry) -> str:
83
+ """Serialize a single entry to its CORTEX v2 text form."""
84
+
85
+ if entry.entry_type == 'attrs-pos' and entry.sigil == 'HDL':
86
+ return _serialize_hdl_entry(entry)
87
+
88
+ if entry.entry_type == 'meta':
89
+ return _serialize_meta_entry(entry)
90
+
91
+ if entry.entry_type == 'sigil_decl':
92
+ # In $0, sigil declarations use SIGIL:name{} format
93
+ # The ! sigil uses !:name{} (with colon)
94
+ if entry.sigil == '!':
95
+ body = _serialize_attrs(entry.value)
96
+ return f'!:{entry.name}{{{body}}}'
97
+ body = _serialize_attrs(entry.value)
98
+ return f'{entry.sigil}:{entry.name}{{{body}}}'
99
+
100
+ if entry.entry_type == 'cuerpo':
101
+ return f'{entry.sigil}:{entry.name}{{{entry.value}}}'
102
+
103
+ if entry.entry_type == 'bloque':
104
+ # DIAG entries are multiline: SIGIL:name{\ncontent\n}
105
+ # The value already contains the content between { and }
106
+ # We need to reproduce: SIGIL:name{\n + content + \n}
107
+ value = entry.value
108
+ if '\n' in value:
109
+ # Multi-line bloque: opening { on same line, content, closing } on own line
110
+ return f'{entry.sigil}:{entry.name}{{{value}}}'
111
+ else:
112
+ return f'{entry.sigil}:{entry.name}{{{value}}}'
113
+
114
+ # Default: attrs
115
+ body = _serialize_attrs(entry.value)
116
+ # ! entries in $4+ use !name{} format (without colon)
117
+ if entry.sigil == '!':
118
+ return f'!{entry.name}{{{body}}}'
119
+ return f'{entry.sigil}:{entry.name}{{{body}}}'
120
+
121
+
122
+ def _serialize_hdl_entry(entry: V2Entry) -> str:
123
+ """Serialize an HDL bare attrs-pos entry."""
124
+
125
+ v = entry.value
126
+ # Build parts list, omitting trailing empty fields
127
+ parts = [
128
+ v.get('operation', ''),
129
+ v.get('status', ''),
130
+ v.get('requires', ''),
131
+ v.get('notes', ''),
132
+ ]
133
+ # Remove trailing empty parts to match original format
134
+ while parts and parts[-1] == '':
135
+ parts.pop()
136
+ return f'HDL:{entry.name}|{"|".join(parts)}'
137
+
138
+
139
+ def _serialize_meta_entry(entry: V2Entry) -> str:
140
+ """Serialize a $0 metadata entry."""
141
+
142
+ body = _serialize_attrs(entry.value)
143
+ return f'$0:{entry.name}{{{body}}}'
144
+
145
+
146
+ def _serialize_sigil_decl(entry: V2Entry) -> str:
147
+ """Serialize a $0 sigil declaration entry."""
148
+
149
+ body = _serialize_attrs(entry.value)
150
+ return f'{entry.sigil}:{entry.name}{{{body}}}'
151
+
152
+
153
+ def _serialize_attrs(attrs: Dict[str, Any]) -> str:
154
+ """Serialize an attrs dict to ``key:value`` or ``key:"value"`` form.
155
+
156
+ Quoting rules (matching the original CORTEX format):
157
+ - Bare values: enum/micro tokens (risk, status, survive, severity, etc.)
158
+ ONLY when the value is a simple identifier (no spaces, pipes, commas)
159
+ - Quoted values: free text (desc, rule, content, etc.)
160
+ - The ``cortex`` field is bare unless it contains ``/``
161
+ - The ``pos`` field is always quoted (contains ``|``)
162
+ - Any value containing spaces, ``|``, ``,``, ``{``, ``}`` must be quoted
163
+ """
164
+
165
+ parts: List[str] = []
166
+ for k, v in attrs.items():
167
+ if v is None:
168
+ parts.append(f'{k}:null')
169
+ elif isinstance(v, bool):
170
+ parts.append(f'{k}:{"true" if v else "false"}')
171
+ elif isinstance(v, (int, float)):
172
+ parts.append(f'{k}:{v}')
173
+ elif isinstance(v, str):
174
+ # Determine if this should be bare or quoted
175
+ needs_quotes = False
176
+ if k not in _BARE_KEYS:
177
+ needs_quotes = True
178
+ elif '/' in v:
179
+ needs_quotes = True
180
+ elif any(c in v for c in ' |,{}'):
181
+ needs_quotes = True
182
+ elif ' ' in v:
183
+ needs_quotes = True
184
+
185
+ if needs_quotes:
186
+ escaped = _escape_string(v)
187
+ parts.append(f'{k}:"{escaped}"')
188
+ else:
189
+ parts.append(f'{k}:{v}')
190
+ else:
191
+ parts.append(f'{k}:{v}')
192
+ return ','.join(parts)
193
+
194
+
195
+ def _escape_string(s: str) -> str:
196
+ """Escape a string for CORTEX double-quoted form."""
197
+
198
+ # In the original, escaped quotes appear as \\" inside rule strings
199
+ # e.g. rule:"pares clave:valor o clave:\"valor\" dentro de {}"
200
+ result = s.replace('\\', '\\\\').replace('"', '\\"')
201
+ return result
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # v0.3.2 — Structure-preserving canonicalizer (B-01/B-05 fix)
206
+ # ---------------------------------------------------------------------------
207
+
208
+ # Section IDs are sorted numerically by their $N component.
209
+ _SECTION_SORT_RE = re.compile(r'^\$(\d+)$')
210
+
211
+
212
+ def _section_sort_key(sec: V2Section):
213
+ """Sort key for sections: $0, $1, ..., $12 (numeric)."""
214
+ m = _SECTION_SORT_RE.match(sec.id)
215
+ if m:
216
+ return (0, int(m.group(1)))
217
+ # Non-numeric section ids sort after numeric ones, alphabetically.
218
+ return (1, sec.id)
219
+
220
+
221
+ def has_view_directives(doc: CortexV2Document) -> bool:
222
+ """Return True if ``doc`` has at least one operational VIEW directive.
223
+
224
+ A VIEW entry inside ``$0`` that has ``type/risk/cortex/desc`` is a sigil
225
+ declaration, NOT an operational directive (mirrors
226
+ ``view.parse_view_entries_from_doc``).
227
+ """
228
+ for sec in doc.sections:
229
+ for entry in sec.entries:
230
+ if entry.sigil != "VIEW":
231
+ continue
232
+ if sec.id == "$0" and isinstance(entry.value, dict):
233
+ if any(k in entry.value for k in ("type", "risk", "cortex", "desc")):
234
+ continue
235
+ return True
236
+ return False
237
+
238
+
239
+ def write_cortex_v2_preserve(doc: CortexV2Document) -> str:
240
+ """Serialize ``doc`` preserving original structure (v0.3.2 — B-01/B-05 fix).
241
+
242
+ Unlike :func:`write_cortex_v2`, this serializer:
243
+
244
+ * Keeps the original entry order within each section (no reordering).
245
+ * Preserves the original entry ``raw`` text when available, falling
246
+ back to a normal serialization only if ``raw`` is missing.
247
+ * Normalizes only whitespace and blank lines between sections.
248
+ * Sorts sections numerically ($0, $1, ..., $12) so the output is
249
+ deterministic, but never reorders entries inside a section.
250
+ * Does NOT touch the markdown wrapper or CODEC-CORTEX header — those
251
+ are reproduced verbatim if present, omitted if absent.
252
+
253
+ This guarantees v1-render compatibility for artefacts that do not
254
+ declare VIEW directives (the corpus case), and provides the
255
+ ``--preserve`` escape hatch for artefacts that DO have VIEW but where
256
+ the user explicitly wants the original structure kept.
257
+ """
258
+ parts: List[str] = []
259
+
260
+ # 1. Opening wrapper (only if the source had one — detected via raw_text)
261
+ raw = doc.raw_text or ""
262
+ had_wrapper = raw.lstrip().startswith('```markdown')
263
+
264
+ if had_wrapper:
265
+ parts.append('```markdown')
266
+
267
+ # 2. CODEC-CORTEX header (reproduced verbatim from doc.header)
268
+ if doc.header:
269
+ parts.append('<!-- CODEC-CORTEX')
270
+ for k, v in doc.header.items():
271
+ if isinstance(v, str) and (' ' in v or ',' in v or '"' in v or '—' in v):
272
+ escaped = v.replace('"', '\\"')
273
+ parts.append(f'{k}: "{escaped}"')
274
+ else:
275
+ parts.append(f'{k}: {v}')
276
+ parts.append('-->')
277
+ parts.append('')
278
+
279
+ # 3. Sections, numerically sorted; entries preserved in original order.
280
+ sorted_sections = sorted(doc.sections, key=_section_sort_key)
281
+ for sec in sorted_sections:
282
+ parts.append(sec.id)
283
+ for entry in sec.entries:
284
+ # Prefer the original raw text when available — this is what
285
+ # makes the operation structure-preserving.
286
+ if entry.raw:
287
+ parts.append(entry.raw)
288
+ else:
289
+ parts.append(_serialize_entry(entry))
290
+ parts.append('')
291
+
292
+ # 4. Trim trailing blank lines and close wrapper if it was opened.
293
+ text = '\n'.join(parts)
294
+ text = text.rstrip('\n')
295
+ if had_wrapper:
296
+ text += '\n```'
297
+ else:
298
+ # Ensure a single trailing newline for non-wrapped artefacts
299
+ # (matches the corpus files convention).
300
+ text += '\n'
301
+ return text