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/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
|