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,474 @@
|
|
|
1
|
+
r"""VIEW renderer — renders CORTEX entries as HCORTEX with VIEW markers.
|
|
2
|
+
|
|
3
|
+
v2.2.1: HCORTEX-R eliminated. HCORTEX is reversible by definition when
|
|
4
|
+
VIEW coverage is valid. Header uses internal_encoding: HCORTEX (not HCORTEX-R).
|
|
5
|
+
DIAG with preserve:verbatim is byte-identical (no strip/trim).
|
|
6
|
+
VIEW markers preserve all metadata fields.
|
|
7
|
+
E_VIEW_* errors produce rc!=0.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import List, Tuple
|
|
13
|
+
|
|
14
|
+
from .parser import CortexV2Document, V2Entry
|
|
15
|
+
from .view import (
|
|
16
|
+
ViewDirective, ViewKind, ViewDiagnostic,
|
|
17
|
+
parse_view_entries_from_doc, resolve_target, calculate_view_coverage,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_hcortex(doc: CortexV2Document, mode: str = "normal") -> Tuple[str, List[ViewDiagnostic]]:
|
|
22
|
+
"""Render a CORTEX v2 document as canonical HCORTEX (reversible).
|
|
23
|
+
|
|
24
|
+
v2.2.1: Uses internal_encoding: HCORTEX (not HCORTEX-R).
|
|
25
|
+
v2.2.3 PRE-04: ``reversible: true`` only if coverage == 1.0 AND no E_VIEW_* errors.
|
|
26
|
+
v2.2.3 PRE-05: ``mode = "display"`` produces Markdown without reversible contract.
|
|
27
|
+
Returns (markdown, diagnostics).
|
|
28
|
+
Errors in diagnostics mean the caller should return rc!=0.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
diags: List[ViewDiagnostic] = []
|
|
32
|
+
|
|
33
|
+
# 1. Extract VIEW directives
|
|
34
|
+
directives, view_diags = parse_view_entries_from_doc(doc)
|
|
35
|
+
diags.extend(view_diags)
|
|
36
|
+
|
|
37
|
+
# 2. Calculate coverage
|
|
38
|
+
coverage, uncovered = calculate_view_coverage(doc, directives)
|
|
39
|
+
if coverage < 1.0:
|
|
40
|
+
for desc in uncovered[:20]:
|
|
41
|
+
diags.append(ViewDiagnostic(
|
|
42
|
+
"W_VIEW_UNUSED_ENTRY",
|
|
43
|
+
f"entry {desc} is not covered by any VIEW directive",
|
|
44
|
+
severity="warning"
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
# v2.2.3 PRE-04: Gate reversible:true
|
|
48
|
+
# Only reversible if coverage == 100% AND no E_VIEW_* errors
|
|
49
|
+
has_errors = any(d.severity == "error" and d.code.startswith("E_VIEW_") for d in diags)
|
|
50
|
+
is_reversible = (coverage == 1.0) and (not has_errors) and (mode != "display")
|
|
51
|
+
|
|
52
|
+
# v2.2.3 PRE-05: Display mode → not canonical HCORTEX
|
|
53
|
+
if mode == "display":
|
|
54
|
+
diags.append(ViewDiagnostic(
|
|
55
|
+
"W_HCORTEX_DISPLAY_ONLY",
|
|
56
|
+
"display mode produces Markdown without reversible contract — not canonical HCORTEX",
|
|
57
|
+
severity="warning"
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
# 3. Build header — v2.2.1: HCORTEX (not HCORTEX-R)
|
|
61
|
+
lines: List[str] = []
|
|
62
|
+
lines.append("<!-- CODEC-CORTEX")
|
|
63
|
+
lines.append("internal_encoding: HCORTEX")
|
|
64
|
+
if "source_artifact" in doc.header:
|
|
65
|
+
lines.append(f"source_artifact: {doc.header['source_artifact']}")
|
|
66
|
+
if "source_version" in doc.header:
|
|
67
|
+
lines.append(f"source_version: {doc.header['source_version']}")
|
|
68
|
+
if "status" in doc.header:
|
|
69
|
+
lines.append(f"status: {doc.header['status']}")
|
|
70
|
+
lines.append(f"derived_from: {doc.header.get('source_artifact', 'skill/cortex/SKILL.md')}")
|
|
71
|
+
# v2.2.3 PRE-04: reversible reflects real state
|
|
72
|
+
lines.append(f"reversible: {'true' if is_reversible else 'false'}")
|
|
73
|
+
lines.append("view_schema: 1")
|
|
74
|
+
lines.append(f"view_coverage: {int(coverage * 100)}")
|
|
75
|
+
if mode == "display":
|
|
76
|
+
lines.append("mode: display")
|
|
77
|
+
lines.append("-->")
|
|
78
|
+
lines.append("")
|
|
79
|
+
lines.append("**Perfil: CORTEX-FULL**")
|
|
80
|
+
lines.append("")
|
|
81
|
+
lines.append("---")
|
|
82
|
+
lines.append("")
|
|
83
|
+
|
|
84
|
+
# 4. Render each VIEW directive in order
|
|
85
|
+
covered_entries: set = set()
|
|
86
|
+
for directive in directives:
|
|
87
|
+
block_lines = _render_view_block(doc, directive, covered_entries, diags)
|
|
88
|
+
if block_lines:
|
|
89
|
+
lines.extend(block_lines)
|
|
90
|
+
lines.append("")
|
|
91
|
+
|
|
92
|
+
# 5. Report coverage
|
|
93
|
+
lines.append("---")
|
|
94
|
+
lines.append("")
|
|
95
|
+
lines.append(f"<!-- VIEW coverage: {coverage:.1%} ({len(covered_entries)} entries covered) -->")
|
|
96
|
+
if uncovered:
|
|
97
|
+
lines.append(f"<!-- Uncovered entries: {len(uncovered)} -->")
|
|
98
|
+
|
|
99
|
+
return "\n".join(lines).rstrip() + "\n", diags
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Keep old name as alias for backward compatibility
|
|
103
|
+
render_hcortex_r = render_hcortex
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def has_view_errors(diags: List[ViewDiagnostic]) -> bool:
|
|
107
|
+
"""Check if diagnostics contain any E_VIEW_* errors."""
|
|
108
|
+
return any(d.code.startswith("E_VIEW_") and d.severity == "error" for d in diags)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def has_view_warnings(diags: List[ViewDiagnostic]) -> bool:
|
|
112
|
+
"""v2.2.2: Check if diagnostics contain any W_VIEW_* warnings."""
|
|
113
|
+
return any(d.code.startswith("W_VIEW_") and d.severity == "warning" for d in diags)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _render_view_block(
|
|
117
|
+
doc: CortexV2Document,
|
|
118
|
+
directive: ViewDirective,
|
|
119
|
+
covered_entries: set,
|
|
120
|
+
diags: List[ViewDiagnostic],
|
|
121
|
+
) -> List[str]:
|
|
122
|
+
"""Render a single VIEW directive as an HCORTEX block."""
|
|
123
|
+
|
|
124
|
+
entries = resolve_target(directive.target, doc)
|
|
125
|
+
if not entries and not directive.target.startswith("HUMAN_BLOCK:"):
|
|
126
|
+
# v2.2.2 P0-2: renamed E_VIEW_EMPTY_TARGET → W_VIEW_EMPTY_TARGET
|
|
127
|
+
# Empty target is recoverable (e.g., missing optional section), so warning.
|
|
128
|
+
diags.append(ViewDiagnostic(
|
|
129
|
+
"W_VIEW_EMPTY_TARGET",
|
|
130
|
+
f"VIEW:{directive.name} target '{directive.target}' resolved to 0 entries",
|
|
131
|
+
directive.name, "warning"
|
|
132
|
+
))
|
|
133
|
+
|
|
134
|
+
# v2.2.2 P0-5: Heterogeneous target detection
|
|
135
|
+
# If target matches entries with different entry_types AND no explicit fields,
|
|
136
|
+
# the renderer can't infer consistent columns → emit W_VIEW_HETEROGENEOUS_TARGET
|
|
137
|
+
if entries and not directive.fields and len(entries) > 1:
|
|
138
|
+
entry_types = {e.entry_type for e in entries}
|
|
139
|
+
attr_key_sets = set()
|
|
140
|
+
for e in entries:
|
|
141
|
+
if isinstance(e.value, dict):
|
|
142
|
+
attr_key_sets.add(frozenset(e.value.keys()))
|
|
143
|
+
# Heterogeneous if: different entry_types OR different attr key sets
|
|
144
|
+
if len(entry_types) > 1 or len(attr_key_sets) > 1:
|
|
145
|
+
diags.append(ViewDiagnostic(
|
|
146
|
+
"W_VIEW_HETEROGENEOUS_TARGET",
|
|
147
|
+
f"VIEW:{directive.name} target '{directive.target}' is heterogeneous "
|
|
148
|
+
f"(entry_types={sorted(entry_types)}, {len(attr_key_sets)} distinct attr schemas). "
|
|
149
|
+
f"Add explicit `fields` to silence.",
|
|
150
|
+
directive.name, "warning"
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
for e in entries:
|
|
154
|
+
covered_entries.add(f"{e.section}/{e.sigil}:{e.name}")
|
|
155
|
+
|
|
156
|
+
lines: List[str] = []
|
|
157
|
+
|
|
158
|
+
# v2.2.1 P0-6: Opening marker with ALL metadata fields
|
|
159
|
+
marker_parts = [f"VIEW:{directive.name}"]
|
|
160
|
+
marker_parts.append(f"kind={directive.kind.value}")
|
|
161
|
+
marker_parts.append(f'target="{directive.target}"')
|
|
162
|
+
marker_parts.append(f"reverse={directive.reverse.value}")
|
|
163
|
+
if directive.fields:
|
|
164
|
+
marker_parts.append(f'fields="{directive.fields}"')
|
|
165
|
+
if directive.order:
|
|
166
|
+
marker_parts.append(f"order={directive.order}")
|
|
167
|
+
if directive.title:
|
|
168
|
+
marker_parts.append(f'title="{directive.title}"')
|
|
169
|
+
if directive.status:
|
|
170
|
+
marker_parts.append(f"status={directive.status}")
|
|
171
|
+
if directive.scope:
|
|
172
|
+
marker_parts.append(f'scope="{directive.scope}"')
|
|
173
|
+
if directive.section:
|
|
174
|
+
marker_parts.append(f'section="{directive.section}"')
|
|
175
|
+
if directive.source_section:
|
|
176
|
+
marker_parts.append(f'source_section="{directive.source_section}"')
|
|
177
|
+
if directive.preserve:
|
|
178
|
+
marker_parts.append(f"preserve={directive.preserve}")
|
|
179
|
+
if directive.hash:
|
|
180
|
+
marker_parts.append(f'hash="{directive.hash}"')
|
|
181
|
+
if directive.fallback:
|
|
182
|
+
marker_parts.append(f'fallback="{directive.fallback}"')
|
|
183
|
+
lines.append(f"<!-- {' '.join(marker_parts)} -->")
|
|
184
|
+
|
|
185
|
+
if directive.title:
|
|
186
|
+
lines.append(f"## {directive.title}")
|
|
187
|
+
lines.append("")
|
|
188
|
+
|
|
189
|
+
if directive.kind == ViewKind.TABLE:
|
|
190
|
+
lines.extend(_render_table(entries, directive))
|
|
191
|
+
elif directive.kind == ViewKind.KV_TABLE:
|
|
192
|
+
lines.extend(_render_kv_table(entries, directive))
|
|
193
|
+
elif directive.kind == ViewKind.PROSE:
|
|
194
|
+
lines.extend(_render_prose(entries, directive))
|
|
195
|
+
elif directive.kind == ViewKind.QUOTE:
|
|
196
|
+
lines.extend(_render_quote(entries, directive))
|
|
197
|
+
elif directive.kind == ViewKind.PUML:
|
|
198
|
+
lines.extend(_render_puml(entries, directive))
|
|
199
|
+
elif directive.kind == ViewKind.CODE:
|
|
200
|
+
lines.extend(_render_code(entries, directive))
|
|
201
|
+
elif directive.kind == ViewKind.LIST:
|
|
202
|
+
lines.extend(_render_list(entries, directive))
|
|
203
|
+
elif directive.kind == ViewKind.NUMBERED_LIST:
|
|
204
|
+
lines.extend(_render_numbered_list(entries, directive))
|
|
205
|
+
elif directive.kind == ViewKind.CHECKLIST:
|
|
206
|
+
lines.extend(_render_checklist(entries, directive))
|
|
207
|
+
else:
|
|
208
|
+
lines.extend(_render_raw(entries, directive))
|
|
209
|
+
|
|
210
|
+
lines.append(f"<!-- /VIEW:{directive.name} -->")
|
|
211
|
+
return lines
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _escape_pipe(value) -> str:
|
|
215
|
+
"""v2.4.0: Escape | as \\| in table cell values."""
|
|
216
|
+
if not isinstance(value, str):
|
|
217
|
+
value = str(value)
|
|
218
|
+
return value.replace('|', '\\|')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _render_table(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
222
|
+
"""v2.4.0: Add `source` column (SIGIL:name) for tables without name column.
|
|
223
|
+
v2.4.0: Escape | in cell values; include ALL attrs not just declared fields.
|
|
224
|
+
"""
|
|
225
|
+
if not entries:
|
|
226
|
+
return ["_(no entries)_", ""]
|
|
227
|
+
|
|
228
|
+
if directive.target == "$0:canonical_sigils":
|
|
229
|
+
lines = [
|
|
230
|
+
"| Sigilo | Nombre | Tipo | Riesgo | Corteza | Descripción |",
|
|
231
|
+
"|---|---|---|:---:|---|---|",
|
|
232
|
+
]
|
|
233
|
+
for e in entries:
|
|
234
|
+
v = e.value
|
|
235
|
+
lines.append(
|
|
236
|
+
f"| `{e.sigil}` | {e.name} | `{v.get('type', '')}` | "
|
|
237
|
+
f"{v.get('risk', '')} | {v.get('cortex', '')} | {v.get('desc', '')} |"
|
|
238
|
+
)
|
|
239
|
+
lines.append("")
|
|
240
|
+
return lines
|
|
241
|
+
|
|
242
|
+
if directive.target == "$0:contracts":
|
|
243
|
+
lines = ["| Sigilo | Campos posicionales |", "|---|---|"]
|
|
244
|
+
for e in entries:
|
|
245
|
+
sigil_name = e.name.replace("contract_", "").upper()
|
|
246
|
+
lines.append(f"| `{sigil_name}` | {_escape_pipe(e.value.get('pos', ''))} |")
|
|
247
|
+
lines.append("")
|
|
248
|
+
return lines
|
|
249
|
+
|
|
250
|
+
if directive.target == "$0:microtokens":
|
|
251
|
+
lines = ["| Token | Expansión |", "|---|---|"]
|
|
252
|
+
for e in entries:
|
|
253
|
+
token = e.name.replace("micro_", "")
|
|
254
|
+
lines.append(f"| `{token}` | {e.value.get('expand', '')} |")
|
|
255
|
+
lines.append("")
|
|
256
|
+
return lines
|
|
257
|
+
|
|
258
|
+
if directive.target == "$0:type_decls":
|
|
259
|
+
lines = ["| Tipo | Regla |", "|---|---|"]
|
|
260
|
+
for e in entries:
|
|
261
|
+
type_name = e.name.replace("type_", "")
|
|
262
|
+
lines.append(f"| `{type_name}` | {e.value.get('rule', '')} |")
|
|
263
|
+
lines.append("")
|
|
264
|
+
return lines
|
|
265
|
+
|
|
266
|
+
if entries and entries[0].entry_type == "attrs-pos":
|
|
267
|
+
# v2.4.0: HDL attrs-pos table — include source column
|
|
268
|
+
lines = ["| Source | Operación | Estado | Requiere | Notas |", "|---|---|---|---|---|"]
|
|
269
|
+
for e in entries:
|
|
270
|
+
v = e.value
|
|
271
|
+
source = f"`{e.sigil}:{e.name}`"
|
|
272
|
+
lines.append(f"| {source} | {v.get('operation', '')} | {v.get('status', '')} | {v.get('requires', '')} | {v.get('notes', '')} |")
|
|
273
|
+
lines.append("")
|
|
274
|
+
return lines
|
|
275
|
+
|
|
276
|
+
# v2.4.0: For tables without explicit name column, add a `Source` column
|
|
277
|
+
# with SIGIL:name so the encoder can recover original names.
|
|
278
|
+
if directive.fields:
|
|
279
|
+
fields = [f.strip() for f in directive.fields.split(",")]
|
|
280
|
+
else:
|
|
281
|
+
if isinstance(entries[0].value, dict):
|
|
282
|
+
fields = list(entries[0].value.keys())
|
|
283
|
+
else:
|
|
284
|
+
fields = ["value"]
|
|
285
|
+
|
|
286
|
+
# v2.4.0: Include ALL attrs from entries, not just declared fields
|
|
287
|
+
all_attrs: list = list(fields)
|
|
288
|
+
for e in entries:
|
|
289
|
+
if isinstance(e.value, dict):
|
|
290
|
+
for k in e.value.keys():
|
|
291
|
+
if k not in all_attrs:
|
|
292
|
+
all_attrs.append(k)
|
|
293
|
+
ordered_fields = all_attrs
|
|
294
|
+
|
|
295
|
+
# Check if any field is "name" or "sigil" — if not, add Source column
|
|
296
|
+
has_name_col = any(f.lower() in ("name", "nombre", "sigil", "sigilo") for f in ordered_fields)
|
|
297
|
+
if has_name_col:
|
|
298
|
+
header_labels = [f.capitalize() for f in ordered_fields]
|
|
299
|
+
lines = [f"| {' | '.join(header_labels)} |"]
|
|
300
|
+
lines.append(f"|{'|'.join(['---'] * len(ordered_fields))}|")
|
|
301
|
+
for e in entries:
|
|
302
|
+
if isinstance(e.value, dict):
|
|
303
|
+
row = [_escape_pipe(e.value.get(f, "")) for f in ordered_fields]
|
|
304
|
+
else:
|
|
305
|
+
row = [_escape_pipe(e.value) if i == 0 else "" for i, f in enumerate(ordered_fields)]
|
|
306
|
+
lines.append(f"| {' | '.join(row)} |")
|
|
307
|
+
else:
|
|
308
|
+
# v2.4.0: Add Source column
|
|
309
|
+
header_labels = ["Source"] + [f.capitalize() for f in ordered_fields]
|
|
310
|
+
col_count = len(ordered_fields) + 1
|
|
311
|
+
lines = [f"| {' | '.join(header_labels)} |"]
|
|
312
|
+
lines.append(f"|{'|'.join(['---'] * col_count)}|")
|
|
313
|
+
for e in entries:
|
|
314
|
+
source = f"`{e.sigil}:{e.name}`"
|
|
315
|
+
if isinstance(e.value, dict):
|
|
316
|
+
row = [source] + [_escape_pipe(e.value.get(f, "")) for f in ordered_fields]
|
|
317
|
+
else:
|
|
318
|
+
row = [source] + [_escape_pipe(e.value) if i == 0 else "" for i, f in enumerate(ordered_fields)]
|
|
319
|
+
lines.append(f"| {' | '.join(row)} |")
|
|
320
|
+
lines.append("")
|
|
321
|
+
return lines
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _render_kv_table(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
325
|
+
"""v2.4.0: For group targets, render each entry with a Source heading.
|
|
326
|
+
v2.4.0: Escape | in values."""
|
|
327
|
+
if not entries:
|
|
328
|
+
return ["_(no entries)_", ""]
|
|
329
|
+
lines: List[str] = []
|
|
330
|
+
for e in entries:
|
|
331
|
+
# v2.4.0: Always add Source heading for reversibility
|
|
332
|
+
lines.append(f"**Source:** `{e.sigil}:{e.name}`")
|
|
333
|
+
lines.append("")
|
|
334
|
+
if not isinstance(e.value, dict):
|
|
335
|
+
lines.append(str(e.value))
|
|
336
|
+
lines.append("")
|
|
337
|
+
continue
|
|
338
|
+
fields = [f.strip() for f in directive.fields.split(",")] if directive.fields else list(e.value.keys())
|
|
339
|
+
# v2.4.0: Include ALL attrs, not just declared fields
|
|
340
|
+
all_keys = list(fields)
|
|
341
|
+
for k in e.value.keys():
|
|
342
|
+
if k not in all_keys:
|
|
343
|
+
all_keys.append(k)
|
|
344
|
+
lines.append("| Campo | Valor |")
|
|
345
|
+
lines.append("|---|---|")
|
|
346
|
+
for f in all_keys:
|
|
347
|
+
lines.append(f"| {f} | {_escape_pipe(e.value.get(f, ''))} |")
|
|
348
|
+
lines.append("")
|
|
349
|
+
return lines
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _render_prose(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
353
|
+
"""v2.4.0: Render prose faithfully — preserve original cuerpo text."""
|
|
354
|
+
lines = []
|
|
355
|
+
for e in entries:
|
|
356
|
+
if e.entry_type == "cuerpo" or isinstance(e.value, str):
|
|
357
|
+
lines.append(str(e.value))
|
|
358
|
+
lines.append("")
|
|
359
|
+
return lines
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _render_quote(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
363
|
+
lines = []
|
|
364
|
+
for e in entries:
|
|
365
|
+
if e.entry_type == "cuerpo" or isinstance(e.value, str):
|
|
366
|
+
lines.append(f"> {e.value}")
|
|
367
|
+
lines.append("")
|
|
368
|
+
return lines
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _render_puml(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
372
|
+
"""v2.4.0: Add Source heading; preserve verbatim content exactly."""
|
|
373
|
+
lines = []
|
|
374
|
+
for e in entries:
|
|
375
|
+
if e.entry_type == "bloque" or isinstance(e.value, str):
|
|
376
|
+
# v2.4.0: Add Source heading
|
|
377
|
+
lines.append(f"**Source:** `{e.sigil}:{e.name}`")
|
|
378
|
+
lines.append("")
|
|
379
|
+
# v2.4.0: Preserve value as-is (including leading/trailing newlines)
|
|
380
|
+
value = str(e.value)
|
|
381
|
+
lines.append("```puml")
|
|
382
|
+
lines.append(value)
|
|
383
|
+
lines.append("```")
|
|
384
|
+
lines.append("")
|
|
385
|
+
return lines
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _render_code(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
389
|
+
lines = []
|
|
390
|
+
for e in entries:
|
|
391
|
+
if e.entry_type == "bloque" or isinstance(e.value, str):
|
|
392
|
+
lines.append("```")
|
|
393
|
+
if directive.preserve == "verbatim":
|
|
394
|
+
lines.append(str(e.value))
|
|
395
|
+
else:
|
|
396
|
+
lines.append(str(e.value).strip())
|
|
397
|
+
lines.append("```")
|
|
398
|
+
lines.append("")
|
|
399
|
+
return lines
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _render_list(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
403
|
+
"""v2.3.1: Render list with proper field extraction, not dict literal."""
|
|
404
|
+
lines = []
|
|
405
|
+
for e in entries:
|
|
406
|
+
if isinstance(e.value, dict):
|
|
407
|
+
desc = (
|
|
408
|
+
e.value.get("desc")
|
|
409
|
+
or e.value.get("rule")
|
|
410
|
+
or e.value.get("content")
|
|
411
|
+
or e.value.get("action")
|
|
412
|
+
or e.value.get("notes")
|
|
413
|
+
)
|
|
414
|
+
if not desc:
|
|
415
|
+
desc = str(e.value)
|
|
416
|
+
survive = e.value.get("survive")
|
|
417
|
+
if survive and survive != desc:
|
|
418
|
+
desc = f"{desc} (survive:{survive})"
|
|
419
|
+
lines.append(f"- `{e.sigil}:{e.name}` — {desc}")
|
|
420
|
+
else:
|
|
421
|
+
lines.append(f"- {e.value}")
|
|
422
|
+
lines.append("")
|
|
423
|
+
return lines
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _render_numbered_list(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
427
|
+
"""v2.4.0: Render numbered list with Source marker for reversibility."""
|
|
428
|
+
lines = []
|
|
429
|
+
for i, e in enumerate(entries, 1):
|
|
430
|
+
if isinstance(e.value, dict):
|
|
431
|
+
text = (
|
|
432
|
+
e.value.get("rule")
|
|
433
|
+
or e.value.get("desc")
|
|
434
|
+
or e.value.get("action")
|
|
435
|
+
or e.value.get("notes")
|
|
436
|
+
or str(e.value)
|
|
437
|
+
)
|
|
438
|
+
survive = e.value.get("survive")
|
|
439
|
+
if survive and survive != text:
|
|
440
|
+
text = f"{text} (survive:{survive})"
|
|
441
|
+
# v2.4.0: Include source for reversibility
|
|
442
|
+
lines.append(f"{i}. `{e.sigil}:{e.name}` — {text}")
|
|
443
|
+
else:
|
|
444
|
+
lines.append(f"{i}. `{e.sigil}:{e.name}` — {e.value}")
|
|
445
|
+
lines.append("")
|
|
446
|
+
return lines
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _render_checklist(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
450
|
+
lines = []
|
|
451
|
+
for e in entries:
|
|
452
|
+
if isinstance(e.value, dict):
|
|
453
|
+
status = e.value.get("status", "")
|
|
454
|
+
checked = "x" if status in ("done", "cur") else " "
|
|
455
|
+
desc = e.value.get("desc", e.value.get("rule", str(e.value)))
|
|
456
|
+
lines.append(f"- [{checked}] `{e.sigil}:{e.name}` — {desc}")
|
|
457
|
+
else:
|
|
458
|
+
lines.append(f"- [ ] {e.value}")
|
|
459
|
+
lines.append("")
|
|
460
|
+
return lines
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _render_raw(entries: List[V2Entry], directive: ViewDirective) -> List[str]:
|
|
464
|
+
lines = []
|
|
465
|
+
for e in entries:
|
|
466
|
+
lines.append(f"### {e.sigil}:{e.name}")
|
|
467
|
+
lines.append("")
|
|
468
|
+
if isinstance(e.value, dict):
|
|
469
|
+
for k, v in e.value.items():
|
|
470
|
+
lines.append(f"- {k}: {v}")
|
|
471
|
+
else:
|
|
472
|
+
lines.append(str(e.value))
|
|
473
|
+
lines.append("")
|
|
474
|
+
return lines
|