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,268 @@
1
+ """Validator — checks an AST against the CODEC-CORTEX contract.
2
+
3
+ The validator runs a series of structural rules:
4
+
5
+ 1. ``$0`` exists and is the first section (already enforced by parser).
6
+ 2. Every sigil used by an entry is declared in ``$0``.
7
+ 3. Every type referenced by a sigil is declared.
8
+ 4. ``attrs-pos`` sigils have a positional contract.
9
+ 5. Critical sigils (FCS, OBJ, WRK, STP, CNST, RSK, CLAIM, LIM, AUD,
10
+ SES, LNG, KNW, HDL) have their required fields.
11
+ 6. Status / severity / priority values are in the allowed sets.
12
+ 7. No duplicate entries (same sigil+name) unless explicitly allowed.
13
+ 8. **Cognitive governance** (1.1.0): level separation (Nivel 1 sin estado
14
+ vivo, Nivel 2 con FCS+OBJ, Nivel 3 sin WRK), ``survive`` domain,
15
+ ``CNST:blocking`` → ``survive:min``, secret scanning.
16
+
17
+ Two modes are supported:
18
+ - ``validate(doc, strict=False)`` (default): warnings stay warnings,
19
+ errors stay errors.
20
+ - ``validate(doc, strict=True)``: all warnings are promoted to errors
21
+ so ``verify --strict`` fails on any contract violation.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Dict, List, Optional
27
+
28
+ from .ast import CortexDocument, Entry
29
+ from .errors import (
30
+ ALLOWED_PRIORITY,
31
+ ALLOWED_SEVERITY,
32
+ ALLOWED_STATUS,
33
+ E003_UNKNOWN_SIGIL,
34
+ E004_UNKNOWN_TYPE,
35
+ E007_ATTRS_POS_CONTRACT_MISSING,
36
+ E008_DUPLICATE_ENTRY,
37
+ )
38
+ from .document_kind import (
39
+ DocumentKind,
40
+ validate_level_policy,
41
+ )
42
+
43
+
44
+ # Required fields per critical sigil (Section 6 of SKILL.md)
45
+ REQUIRED_FIELDS: Dict[str, List[str]] = {
46
+ "FCS": ["what", "priority", "status", "survive"],
47
+ "OBJ": ["goal", "status", "success", "survive"],
48
+ "WRK": ["phase", "current", "blocked", "survive"],
49
+ "STP": ["action", "reason", "owner", "status", "survive"],
50
+ "CNST": ["rule", "severity", "survive"],
51
+ "CLAIM": ["statement", "evidence", "status"],
52
+ "LIM": ["limit", "scope", "status"],
53
+ "RSK": ["risk", "impact", "mitigation", "status", "survive"],
54
+ "AUD": ["event", "evidence", "result", "date"],
55
+ "SES": ["input", "output", "outcome", "date"],
56
+ "LNG": ["type", "cause", "lesson", "prevention"],
57
+ "KNW": ["topic", "content", "status"],
58
+ }
59
+
60
+
61
+ # Sigils protected from deletion without --force (P0 / blocking severity)
62
+ def _is_protected(entry: Entry) -> bool:
63
+ if entry.sigil in ("FCS", "OBJ", "CNST"):
64
+ attrs = entry.value if isinstance(entry.value, dict) else {}
65
+ sev = attrs.get("severity", "")
66
+ if sev == "blocking":
67
+ return True
68
+ if attrs.get("survive") == "min":
69
+ return True
70
+ return False
71
+
72
+
73
+ def is_protected_entry(entry: Entry) -> bool:
74
+ """Public wrapper used by the CRUD layer."""
75
+
76
+ return _is_protected(entry)
77
+
78
+
79
+ def validate(
80
+ doc: CortexDocument,
81
+ strict: bool = False,
82
+ kind: Optional[DocumentKind] = None,
83
+ ) -> List[Dict]:
84
+ """Run all validators and return a list of diagnostic dicts.
85
+
86
+ Existing ``doc.diagnostics`` are preserved; new findings are appended.
87
+
88
+ Parameters
89
+ ----------
90
+ doc : CortexDocument
91
+ The document to validate.
92
+ strict : bool, default False
93
+ When True, all ``warning`` findings are promoted to ``error``
94
+ severity so that ``verify --strict`` fails on any contract
95
+ violation (audit gap M-02).
96
+ kind : DocumentKind, optional
97
+ Explicit document kind for level-policy checks. When None, the
98
+ kind is inferred from the document (filename, IDN, sigil
99
+ signature). Passing an explicit kind overrides inference so
100
+ ``verify --kind package`` actually applies Nivel-3 rules —
101
+ closing re-audit gap H-RA-01.
102
+ """
103
+
104
+ findings: List[Dict] = list(doc.diagnostics)
105
+
106
+ # 1. Unknown sigils (parser may already have flagged some)
107
+ for sec, entry in doc.iter_entries():
108
+ if entry.sigil in {"GSIG", "GTYP", "GMIC", "GCON"}:
109
+ continue
110
+ if entry.sigil not in doc.glossary.sigils:
111
+ findings.append({
112
+ "code": E003_UNKNOWN_SIGIL,
113
+ "message": f"sigil {entry.sigil!r} (entry {entry.name!r}) not declared in $0",
114
+ "line": entry.line_start,
115
+ "section": sec.id,
116
+ "sigil": entry.sigil,
117
+ "entry": entry.name,
118
+ "severity": "error",
119
+ })
120
+ continue
121
+ sd = doc.glossary.sigils[entry.sigil]
122
+ if sd.type not in doc.glossary.types:
123
+ findings.append({
124
+ "code": E004_UNKNOWN_TYPE,
125
+ "message": f"type {sd.type!r} (sigil {entry.sigil!r}) not declared in $0",
126
+ "line": entry.line_start,
127
+ "section": sec.id,
128
+ "sigil": entry.sigil,
129
+ "entry": entry.name,
130
+ "severity": "error",
131
+ })
132
+ if sd.type == "attrs-pos" and doc.glossary.contract_for(entry.sigil) is None:
133
+ findings.append({
134
+ "code": E007_ATTRS_POS_CONTRACT_MISSING,
135
+ "message": f"attrs-pos sigil {entry.sigil!r} has no positional contract",
136
+ "line": entry.line_start,
137
+ "section": sec.id,
138
+ "sigil": entry.sigil,
139
+ "entry": entry.name,
140
+ "severity": "error",
141
+ })
142
+
143
+ # 2. Required fields for critical sigils
144
+ for sec, entry in doc.iter_entries():
145
+ required = REQUIRED_FIELDS.get(entry.sigil)
146
+ if not required:
147
+ continue
148
+ if not isinstance(entry.value, dict):
149
+ findings.append({
150
+ "code": "W001_MISSING_FIELDS",
151
+ "message": f"{entry.sigil}:{entry.name} value is not a dict; cannot check fields",
152
+ "line": entry.line_start,
153
+ "section": sec.id,
154
+ "sigil": entry.sigil,
155
+ "entry": entry.name,
156
+ "severity": "warning",
157
+ })
158
+ continue
159
+ missing = [f for f in required if f not in entry.value]
160
+ if missing:
161
+ findings.append({
162
+ "code": "W001_MISSING_FIELDS",
163
+ "message": f"{entry.sigil}:{entry.name} missing required fields: {missing}",
164
+ "line": entry.line_start,
165
+ "section": sec.id,
166
+ "sigil": entry.sigil,
167
+ "entry": entry.name,
168
+ "severity": "warning",
169
+ })
170
+
171
+ # 3. Status / severity / priority allowed values
172
+ for sec, entry in doc.iter_entries():
173
+ if not isinstance(entry.value, dict):
174
+ continue
175
+ status = entry.value.get("status")
176
+ if isinstance(status, str) and status not in ALLOWED_STATUS:
177
+ findings.append({
178
+ "code": "W002_INVALID_STATUS",
179
+ "message": f"{entry.sigil}:{entry.name} status={status!r} not in {sorted(ALLOWED_STATUS)}",
180
+ "line": entry.line_start,
181
+ "section": sec.id,
182
+ "sigil": entry.sigil,
183
+ "entry": entry.name,
184
+ "severity": "warning",
185
+ })
186
+ severity = entry.value.get("severity")
187
+ if isinstance(severity, str) and severity not in ALLOWED_SEVERITY:
188
+ findings.append({
189
+ "code": "W003_INVALID_SEVERITY",
190
+ "message": f"{entry.sigil}:{entry.name} severity={severity!r} not in {sorted(ALLOWED_SEVERITY)}",
191
+ "line": entry.line_start,
192
+ "section": sec.id,
193
+ "sigil": entry.sigil,
194
+ "entry": entry.name,
195
+ "severity": "warning",
196
+ })
197
+ priority = entry.value.get("priority")
198
+ if isinstance(priority, str) and priority not in ALLOWED_PRIORITY:
199
+ findings.append({
200
+ "code": "W004_INVALID_PRIORITY",
201
+ "message": f"{entry.sigil}:{entry.name} priority={priority!r} not in {sorted(ALLOWED_PRIORITY)}",
202
+ "line": entry.line_start,
203
+ "section": sec.id,
204
+ "sigil": entry.sigil,
205
+ "entry": entry.name,
206
+ "severity": "warning",
207
+ })
208
+
209
+ # 4. Duplicate entries (same sigil+name) within the same section
210
+ seen: Dict[str, List[str]] = {}
211
+ for sec, entry in doc.iter_entries():
212
+ if entry.sigil in {"GSIG", "GTYP", "GMIC", "GCON"}:
213
+ continue
214
+ key = f"{sec.id}::{entry.sigil}::{entry.name}"
215
+ if key in seen:
216
+ findings.append({
217
+ "code": E008_DUPLICATE_ENTRY,
218
+ "message": f"duplicate entry {entry.sigil}:{entry.name} in {sec.id}",
219
+ "line": entry.line_start,
220
+ "section": sec.id,
221
+ "sigil": entry.sigil,
222
+ "entry": entry.name,
223
+ "severity": "error",
224
+ })
225
+ seen.setdefault(key, []).append(entry.name)
226
+
227
+ # 5. Cognitive governance (audit hardening, 1.1.0):
228
+ # level separation, FCS/OBJ in brain, survive domain, blocking→min,
229
+ # attrs-pos arity, secret scanning.
230
+ # Re-audit H-RA-01: pass explicit `kind` through so `verify --kind X`
231
+ # actually applies the right level-policy rules.
232
+ findings.extend(validate_level_policy(doc, kind=kind))
233
+
234
+ # 6. Strict mode: promote all warnings to errors
235
+ if strict:
236
+ for f in findings:
237
+ if f.get("severity") == "warning":
238
+ f["severity"] = "error"
239
+ f["message"] = f"[strict] {f.get('message', '')}"
240
+
241
+ # Deduplicate findings by (code, line, sigil, entry, message)
242
+ seen_keys = set()
243
+ deduped: List[Dict] = []
244
+ for f in findings:
245
+ k = (f.get("code"), f.get("line"), f.get("sigil"),
246
+ f.get("entry"), f.get("message"))
247
+ if k in seen_keys:
248
+ continue
249
+ seen_keys.add(k)
250
+ deduped.append(f)
251
+
252
+ return deduped
253
+
254
+
255
+ def is_valid(
256
+ doc: CortexDocument,
257
+ strict: bool = False,
258
+ kind: Optional[DocumentKind] = None,
259
+ ) -> bool:
260
+ """Return True if the document has no error-severity diagnostics.
261
+
262
+ When ``strict=True``, warnings also count as failures.
263
+ """
264
+
265
+ for f in validate(doc, strict=strict, kind=kind):
266
+ if f.get("severity") == "error":
267
+ return False
268
+ return True
cortex/core/writer.py ADDED
@@ -0,0 +1,249 @@
1
+ """Canonical ``.cortex`` writer.
2
+
3
+ Serialises a :class:`CortexDocument` AST back to ``.cortex`` source text.
4
+ The writer is deterministic: the same AST always produces byte-identical
5
+ output, which is what makes roundtrip verification meaningful.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, Dict, List
12
+
13
+ from .ast import (
14
+ AttrsPosContract,
15
+ CortexDocument,
16
+ Entry,
17
+ Glossary,
18
+ MicroDef,
19
+ SigilDef,
20
+ )
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Value serialisation
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def _escape_string(s: str) -> str:
28
+ """Escape a string for ``attrs`` double-quoted form."""
29
+
30
+ out = s.replace("\\", "\\\\").replace('"', '\\"')
31
+ out = out.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r")
32
+ return f'"{out}"'
33
+
34
+
35
+ def serialize_value(value: Any) -> str:
36
+ if isinstance(value, bool):
37
+ return "true" if value else "false"
38
+ if isinstance(value, (int, float)):
39
+ return str(value)
40
+ if isinstance(value, str):
41
+ return _escape_string(value)
42
+ if value is None:
43
+ return '""'
44
+ # fallback: JSON
45
+ return _escape_string(json.dumps(value, ensure_ascii=False))
46
+
47
+
48
+ def serialize_attrs(attrs: Dict[str, Any]) -> str:
49
+ """Serialise a dict to ``key:"value", key2:val2`` form."""
50
+
51
+ parts = []
52
+ for k, v in attrs.items():
53
+ parts.append(f"{k}:{serialize_value(v)}")
54
+ return ", ".join(parts)
55
+
56
+
57
+ def serialize_attrs_pos(attrs: Dict[str, Any], contract: AttrsPosContract) -> str:
58
+ """Serialise positional attrs using the contract's field order.
59
+
60
+ Re-audit H-RA-05: ``|`` is the delimiter and CANNOT appear inside
61
+ values. We raise :class:`~cortex.core.errors.InvalidValueError`
62
+ if any value contains ``|`` — the caller should use ``attrs`` form
63
+ instead for values that need pipes.
64
+ """
65
+
66
+ from .errors import InvalidValueError
67
+ parts = []
68
+ for field_name in contract.fields:
69
+ v = attrs.get(field_name)
70
+ if v is None:
71
+ parts.append("")
72
+ continue
73
+ s = serialize_value(v)
74
+ # Detect literal pipe inside the serialised value (after quote stripping)
75
+ # The serialised form is "..." for strings; check the inner content.
76
+ inner = s
77
+ if len(inner) >= 2 and inner[0] == '"' and inner[-1] == '"':
78
+ inner = inner[1:-1]
79
+ if "|" in inner:
80
+ raise InvalidValueError(
81
+ f"attrs-pos value for field {field_name!r} contains '|' "
82
+ f"({inner!r}); use 'attrs' type instead — pipes are forbidden "
83
+ "in attrs-pos values per SKILL.md §4.3"
84
+ )
85
+ parts.append(s)
86
+ return " | ".join(parts)
87
+
88
+
89
+ def serialize_entry_value(value: Any, type_: str) -> str:
90
+ """Serialise the *body* of an entry (content between braces)."""
91
+
92
+ if type_ == "attrs":
93
+ return serialize_attrs(value) if isinstance(value, dict) else ""
94
+ if type_ == "attrs-pos":
95
+ # Without a contract we fall back to attrs form
96
+ return serialize_attrs(value) if isinstance(value, dict) else ""
97
+ if type_ == "cuerpo":
98
+ return str(value) if value is not None else ""
99
+ if type_ == "bloque":
100
+ text = str(value) if value is not None else ""
101
+ # Ensure a leading newline if the bloque spans multiple lines
102
+ if "\n" in text:
103
+ return "\n" + text + "\n"
104
+ return text
105
+ if type_ == "relación":
106
+ return str(value) if value is not None else ""
107
+ # Unknown type — fallback to attrs
108
+ return serialize_attrs(value) if isinstance(value, dict) else str(value or "")
109
+
110
+
111
+ def serialize_entry(entry: Entry, glossary: Glossary | None = None) -> str:
112
+ """Serialise a single :class:`Entry` to canonical ``.cortex`` text."""
113
+
114
+ if entry.type == "attrs-pos" and glossary is not None:
115
+ contract = glossary.contract_for(entry.sigil)
116
+ if contract is not None:
117
+ body = serialize_attrs_pos(entry.value, contract)
118
+ return f"{entry.sigil}:{entry.name}{{{body}}}"
119
+ body = serialize_entry_value(entry.value, entry.type)
120
+ if entry.type == "bloque" and "\n" in body:
121
+ # Multi-line bloque: put braces on their own lines for readability
122
+ # Ensure body starts and ends with newline
123
+ if not body.startswith("\n"):
124
+ body = "\n" + body
125
+ if not body.endswith("\n"):
126
+ body = body + "\n"
127
+ return f"{entry.sigil}:{entry.name}{{{body}}}"
128
+ return f"{entry.sigil}:{entry.name}{{{body}}}"
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Glossary serialisation
133
+ # ---------------------------------------------------------------------------
134
+
135
+ def serialize_glossary(glossary: Glossary) -> str:
136
+ """Serialise the ``$0`` section.
137
+
138
+ Uses the pipe-separated comment form (canonical SKILL.md template).
139
+ All declaration lines are prefixed with ``#`` so the lexer classifies
140
+ them as comments and preserves them verbatim for roundtrip.
141
+ """
142
+
143
+ lines: List[str] = []
144
+ lines.append("# -- $0: MINIMAL LOCAL GLOSSARY --")
145
+ lines.append("# Required: every .cortex artifact must be locally self-contained.")
146
+ lines.append("# Sigil | Name | Type | Risk | Cognitive Layer | Description")
147
+ # Order sigils deterministically: canonical ones first, then custom
148
+ canonical_order = [
149
+ "IDN", "DOM", "KNW", "REF", "TAG", "AXM", "CNST", "!",
150
+ "CLAIM", "LIM", "AUD", "RSK", "FCS", "OBJ", "WRK", "STP",
151
+ "NXT", "SES", "LNG", "DIAG", "HDL", "PFL", "DEP", "DESC", "ERR",
152
+ ]
153
+ sigils_in_glossary = list(glossary.sigils.values())
154
+ # Sort: canonical first (in canonical order), then custom alphabetically
155
+ def _sort_key(sd: SigilDef) -> tuple:
156
+ if sd.sigil in canonical_order:
157
+ return (0, canonical_order.index(sd.sigil))
158
+ return (1, sd.sigil)
159
+ sigils_in_glossary.sort(key=_sort_key)
160
+ for sd in sigils_in_glossary:
161
+ lines.append(
162
+ f"# {sd.sigil:<5} | {sd.name:<10} | {sd.type:<10} | {sd.risk:<1} | "
163
+ f"{sd.layer:<14} | {sd.description}"
164
+ )
165
+ # Types
166
+ lines.append("#")
167
+ lines.append("# Types:")
168
+ for td in glossary.types.values():
169
+ lines.append(f"# {td.name} = {td.description}")
170
+ # Micro-tokens
171
+ lines.append("#")
172
+ lines.append("# Micro-glossary:")
173
+ micro_items = list(glossary.micro.values())
174
+ # Sort: canonical first (in canonical order), then custom
175
+ canonical_micro_order = list(CANONICAL_MICRO_ORDER)
176
+ def _micro_sort_key(md: MicroDef) -> tuple:
177
+ if md.token in canonical_micro_order:
178
+ return (0, canonical_micro_order.index(md.token))
179
+ return (1, md.token)
180
+ micro_items.sort(key=_micro_sort_key)
181
+ if micro_items:
182
+ line_parts = [f"{md.token}={md.value}" for md in micro_items]
183
+ # Group 4 per line for readability
184
+ for i in range(0, len(line_parts), 4):
185
+ lines.append("# " + " ".join(line_parts[i : i + 4]))
186
+ # Contracts
187
+ if glossary.contracts:
188
+ lines.append("#")
189
+ lines.append("# Positional contracts:")
190
+ for c in glossary.contracts.values():
191
+ lines.append(f"# contract: {c.sigil} | " + " | ".join(c.fields))
192
+ return "\n".join(lines)
193
+
194
+
195
+ # Canonical order for micro-tokens (Section 4.1.1)
196
+ CANONICAL_MICRO_ORDER = [
197
+ "cur", "pln", "fut", "blk", "min", "rec", "wrk", "full", "ok", "fail", "part",
198
+ ]
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Document serialisation
203
+ # ---------------------------------------------------------------------------
204
+
205
+ def write_cortex(doc: CortexDocument) -> str:
206
+ """Serialise a :class:`CortexDocument` to canonical ``.cortex`` text.
207
+
208
+ The ``$0`` glossary section is ALWAYS regenerated from
209
+ ``doc.glossary`` so mutations are reflected in the output. Other
210
+ sections preserve their comments verbatim.
211
+ """
212
+
213
+ out: List[str] = []
214
+ # $0 first — always regenerated from glossary
215
+ if doc.sections and doc.sections[0].id == "$0":
216
+ sec0 = doc.sections[0]
217
+ glossary_text = serialize_glossary(doc.glossary)
218
+ header = "$0"
219
+ if sec0.title:
220
+ header = f"$0: {sec0.title}"
221
+ out.append(header)
222
+ out.append("")
223
+ out.append(glossary_text)
224
+ out.append("")
225
+ # Append any non-glossary entries from $0 section (rare, but supported)
226
+ for entry in sec0.entries:
227
+ if entry.sigil in {"GSIG", "GTYP", "GMIC", "GCON"}:
228
+ continue
229
+ out.append(serialize_entry(entry, doc.glossary))
230
+ out.append("")
231
+
232
+ # Other sections
233
+ for sec in doc.sections[1:]:
234
+ header = sec.id
235
+ if sec.title:
236
+ header = f"{sec.id}: {sec.title}"
237
+ out.append(header)
238
+ out.append("")
239
+ # Preserve section comments (after header, before entries)
240
+ if sec.comments:
241
+ out.extend(sec.comments)
242
+ out.append("")
243
+ for entry in sec.entries:
244
+ out.append(serialize_entry(entry, doc.glossary))
245
+ out.append("")
246
+
247
+ # Trailing newline
248
+ text = "\n".join(out).rstrip("\n") + "\n"
249
+ return text
@@ -0,0 +1,17 @@
1
+ """CRUD package — selectors, mutations, atomic transactions."""
2
+
3
+ from .selectors import Selector, parse_selector, select, select_one
4
+ from .mutations import (
5
+ add_entry, update_entry, delete_entry, move_entry,
6
+ add_sigil_to_glossary, update_sigil_in_glossary, delete_sigil_from_glossary,
7
+ add_micro_to_glossary, update_micro_in_glossary, delete_micro_from_glossary,
8
+ )
9
+ from .transactions import atomic_write_cortex, atomic_write_text, WriteResult
10
+
11
+ __all__ = [
12
+ "Selector", "parse_selector", "select", "select_one",
13
+ "add_entry", "update_entry", "delete_entry", "move_entry",
14
+ "add_sigil_to_glossary", "update_sigil_in_glossary", "delete_sigil_from_glossary",
15
+ "add_micro_to_glossary", "update_micro_in_glossary", "delete_micro_from_glossary",
16
+ "atomic_write_cortex", "atomic_write_text", "WriteResult",
17
+ ]