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