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,89 @@
1
+ """``cortex add`` (alias: ``cortex patch_add``) — add a new entry to a .cortex file.
2
+
3
+ v1.1.2: post-mutation validation via :func:`~cortex.cli.commands.post_mutation_gate`.
4
+ v1.1.4 P0-3: ``--unsafe-allow-secret-forensics`` now marks the artefact as
5
+ ``non_conformant_forensic_artifact`` via a ``STAT:forensic_quarantine`` entry,
6
+ so the file is explicitly flagged even though it's written.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+ from ...core.errors import CortexError
14
+ from ...core.parser import build_entry_from_value
15
+ from ...crud.mutations import add_entry
16
+ from ...crud.transactions import atomic_write_cortex
17
+ from ..commands import load_doc, post_mutation_gate
18
+
19
+
20
+ def run(args) -> int:
21
+ doc = load_doc(args.input)
22
+ try:
23
+ entry = add_entry(
24
+ doc,
25
+ section=args.section,
26
+ sigil=args.sigil,
27
+ name=args.name,
28
+ value=args.value,
29
+ create_section=args.create_section,
30
+ allow_duplicate=args.allow_duplicate,
31
+ allow_unknown_sigil=getattr(args, "allow_unknown_sigil", False),
32
+ )
33
+ except CortexError as e:
34
+ print(f"error: {e}")
35
+ return 1
36
+
37
+ if args.dry_run:
38
+ print(json.dumps({
39
+ "ok": True,
40
+ "dry_run": True,
41
+ "entry": entry.to_dict(),
42
+ }, indent=2, default=str))
43
+ return 0
44
+
45
+ # Post-mutation validation gate (shared with update/delete/move).
46
+ err = post_mutation_gate(doc, args)
47
+ if err is not None:
48
+ print(json.dumps(err, indent=2, default=str))
49
+ return 1
50
+
51
+ # v1.1.4 P0-3: if --unsafe-allow-secret-forensics was used, mark the
52
+ # artefact as non_conformant_forensic_artifact before writing.
53
+ unsafe = getattr(args, "unsafe_allow_secret_forensics", False)
54
+ if unsafe:
55
+ # Ensure STAT is declared in $0
56
+ if "STAT" not in doc.glossary.sigils:
57
+ from ...glossary.minimal import brain_sigils
58
+ canonical = {sd.sigil: sd for sd in brain_sigils()}
59
+ if "STAT" in canonical:
60
+ doc.glossary.add_sigil(canonical["STAT"])
61
+ # Add forensic quarantine marker
62
+ stat_sec = doc.get_or_create_section("$9", title="FORENSIC QUARANTINE")
63
+ stat_sec.entries.append(build_entry_from_value(
64
+ "$9", "STAT", "forensic_quarantine", "attrs",
65
+ {
66
+ "conformity": "non_conformant_forensic_artifact",
67
+ "reason": "contains clear-text secret persisted via --unsafe-allow-secret-forensics",
68
+ "status": "deprecated",
69
+ "survive": "min",
70
+ },
71
+ ))
72
+
73
+ result = atomic_write_cortex(
74
+ doc, args.input, force=args.force,
75
+ unsafe_allow_secret_forensics=unsafe,
76
+ )
77
+
78
+ payload = {
79
+ "ok": True,
80
+ "entry": entry.to_dict(),
81
+ "written": result.to_dict(),
82
+ }
83
+ if unsafe:
84
+ payload["warning"] = (
85
+ "ARTEFACT MARKED AS non_conformant_forensic_artifact — "
86
+ "contains clear-text secret; do NOT use as operational memory"
87
+ )
88
+ print(json.dumps(payload, indent=2, default=str))
89
+ return 0
@@ -0,0 +1,31 @@
1
+ """``cortex compile`` — compile HCORTEX-EDIT markdown back to .cortex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from ...core.errors import CortexError
8
+ from ...core.parser import parse_cortex
9
+ from ...core.writer import write_cortex
10
+ from ...crud.transactions import atomic_write_text
11
+ from ...hcortex import parse_hcortex_edit
12
+ from ..commands import emit
13
+
14
+
15
+ def run(args) -> int:
16
+ if not os.path.exists(args.input):
17
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
18
+ with open(args.input, "r", encoding="utf-8") as f:
19
+ text = f.read()
20
+ doc = parse_hcortex_edit(text, source=args.input)
21
+ cortex_text = write_cortex(doc)
22
+ # Re-parse to verify compile output is itself valid
23
+ parse_cortex(cortex_text, path=args.out)
24
+ result = atomic_write_text(cortex_text, args.out, keep_backup=False)
25
+ emit({
26
+ "text": f"compiled {args.input} → {args.out} ({result.bytes_written} bytes)",
27
+ "input": args.input,
28
+ "out": args.out,
29
+ "bytes": result.bytes_written,
30
+ }, json_mode=False)
31
+ return 0
@@ -0,0 +1,44 @@
1
+ """``cortex delete`` (alias: ``cortex patch_remove``) — delete an entry.
2
+
3
+ v1.1.2: post-mutation validation via :func:`~cortex.cli.commands.post_mutation_gate`
4
+ (so deleting a critical FCS in a brain is rejected unless ``--force``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ from ...core.errors import CortexError
12
+ from ...crud.mutations import delete_entry
13
+ from ...crud.transactions import atomic_write_cortex
14
+ from ..commands import load_doc, post_mutation_gate
15
+
16
+
17
+ def run(args) -> int:
18
+ doc = load_doc(args.input)
19
+ try:
20
+ entry = delete_entry(doc, args.selector, force=args.force)
21
+ except CortexError as e:
22
+ print(f"error: {e}")
23
+ return 1
24
+
25
+ if args.dry_run:
26
+ print(json.dumps({
27
+ "ok": True, "dry_run": True,
28
+ "deleted": entry.to_dict(),
29
+ }, indent=2, default=str))
30
+ return 0
31
+
32
+ # v1.1.2: post-mutation validation gate (same as add).
33
+ err = post_mutation_gate(doc, args)
34
+ if err is not None:
35
+ print(json.dumps(err, indent=2, default=str))
36
+ return 1
37
+
38
+ result = atomic_write_cortex(doc, args.input, force=args.force)
39
+ print(json.dumps({
40
+ "ok": True,
41
+ "deleted": entry.to_dict(),
42
+ "written": result.to_dict(),
43
+ }, indent=2, default=str))
44
+ return 0
@@ -0,0 +1,130 @@
1
+ """``cortex diagram`` — DIAG bloque operations (list, extract, validate).
2
+
3
+ Closes audit gap B-010: the SKILL plans ``diagram list/extract/validate``
4
+ (Section 22.2). This module implements them as thin wrappers over the AST.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sys
11
+
12
+ from ...core.ast import CortexDocument
13
+ from ...core.errors import CortexError, NotFoundError
14
+ from ..commands import load_doc
15
+
16
+
17
+ def _find_diagrams(doc: CortexDocument, name: str | None = None) -> list:
18
+ """Return all DIAG entries (optionally filtered by name)."""
19
+
20
+ out = []
21
+ for sec, entry in doc.iter_entries():
22
+ if entry.sigil != "DIAG":
23
+ continue
24
+ if name and entry.name != name:
25
+ continue
26
+ out.append((sec, entry))
27
+ return out
28
+
29
+
30
+ def run_list(args) -> int:
31
+ doc = load_doc(args.input)
32
+ diags = _find_diagrams(doc)
33
+ if args.format == "json":
34
+ print(json.dumps({
35
+ "ok": True,
36
+ "count": len(diags),
37
+ "diagrams": [
38
+ {
39
+ "section": sec.id,
40
+ "name": e.name,
41
+ "hash": e.hash,
42
+ "bytes": len(str(e.value or "").encode("utf-8")),
43
+ }
44
+ for sec, e in diags
45
+ ],
46
+ }, indent=2, default=str))
47
+ else:
48
+ if not diags:
49
+ print("(no DIAG entries)")
50
+ return 0
51
+ print(f"{'SECTION':<8} {'NAME':<24} {'BYTES':<8} HASH")
52
+ print("-" * 70)
53
+ for sec, e in diags:
54
+ nbytes = len(str(e.value or "").encode("utf-8"))
55
+ print(f"{sec.id:<8} {e.name:<24} {nbytes:<8} {e.hash[:16]}")
56
+ return 0
57
+
58
+
59
+ def run_extract(args) -> int:
60
+ """Extract a DIAG bloque verbatim.
61
+
62
+ Re-audit H-RA-04: the previous implementation appended a trailing
63
+ newline, which broke byte-exact preservation. Now we write the
64
+ value as-is. Use ``--print-newline`` to add a trailing newline on
65
+ stdout output for terminal friendliness.
66
+ """
67
+
68
+ doc = load_doc(args.input)
69
+ diags = _find_diagrams(doc, name=args.name)
70
+ if not diags:
71
+ raise NotFoundError(f"DIAG:{args.name}")
72
+ if len(diags) > 1:
73
+ raise CortexError(
74
+ "E014_AMBIGUOUS_SELECTOR",
75
+ f"multiple DIAG entries named {args.name!r}",
76
+ )
77
+ _, entry = diags[0]
78
+ text = str(entry.value or "")
79
+ if args.out:
80
+ # Write exactly the bytes — no appended newline.
81
+ with open(args.out, "w", encoding="utf-8") as f:
82
+ f.write(text)
83
+ print(f"extracted DIAG:{args.name} → {args.out} ({len(text)} bytes, verbatim)")
84
+ else:
85
+ # Default: write exactly the bytes (no trailing newline)
86
+ sys.stdout.write(text)
87
+ if getattr(args, "print_newline", False):
88
+ sys.stdout.write("\n")
89
+ return 0
90
+
91
+
92
+ def run_validate(args) -> int:
93
+ doc = load_doc(args.input)
94
+ diags = _find_diagrams(doc, name=args.name)
95
+ if not diags:
96
+ if args.format == "json":
97
+ print(json.dumps({"ok": False, "error": "no DIAG entries found"}))
98
+ else:
99
+ print("no DIAG entries found")
100
+ return 1
101
+ findings = []
102
+ for sec, entry in diags:
103
+ text = str(entry.value or "")
104
+ issues = []
105
+ # Heuristics: check for balanced @startuml/@enduml, balanced braces
106
+ if "@startuml" in text and "@enduml" not in text:
107
+ issues.append("missing @enduml")
108
+ if "@enduml" in text and "@startuml" not in text:
109
+ issues.append("missing @startuml")
110
+ if text.count("{") != text.count("}"):
111
+ issues.append(f"unbalanced braces ({text.count('{')} '{{' vs {text.count('}')} '}}')")
112
+ findings.append({
113
+ "section": sec.id,
114
+ "name": entry.name,
115
+ "bytes": len(text),
116
+ "issues": issues,
117
+ "valid": len(issues) == 0,
118
+ })
119
+ if args.format == "json":
120
+ print(json.dumps({"ok": all(f["valid"] for f in findings), "diagrams": findings},
121
+ indent=2, default=str))
122
+ else:
123
+ all_valid = all(f["valid"] for f in findings)
124
+ print(f"validation: {'OK' if all_valid else 'ISSUES'}")
125
+ for f in findings:
126
+ status = "✓" if f["valid"] else "✗"
127
+ print(f" {status} {f['section']}/DIAG:{f['name']} ({f['bytes']} bytes)")
128
+ for issue in f["issues"]:
129
+ print(f" - {issue}")
130
+ return 0 if all(f["valid"] for f in findings) else 2
@@ -0,0 +1,135 @@
1
+ """``cortex diff`` — structural/semantic/governance diff between two .cortex files.
2
+
3
+ v1.1.2: ``--profile governance`` redefinido. Antes comparaba sólo los
4
+ ``findings`` de ``validate_level_policy`` (ambos archivos válidos → rc=0
5
+ aun si cambiaron restricciones críticas). Ahora compara los **cambios
6
+ reales** en entradas de gobernanza (FCS, OBJ, CNST, RSK, AUD, CLAIM,
7
+ LIM) entre los dos archivos, además de los findings. Retorna non-zero
8
+ si hay cualquier cambio en gobernanza.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+
15
+ from ...core.compare import compare_ast
16
+ from ...core.document_kind import infer_document_kind, validate_level_policy
17
+ from ..commands import load_doc
18
+
19
+
20
+ # Sigilos que constituyen "gobernanza" — cualquier cambio en estas
21
+ # entradas afecta las invariantes cognitivas del protocolo.
22
+ GOVERNANCE_SIGILS = frozenset({
23
+ "FCS", "OBJ", "WRK", "STP", "NXT", # working state
24
+ "CNST", "AXM", "!", # hard constraints
25
+ "RSK", "AUD", "CLAIM", "LIM", # risk / audit / limits
26
+ "KNW", # promoted knowledge
27
+ })
28
+
29
+
30
+ def _extract_governance_changes(left, right) -> list:
31
+ """Return a list of governance-relevant diffs between two ASTs.
32
+
33
+ Filters :func:`~cortex.core.compare.compare_ast` results to only
34
+ those affecting governance sigils. This is the real "governance
35
+ diff": did any constraint, risk, audit or working-state entry
36
+ change between the two files?
37
+ """
38
+
39
+ result = compare_ast(left, right)
40
+ gov_diffs = []
41
+ for d in result.diffs:
42
+ path = d.path or ""
43
+ # path is like "$2/FCS:primary" or "sigils.FCS"
44
+ # We consider it governance-relevant if any GOVERNANCE_SIGILS
45
+ # appears in the path.
46
+ path_upper = path.upper()
47
+ if any(sig in path_upper for sig in GOVERNANCE_SIGILS):
48
+ gov_diffs.append(d.to_dict())
49
+ # Glossary changes to governance sigils also count
50
+ if path.startswith("sigils."):
51
+ sig = path.split(".", 1)[1]
52
+ if sig in GOVERNANCE_SIGILS:
53
+ gov_diffs.append(d.to_dict())
54
+ return gov_diffs
55
+
56
+
57
+ def run(args) -> int:
58
+ left = load_doc(args.left)
59
+ right = load_doc(args.right)
60
+ profile = getattr(args, "profile", "structural")
61
+
62
+ json_mode = getattr(args, "_json_mode", False)
63
+ if args.format == "json" or json_mode:
64
+ if profile == "governance":
65
+ # v1.1.2: governance diff = real changes in governance entries
66
+ # + level-policy findings on both sides
67
+ left_kind = infer_document_kind(left, args.left)
68
+ right_kind = infer_document_kind(right, args.right)
69
+ left_findings = validate_level_policy(left, left_kind)
70
+ right_findings = validate_level_policy(right, right_kind)
71
+ gov_changes = _extract_governance_changes(left, right)
72
+ ok = (not gov_changes) and (not left_findings) and (not right_findings)
73
+ print(json.dumps({
74
+ "ok": ok,
75
+ "profile": profile,
76
+ "left": {
77
+ "kind": left_kind.kind,
78
+ "findings": left_findings,
79
+ },
80
+ "right": {
81
+ "kind": right_kind.kind,
82
+ "findings": right_findings,
83
+ },
84
+ "governance_changes": gov_changes,
85
+ "summary": {
86
+ "governance_changes_count": len(gov_changes),
87
+ "left_findings_count": len(left_findings),
88
+ "right_findings_count": len(right_findings),
89
+ },
90
+ }, indent=2, default=str))
91
+ return 0 if ok else 1
92
+ else:
93
+ result = compare_ast(left, right)
94
+ print(json.dumps({
95
+ "ok": result.equal,
96
+ "profile": profile,
97
+ "left": args.left,
98
+ "right": args.right,
99
+ "diffs": [d.to_dict() for d in result.diffs],
100
+ }, indent=2, default=str))
101
+ return 0 if result.equal else 1
102
+
103
+ # Text mode
104
+ if profile == "governance":
105
+ left_kind = infer_document_kind(left, args.left)
106
+ right_kind = infer_document_kind(right, args.right)
107
+ left_findings = validate_level_policy(left, left_kind)
108
+ right_findings = validate_level_policy(right, right_kind)
109
+ gov_changes = _extract_governance_changes(left, right)
110
+ ok = (not gov_changes) and (not left_findings) and (not right_findings)
111
+ if ok:
112
+ print(f"✓ {args.left} and {args.right} are governance-equal "
113
+ f"(no changes in governance entries, no findings)")
114
+ else:
115
+ print(f"✗ governance diff between {args.left} and {args.right}:")
116
+ print(f" left kind: {left_kind.kind} ({len(left_findings)} finding(s))")
117
+ print(f" right kind: {right_kind.kind} ({len(right_findings)} finding(s))")
118
+ print(f" governance entry changes: {len(gov_changes)}")
119
+ for c in gov_changes[:10]:
120
+ print(f" [{c.get('kind')}] {c.get('path')}: {c.get('message')}")
121
+ for f in left_findings:
122
+ print(f" [L] [{f.get('code')}] {f.get('message')}")
123
+ for f in right_findings:
124
+ print(f" [R] [{f.get('code')}] {f.get('message')}")
125
+ return 0 if ok else 1
126
+
127
+ # structural / semantic
128
+ result = compare_ast(left, right)
129
+ if result.equal:
130
+ print(f"✓ {args.left} and {args.right} are structurally equal (profile={profile})")
131
+ else:
132
+ print(f"✗ {args.left} and {args.right} differ ({len(result.diffs)} diff(s), profile={profile}):")
133
+ for d in result.diffs:
134
+ print(f" [{d.kind}] {d.path}: {d.message}")
135
+ return 0 if result.equal else 1
@@ -0,0 +1,74 @@
1
+ """``cortex doctor`` — deep diagnostic of a .cortex file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from ...core.document_kind import infer_document_kind
8
+ from ...core.validator import validate
9
+ from ..commands import load_doc
10
+
11
+
12
+ def run(args) -> int:
13
+ doc = load_doc(args.input)
14
+ strict = getattr(args, "strict", False)
15
+
16
+ # Apply explicit kind if provided (audit gap H-01/H-02)
17
+ kind = None
18
+ if getattr(args, "kind", None):
19
+ from ...core.document_kind import DocumentKind
20
+ kind = DocumentKind(kind=args.kind, source="cli")
21
+ if kind is None:
22
+ kind = infer_document_kind(doc, args.input)
23
+
24
+ diagnostics = validate(doc, strict=strict, kind=kind)
25
+ errors = [d for d in diagnostics if d.get("severity") == "error"]
26
+ warnings = [d for d in diagnostics if d.get("severity") == "warning"]
27
+ sections = doc.sections
28
+ sigils = list(doc.glossary.sigils.values())
29
+ types = list(doc.glossary.types.values())
30
+ micro = list(doc.glossary.micro.values())
31
+ entries = [(s.id, e) for s in sections if s.id != "$0" for e in s.entries]
32
+
33
+ json_mode = getattr(args, "_json_mode", False)
34
+ if args.format == "json" or json_mode:
35
+ print(json.dumps({
36
+ "ok": len(errors) == 0,
37
+ "input": args.input,
38
+ "kind": kind.kind,
39
+ "kind_source": kind.source,
40
+ "sections": len(sections),
41
+ "entries": len(entries),
42
+ "sigils": len(sigils),
43
+ "types": len(types),
44
+ "micro": len(micro),
45
+ "strict": strict,
46
+ "errors": errors,
47
+ "warnings": warnings,
48
+ "section_ids": [s.id for s in sections],
49
+ "sigil_names": [s.sigil for s in sigils],
50
+ }, indent=2, default=str))
51
+ else:
52
+ lines = []
53
+ lines.append(f"doctor: {args.input}")
54
+ lines.append(f" kind: {kind.kind} (inferred via {kind.source})")
55
+ lines.append(f" strict: {strict}")
56
+ lines.append(f" sections: {len(sections)} ({', '.join(s.id for s in sections)})")
57
+ lines.append(f" entries: {len(entries)}")
58
+ lines.append(f" sigils: {len(sigils)}")
59
+ lines.append(f" types: {len(types)}")
60
+ lines.append(f" micro: {len(micro)}")
61
+ lines.append(f" errors: {len(errors)}")
62
+ lines.append(f" warnings: {len(warnings)}")
63
+ if errors:
64
+ lines.append("")
65
+ lines.append("errors:")
66
+ for e in errors:
67
+ lines.append(f" [{e.get('code')}] {e.get('message')}")
68
+ if warnings:
69
+ lines.append("")
70
+ lines.append("warnings:")
71
+ for w in warnings:
72
+ lines.append(f" [{w.get('code')}] {w.get('message')}")
73
+ print("\n".join(lines))
74
+ return 1 if errors else 0
@@ -0,0 +1,29 @@
1
+ """``cortex format`` — re-serialize a .cortex file canonically."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from ...crud.transactions import atomic_write_cortex
8
+ from ...core.writer import write_cortex
9
+ from ..commands import load_doc
10
+
11
+
12
+ def run(args) -> int:
13
+ doc = load_doc(args.input)
14
+ out_path = args.out or args.input
15
+ if args.dry_run:
16
+ text = write_cortex(doc)
17
+ print(json.dumps({
18
+ "ok": True,
19
+ "dry_run": True,
20
+ "bytes": len(text.encode("utf-8")),
21
+ "preview": text[:500],
22
+ }, indent=2))
23
+ return 0
24
+ result = atomic_write_cortex(doc, out_path, force=args.force)
25
+ print(json.dumps({
26
+ "ok": True,
27
+ "written": result.to_dict(),
28
+ }, indent=2, default=str))
29
+ return 0
@@ -0,0 +1,45 @@
1
+ """``cortex get`` — fetch a single entry by selector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from ...core.errors import CortexError
8
+ from ...crud.selectors import select_one
9
+ from ..commands import load_doc
10
+
11
+
12
+ def run(args) -> int:
13
+ doc = load_doc(args.input)
14
+ try:
15
+ entry = select_one(doc, args.selector)
16
+ except CortexError as e:
17
+ if args.format == "json":
18
+ print(json.dumps({"ok": False, "error": {"code": e.code, "message": e.message}}))
19
+ else:
20
+ print(f"error: {e}")
21
+ return 1
22
+ if args.format == "json":
23
+ print(json.dumps({
24
+ "ok": True,
25
+ "entry": entry.to_dict(),
26
+ }, indent=2, ensure_ascii=False, default=str))
27
+ else:
28
+ lines = []
29
+ lines.append(f"section: {entry.section}")
30
+ lines.append(f"sigil: {entry.sigil}")
31
+ lines.append(f"name: {entry.name}")
32
+ lines.append(f"type: {entry.type}")
33
+ lines.append(f"hash: {entry.hash}")
34
+ lines.append(f"lines: {entry.line_start}-{entry.line_end}")
35
+ lines.append("")
36
+ lines.append("value:")
37
+ if isinstance(entry.value, dict):
38
+ for k, v in entry.value.items():
39
+ lines.append(f" {k}: {v!r}")
40
+ else:
41
+ lines.append(f" {entry.value}")
42
+ lines.append("")
43
+ lines.append(f"raw: {entry.raw}")
44
+ print("\n".join(lines))
45
+ return 0
@@ -0,0 +1,98 @@
1
+ """``cortex glossary`` — glossary CRUD (list/add/update/delete sigils)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from ...core.errors import CortexError
8
+ from ...crud.mutations import (
9
+ add_sigil_to_glossary,
10
+ delete_sigil_from_glossary,
11
+ update_sigil_in_glossary,
12
+ )
13
+ from ...crud.transactions import atomic_write_cortex
14
+ from ..commands import load_doc
15
+
16
+
17
+ def run_list(args) -> int:
18
+ doc = load_doc(args.input)
19
+ sigils = list(doc.glossary.sigils.values())
20
+ if args.format == "json":
21
+ print(json.dumps({
22
+ "ok": True,
23
+ "count": len(sigils),
24
+ "sigils": [s.to_dict() for s in sigils],
25
+ }, indent=2, default=str))
26
+ else:
27
+ if not sigils:
28
+ print("(no sigils declared)")
29
+ return 0
30
+ lines = [
31
+ f"{'SIGIL':<6} {'NAME':<12} {'TYPE':<10} {'RISK':<4} {'LAYER':<14} DESCRIPTION",
32
+ "-" * 80,
33
+ ]
34
+ for s in sigils:
35
+ lines.append(f"{s.sigil:<6} {s.name:<12} {s.type:<10} {s.risk:<4} {s.layer:<14} {s.description}")
36
+ print("\n".join(lines))
37
+ return 0
38
+
39
+
40
+ def run_add(args) -> int:
41
+ doc = load_doc(args.input)
42
+ try:
43
+ add_sigil_to_glossary(
44
+ doc,
45
+ sigil=args.sigil,
46
+ name=args.name,
47
+ type_=args.type,
48
+ risk=args.risk,
49
+ layer=args.layer,
50
+ description=args.description,
51
+ force_governance=args.force_governance,
52
+ )
53
+ except CortexError as e:
54
+ print(f"error: {e}")
55
+ return 1
56
+ if args.dry_run:
57
+ print(json.dumps({"ok": True, "dry_run": True, "sigil": args.sigil}, indent=2))
58
+ return 0
59
+ result = atomic_write_cortex(doc, args.input, force=False)
60
+ print(json.dumps({"ok": True, "sigil": args.sigil, "written": result.to_dict()}, indent=2, default=str))
61
+ return 0
62
+
63
+
64
+ def run_update(args) -> int:
65
+ doc = load_doc(args.input)
66
+ try:
67
+ update_sigil_in_glossary(
68
+ doc,
69
+ sigil=args.sigil,
70
+ description=args.description,
71
+ risk=args.risk,
72
+ layer=args.layer,
73
+ force_governance=args.force_governance,
74
+ )
75
+ except CortexError as e:
76
+ print(f"error: {e}")
77
+ return 1
78
+ if args.dry_run:
79
+ print(json.dumps({"ok": True, "dry_run": True, "sigil": args.sigil}, indent=2))
80
+ return 0
81
+ result = atomic_write_cortex(doc, args.input, force=False)
82
+ print(json.dumps({"ok": True, "sigil": args.sigil, "written": result.to_dict()}, indent=2, default=str))
83
+ return 0
84
+
85
+
86
+ def run_delete(args) -> int:
87
+ doc = load_doc(args.input)
88
+ try:
89
+ delete_sigil_from_glossary(doc, sigil=args.sigil, force=args.force)
90
+ except CortexError as e:
91
+ print(f"error: {e}")
92
+ return 1
93
+ if args.dry_run:
94
+ print(json.dumps({"ok": True, "dry_run": True, "sigil": args.sigil}, indent=2))
95
+ return 0
96
+ result = atomic_write_cortex(doc, args.input, force=False)
97
+ print(json.dumps({"ok": True, "sigil": args.sigil, "written": result.to_dict()}, indent=2, default=str))
98
+ return 0