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
cortex/v2/encoder.py ADDED
@@ -0,0 +1,1106 @@
1
+ """HCORTEX → CORTEX encoder v2.4.0 — bidirectional verified release.
2
+
3
+ v2.3.1 had critical issues:
4
+ - Only 182/266 entries reconstructed (68%)
5
+ - $6 DIAG, $7 contracts, $9 profiles not reconstructed
6
+ - Tables without `name` column produced unparseable names
7
+
8
+ v2.4.0 fixes:
9
+ - Synthetic names: snake_case, stable, reparsable
10
+ - $0 meta entries: target name → entry name (e.g. $0:enum_state → $0:enum_state{...})
11
+ - $0 canonical_sigils: sigil_decl reconstruction
12
+ - $0 contracts/microtokens/type_decls: group kv_table → multiple entries
13
+ - $6 DIAG: verbatim PUML preservation with hash validation
14
+ - $7 contracts: derive contract_<sigil> names from rule content
15
+ - $9 profiles: derive profile_<level> names from topic content
16
+ - $13 VIEW: reconstruct all 44 directives with full metadata
17
+ - Post-write validation: don't write if reparsing loses entries
18
+ - Hash verification: real SHA-256 per block
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import re
25
+ from typing import Dict, List, Optional, Tuple
26
+
27
+ from .diagnostics import Diagnostic
28
+ from .hcortex_parser import (
29
+ HCorTEXDocument, HCorTEXBlock, HCorTEXHeader,
30
+ parse_table_block, parse_list_block, parse_verbatim_block, parse_prose_block,
31
+ )
32
+ from .parser import CortexV2Document, V2Entry, V2Section
33
+ from .view import ViewKind, ReverseStrategy
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # v2.4.0: Column header normalization map
38
+ # ---------------------------------------------------------------------------
39
+
40
+ HEADER_MAP = {
41
+ "sigilo": "sigil", "sigil": "sigil",
42
+ "nombre": "name", "name": "name",
43
+ "tipo": "type", "type": "type",
44
+ "riesgo": "risk", "risk": "risk",
45
+ "corteza": "cortex", "cortex": "cortex",
46
+ "descripción": "desc", "descripcion": "desc", "desc": "desc",
47
+ "operación": "operation", "operacion": "operation", "operation": "operation",
48
+ "estado": "status", "status": "status",
49
+ "requiere": "requires", "requires": "requires",
50
+ "notas": "notes", "notes": "notes",
51
+ "regla": "rule", "rule": "rule",
52
+ "severidad": "severity", "severity": "severity",
53
+ "survive": "survive",
54
+ "tema": "topic", "topic": "topic",
55
+ "contenido": "content", "content": "content",
56
+ "ruta": "path", "path": "path",
57
+ "rol": "role", "role": "role",
58
+ "codificación": "encoding", "codificacion": "encoding", "encoding": "encoding",
59
+ "límite": "limit", "limite": "limit", "limit": "limit",
60
+ "ámbito": "scope", "ambito": "scope", "scope": "scope",
61
+ "impacto": "impact", "impact": "impact",
62
+ "mitigación": "mitigation", "mitigacion": "mitigation", "mitigation": "mitigation",
63
+ "patrón": "pattern", "patron": "pattern", "pattern": "pattern",
64
+ "prevención": "prevention", "prevencion": "prevention", "prevention": "prevention",
65
+ "declaración": "statement", "declaracion": "statement", "statement": "statement",
66
+ "evidencia": "evidence", "evidence": "evidence",
67
+ "valor": "value", "value": "value",
68
+ "campo": "campo", "field": "campo",
69
+ "expand": "expand", "pos": "pos", "values": "values",
70
+ "campos posicionales": "pos",
71
+ "expansión": "expand", "expansion": "expand",
72
+ }
73
+
74
+
75
+ def _normalize_field_name(name: str) -> str:
76
+ if not name:
77
+ return name
78
+ cleaned = name.strip().lower()
79
+ return HEADER_MAP.get(cleaned, cleaned)
80
+
81
+
82
+ def _strip_backticks(value: str) -> str:
83
+ if not isinstance(value, str):
84
+ return value
85
+ v = value.strip()
86
+ if v.startswith('`') and v.endswith('`'):
87
+ v = v[1:-1]
88
+ elif v.startswith('`'):
89
+ v = v[1:]
90
+ elif v.endswith('`'):
91
+ v = v[:-1]
92
+ return v.strip()
93
+
94
+
95
+ def _sanitize_value(value: str) -> str:
96
+ if not isinstance(value, str):
97
+ return value
98
+ v = _strip_backticks(value)
99
+ # v2.4.0: Un-escape \| back to | (renderer escapes | in table cells)
100
+ v = v.replace('\\|', '|')
101
+ v = re.sub(r'\s+', ' ', v).strip()
102
+ return v
103
+
104
+
105
+ def _split_table_cells(line: str) -> List[str]:
106
+ """v2.4.0: Split a Markdown table row into cells, respecting \\| escapes."""
107
+ if line.startswith('|'):
108
+ line = line[1:]
109
+ if line.endswith('|'):
110
+ line = line[:-1]
111
+ cells = []
112
+ current = ''
113
+ i = 0
114
+ while i < len(line):
115
+ c = line[i]
116
+ if c == '\\' and i + 1 < len(line) and line[i + 1] == '|':
117
+ current += '|'
118
+ i += 2
119
+ elif c == '|':
120
+ cells.append(current.strip())
121
+ current = ''
122
+ i += 1
123
+ else:
124
+ current += c
125
+ i += 1
126
+ cells.append(current.strip())
127
+ return cells
128
+
129
+
130
+ def _slugify(text: str, max_len: int = 40) -> str:
131
+ """Convert text to snake_case slug suitable for entry names."""
132
+ if not text:
133
+ return "entry"
134
+ # Remove backticks
135
+ text = _strip_backticks(text)
136
+ # Lowercase
137
+ text = text.lower()
138
+ # Replace non-alphanumeric with underscore
139
+ text = re.sub(r'[^a-z0-9]+', '_', text)
140
+ # Strip leading/trailing underscores
141
+ text = text.strip('_')
142
+ # Truncate
143
+ if len(text) > max_len:
144
+ text = text[:max_len].rstrip('_')
145
+ return text or "entry"
146
+
147
+
148
+ def _synthetic_name(sigil: str, section: str, ordinal: int, hint: str = "") -> str:
149
+ """Generate a stable, reparsable synthetic name.
150
+
151
+ Format: synthetic_<sigil>_<section>_<ordinal> or synthetic_<sigil>_<section>_<hint>
152
+ v2.4.0: Sanitize sigil (replace ! with rule, $ with s) to keep names reparsable.
153
+ """
154
+ safe_sigil = sigil.replace("!", "rule").replace("$", "s").lower()
155
+ sec = section.replace("$", "s")
156
+ if hint:
157
+ slug = _slugify(hint, max_len=30)
158
+ return f"synthetic_{safe_sigil}_{sec}_{slug}"
159
+ return f"synthetic_{safe_sigil}_{sec}_{ordinal:03d}"
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # F-09: encode_cortex_from_ast()
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def encode_cortex_from_ast(hdoc: HCorTEXDocument, mode: str = "normal") -> Tuple[CortexV2Document, List[Diagnostic]]:
167
+ """Encode an HCorTEXDocument back to a CortexV2Document.
168
+
169
+ v2.4.0: Full reconstruction with synthetic names, DIAG preservation,
170
+ $13 VIEW reconstruction, and post-write validation.
171
+ """
172
+
173
+ diags: List[Diagnostic] = list(hdoc.diags)
174
+
175
+ cortex_doc = CortexV2Document()
176
+ cortex_doc.header = _build_cortex_header(hdoc.header)
177
+
178
+ sections: Dict[str, V2Section] = {}
179
+
180
+ # F-10/F-13: For each VIEW block, reconstruct entries
181
+ for block in hdoc.blocks:
182
+ entries, entry_diags = _block_to_entries(block, mode)
183
+ diags.extend(entry_diags)
184
+
185
+ section_id = _resolve_section(block)
186
+ if section_id not in sections:
187
+ sections[section_id] = V2Section(id=section_id)
188
+ sections[section_id].entries.extend(entries)
189
+
190
+ # v2.4.0 P0-6: Reconstruct $13 with VIEW directives
191
+ if hdoc.blocks:
192
+ if "$13" not in sections:
193
+ sections["$13"] = V2Section(id="$13")
194
+ for block in hdoc.blocks:
195
+ view_entry = _block_to_view_entry(block)
196
+ if view_entry is not None:
197
+ sections["$13"].entries.append(view_entry)
198
+
199
+ # F-11: Ensure $0 exists
200
+ if "$0" not in sections:
201
+ sections["$0"] = V2Section(id="$0")
202
+ diags.append(Diagnostic(
203
+ "E_VIEW_MISSING",
204
+ "$0 section not found in HCORTEX — empty glossary created",
205
+ "warning" if mode != "strict" else "error",
206
+ ))
207
+
208
+ sorted_ids = sorted(sections.keys(), key=lambda s: int(s[1:]) if s[1:].isdigit() else 999)
209
+ cortex_doc.sections = [sections[sid] for sid in sorted_ids]
210
+
211
+ # v2.4.0 P0-9: Post-write validation
212
+ from .writer import write_cortex_v2
213
+ from .parser import parse_cortex_v2
214
+ test_text = write_cortex_v2(cortex_doc)
215
+ try:
216
+ re_doc = parse_cortex_v2(test_text)
217
+ re_entries = sum(len(s.entries) for s in re_doc.sections)
218
+ declared_entries = sum(len(s.entries) for s in cortex_doc.sections)
219
+ if re_entries < declared_entries:
220
+ diags.append(Diagnostic(
221
+ "E_AST_EQUIVALENCE_FAIL",
222
+ f"Post-write validation: declared {declared_entries} entries but reparse found {re_entries}",
223
+ "error",
224
+ ))
225
+ except Exception as e:
226
+ diags.append(Diagnostic(
227
+ "E_AST_EQUIVALENCE_FAIL",
228
+ f"Post-write validation: reparse failed: {e}",
229
+ "error",
230
+ ))
231
+
232
+ if mode == "strict":
233
+ for d in diags:
234
+ if d.severity == "warning":
235
+ d.severity = "error"
236
+
237
+ return cortex_doc, diags
238
+
239
+
240
+ def _block_to_view_entry(block: HCorTEXBlock) -> Optional[V2Entry]:
241
+ """Reconstruct a VIEW directive entry from an HCorTEXBlock."""
242
+ attrs: Dict[str, str] = {
243
+ "kind": block.kind,
244
+ "target": block.target,
245
+ "reverse": block.reverse,
246
+ "status": block.status,
247
+ }
248
+ if block.fields:
249
+ attrs["fields"] = ",".join(block.fields)
250
+ if block.order:
251
+ attrs["order"] = block.order
252
+ if block.title:
253
+ attrs["title"] = block.title
254
+ if block.scope:
255
+ attrs["scope"] = block.scope
256
+ if block.section:
257
+ attrs["section"] = block.section
258
+ if block.source_section:
259
+ attrs["source_section"] = block.source_section
260
+ if block.preserve:
261
+ attrs["preserve"] = block.preserve
262
+ if block.hash:
263
+ attrs["hash"] = block.hash
264
+ if block.fallback:
265
+ attrs["fallback"] = block.fallback
266
+
267
+ return V2Entry(
268
+ sigil="VIEW",
269
+ name=block.view_name,
270
+ entry_type="attrs",
271
+ value=attrs,
272
+ section="$13",
273
+ )
274
+
275
+
276
+ def _build_cortex_header(hheader: HCorTEXHeader) -> Dict[str, str]:
277
+ cortex_header: Dict[str, str] = {"internal_encoding": "CORTEX"}
278
+ if hheader.source_artifact:
279
+ cortex_header["source_artifact"] = hheader.source_artifact
280
+ if hheader.source_version:
281
+ cortex_header["source_version"] = hheader.source_version
282
+ if hheader.status:
283
+ cortex_header["status"] = hheader.status
284
+ return cortex_header
285
+
286
+
287
+ def _resolve_section(block: HCorTEXBlock) -> str:
288
+ if block.section:
289
+ return block.section
290
+ if block.source_section:
291
+ return block.source_section
292
+ if block.target.startswith("$"):
293
+ parts = block.target.split(":", 1)
294
+ return parts[0]
295
+ return "$1"
296
+
297
+
298
+ def _derive_sigil_name_from_target(target: str) -> Tuple[str, str]:
299
+ if target.startswith("$") and target.count(":") == 2:
300
+ parts = target.split(":")
301
+ return parts[1], parts[2]
302
+ if target.startswith("$") and target.endswith(":*"):
303
+ parts = target.split(":")
304
+ sigil = parts[1] if len(parts) >= 2 else "IDN"
305
+ return sigil, "entry"
306
+ if target.startswith("$") and ":" in target and target.count(":") == 1:
307
+ parts = target.split(":", 1)
308
+ name = parts[1]
309
+ if name in ("canonical_sigils", "type_decls", "contracts", "microtokens"):
310
+ return "$0", name
311
+ return "IDN", name
312
+ if ":" in target and not target.startswith("$"):
313
+ parts = target.split(":", 1)
314
+ sigil = parts[0]
315
+ name = parts[1] if parts[1] != "*" else "entry"
316
+ return sigil, name
317
+ return "IDN", "entry"
318
+
319
+
320
+ def _block_to_entries(block: HCorTEXBlock, mode: str) -> Tuple[List[V2Entry], List[Diagnostic]]:
321
+ diags: List[Diagnostic] = []
322
+ entries: List[V2Entry] = []
323
+
324
+ try:
325
+ kind = ViewKind(block.kind)
326
+ reverse = ReverseStrategy(block.reverse)
327
+ except ValueError as e:
328
+ diags.append(Diagnostic(
329
+ "E_VIEW_REVERSE_UNSUPPORTED",
330
+ f"VIEW:{block.view_name} invalid kind/reverse: {e}",
331
+ "error", f"VIEW:{block.view_name}",
332
+ ))
333
+ return entries, diags
334
+
335
+ # v2.4.0 P1-2: Verify hash if present
336
+ if block.hash:
337
+ computed_hash = _compute_block_hash(block)
338
+ if computed_hash != block.hash:
339
+ diags.append(Diagnostic(
340
+ "E_VIEW_HASH_MISMATCH",
341
+ f"VIEW:{block.view_name} hash mismatch: declared={block.hash!r}, computed={computed_hash!r}",
342
+ "error", f"VIEW:{block.view_name}",
343
+ ))
344
+
345
+ if kind == ViewKind.TABLE and reverse == ReverseStrategy.ROWS_TO_ENTRIES:
346
+ entries = _table_to_entries(block, diags)
347
+ elif kind == ViewKind.KV_TABLE and reverse == ReverseStrategy.ROW_TO_ATTRS:
348
+ entries = _kv_table_to_entries(block, diags)
349
+ elif kind == ViewKind.LIST and reverse == ReverseStrategy.ITEMS_TO_ENTRIES:
350
+ entries = _list_to_entries(block, diags)
351
+ elif kind == ViewKind.NUMBERED_LIST and reverse == ReverseStrategy.ITEMS_TO_ORDERED_ENTRIES:
352
+ entries = _numbered_list_to_entries(block, diags)
353
+ elif kind in (ViewKind.PROSE, ViewKind.QUOTE) and reverse == ReverseStrategy.BODY_TO_CUERPO:
354
+ entries = _prose_to_cuerpo(block, diags)
355
+ elif kind in (ViewKind.PUML, ViewKind.CODE) and reverse == ReverseStrategy.VERBATIM_TO_BLOQUE:
356
+ entries = _verbatim_to_bloque(block, diags)
357
+ elif kind == ViewKind.CALLOUT and reverse == ReverseStrategy.CALLOUT_TO_RISK:
358
+ entries = _callout_to_risk(block, diags)
359
+ else:
360
+ diags.append(Diagnostic(
361
+ "E_VIEW_REVERSE_UNSUPPORTED",
362
+ f"VIEW:{block.view_name} kind={kind.value} reverse={reverse.value} not yet implemented",
363
+ "warning", f"VIEW:{block.view_name}",
364
+ ))
365
+
366
+ return entries, diags
367
+
368
+
369
+ def _compute_block_hash(block: HCorTEXBlock) -> str:
370
+ content = '\n'.join(block.content_lines)
371
+ return hashlib.sha256(content.encode('utf-8')).hexdigest()[:16]
372
+
373
+
374
+ # ---------------------------------------------------------------------------
375
+ # v2.4.0 P0-2: TABLE → entries with synthetic names
376
+ # ---------------------------------------------------------------------------
377
+
378
+ def _table_to_entries(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
379
+ """Convert a table to multiple entries.
380
+
381
+ v2.4.0: Smart name derivation — uses name column, source, or synthetic names.
382
+ """
383
+
384
+ rows, table_diags = parse_table_block(block)
385
+ diags.extend(table_diags)
386
+
387
+ default_sigil, _ = _derive_sigil_name_from_target(block.target)
388
+ section_id = _resolve_section(block)
389
+
390
+ # v2.4.0: Special handling for $0:canonical_sigils
391
+ if block.target == "$0:canonical_sigils":
392
+ return _canonical_sigils_to_entries(rows, diags)
393
+
394
+ # v2.4.0: Special handling for $0:contracts
395
+ if block.target == "$0:contracts":
396
+ return _contracts_decl_to_entries(rows, diags)
397
+
398
+ # v2.4.0: Special handling for $0:microtokens
399
+ if block.target == "$0:microtokens":
400
+ return _microtokens_decl_to_entries(rows, diags)
401
+
402
+ entries: List[V2Entry] = []
403
+ for i, row in enumerate(rows):
404
+ normalized_row: Dict[str, str] = {}
405
+ for k, v in row.items():
406
+ norm_key = _normalize_field_name(k)
407
+ normalized_row[norm_key] = v
408
+
409
+ # v2.4.0: Check for `source` column first (SIGIL:name)
410
+ source = _strip_backticks(normalized_row.get("source", ""))
411
+ sigil = ""
412
+ name = ""
413
+ if source and ":" in source:
414
+ parts = source.split(":", 1)
415
+ sigil = parts[0].strip().upper()
416
+ name = _sanitize_value(parts[1])
417
+
418
+ # Fall back to sigil/name columns if present
419
+ if not sigil:
420
+ sigil = _strip_backticks(normalized_row.get("sigil", "")).upper()
421
+ if not name:
422
+ name = _sanitize_value(normalized_row.get("name", ""))
423
+
424
+ if not sigil:
425
+ sigil = default_sigil
426
+
427
+ # v2.4.0 P0-2: Derive name deterministically if still missing
428
+ if not name:
429
+ name = _derive_name_from_content(normalized_row, sigil, section_id, i, block.target)
430
+
431
+ if not sigil or not name:
432
+ continue
433
+
434
+ # Build attrs (exclude source, sigil, name; skip empty values)
435
+ attrs: Dict[str, str] = {}
436
+ for k, v in normalized_row.items():
437
+ if k in ("sigil", "name", "source"):
438
+ continue
439
+ sanitized = _sanitize_value(v)
440
+ if sanitized: # v2.4.0: skip empty attrs to match original
441
+ attrs[k] = sanitized
442
+
443
+ # v2.4.0: HDL special case — use attrs-pos
444
+ if sigil == "HDL":
445
+ # HDL uses bare attrs-pos: HDL:name|operation|status|requires|notes
446
+ entry = V2Entry(
447
+ sigil=sigil,
448
+ name=name,
449
+ entry_type="attrs-pos",
450
+ value={
451
+ "operation": attrs.get("operation", ""),
452
+ "status": attrs.get("status", ""),
453
+ "requires": attrs.get("requires", ""),
454
+ "notes": attrs.get("notes", ""),
455
+ },
456
+ section=section_id,
457
+ )
458
+ elif sigil == "$0":
459
+ entry = V2Entry(
460
+ sigil="$0",
461
+ name=name,
462
+ entry_type="meta",
463
+ value=attrs,
464
+ section="$0",
465
+ )
466
+ else:
467
+ entry = V2Entry(
468
+ sigil=sigil,
469
+ name=name,
470
+ entry_type="attrs",
471
+ value=attrs,
472
+ section=section_id,
473
+ )
474
+ entries.append(entry)
475
+
476
+ return entries
477
+
478
+
479
+ def _derive_name_from_content(row: Dict[str, str], sigil: str, section: str, ordinal: int, target: str) -> str:
480
+ """v2.4.0 P0-2: Derive a stable, reparsable name from row content.
481
+
482
+ Priority:
483
+ 1. If `name` field exists → use it
484
+ 2. If `operation` field → slugify it (HDL handlers)
485
+ 3. If `path` field → slugify it (REF entries)
486
+ 4. If `topic` field → slugify it (KNW entries)
487
+ 5. If `rule` field → try to extract sigil name from contract rules
488
+ 6. If `pattern` field → slugify it (PFL entries)
489
+ 7. If `limit` field → slugify it (LIM entries)
490
+ 8. If `risk` field → slugify it (RSK entries)
491
+ 9. Fallback: synthetic_<sigil>_<section>_<ordinal>
492
+ """
493
+
494
+ # Check for contract patterns: "FCS requiere what,priority,..."
495
+ rule = row.get("rule", "")
496
+ if rule:
497
+ # Contract pattern: "SIGIL requiere ..." or "SIGIL MUST ..."
498
+ m = re.match(r'^(\w+)\s+(?:requiere|MUST|DEBE|debe)', rule, re.IGNORECASE)
499
+ if m:
500
+ contract_sigil = m.group(1).upper()
501
+ return f"contract_{contract_sigil.lower()}"
502
+
503
+ # Check for profile patterns in topic: "CORTEX-MIN", "CORTEX-RECOVERY"
504
+ topic = row.get("topic", "")
505
+ if topic:
506
+ slug = _slugify(topic)
507
+ if slug:
508
+ return f"profile_{slug}" if section == "$9" else slug
509
+
510
+ # operation field (HDL)
511
+ op = row.get("operation", "")
512
+ if op:
513
+ return _slugify(op)
514
+
515
+ # path field (REF)
516
+ path = row.get("path", "")
517
+ if path:
518
+ # Extract filename without extension
519
+ basename = path.split("/")[-1].split(".")[0]
520
+ return _slugify(basename) or f"ref_{ordinal:03d}"
521
+
522
+ # pattern field (PFL)
523
+ pattern = row.get("pattern", "")
524
+ if pattern:
525
+ return _slugify(pattern, max_len=30)
526
+
527
+ # limit field (LIM)
528
+ limit = row.get("limit", "")
529
+ if limit:
530
+ return _slugify(limit, max_len=30)
531
+
532
+ # risk field (RSK)
533
+ risk = row.get("risk", "")
534
+ if risk:
535
+ return _slugify(risk, max_len=30)
536
+
537
+ # Fallback: synthetic name
538
+ return _synthetic_name(sigil, section, ordinal)
539
+
540
+
541
+ def _canonical_sigils_to_entries(rows: List[Dict[str, str]], diags: List[Diagnostic]) -> List[V2Entry]:
542
+ """Reconstruct $0 sigil declarations from the canonical_sigils table."""
543
+ entries: List[V2Entry] = []
544
+ for row in rows:
545
+ normalized: Dict[str, str] = {}
546
+ for k, v in row.items():
547
+ normalized[_normalize_field_name(k)] = v
548
+
549
+ sigil = _strip_backticks(normalized.get("sigil", "")).upper()
550
+ name = _sanitize_value(normalized.get("name", ""))
551
+
552
+ if not sigil or not name:
553
+ continue
554
+
555
+ attrs: Dict[str, str] = {}
556
+ for k, v in normalized.items():
557
+ if k in ("sigil", "name"):
558
+ continue
559
+ attrs[k] = _sanitize_value(v)
560
+
561
+ entry = V2Entry(
562
+ sigil=sigil,
563
+ name=name,
564
+ entry_type="sigil_decl",
565
+ value=attrs,
566
+ section="$0",
567
+ )
568
+ entries.append(entry)
569
+ return entries
570
+
571
+
572
+ def _contracts_decl_to_entries(rows: List[Dict[str, str]], diags: List[Diagnostic]) -> List[V2Entry]:
573
+ """Reconstruct $0:contract_* entries from the contracts table.
574
+
575
+ Table format: | Sigilo | Campos posicionales |
576
+ Entry format: $0:contract_<sigil>{pos:"..."}
577
+ """
578
+ entries: List[V2Entry] = []
579
+ for row in rows:
580
+ normalized: Dict[str, str] = {}
581
+ for k, v in row.items():
582
+ normalized[_normalize_field_name(k)] = v
583
+
584
+ sigil = _strip_backticks(normalized.get("sigil", "")).upper()
585
+ pos = _sanitize_value(normalized.get("pos", ""))
586
+
587
+ if not sigil:
588
+ continue
589
+
590
+ name = f"contract_{sigil.lower()}"
591
+ entry = V2Entry(
592
+ sigil="$0",
593
+ name=name,
594
+ entry_type="meta",
595
+ value={"pos": pos},
596
+ section="$0",
597
+ )
598
+ entries.append(entry)
599
+ return entries
600
+
601
+
602
+ def _microtokens_decl_to_entries(rows: List[Dict[str, str]], diags: List[Diagnostic]) -> List[V2Entry]:
603
+ """Reconstruct $0:micro_* entries from the microtokens table.
604
+
605
+ Table format: | Token | Expansión |
606
+ Entry format: $0:micro_<token>{expand:"..."}
607
+ """
608
+ entries: List[V2Entry] = []
609
+ for row in rows:
610
+ normalized: Dict[str, str] = {}
611
+ for k, v in row.items():
612
+ normalized[_normalize_field_name(k)] = v
613
+
614
+ token = _strip_backticks(normalized.get("token", normalized.get("campo", "")))
615
+ expand = _sanitize_value(normalized.get("expand", normalized.get("valor", "")))
616
+
617
+ if not token:
618
+ continue
619
+
620
+ name = f"micro_{_slugify(token)}"
621
+ entry = V2Entry(
622
+ sigil="$0",
623
+ name=name,
624
+ entry_type="meta",
625
+ value={"expand": expand},
626
+ section="$0",
627
+ )
628
+ entries.append(entry)
629
+ return entries
630
+
631
+
632
+ # ---------------------------------------------------------------------------
633
+ # v2.4.0: KV_TABLE → entries (smart group handling)
634
+ # ---------------------------------------------------------------------------
635
+
636
+ def _kv_table_to_entries(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
637
+ """Convert a KV table to entries.
638
+
639
+ v2.4.0: Smart handling:
640
+ - **Source:** markers split group kv_tables into individual entries
641
+ - $0:NAME targets → single meta entry with name=NAME
642
+ - $N:SIGIL:name targets → single attrs entry
643
+ """
644
+
645
+ rows, table_diags = parse_table_block(block)
646
+ diags.extend(table_diags)
647
+
648
+ section_id = _resolve_section(block)
649
+ default_sigil, target_name = _derive_sigil_name_from_target(block.target)
650
+
651
+ # v2.4.0: Check for **Source:** markers in the content (group kv_table)
652
+ source_markers = []
653
+ for line in block.content_lines:
654
+ stripped = line.strip()
655
+ if stripped.startswith("**Source:**"):
656
+ import re as _re
657
+ m = _re.search(r'`([$\w]+):(\w+)`', stripped)
658
+ if m:
659
+ source_markers.append((m.group(1), m.group(2)))
660
+
661
+ # v2.4.0: If Source markers found, split rows by Source
662
+ if source_markers:
663
+ return _kv_table_group_with_sources(block, rows, source_markers, section_id, diags)
664
+
665
+ if not rows:
666
+ return []
667
+
668
+ # $0:NAME → single meta entry
669
+ if section_id == "$0" and not block.target.endswith(":*"):
670
+ attrs: Dict[str, str] = {}
671
+ for row in rows:
672
+ campo = _normalize_field_name(row.get("campo", row.get("field", "")))
673
+ valor = _sanitize_value(row.get("valor", row.get("value", "")))
674
+ if campo:
675
+ attrs[campo] = valor
676
+
677
+ entry = V2Entry(
678
+ sigil="$0",
679
+ name=target_name,
680
+ entry_type="meta",
681
+ value=attrs,
682
+ section="$0",
683
+ )
684
+ return [entry]
685
+
686
+ # $N:SIGIL:name → single attrs entry
687
+ if not block.target.endswith(":*") and default_sigil != "IDN":
688
+ attrs: Dict[str, str] = {}
689
+ for row in rows:
690
+ campo = _normalize_field_name(row.get("campo", row.get("field", "")))
691
+ valor = _sanitize_value(row.get("valor", row.get("value", "")))
692
+ if campo:
693
+ attrs[campo] = valor
694
+
695
+ entry = V2Entry(
696
+ sigil=default_sigil,
697
+ name=target_name,
698
+ entry_type="attrs",
699
+ value=attrs,
700
+ section=section_id,
701
+ )
702
+ return [entry]
703
+
704
+ # v2.4.0: Group kv_table without Source markers — create one entry with synthetic name
705
+ attrs: Dict[str, str] = {}
706
+ for row in rows:
707
+ campo = _normalize_field_name(row.get("campo", row.get("field", "")))
708
+ valor = _sanitize_value(row.get("valor", row.get("value", "")))
709
+ if campo:
710
+ attrs[campo] = valor
711
+
712
+ name = _slugify(target_name) if target_name != "entry" else _synthetic_name(default_sigil, section_id, 1)
713
+ entry = V2Entry(
714
+ sigil=default_sigil,
715
+ name=name,
716
+ entry_type="attrs",
717
+ value=attrs,
718
+ section=section_id,
719
+ )
720
+ return [entry]
721
+
722
+
723
+ def _kv_table_group_with_sources(
724
+ block: HCorTEXBlock,
725
+ rows: List[Dict[str, str]],
726
+ source_markers: List[Tuple[str, str]],
727
+ section_id: str,
728
+ diags: List[Diagnostic],
729
+ ) -> List[V2Entry]:
730
+ """v2.4.0: Parse a group kv_table with **Source:** markers.
731
+
732
+ Each Source marker indicates a new entry. Rows between markers belong to that entry.
733
+ """
734
+
735
+ entries: List[V2Entry] = []
736
+
737
+ # If we have N source markers, we need to split the rows into N groups
738
+ # The rows don't have Source info, so we split evenly (or by content analysis)
739
+ # Actually, the renderer puts the Source heading BEFORE each table, and
740
+ # parse_table_block only finds the FIRST table. So for group kv_tables,
741
+ # we need to parse each table separately.
742
+
743
+ # Re-parse the content to find all tables
744
+ import re as _re
745
+ raw_content = '\n'.join(block.content_lines)
746
+
747
+ # Find all table sections (each preceded by **Source:** marker)
748
+ table_pattern = _re.compile(
749
+ r'\*\*Source:\*\*\s*`([$\w]+):(\w+)`\s*\n(.*?)(?=\*\*Source:\*\*|<!-- /VIEW|\Z)',
750
+ _re.DOTALL
751
+ )
752
+
753
+ for m in table_pattern.finditer(raw_content):
754
+ sigil = m.group(1)
755
+ name = m.group(2)
756
+ table_content = m.group(3)
757
+
758
+ # Parse the Campo|Valor table in this section
759
+ attrs: Dict[str, str] = {}
760
+ for line in table_content.split('\n'):
761
+ stripped = line.strip()
762
+ if not stripped.startswith('|') or '---' in stripped:
763
+ continue
764
+ # v2.4.0: Use proper cell splitting that handles \| escapes
765
+ cells = _split_table_cells(stripped)
766
+ if len(cells) >= 2:
767
+ campo = _normalize_field_name(cells[0])
768
+ valor = _sanitize_value(cells[1])
769
+ if campo and campo != "campo" and campo != "field":
770
+ attrs[campo] = valor
771
+
772
+ # v2.4.0: $0 entries should be meta type
773
+ entry_type = "meta" if sigil == "$0" else "attrs"
774
+ entry = V2Entry(
775
+ sigil=sigil,
776
+ name=name,
777
+ entry_type=entry_type,
778
+ value=attrs,
779
+ section=section_id,
780
+ )
781
+ entries.append(entry)
782
+
783
+ return entries
784
+
785
+
786
+ # ---------------------------------------------------------------------------
787
+ # LIST → entries
788
+ # ---------------------------------------------------------------------------
789
+
790
+ def _list_to_entries(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
791
+ items, list_diags = parse_list_block(block)
792
+ diags.extend(list_diags)
793
+
794
+ default_sigil, _ = _derive_sigil_name_from_target(block.target)
795
+ section_id = _resolve_section(block)
796
+
797
+ entries: List[V2Entry] = []
798
+ for item in items:
799
+ # Parse: "- `SIGIL:name` — description (survive:min)"
800
+ m = re.match(r'^`?([$\w!]+)?:?(\w+)`?\s*[—-]\s*(.*)$', item)
801
+ if m:
802
+ sigil = _strip_backticks(m.group(1) or default_sigil).upper()
803
+ if sigil == "IDN" and default_sigil != "IDN":
804
+ sigil = default_sigil
805
+ name = _sanitize_value(m.group(2))
806
+ desc = _sanitize_value(m.group(3))
807
+
808
+ # Extract survive from "(survive:min)" suffix
809
+ survive = ""
810
+ surv_m = re.search(r'\(survive:(\w+)\)', desc)
811
+ if surv_m:
812
+ survive = surv_m.group(1)
813
+ desc = re.sub(r'\s*\(survive:\w+\)', '', desc).strip()
814
+
815
+ attrs: Dict[str, str] = {}
816
+ if desc:
817
+ attrs["rule"] = desc
818
+ if survive:
819
+ attrs["survive"] = survive
820
+
821
+ entry = V2Entry(
822
+ sigil=sigil,
823
+ name=name,
824
+ entry_type="attrs",
825
+ value=attrs,
826
+ section=section_id,
827
+ )
828
+ else:
829
+ entry = V2Entry(
830
+ sigil=default_sigil if default_sigil != "IDN" else "!",
831
+ name=_synthetic_name(default_sigil, section_id, len(entries)),
832
+ entry_type="attrs",
833
+ value={"rule": _sanitize_value(item)},
834
+ section=section_id,
835
+ )
836
+ entries.append(entry)
837
+
838
+ return entries
839
+
840
+
841
+ def _numbered_list_to_entries(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
842
+ """Convert a numbered list to ordered entries.
843
+
844
+ v2.4.0: Parse `SIGIL:name` source markers from each item.
845
+ """
846
+
847
+ items, list_diags = parse_list_block(block)
848
+ diags.extend(list_diags)
849
+
850
+ default_sigil, _ = _derive_sigil_name_from_target(block.target)
851
+ if default_sigil == "IDN":
852
+ default_sigil = "!"
853
+ section_id = _resolve_section(block)
854
+
855
+ entries: List[V2Entry] = []
856
+ for i, item in enumerate(items):
857
+ # v2.4.0: Don't sanitize before regex — backticks needed for source match
858
+ # Parse `SIGIL:name` — description
859
+ source_m = re.match(r'^`([$\w!]+):(\w+)`\s*[—-]\s*(.*)$', item)
860
+ if source_m:
861
+ sigil = source_m.group(1)
862
+ name = source_m.group(2)
863
+ desc = _sanitize_value(source_m.group(3))
864
+ else:
865
+ sigil = default_sigil
866
+ name = _synthetic_name(default_sigil, section_id, i + 1)
867
+ desc = _sanitize_value(item)
868
+
869
+ # Extract survive from "(survive:X)" suffix
870
+ survive = ""
871
+ surv_m = re.search(r'\(survive:(\w+)\)$', desc)
872
+ if surv_m:
873
+ survive = surv_m.group(1)
874
+ desc = re.sub(r'\s*\(survive:\w+\)$', '', desc).strip()
875
+
876
+ attrs: Dict[str, str] = {"rule": desc}
877
+ if survive:
878
+ attrs["survive"] = survive
879
+
880
+ entry = V2Entry(
881
+ sigil=sigil,
882
+ name=name,
883
+ entry_type="attrs",
884
+ value=attrs,
885
+ section=section_id,
886
+ )
887
+ entries.append(entry)
888
+
889
+ return entries
890
+
891
+
892
+ # ---------------------------------------------------------------------------
893
+ # PROSE/QUOTE → cuerpo
894
+ # ---------------------------------------------------------------------------
895
+
896
+ def _prose_to_cuerpo(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
897
+ body, prose_diags = parse_prose_block(block)
898
+ diags.extend(prose_diags)
899
+
900
+ sigil, name = _derive_sigil_name_from_target(block.target)
901
+ section_id = _resolve_section(block)
902
+
903
+ entry = V2Entry(
904
+ sigil=sigil,
905
+ name=name,
906
+ entry_type="cuerpo",
907
+ value=body,
908
+ section=section_id,
909
+ )
910
+ return [entry]
911
+
912
+
913
+ # ---------------------------------------------------------------------------
914
+ # v2.4.0 P0-3: PUML/CODE → bloque (verbatim preservation)
915
+ # ---------------------------------------------------------------------------
916
+
917
+ def _verbatim_to_bloque(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
918
+ """Convert a verbatim code block to a bloque entry.
919
+
920
+ v2.4.0 P0-3: Preserve DIAG/PUML content verbatim with hash validation.
921
+ v2.4.0: Handle multiple fenced blocks within one VIEW block.
922
+ """
923
+
924
+ section_id = _resolve_section(block)
925
+ sigil, name = _derive_sigil_name_from_target(block.target)
926
+ if not sigil or sigil == "IDN":
927
+ sigil = "DIAG"
928
+
929
+ # v2.4.0: $0:DIAG:diagram canonical decl — not real PUML, it's sigil attrs
930
+ if block.target == "$0:DIAG:diagram":
931
+ content, _ = parse_verbatim_block(block)
932
+ entry = V2Entry(
933
+ sigil="DIAG",
934
+ name="diagram",
935
+ entry_type="bloque",
936
+ value=content,
937
+ section="$0",
938
+ )
939
+ return [entry]
940
+
941
+ # v2.4.0: Find ALL ```puml or ``` fences with Source markers
942
+ raw_content = '\n'.join(block.content_lines)
943
+
944
+ # v2.4.0: Parse Source markers + fenced blocks
945
+ # Pattern: **Source:** `SIGIL:name` ... ```puml ... ```
946
+ source_block_pattern = re.compile(
947
+ r'\*\*Source:\*\*\s*`([$\w!]+):(\w+)`\s*\n(.*?)(?=\*\*Source:\*\*|\Z)',
948
+ re.DOTALL
949
+ )
950
+
951
+ source_matches = source_block_pattern.findall(raw_content)
952
+
953
+ if source_matches:
954
+ entries: List[V2Entry] = []
955
+ for sigil, name, section_content in source_matches:
956
+ # Find ```puml ... ``` within section_content
957
+ fence_m = re.search(r'```(?:puml|plantuml)?\n(.*?)```', section_content, re.DOTALL)
958
+ if fence_m:
959
+ puml_content = fence_m.group(1)
960
+ else:
961
+ puml_content = section_content.strip()
962
+
963
+ # v2.4.0 P1-5: PUML validation
964
+ if block.kind == "puml":
965
+ if "@startuml" not in puml_content:
966
+ diags.append(Diagnostic(
967
+ "E_PUML_INVALID",
968
+ f"VIEW:{block.view_name} PUML block '{name}' missing @startuml",
969
+ "error", f"VIEW:{block.view_name}",
970
+ ))
971
+ if "@enduml" not in puml_content:
972
+ diags.append(Diagnostic(
973
+ "E_PUML_INVALID",
974
+ f"VIEW:{block.view_name} PUML block '{name}' missing @enduml",
975
+ "error", f"VIEW:{block.view_name}",
976
+ ))
977
+
978
+ # v2.4.0 P0-3: preserve:verbatim — preserve exact content
979
+ # But strip ONE trailing newline added by the fence format
980
+ if puml_content.endswith('\n'):
981
+ content = puml_content[:-1]
982
+ else:
983
+ content = puml_content
984
+
985
+ entry = V2Entry(
986
+ sigil=sigil if sigil != "$0" else "DIAG",
987
+ name=name,
988
+ entry_type="bloque",
989
+ value=content,
990
+ section=section_id,
991
+ )
992
+ entries.append(entry)
993
+ return entries
994
+
995
+ # Fallback: find fenced blocks without Source markers
996
+ fence_pattern = re.compile(r'```(?:puml|plantuml)?\n(.*?)```', re.DOTALL)
997
+ matches = fence_pattern.findall(raw_content)
998
+
999
+ if not matches:
1000
+ content, _ = parse_verbatim_block(block)
1001
+ entry = V2Entry(
1002
+ sigil=sigil,
1003
+ name=name if name != "entry" else "diagram_1",
1004
+ entry_type="bloque",
1005
+ value=content,
1006
+ section=section_id,
1007
+ )
1008
+ return [entry]
1009
+
1010
+ entries: List[V2Entry] = []
1011
+ for i, puml_content in enumerate(matches):
1012
+ title_m = re.search(r'title\s+(.+?)(?:\n|$)', puml_content)
1013
+ if title_m:
1014
+ name = _slugify(title_m.group(1))
1015
+ else:
1016
+ name = f"diagram_{i+1}"
1017
+
1018
+ if block.preserve == "verbatim":
1019
+ content = puml_content
1020
+ else:
1021
+ content = puml_content.strip()
1022
+
1023
+ entry = V2Entry(
1024
+ sigil="DIAG",
1025
+ name=name,
1026
+ entry_type="bloque",
1027
+ value=content,
1028
+ section=section_id,
1029
+ )
1030
+ entries.append(entry)
1031
+
1032
+ return entries
1033
+
1034
+
1035
+ # ---------------------------------------------------------------------------
1036
+ # CALLOUT → RSK
1037
+ # ---------------------------------------------------------------------------
1038
+
1039
+ def _callout_to_risk(block: HCorTEXBlock, diags: List[Diagnostic]) -> List[V2Entry]:
1040
+ """Convert a callout block to RSK entries.
1041
+
1042
+ v2.4.0: Parse ### RSK:name headers as entry boundaries.
1043
+ Each header starts a new RSK entry; list items below become attrs.
1044
+ """
1045
+
1046
+ section_id = _resolve_section(block)
1047
+ entries: List[V2Entry] = []
1048
+
1049
+ current_name = None
1050
+ current_attrs: Dict[str, str] = {}
1051
+
1052
+ for line in block.content_lines:
1053
+ stripped = line.strip()
1054
+ if not stripped:
1055
+ continue
1056
+
1057
+ # Skip markdown headers (## title)
1058
+ if stripped.startswith('## ') and not stripped.startswith('### '):
1059
+ continue
1060
+
1061
+ # ### RSK:name — new entry boundary
1062
+ import re as _re
1063
+ header_m = _re.match(r'^###\s+RSK:(\w+)', stripped)
1064
+ if header_m:
1065
+ # Save previous entry
1066
+ if current_name:
1067
+ # Ensure required fields
1068
+ current_attrs.setdefault("impact", "")
1069
+ current_attrs.setdefault("mitigation", "")
1070
+ current_attrs.setdefault("status", "cur")
1071
+ current_attrs.setdefault("survive", "min")
1072
+ entry = V2Entry(
1073
+ sigil="RSK",
1074
+ name=current_name,
1075
+ entry_type="attrs",
1076
+ value=current_attrs,
1077
+ section=section_id,
1078
+ )
1079
+ entries.append(entry)
1080
+ current_name = header_m.group(1)
1081
+ current_attrs = {}
1082
+ continue
1083
+
1084
+ # List item: "- key: value"
1085
+ item_m = _re.match(r'^-\s+(\w+):\s*(.*)$', stripped)
1086
+ if item_m and current_name:
1087
+ key = item_m.group(1).lower()
1088
+ value = _sanitize_value(item_m.group(2))
1089
+ current_attrs[key] = value
1090
+
1091
+ # Save last entry
1092
+ if current_name:
1093
+ current_attrs.setdefault("impact", "")
1094
+ current_attrs.setdefault("mitigation", "")
1095
+ current_attrs.setdefault("status", "cur")
1096
+ current_attrs.setdefault("survive", "min")
1097
+ entry = V2Entry(
1098
+ sigil="RSK",
1099
+ name=current_name,
1100
+ entry_type="attrs",
1101
+ value=current_attrs,
1102
+ section=section_id,
1103
+ )
1104
+ entries.append(entry)
1105
+
1106
+ return entries