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,450 @@
1
+ r"""HCORTEX v2 renderer — converts SkillIR to canonical HCORTEX Markdown.
2
+
3
+ Produces human-readable Markdown with:
4
+ - Header with internal_encoding:HCORTEX
5
+ - **Perfil: CORTEX-FULL** as first content line
6
+ - Numbered section headers (# N. Title)
7
+ - Tables for sigil definitions, attrs, contracts
8
+ - PUML blocks for DIAG entries
9
+ - Prose for DESC/AXM entries
10
+ - Cross-references where applicable
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import List
16
+
17
+ from .ir import SkillIR, IREntry
18
+
19
+
20
+ # Human-readable sigil names
21
+ SIGIL_NAMES = {
22
+ "IDN": "Identity", "DOM": "Domain", "KNW": "Knowledge", "REF": "Reference",
23
+ "TAG": "Tag", "AXM": "Axiom", "CNST": "Constraint", "!": "Rule",
24
+ "CLAIM": "Claim", "LIM": "Limit", "AUD": "Audit", "RSK": "Risk",
25
+ "FCS": "Focus", "OBJ": "Objective", "WRK": "Work State", "STP": "Step",
26
+ "NXT": "Next", "SES": "Session", "LNG": "Lesson", "DIAG": "Diagram",
27
+ "HDL": "Handler", "PFL": "Pitfall", "DEP": "Dependency",
28
+ "DESC": "Description", "ERR": "Error",
29
+ }
30
+
31
+ # Section titles for HCORTEX
32
+ SECTION_TITLES = {
33
+ "$0": "Glosario cognitivo universal",
34
+ "$1": "Identidad y dominio",
35
+ "$2": "Propósito y contexto",
36
+ "$3": "Operaciones y handlers",
37
+ "$4": "Reglas de sintaxis y render",
38
+ "$5": "Restricciones, riesgos y pitfalls",
39
+ "$6": "Diagramas",
40
+ "$7": "Contratos mínimos por sigilo",
41
+ "$8": "Supervivencia y prioridades",
42
+ "$9": "Perfiles de contexto",
43
+ "$10": "Política de degradación",
44
+ "$11": "HCORTEX",
45
+ "$12": "CORTEX-OUT",
46
+ }
47
+
48
+ # HCORTEX section number mapping
49
+ CORTEX_TO_HCORTEX_NUM = {
50
+ "$0": "4", "$1": "0", "$2": "1", "$3": "8", "$4": "5",
51
+ "$5": "7", "$6": "2", "$7": "6", "$8": "11", "$9": "9.6.3",
52
+ "$10": "11.5", "$11": "9", "$12": "10",
53
+ }
54
+
55
+
56
+ def render_hcortex_v2(ir: SkillIR) -> str:
57
+ """Render a SkillIR as canonical HCORTEX v2 Markdown."""
58
+
59
+ lines: List[str] = []
60
+
61
+ # 1. Header
62
+ lines.append("<!-- CODEC-CORTEX")
63
+ lines.append("internal_encoding: HCORTEX")
64
+ if "source_artifact" in ir.header:
65
+ lines.append(f"source_artifact: {ir.header['source_artifact']}")
66
+ if "source_version" in ir.header:
67
+ lines.append(f"source_version: {ir.header['source_version']}")
68
+ if "status" in ir.header:
69
+ lines.append(f"status: {ir.header['status']}")
70
+ lines.append(f'derived_from: {ir.header.get("source_artifact", "skill/cortex/SKILL.md")}')
71
+ lines.append('-->')
72
+ lines.append("")
73
+
74
+ # 2. Profile
75
+ lines.append("**Perfil: CORTEX-FULL**")
76
+ lines.append("")
77
+ lines.append("---")
78
+ lines.append("")
79
+
80
+ # 3. Render sections
81
+ for sec_id in sorted(set(e.section for e in ir.entries if e.section), key=lambda x: int(x[1:])):
82
+ _render_section(lines, ir, sec_id)
83
+
84
+ return "\n".join(lines).rstrip() + "\n"
85
+
86
+
87
+ def _render_section(lines: List[str], ir: SkillIR, sec_id: str) -> None:
88
+ """Render a single CORTEX section as HCORTEX."""
89
+ hcortex_num = CORTEX_TO_HCORTEX_NUM.get(sec_id, sec_id[1:])
90
+ title = SECTION_TITLES.get(sec_id, sec_id)
91
+ entries = ir.get_entries(section=sec_id)
92
+
93
+ if not entries:
94
+ return
95
+
96
+ lines.append(f"# {hcortex_num}. {title}")
97
+ lines.append("")
98
+
99
+ if sec_id == "$0":
100
+ _render_glossary(lines, ir, entries)
101
+ elif sec_id == "$1":
102
+ _render_identity(lines, ir, entries)
103
+ elif sec_id == "$2":
104
+ _render_purpose(lines, ir, entries)
105
+ elif sec_id == "$3":
106
+ _render_handlers(lines, ir, entries)
107
+ elif sec_id == "$4":
108
+ _render_rules(lines, ir, entries)
109
+ elif sec_id == "$5":
110
+ _render_constraints(lines, ir, entries)
111
+ elif sec_id == "$6":
112
+ _render_diagrams(lines, ir, entries)
113
+ elif sec_id == "$7":
114
+ _render_contracts(lines, ir, entries)
115
+ elif sec_id == "$8":
116
+ _render_survival(lines, ir, entries)
117
+ elif sec_id == "$9":
118
+ _render_profiles(lines, ir, entries)
119
+ elif sec_id == "$10":
120
+ _render_degradation(lines, ir, entries)
121
+ elif sec_id == "$11":
122
+ _render_hcortex_directives(lines, ir, entries)
123
+ elif sec_id == "$12":
124
+ _render_cortex_out(lines, ir, entries)
125
+ else:
126
+ _render_generic(lines, ir, entries)
127
+
128
+ lines.append("")
129
+
130
+
131
+ def _render_glossary(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
132
+ """Render $0 glossary as sigil table + types + micro + contracts."""
133
+ # Sigil declarations
134
+ sigil_decls = [e for e in entries if e.entry_type == "sigil_decl"]
135
+ if sigil_decls:
136
+ lines.append("## Sigilos canónicos")
137
+ lines.append("")
138
+ lines.append("| Sigilo | Nombre | Tipo | Riesgo | Capa | Descripción |")
139
+ lines.append("|---|---|---|:---:|---|---|")
140
+ for e in sigil_decls:
141
+ v = e.value
142
+ lines.append(
143
+ f"| `{e.sigil}` | {e.name} | `{v.get('type', '')}` | "
144
+ f"{v.get('risk', '')} | {v.get('cortex', '')} | {v.get('desc', '')} |"
145
+ )
146
+ lines.append("")
147
+
148
+ # Type declarations
149
+ type_decls = [e for e in entries if e.sigil == "$0" and e.name.startswith("type_")]
150
+ if type_decls:
151
+ lines.append("## Tipos de expansión")
152
+ lines.append("")
153
+ lines.append("| Tipo | Regla |")
154
+ lines.append("|---|---|")
155
+ for e in type_decls:
156
+ type_name = e.name.replace("type_", "")
157
+ lines.append(f"| `{type_name}` | {e.value.get('rule', '')} |")
158
+ lines.append("")
159
+
160
+ # Contract declarations
161
+ contract_decls = [e for e in entries if e.sigil == "$0" and e.name.startswith("contract_")]
162
+ if contract_decls:
163
+ lines.append("## Contratos posicionales")
164
+ lines.append("")
165
+ lines.append("| Sigilo | Campos posicionales |")
166
+ lines.append("|---|---|")
167
+ for e in contract_decls:
168
+ sigil_name = e.name.replace("contract_", "").upper()
169
+ lines.append(f"| `{sigil_name}` | {e.value.get('pos', '')} |")
170
+ lines.append("")
171
+
172
+ # Micro-token declarations
173
+ micro_decls = [e for e in entries if e.sigil == "$0" and e.name.startswith("micro_")]
174
+ if micro_decls:
175
+ lines.append("## Micro-glosario")
176
+ lines.append("")
177
+ lines.append("| Token | Expansión |")
178
+ lines.append("|---|---|")
179
+ for e in micro_decls:
180
+ lines.append(f"| `{e.name.replace('micro_', '')}` | {e.value.get('expand', '')} |")
181
+ lines.append("")
182
+
183
+ # Enum declarations
184
+ enum_decls = [e for e in entries if e.sigil == "$0" and e.name.startswith("enum_")]
185
+ if enum_decls:
186
+ lines.append("## Enumeraciones")
187
+ lines.append("")
188
+ for e in enum_decls:
189
+ enum_name = e.name.replace("enum_", "")
190
+ lines.append(f"**{enum_name}:** {e.value.get('values', '')}")
191
+ lines.append("")
192
+
193
+
194
+ def _render_identity(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
195
+ """Render $1 identity entries."""
196
+ for e in entries:
197
+ if e.sigil == "IDN" and isinstance(e.value, dict):
198
+ lines.append("## Identidad del proyecto")
199
+ lines.append("")
200
+ lines.append("| Campo | Valor |")
201
+ lines.append("|---|---|")
202
+ for k, v in e.value.items():
203
+ lines.append(f"| {k} | {v} |")
204
+ lines.append("")
205
+ elif e.sigil == "DOM" and isinstance(e.value, dict):
206
+ lines.append("## Dominio")
207
+ lines.append("")
208
+ lines.append("| Campo | Valor |")
209
+ lines.append("|---|---|")
210
+ for k, v in e.value.items():
211
+ lines.append(f"| {k} | {v} |")
212
+ lines.append("")
213
+ elif e.sigil == "REF" and isinstance(e.value, dict):
214
+ # Group REF entries into a table
215
+ pass # Will be handled in a batch
216
+
217
+ # Render REF entries as a table
218
+ ref_entries = [e for e in entries if e.sigil == "REF"]
219
+ if ref_entries:
220
+ lines.append("## Artefactos referenciados")
221
+ lines.append("")
222
+ lines.append("| Artefacto | Ruta | Rol |")
223
+ lines.append("|---|---|---|")
224
+ for e in ref_entries:
225
+ v = e.value
226
+ lines.append(f"| `{e.name}` | `{v.get('path', '')}` | {v.get('role', '')} |")
227
+ lines.append("")
228
+
229
+
230
+ def _render_purpose(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
231
+ """Render $2 purpose/context entries."""
232
+ for e in entries:
233
+ if e.entry_type == "cuerpo":
234
+ if e.sigil == "AXM":
235
+ # Axioms as blockquotes
236
+ lines.append(f"> {e.value}")
237
+ lines.append("")
238
+ else:
239
+ # DESC as prose
240
+ lines.append(e.value)
241
+ lines.append("")
242
+ elif e.sigil == "KNW" and isinstance(e.value, dict):
243
+ lines.append(f"### {e.value.get('topic', e.name)}")
244
+ lines.append("")
245
+ lines.append(e.value.get('content', ''))
246
+ lines.append("")
247
+
248
+
249
+ def _render_handlers(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
250
+ """Render $3 HDL handler entries."""
251
+ for e in entries:
252
+ if e.entry_type == "attrs-pos":
253
+ v = e.value
254
+ lines.append(f"## {e.name.replace('_', ' ').title()}")
255
+ lines.append("")
256
+ lines.append(f"**Operación:** {v.get('operation', '')} ")
257
+ lines.append(f"**Estado:** {v.get('status', '')} ")
258
+ lines.append(f"**Requiere:** {v.get('requires', '')}")
259
+ lines.append("")
260
+ notes = v.get('notes', '')
261
+ if notes:
262
+ lines.append(notes)
263
+ lines.append("")
264
+
265
+
266
+ def _render_rules(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
267
+ """Render $4 ! rule entries as a table."""
268
+ if entries:
269
+ lines.append("## Reglas operacionales")
270
+ lines.append("")
271
+ lines.append("| Regla | survive |")
272
+ lines.append("|---|---|")
273
+ for e in entries:
274
+ e.value.get('rule', '') if isinstance(e.value, dict) else ''
275
+ survive = e.value.get('survive', '') if isinstance(e.value, dict) else ''
276
+ lines.append(f"| `!{e.name}` | {survive} |")
277
+ lines.append("")
278
+
279
+
280
+ def _render_constraints(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
281
+ """Render $5 constraints, limits, risks, pitfalls."""
282
+ cnst_entries = [e for e in entries if e.sigil == "CNST"]
283
+ if cnst_entries:
284
+ lines.append("## Restricciones")
285
+ lines.append("")
286
+ lines.append("| Restricción | Regla | Severidad | survive |")
287
+ lines.append("|---|---|---|---|")
288
+ for e in cnst_entries:
289
+ v = e.value
290
+ lines.append(f"| `{e.name}` | {v.get('rule', '')} | {v.get('severity', '')} | {v.get('survive', '')} |")
291
+ lines.append("")
292
+
293
+ lim_entries = [e for e in entries if e.sigil == "LIM"]
294
+ if lim_entries:
295
+ lines.append("## Límites")
296
+ lines.append("")
297
+ lines.append("| Límite | Alcance | Estado |")
298
+ lines.append("|---|---|---|")
299
+ for e in lim_entries:
300
+ v = e.value
301
+ lines.append(f"| `{e.name}` | {v.get('scope', '')} | {v.get('status', '')} |")
302
+ lines.append("")
303
+
304
+ rsk_entries = [e for e in entries if e.sigil == "RSK"]
305
+ if rsk_entries:
306
+ lines.append("## Riesgos")
307
+ lines.append("")
308
+ lines.append("| Riesgo | Impacto | Mitigación | Estado |")
309
+ lines.append("|---|---|---|---|")
310
+ for e in rsk_entries:
311
+ v = e.value
312
+ lines.append(f"| `{e.name}` | {v.get('impact', '')} | {v.get('mitigation', '')} | {v.get('status', '')} |")
313
+ lines.append("")
314
+
315
+ pfl_entries = [e for e in entries if e.sigil == "PFL"]
316
+ if pfl_entries:
317
+ lines.append("## Pitfalls")
318
+ lines.append("")
319
+ lines.append("| Patrón | Efecto | Prevención |")
320
+ lines.append("|---|---|---|")
321
+ for e in pfl_entries:
322
+ v = e.value
323
+ lines.append(f"| `{e.name}` | {v.get('effect', '')} | {v.get('prevention', '')} |")
324
+ lines.append("")
325
+
326
+
327
+ def _render_diagrams(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
328
+ """Render $6 DIAG bloque entries as PUML blocks."""
329
+ for e in entries:
330
+ if e.entry_type == "bloque":
331
+ lines.append(f"## {e.name}")
332
+ lines.append("")
333
+ lines.append("```puml")
334
+ lines.append(e.value.strip())
335
+ lines.append("```")
336
+ lines.append("")
337
+
338
+
339
+ def _render_contracts(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
340
+ """Render $7 field contracts as a table."""
341
+ if entries:
342
+ lines.append("## Contratos mínimos por sigilo crítico")
343
+ lines.append("")
344
+ lines.append("| Sigilo | Campos requeridos | Severidad |")
345
+ lines.append("|---|---|---|")
346
+ for e in entries:
347
+ v = e.value
348
+ sigil_name = e.name.replace("contract_", "").upper()
349
+ lines.append(f"| `{sigil_name}` | {v.get('rule', '')} | {v.get('severity', '')} |")
350
+ lines.append("")
351
+
352
+
353
+ def _render_survival(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
354
+ """Render $8 survival/priorities."""
355
+ for e in entries:
356
+ if e.sigil == "KNW" and isinstance(e.value, dict):
357
+ lines.append(f"### {e.value.get('topic', e.name)}")
358
+ lines.append("")
359
+ lines.append(e.value.get('content', ''))
360
+ lines.append("")
361
+ elif e.sigil == "!":
362
+ v = e.value
363
+ lines.append(f"- **`!{e.name}`**: {v.get('rule', '')}")
364
+ lines.append("")
365
+
366
+
367
+ def _render_profiles(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
368
+ """Render $9 context profiles."""
369
+ if entries:
370
+ lines.append("## Perfiles de contexto")
371
+ lines.append("")
372
+ lines.append("| Perfil | Contenido | Estado |")
373
+ lines.append("|---|---|---|")
374
+ for e in entries:
375
+ if isinstance(e.value, dict):
376
+ v = e.value
377
+ lines.append(f"| `{e.name}` | {v.get('content', '')} | {v.get('status', '')} |")
378
+ lines.append("")
379
+
380
+
381
+ def _render_degradation(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
382
+ """Render $10 degradation rules."""
383
+ for e in entries:
384
+ v = e.value if isinstance(e.value, dict) else {}
385
+ lines.append(f"- **`!{e.name}`**: {v.get('rule', '')}")
386
+ lines.append("")
387
+
388
+
389
+ def _render_hcortex_directives(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
390
+ """Render $11 HCORTEX directives."""
391
+ for e in entries:
392
+ if e.entry_type == "cuerpo":
393
+ lines.append(e.value)
394
+ lines.append("")
395
+ elif e.sigil == "KNW" and isinstance(e.value, dict):
396
+ lines.append(f"### {e.value.get('topic', e.name)}")
397
+ lines.append("")
398
+ lines.append(e.value.get('content', ''))
399
+ lines.append("")
400
+ elif e.sigil == "!":
401
+ v = e.value
402
+ lines.append(f"- **`!{e.name}`**: {v.get('rule', '')}")
403
+ lines.append("")
404
+ elif e.sigil == "PFL" and isinstance(e.value, dict):
405
+ v = e.value
406
+ lines.append(f"- **{e.name}**: {v.get('pattern', '')} → {v.get('prevention', '')}")
407
+ lines.append("")
408
+
409
+
410
+ def _render_cortex_out(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
411
+ """Render $12 CORTEX-OUT entries."""
412
+ for e in entries:
413
+ if e.entry_type == "cuerpo":
414
+ lines.append(e.value)
415
+ lines.append("")
416
+ elif e.sigil == "AXM":
417
+ lines.append(f"> {e.value}")
418
+ lines.append("")
419
+ elif e.sigil == "!":
420
+ v = e.value
421
+ lines.append(f"- **`!{e.name}`**: {v.get('rule', '')}")
422
+ lines.append("")
423
+ elif e.sigil == "KNW" and isinstance(e.value, dict):
424
+ lines.append(f"### {e.value.get('topic', e.name)}")
425
+ lines.append("")
426
+ lines.append(e.value.get('content', ''))
427
+ lines.append("")
428
+
429
+
430
+ def _render_generic(lines: List[str], ir: SkillIR, entries: List[IREntry]) -> None:
431
+ """Generic renderer for unmapped sections."""
432
+ for e in entries:
433
+ if e.entry_type == "cuerpo" or e.entry_type == "bloque":
434
+ lines.append(f"### {e.sigil}:{e.name}")
435
+ lines.append("")
436
+ if e.entry_type == "bloque":
437
+ lines.append("```")
438
+ lines.append(e.value.strip())
439
+ lines.append("```")
440
+ else:
441
+ lines.append(e.value)
442
+ lines.append("")
443
+ elif isinstance(e.value, dict):
444
+ lines.append(f"### {e.sigil}:{e.name}")
445
+ lines.append("")
446
+ lines.append("| Campo | Valor |")
447
+ lines.append("|---|---|")
448
+ for k, v in e.value.items():
449
+ lines.append(f"| {k} | {v} |")
450
+ lines.append("")
cortex/v2/ir.py ADDED
@@ -0,0 +1,223 @@
1
+ r"""SkillIR — Intermediate Representation for bidirectional CORTEX ⇄ HCORTEX.
2
+
3
+ The IR preserves:
4
+ - CORTEX entries (sigil, name, type, value, section, raw)
5
+ - HCORTEX blocks (heading path, kind, raw markdown, source mapping)
6
+ - Mappings between CORTEX entries and HCORTEX blocks
7
+ - Human blocks (HCORTEX content not derivable from CORTEX)
8
+ - Warnings (orphan content, ambiguities, unmapped entries)
9
+
10
+ The IR is the single source of truth for conversion:
11
+ CORTEX → parse_cortex_v2 → SkillIR → render_hcortex_v2 → HCORTEX
12
+ HCORTEX → parse_hcortex_v2 → SkillIR → write_cortex_v2 → CORTEX
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from typing import Any, Dict, List, Tuple
19
+
20
+ from .parser import CortexV2Document, V2Entry, V2Section
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # IR data model
25
+ # ---------------------------------------------------------------------------
26
+
27
+ @dataclass
28
+ class IREntry:
29
+ """A CORTEX entry in the IR."""
30
+ sigil: str
31
+ name: str
32
+ entry_type: str # attrs | attrs-pos | cuerpo | bloque | sigil_decl | meta
33
+ value: Any
34
+ section: str = ""
35
+ raw: str = ""
36
+ # Mapping to HCORTEX block(s) this entry was rendered into
37
+ hcortex_blocks: List[str] = field(default_factory=list) # block IDs
38
+
39
+ def to_dict(self) -> dict:
40
+ return {
41
+ "sigil": self.sigil, "name": self.name, "type": self.entry_type,
42
+ "value": self.value, "section": self.section,
43
+ }
44
+
45
+
46
+ @dataclass
47
+ class IRBlock:
48
+ """An HCORTEX block in the IR."""
49
+ block_id: str # unique ID
50
+ heading_path: str # e.g. "4.2" or "9.6.1"
51
+ heading_text: str # e.g. "Sigilos canónicos"
52
+ kind: str # table | prose | puml | list | checklist | code | blockquote
53
+ raw_markdown: str # the actual markdown content
54
+ # Mapping to CORTEX entry(ies) this block was derived from
55
+ source_entries: List[Tuple[str, str]] = field(default_factory=list) # [(sigil, name), ...]
56
+ # Whether this block is derivable from CORTEX or is human-added
57
+ derivable: bool = True
58
+
59
+ def to_dict(self) -> dict:
60
+ return {
61
+ "block_id": self.block_id,
62
+ "heading_path": self.heading_path,
63
+ "heading_text": self.heading_text,
64
+ "kind": self.kind,
65
+ "source_entries": self.source_entries,
66
+ "derivable": self.derivable,
67
+ }
68
+
69
+
70
+ @dataclass
71
+ class IRWarning:
72
+ """A warning about content that couldn't be mapped."""
73
+ code: str
74
+ message: str
75
+ severity: str = "warning" # warning | info
76
+
77
+
78
+ @dataclass
79
+ class SkillIR:
80
+ """Intermediate Representation for bidirectional CORTEX ⇄ HCORTEX.
81
+
82
+ This is the single source of truth for conversion. Both CORTEX
83
+ and HCORTEX parse into this IR, and both render from it.
84
+ """
85
+ header: Dict[str, str] = field(default_factory=dict)
86
+ entries: List[IREntry] = field(default_factory=list)
87
+ blocks: List[IRBlock] = field(default_factory=list)
88
+ warnings: List[IRWarning] = field(default_factory=list)
89
+ # Section structure: list of (section_id, title) for HCORTEX rendering
90
+ section_map: List[Tuple[str, str]] = field(default_factory=list)
91
+
92
+ def add_entry(self, entry: V2Entry) -> IREntry:
93
+ ir_entry = IREntry(
94
+ sigil=entry.sigil, name=entry.name, entry_type=entry.entry_type,
95
+ value=entry.value, section=entry.section, raw=entry.raw,
96
+ )
97
+ self.entries.append(ir_entry)
98
+ return ir_entry
99
+
100
+ def add_block(self, block: IRBlock) -> None:
101
+ self.blocks.append(block)
102
+
103
+ def add_warning(self, code: str, message: str, severity: str = "warning") -> None:
104
+ self.warnings.append(IRWarning(code=code, message=message, severity=severity))
105
+
106
+ def get_entries(self, sigil: str = None, section: str = None) -> List[IREntry]:
107
+ result = []
108
+ for e in self.entries:
109
+ if sigil and e.sigil != sigil:
110
+ continue
111
+ if section and e.section != section:
112
+ continue
113
+ result.append(e)
114
+ return result
115
+
116
+ def to_dict(self) -> dict:
117
+ return {
118
+ "header": dict(self.header),
119
+ "entries": [e.to_dict() for e in self.entries],
120
+ "blocks": [b.to_dict() for b in self.blocks],
121
+ "warnings": [{"code": w.code, "message": w.message, "severity": w.severity} for w in self.warnings],
122
+ "section_map": self.section_map,
123
+ }
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # CORTEX → IR
128
+ # ---------------------------------------------------------------------------
129
+
130
+ def cortex_to_ir(doc: CortexV2Document) -> SkillIR:
131
+ """Convert a parsed CORTEX v2 document into a SkillIR."""
132
+ ir = SkillIR()
133
+ ir.header = dict(doc.header)
134
+
135
+ for sec in doc.sections:
136
+ for entry in sec.entries:
137
+ ir.add_entry(entry)
138
+
139
+ # Build section map for HCORTEX rendering
140
+ _build_section_map(ir, doc)
141
+
142
+ return ir
143
+
144
+
145
+ def _build_section_map(ir: SkillIR, doc: CortexV2Document) -> None:
146
+ """Build the mapping from CORTEX sections to HCORTEX section numbers.
147
+
148
+ This mapping is based on the analysis of the canonical SKILL.md ↔ SKILL_HCORTEX.md pair.
149
+ """
150
+ # Mapping: CORTEX $N → HCORTEX section number(s)
151
+ SECTION_MAP = {
152
+ "$0": [("4", "Glosario cognitivo universal"),
153
+ ("4.1", "Regla de autoridad y autocontención"),
154
+ ("4.2", "Sigilos canónicos"),
155
+ ("4.3", "Tipos de expansión"),
156
+ ("4.4", "Micro-glosario")],
157
+ "$1": [("0", "Control del documento"),
158
+ ("0.1", "Relación con otros artefactos")],
159
+ "$2": [("1", "Resumen ejecutivo"),
160
+ ("1.1", "Canon mínimo"),
161
+ ("1.2", "META-SKILL"),
162
+ ("1.3", "Problema que resuelve"),
163
+ ("1.4", "Principio rector"),
164
+ ("3", "Ontología cognitiva")],
165
+ "$3": [("8", "Operación de agentes")],
166
+ "$4": [("5", "Sintaxis .cortex"),
167
+ ("9.6", "Directrices canónicas de construcción documental HCORTEX")],
168
+ "$5": [("7", "Reglas de separación de niveles"),
169
+ ("20", "Pitfalls empresariales")],
170
+ "$6": [("2.1", "Diagrama canónico"),
171
+ ("3.1", "Diagrama de capas"),
172
+ ("8.1", "Secuencia mínima al iniciar"),
173
+ ("8.3", "Al absorber un paquete Nivel 3"),
174
+ ("9.4", "Diagrama HCORTEX")],
175
+ "$7": [("6", "Contratos mínimos por sigilo crítico")],
176
+ "$8": [("11", "Supervivencia contextual y triaje P0-P5")],
177
+ "$9": [("9.6.3", "Perfiles de contexto"),
178
+ ("11.4", "Perfiles")],
179
+ "$10": [("11.1", "Principio"),
180
+ ("11.5", "Diagrama de degradación")],
181
+ "$11": [("9", "HCORTEX"),
182
+ ("9.6", "Directrices canónicas de construcción documental HCORTEX")],
183
+ "$12": [("10", "CORTEX-OUT")],
184
+ }
185
+
186
+ for sec in doc.sections:
187
+ if sec.id in SECTION_MAP:
188
+ for hcortex_num, hcortex_title in SECTION_MAP[sec.id]:
189
+ ir.section_map.append((sec.id, hcortex_num, hcortex_title))
190
+ else:
191
+ ir.add_warning("UNMAPPED_SECTION", f"section {sec.id} has no HCORTEX mapping")
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # IR → CORTEX
196
+ # ---------------------------------------------------------------------------
197
+
198
+ def ir_to_cortex(ir: SkillIR) -> CortexV2Document:
199
+ """Convert a SkillIR back to a CortexV2Document for writing."""
200
+ doc = CortexV2Document()
201
+ doc.header = dict(ir.header)
202
+
203
+ # Group entries by section
204
+ sections_map: Dict[str, V2Section] = {}
205
+ for ir_entry in ir.entries:
206
+ sec_id = ir_entry.section or "$0"
207
+ if sec_id not in sections_map:
208
+ sections_map[sec_id] = V2Section(id=sec_id)
209
+ v2_entry = V2Entry(
210
+ sigil=ir_entry.sigil,
211
+ name=ir_entry.name,
212
+ entry_type=ir_entry.entry_type,
213
+ value=ir_entry.value,
214
+ raw=ir_entry.raw,
215
+ section=sec_id,
216
+ )
217
+ sections_map[sec_id].entries.append(v2_entry)
218
+
219
+ # Sort sections by number
220
+ for sec_id in sorted(sections_map.keys(), key=lambda x: int(x[1:])):
221
+ doc.sections.append(sections_map[sec_id])
222
+
223
+ return doc