codec-cortex 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codec_cortex-0.3.2.dist-info/METADATA +183 -0
- codec_cortex-0.3.2.dist-info/RECORD +81 -0
- codec_cortex-0.3.2.dist-info/WHEEL +5 -0
- codec_cortex-0.3.2.dist-info/entry_points.txt +2 -0
- codec_cortex-0.3.2.dist-info/licenses/LICENSE +21 -0
- codec_cortex-0.3.2.dist-info/scm_file_list.json +124 -0
- codec_cortex-0.3.2.dist-info/scm_version.json +8 -0
- codec_cortex-0.3.2.dist-info/top_level.txt +1 -0
- cortex/__init__.py +22 -0
- cortex/__main__.py +7 -0
- cortex/_version.py +24 -0
- cortex/cli/__init__.py +5 -0
- cortex/cli/commands/__init__.py +150 -0
- cortex/cli/commands/add.py +89 -0
- cortex/cli/commands/compile.py +31 -0
- cortex/cli/commands/delete.py +44 -0
- cortex/cli/commands/diagram.py +130 -0
- cortex/cli/commands/diff.py +135 -0
- cortex/cli/commands/doctor.py +74 -0
- cortex/cli/commands/format.py +29 -0
- cortex/cli/commands/get.py +45 -0
- cortex/cli/commands/glossary.py +98 -0
- cortex/cli/commands/list.py +44 -0
- cortex/cli/commands/micro.py +79 -0
- cortex/cli/commands/move.py +45 -0
- cortex/cli/commands/new.py +80 -0
- cortex/cli/commands/recover.py +90 -0
- cortex/cli/commands/render.py +93 -0
- cortex/cli/commands/update.py +81 -0
- cortex/cli/commands/v2_canonicalize.py +162 -0
- cortex/cli/commands/v2_compare.py +74 -0
- cortex/cli/commands/v2_convert.py +183 -0
- cortex/cli/commands/v2_explain_loss.py +97 -0
- cortex/cli/commands/v2_inspect.py +113 -0
- cortex/cli/commands/v2_roundtrip.py +104 -0
- cortex/cli/commands/v2_roundtrip_bidir.py +128 -0
- cortex/cli/commands/v2_verify_view.py +65 -0
- cortex/cli/commands/verify.py +119 -0
- cortex/cli/main.py +636 -0
- cortex/core/__init__.py +85 -0
- cortex/core/ast.py +313 -0
- cortex/core/compare.py +180 -0
- cortex/core/document_kind.py +525 -0
- cortex/core/errors.py +399 -0
- cortex/core/lexer.py +267 -0
- cortex/core/parser.py +671 -0
- cortex/core/validator.py +268 -0
- cortex/core/writer.py +249 -0
- cortex/crud/__init__.py +17 -0
- cortex/crud/mutations.py +342 -0
- cortex/crud/selectors.py +95 -0
- cortex/crud/transactions.py +213 -0
- cortex/glossary/__init__.py +21 -0
- cortex/glossary/contracts.py +37 -0
- cortex/glossary/minimal.py +94 -0
- cortex/glossary/model.py +77 -0
- cortex/glossary/resolver.py +96 -0
- cortex/hcortex/__init__.py +35 -0
- cortex/hcortex/edit_parser.py +489 -0
- cortex/hcortex/edit_renderer.py +158 -0
- cortex/hcortex/markdown_model.py +81 -0
- cortex/hcortex/profiles.py +166 -0
- cortex/hcortex/read_renderer.py +342 -0
- cortex/hcortex/recovery.py +782 -0
- cortex/py.typed +0 -0
- cortex/templates/__init__.py +8 -0
- cortex/templates/brain.py +118 -0
- cortex/templates/minimal_glossary.py +42 -0
- cortex/templates/package.py +91 -0
- cortex/templates/skill.py +91 -0
- cortex/v2/__init__.py +30 -0
- cortex/v2/diagnostics.py +57 -0
- cortex/v2/encoder.py +1106 -0
- cortex/v2/equivalence.py +323 -0
- cortex/v2/hcortex_parser.py +615 -0
- cortex/v2/hcortex_renderer.py +450 -0
- cortex/v2/ir.py +223 -0
- cortex/v2/parser.py +655 -0
- cortex/v2/view.py +425 -0
- cortex/v2/view_renderer.py +474 -0
- cortex/v2/writer.py +301 -0
cortex/core/validator.py
ADDED
|
@@ -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
|
cortex/crud/__init__.py
ADDED
|
@@ -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
|
+
]
|