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