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/view.py ADDED
@@ -0,0 +1,425 @@
1
+ r"""VIEW directives — declarative reversible render for CORTEX ⇄ HCORTEX-R.
2
+
3
+ Implements the VIEW sigilo: a declarative contract inside CORTEX that
4
+ specifies how entries should be rendered as human Markdown (HCORTEX-R)
5
+ and how to reconstruct them back.
6
+
7
+ Architecture:
8
+ CORTEX + VIEW → CortexV2Document → SkillIR → render with VIEW → HCORTEX-R
9
+ HCORTEX-R → parse VIEW markers → reverse strategies → CORTEX + VIEW
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ from typing import Any, Dict, List, Optional, Set, Tuple
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Enums
21
+ # ---------------------------------------------------------------------------
22
+
23
+ class ViewKind(str, Enum):
24
+ TABLE = "table"
25
+ KV_TABLE = "kv_table"
26
+ MATRIX = "matrix"
27
+ LIST = "list"
28
+ NUMBERED_LIST = "numbered_list"
29
+ CHECKLIST = "checklist"
30
+ PROSE = "prose"
31
+ QUOTE = "quote"
32
+ PUML = "puml"
33
+ CODE = "code"
34
+ CALLOUT = "callout"
35
+ SECTION = "section"
36
+ RAW = "raw"
37
+
38
+
39
+ class ReverseStrategy(str, Enum):
40
+ ROWS_TO_ENTRIES = "rows_to_entries"
41
+ ROW_TO_ATTRS = "row_to_attrs"
42
+ COLUMNS_TO_ATTRS = "columns_to_attrs"
43
+ ITEMS_TO_ENTRIES = "items_to_entries"
44
+ ITEMS_TO_ORDERED_ENTRIES = "items_to_ordered_entries"
45
+ ITEMS_TO_STATUS_ENTRIES = "items_to_status_entries"
46
+ BODY_TO_CUERPO = "body_to_cuerpo"
47
+ VERBATIM_TO_BLOQUE = "verbatim_to_bloque"
48
+ CALLOUT_TO_RISK = "callout_to_risk"
49
+ CALLOUT_TO_LIMIT = "callout_to_limit"
50
+ PRESERVE_HUMAN_BLOCK = "preserve_human_block"
51
+ IGNORE_WITH_WARNING = "ignore_with_warning"
52
+ MANUAL_REVIEW = "manual_review"
53
+
54
+
55
+ # Compatibility matrix: which reverse strategies are valid for each kind
56
+ KIND_REVERSE_COMPAT: Dict[ViewKind, Set[ReverseStrategy]] = {
57
+ ViewKind.TABLE: {ReverseStrategy.ROWS_TO_ENTRIES, ReverseStrategy.COLUMNS_TO_ATTRS},
58
+ ViewKind.KV_TABLE: {ReverseStrategy.ROW_TO_ATTRS},
59
+ ViewKind.MATRIX: {ReverseStrategy.COLUMNS_TO_ATTRS, ReverseStrategy.ROWS_TO_ENTRIES},
60
+ ViewKind.LIST: {ReverseStrategy.ITEMS_TO_ENTRIES, ReverseStrategy.BODY_TO_CUERPO},
61
+ ViewKind.NUMBERED_LIST: {ReverseStrategy.ITEMS_TO_ORDERED_ENTRIES},
62
+ ViewKind.CHECKLIST: {ReverseStrategy.ITEMS_TO_STATUS_ENTRIES},
63
+ ViewKind.PROSE: {ReverseStrategy.BODY_TO_CUERPO, ReverseStrategy.PRESERVE_HUMAN_BLOCK},
64
+ ViewKind.QUOTE: {ReverseStrategy.BODY_TO_CUERPO},
65
+ ViewKind.PUML: {ReverseStrategy.VERBATIM_TO_BLOQUE},
66
+ ViewKind.CODE: {ReverseStrategy.VERBATIM_TO_BLOQUE},
67
+ ViewKind.CALLOUT: {ReverseStrategy.CALLOUT_TO_RISK, ReverseStrategy.CALLOUT_TO_LIMIT, ReverseStrategy.PRESERVE_HUMAN_BLOCK},
68
+ ViewKind.SECTION: {ReverseStrategy.PRESERVE_HUMAN_BLOCK},
69
+ ViewKind.RAW: {ReverseStrategy.VERBATIM_TO_BLOQUE, ReverseStrategy.PRESERVE_HUMAN_BLOCK},
70
+ }
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # ViewDirective
75
+ # ---------------------------------------------------------------------------
76
+
77
+ @dataclass
78
+ class ViewDirective:
79
+ """A VIEW directive parsed from CORTEX.
80
+
81
+ Fields match the contract: ``kind|target|fields|order|reverse|scope|title|status``
82
+ """
83
+
84
+ name: str
85
+ kind: ViewKind
86
+ target: str
87
+ reverse: ReverseStrategy
88
+ status: str = "cur"
89
+ fields: Optional[str] = None
90
+ order: Optional[str] = None
91
+ title: Optional[str] = None
92
+ scope: Optional[str] = None
93
+ section: Optional[str] = None
94
+ source_section: Optional[str] = None
95
+ preserve: Optional[str] = None
96
+ hash: Optional[str] = None
97
+ fallback: Optional[str] = None
98
+ raw: str = ""
99
+
100
+ def to_dict(self) -> dict:
101
+ return {
102
+ "name": self.name,
103
+ "kind": self.kind.value,
104
+ "target": self.target,
105
+ "reverse": self.reverse.value,
106
+ "status": self.status,
107
+ "fields": self.fields,
108
+ "order": self.order,
109
+ "title": self.title,
110
+ "scope": self.scope,
111
+ "preserve": self.preserve,
112
+ }
113
+
114
+
115
+ @dataclass
116
+ class ViewDiagnostic:
117
+ """Diagnostic from VIEW parsing/validation."""
118
+
119
+ code: str
120
+ message: str
121
+ view_name: Optional[str] = None
122
+ severity: str = "warning" # warning | error | info
123
+
124
+ def to_dict(self) -> dict:
125
+ return {"code": self.code, "message": self.message, "view_name": self.view_name, "severity": self.severity}
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Parser
130
+ # ---------------------------------------------------------------------------
131
+
132
+ VALID_KINDS = frozenset(k.value for k in ViewKind)
133
+ VALID_REVERSES = frozenset(r.value for r in ReverseStrategy)
134
+ VALID_VIEW_STATUSES = frozenset({"cur", "planned", "deprecated", "human_only", "generated", "edited"})
135
+
136
+
137
+ def parse_view_entry(name: str, attrs: Dict[str, Any], raw: str = "") -> Tuple[Optional[ViewDirective], List[ViewDiagnostic]]:
138
+ """Parse a VIEW entry from its attrs dict.
139
+
140
+ Returns (directive, diagnostics). If the directive is invalid,
141
+ returns (None, diagnostics).
142
+ """
143
+
144
+ diags: List[ViewDiagnostic] = []
145
+
146
+ # Required fields
147
+ kind_str = attrs.get("kind")
148
+ target = attrs.get("target")
149
+ reverse_str = attrs.get("reverse")
150
+ status = attrs.get("status", "cur")
151
+
152
+ # Validate kind
153
+ if not kind_str:
154
+ diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_KIND", f"VIEW:{name} missing required field 'kind'", name, "error"))
155
+ return None, diags
156
+ if kind_str not in VALID_KINDS:
157
+ diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_KIND", f"VIEW:{name} has unknown kind '{kind_str}'", name, "error"))
158
+ return None, diags
159
+
160
+ # Validate target
161
+ if not target:
162
+ diags.append(ViewDiagnostic("E_VIEW_EMPTY_TARGET", f"VIEW:{name} missing required field 'target'", name, "error"))
163
+ return None, diags
164
+
165
+ # Validate reverse
166
+ if not reverse_str:
167
+ diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_REVERSE", f"VIEW:{name} missing required field 'reverse'", name, "error"))
168
+ return None, diags
169
+ if reverse_str not in VALID_REVERSES:
170
+ diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_REVERSE", f"VIEW:{name} has unknown reverse '{reverse_str}'", name, "error"))
171
+ return None, diags
172
+
173
+ kind = ViewKind(kind_str)
174
+ reverse = ReverseStrategy(reverse_str)
175
+
176
+ # Validate kind/reverse compatibility
177
+ if reverse not in KIND_REVERSE_COMPAT.get(kind, set()):
178
+ diags.append(ViewDiagnostic(
179
+ "E_VIEW_INCOMPATIBLE_REVERSE",
180
+ f"VIEW:{name} kind='{kind_str}' is incompatible with reverse='{reverse_str}'",
181
+ name, "error"
182
+ ))
183
+ return None, diags
184
+
185
+ # Validate status
186
+ if status not in VALID_VIEW_STATUSES:
187
+ diags.append(ViewDiagnostic(
188
+ "W_VIEW_UNKNOWN_STATUS",
189
+ f"VIEW:{name} has unknown status '{status}', defaulting to 'cur'",
190
+ name, "warning"
191
+ ))
192
+ status = "cur"
193
+
194
+ directive = ViewDirective(
195
+ name=name,
196
+ kind=kind,
197
+ target=target,
198
+ reverse=reverse,
199
+ status=status,
200
+ fields=attrs.get("fields"),
201
+ order=attrs.get("order"),
202
+ title=attrs.get("title"),
203
+ scope=attrs.get("scope"),
204
+ section=attrs.get("section"),
205
+ source_section=attrs.get("source_section"),
206
+ preserve=attrs.get("preserve"),
207
+ hash=attrs.get("hash"),
208
+ fallback=attrs.get("fallback"),
209
+ raw=raw,
210
+ )
211
+
212
+ return directive, diags
213
+
214
+
215
+ def parse_view_entries_from_doc(doc) -> Tuple[List[ViewDirective], List[ViewDiagnostic]]:
216
+ """Extract and parse all VIEW entries from a CortexV2Document.
217
+
218
+ v2.2.1: VIEW entries in $0 with type/risk/cortex/desc are sigil declarations,
219
+ NOT operational directives. Only VIEW entries outside $0 (or in $0 without
220
+ sigil_decl characteristics) are treated as directives.
221
+
222
+ Returns (directives, diagnostics).
223
+ """
224
+
225
+ directives: List[ViewDirective] = []
226
+ all_diags: List[ViewDiagnostic] = []
227
+ seen_names: Set[str] = set()
228
+
229
+ for sec in doc.sections:
230
+ for entry in sec.entries:
231
+ if entry.sigil != "VIEW":
232
+ continue
233
+
234
+ # v2.2.1 P0-2: In $0, if entry has type/risk/cortex/desc, it's a sigil declaration
235
+ if sec.id == "$0" and isinstance(entry.value, dict):
236
+ if any(k in entry.value for k in ("type", "risk", "cortex", "desc")):
237
+ # This is a sigil declaration, not a directive — skip
238
+ continue
239
+
240
+ # Outside $0 (or $0 without sigil_decl characteristics): parse as directive
241
+ directive, diags = parse_view_entry(entry.name, entry.value, entry.raw)
242
+ all_diags.extend(diags)
243
+ if directive is not None:
244
+ if directive.name in seen_names:
245
+ all_diags.append(ViewDiagnostic(
246
+ "E_VIEW_DUPLICATE_NAME",
247
+ f"VIEW:{directive.name} is defined more than once",
248
+ directive.name, "error"
249
+ ))
250
+ else:
251
+ seen_names.add(directive.name)
252
+ directives.append(directive)
253
+
254
+ return directives, all_diags
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # Target resolution
259
+ # ---------------------------------------------------------------------------
260
+
261
+ def resolve_target(target: str, doc) -> List:
262
+ """Resolve a VIEW target selector against a CortexV2Document.
263
+
264
+ Returns a list of matching V2Entry objects.
265
+ """
266
+
267
+ from .parser import V2Entry
268
+
269
+ results: List[V2Entry] = []
270
+
271
+ # $0:canonical_sigils — sigil declarations in $0
272
+ if target == "$0:canonical_sigils":
273
+ sec0 = doc.get_section("$0")
274
+ if sec0:
275
+ for e in sec0.entries:
276
+ if e.entry_type == "sigil_decl":
277
+ results.append(e)
278
+ return results
279
+
280
+ # $0:contracts — contract entries in $0
281
+ if target == "$0:contracts":
282
+ sec0 = doc.get_section("$0")
283
+ if sec0:
284
+ for e in sec0.entries:
285
+ if e.sigil == "$0" and e.name.startswith("contract_"):
286
+ results.append(e)
287
+ return results
288
+
289
+ # $0:microtokens — micro-token entries in $0
290
+ if target == "$0:microtokens":
291
+ sec0 = doc.get_section("$0")
292
+ if sec0:
293
+ for e in sec0.entries:
294
+ if e.sigil == "$0" and e.name.startswith("micro_"):
295
+ results.append(e)
296
+ return results
297
+
298
+ # $0:type_decls — type declaration entries in $0
299
+ if target == "$0:type_decls":
300
+ sec0 = doc.get_section("$0")
301
+ if sec0:
302
+ for e in sec0.entries:
303
+ if e.sigil == "$0" and e.name.startswith("type_"):
304
+ results.append(e)
305
+ return results
306
+
307
+ # $N:NAME — specific entry by name within section $N (v2.2.2: handles $0:enum_state, $0:delimiters, etc.)
308
+ # Single-colon, starts with $ → match by entry name within the section
309
+ if ":" in target and target.count(":") == 1 and target.startswith("$"):
310
+ parts = target.split(":", 1)
311
+ sec_id = parts[0]
312
+ name = parts[1]
313
+ sec = doc.get_section(sec_id)
314
+ if sec:
315
+ for e in sec.entries:
316
+ if e.name == name:
317
+ results.append(e)
318
+ return results
319
+
320
+ # $N — entire section
321
+ if target.startswith("$") and ":" not in target:
322
+ sec = doc.get_section(target)
323
+ if sec:
324
+ results.extend(sec.entries)
325
+ return results
326
+
327
+ # $N:SIGIL:* — all entries of a sigil within a section
328
+ # $N:SIGIL:name — specific entry within a section (v2.2.2: fix 3-part name selector)
329
+ if ":" in target and target.count(":") == 2 and target.startswith("$"):
330
+ parts = target.split(":")
331
+ sec_id = parts[0]
332
+ sigil = parts[1]
333
+ third = parts[2]
334
+ sec = doc.get_section(sec_id)
335
+ if sec:
336
+ if third == "*":
337
+ for e in sec.entries:
338
+ if e.sigil == sigil:
339
+ results.append(e)
340
+ else:
341
+ # specific entry name within section
342
+ for e in sec.entries:
343
+ if e.sigil == sigil and e.name == third:
344
+ results.append(e)
345
+ return results
346
+
347
+ # SIGIL:* — all entries of a sigil (any section)
348
+ if ":" in target and target.endswith(":*"):
349
+ sigil = target.split(":")[0]
350
+ for sec in doc.sections:
351
+ for e in sec.entries:
352
+ if e.sigil == sigil:
353
+ results.append(e)
354
+ return results
355
+
356
+ # SIGIL:name — exact entry (2-part, no section qualifier)
357
+ if ":" in target and "*" not in target and not target.startswith("$"):
358
+ parts = target.split(":", 1)
359
+ sigil = parts[0]
360
+ name = parts[1]
361
+ for sec in doc.sections:
362
+ for e in sec.entries:
363
+ if e.sigil == sigil and e.name == name:
364
+ results.append(e)
365
+ return results
366
+
367
+ # HUMAN_BLOCK:name — no CORTEX entries, returns empty
368
+ if target.startswith("HUMAN_BLOCK:"):
369
+ return results
370
+
371
+ # group:name — resolve via group (not yet implemented)
372
+ if target.startswith("group:"):
373
+ return results
374
+
375
+ return results
376
+
377
+
378
+ # ---------------------------------------------------------------------------
379
+ # Coverage
380
+ # ---------------------------------------------------------------------------
381
+
382
+ def calculate_view_coverage(doc, directives: List[ViewDirective]) -> Tuple[float, List[str]]:
383
+ """Calculate VIEW coverage: what fraction of eligible entries are covered.
384
+
385
+ v2.3.1 P0-5: 0 eligible entries + 0 directives = 0% (not 100%).
386
+ A document with no content cannot be "fully covered" — it's empty.
387
+
388
+ Returns (coverage_fraction, uncovered_entry_descriptions).
389
+ """
390
+
391
+ # Collect all eligible entries (exclude VIEW entries and $0 metadata)
392
+ eligible: List[str] = []
393
+ covered: Set[str] = set()
394
+
395
+ for sec in doc.sections:
396
+ for entry in sec.entries:
397
+ if entry.sigil == "VIEW":
398
+ continue
399
+ if entry.sigil == "$0" and entry.entry_type == "meta":
400
+ continue
401
+ desc = f"{entry.section}/{entry.sigil}:{entry.name}"
402
+ eligible.append(desc)
403
+
404
+ # v2.3.1 P0-5: If there are no eligible entries, coverage is 0% (not 100%)
405
+ # unless there are also no directives AND no sections (truly empty doc).
406
+ # A document with content but no VIEW directives must report 0%.
407
+ if len(eligible) == 0:
408
+ if len(directives) == 0:
409
+ # Truly empty document
410
+ return 0.0, []
411
+ # Directives exist but nothing to cover (shouldn't happen)
412
+ return 0.0, []
413
+
414
+ # Resolve each directive's target and mark covered entries
415
+ for directive in directives:
416
+ matched = resolve_target(directive.target, doc)
417
+ for entry in matched:
418
+ desc = f"{entry.section}/{entry.sigil}:{entry.name}"
419
+ covered.add(desc)
420
+
421
+ uncovered = [desc for desc in eligible if desc not in covered]
422
+ total = len(eligible)
423
+ coverage = (total - len(uncovered)) / total if total > 0 else 0.0
424
+
425
+ return coverage, uncovered