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,74 @@
1
+ """``cortex compare`` — compare two CORTEX/HCORTEX artefacts.
2
+
3
+ Canonical name: ``compare`` (since v0.3.2).
4
+ Deprecated alias: ``v2-compare`` (still accepted).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+
12
+ from ...core.errors import CortexError
13
+ from ...v2.parser import parse_cortex_v2
14
+ from ...v2.hcortex_parser import parse_hcortex
15
+ from ...v2.encoder import encode_cortex_from_ast
16
+ from ...v2.equivalence import compare_documents, diff_by_sigil, diff_by_section, diff_by_view
17
+
18
+
19
+ def run(args) -> int:
20
+ if not os.path.exists(args.left):
21
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.left}")
22
+ if not os.path.exists(args.right):
23
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.right}")
24
+
25
+ with open(args.left, "r", encoding="utf-8") as f:
26
+ left_text = f.read()
27
+ with open(args.right, "r", encoding="utf-8") as f:
28
+ right_text = f.read()
29
+
30
+ # Parse both
31
+ left_doc = _parse_any(left_text)
32
+ right_doc = _parse_any(right_text)
33
+
34
+ left_bytes = left_text.encode("utf-8")
35
+ right_bytes = right_text.encode("utf-8")
36
+
37
+ result = compare_documents(left_doc, right_doc, left_bytes, right_bytes)
38
+
39
+ if args.format == "json":
40
+ print(json.dumps(result.to_dict(), indent=2, default=str))
41
+ else:
42
+ print(f"byte_identical: {result.byte_identical}")
43
+ print(f"ast_equivalent: {result.ast_equivalent}")
44
+ print(f"semantic_equivalent: {result.semantic_equivalent}")
45
+ print(f"content_equivalent: {result.content_equivalent}")
46
+ print(f"diff_count: {len(result.diffs)}")
47
+
48
+ if result.diffs:
49
+ print("\nDiffs by section:")
50
+ for sec, ds in diff_by_section(result.diffs).items():
51
+ print(f" {sec}: {len(ds)} diffs")
52
+ print("\nDiffs by sigil:")
53
+ for sig, ds in diff_by_sigil(result.diffs).items():
54
+ print(f" {sig}: {len(ds)} diffs")
55
+ print("\nDiffs by VIEW:")
56
+ for v, ds in diff_by_view(result.diffs).items():
57
+ print(f" {v}: {len(ds)} diffs")
58
+
59
+ if args.verbose:
60
+ print("\nFirst 20 diffs:")
61
+ for d in result.diffs[:20]:
62
+ print(f" {d.kind} at {d.location}" + (f".{d.field}" if d.field else ""))
63
+
64
+ return 0 if result.equivalent else 1
65
+
66
+
67
+ def _parse_any(text):
68
+ """Parse text as CORTEX or HCORTEX, return CortexV2Document."""
69
+ if "internal_encoding: HCORTEX" in text:
70
+ hdoc = parse_hcortex(text, strict=False)
71
+ doc, _ = encode_cortex_from_ast(hdoc)
72
+ return doc
73
+ else:
74
+ return parse_cortex_v2(text)
@@ -0,0 +1,183 @@
1
+ """``cortex convert`` — convert between CORTEX v2 and HCORTEX.
2
+
3
+ Canonical name: ``convert`` (since v0.3.2).
4
+ Deprecated alias: ``v2-convert`` (still accepted).
5
+
6
+ v2.3.0: Supports reverse conversion HCORTEX → CORTEX via parse_hcortex + encode_cortex_from_ast.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+
14
+ from ...core.errors import CortexError
15
+ from ...v2.parser import parse_cortex_v2
16
+ from ...v2.writer import write_cortex_v2
17
+ from ...v2.view_renderer import render_hcortex
18
+ from ...v2.hcortex_parser import parse_hcortex
19
+ from ...v2.encoder import encode_cortex_from_ast
20
+
21
+
22
+ def run(args) -> int:
23
+ if not os.path.exists(args.input):
24
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
25
+
26
+ with open(args.input, "r", encoding="utf-8") as f:
27
+ text = f.read()
28
+
29
+ from_format = args.from_format
30
+ to_format = args.to_format
31
+ deprecated_warning = None
32
+
33
+ if from_format == "hcortex-r":
34
+ deprecated_warning = "--from hcortex-r is deprecated. Use --from hcortex."
35
+ from_format = "hcortex"
36
+ if to_format == "hcortex-r":
37
+ deprecated_warning = "--to hcortex-r is deprecated. Use --to hcortex."
38
+ to_format = "hcortex"
39
+
40
+ force_write_on_error = getattr(args, "force_write_on_error", False)
41
+ strict = getattr(args, "strict", False)
42
+ mode = getattr(args, "mode", "normal")
43
+ if strict and mode == "normal":
44
+ mode = "strict"
45
+
46
+ if deprecated_warning:
47
+ print(f"WARNING: {deprecated_warning}", file=sys.stderr)
48
+
49
+ # ---------------------------------------------------------------
50
+ # CORTEX → HCORTEX
51
+ # ---------------------------------------------------------------
52
+ if from_format == "cortex" and to_format == "hcortex":
53
+ doc = parse_cortex_v2(text)
54
+ hcortex_md, diags = render_hcortex(doc, mode=mode)
55
+
56
+ errors = [d for d in diags if d.severity == "error"]
57
+ warnings = [d for d in diags if d.severity == "warning"]
58
+
59
+ effective_errors = list(errors)
60
+ if strict:
61
+ strict_errors = [d for d in warnings if d.code.startswith("W_VIEW_") or d.code == "W_HCORTEX_DISPLAY_ONLY"]
62
+ effective_errors.extend(strict_errors)
63
+ warnings = [d for d in warnings if d not in strict_errors]
64
+
65
+ has_errors = bool(effective_errors)
66
+ should_write_out = bool(args.out) and (not has_errors or force_write_on_error)
67
+
68
+ if should_write_out:
69
+ with open(args.out, "w", encoding="utf-8") as f:
70
+ f.write(hcortex_md)
71
+ _print_cortex_to_hcortex_summary(args, doc, hcortex_md, effective_errors, warnings)
72
+ if has_errors:
73
+ print(" NOTE: --out written despite errors (--force-write-on-error).", file=sys.stderr)
74
+ return 1
75
+ elif args.out and has_errors and not force_write_on_error:
76
+ _print_cortex_to_hcortex_skip(args, doc, hcortex_md, effective_errors, warnings)
77
+ return 1
78
+ else:
79
+ sys.stdout.write(hcortex_md)
80
+ if has_errors:
81
+ _print_diags_to_stderr(effective_errors, warnings)
82
+ return 1
83
+ return 0
84
+
85
+ # ---------------------------------------------------------------
86
+ # HCORTEX → CORTEX (v2.3.0 F-09)
87
+ # ---------------------------------------------------------------
88
+ elif from_format == "hcortex" and to_format == "cortex":
89
+ hdoc = parse_hcortex(text, strict=strict)
90
+ doc, enc_diags = encode_cortex_from_ast(hdoc, mode=mode)
91
+
92
+ all_diags = list(hdoc.diags) + list(enc_diags)
93
+ errors = [d for d in all_diags if d.severity == "error"]
94
+ warnings = [d for d in all_diags if d.severity == "warning"]
95
+
96
+ has_errors = bool(errors)
97
+ should_write_out = bool(args.out) and (not has_errors or force_write_on_error)
98
+
99
+ cortex_text = write_cortex_v2(doc)
100
+
101
+ if should_write_out:
102
+ with open(args.out, "w", encoding="utf-8") as f:
103
+ f.write(cortex_text)
104
+ print(f"converted HCORTEX → CORTEX: {args.input} → {args.out}")
105
+ print(f" sections: {len(doc.sections)}")
106
+ print(f" entries: {sum(len(s.entries) for s in doc.sections)}")
107
+ print(f" blocks: {len(hdoc.blocks)}")
108
+ print(f" bytes: {len(cortex_text.encode('utf-8'))}")
109
+ print(f" errors: {len(errors)}")
110
+ print(f" warnings: {len(warnings)}")
111
+ for d in all_diags[:10]:
112
+ print(f" [{d.severity}] {d.code}: {d.message}")
113
+ if has_errors:
114
+ print(" NOTE: --out written despite errors (--force-write-on-error).", file=sys.stderr)
115
+ return 1
116
+ elif args.out and has_errors and not force_write_on_error:
117
+ print(f"converted HCORTEX → CORTEX: {args.input} (NOT written — {len(errors)} errors)", file=sys.stderr)
118
+ print(f" blocks: {len(hdoc.blocks)}")
119
+ print(f" errors: {len(errors)}")
120
+ print(f" warnings: {len(warnings)}")
121
+ for d in all_diags[:10]:
122
+ print(f" [{d.severity}] {d.code}: {d.message}")
123
+ print(" Use --force-write-on-error to write --out anyway.", file=sys.stderr)
124
+ return 1
125
+ else:
126
+ sys.stdout.write(cortex_text)
127
+ if has_errors:
128
+ print(f"\n<!-- {len(errors)} errors, {len(warnings)} warnings -->", file=sys.stderr)
129
+ for d in all_diags[:10]:
130
+ print(f" [{d.severity}] {d.code}: {d.message}", file=sys.stderr)
131
+ return 1
132
+ return 0
133
+
134
+ # ---------------------------------------------------------------
135
+ # CORTEX → CORTEX (identity / format check)
136
+ # ---------------------------------------------------------------
137
+ elif from_format == "cortex" and to_format == "cortex":
138
+ doc = parse_cortex_v2(text)
139
+ reproduced = write_cortex_v2(doc)
140
+ if args.out:
141
+ with open(args.out, "w", encoding="utf-8") as f:
142
+ f.write(reproduced)
143
+ else:
144
+ sys.stdout.write(reproduced)
145
+ return 0
146
+
147
+ else:
148
+ raise CortexError("E021_INVALID_VALUE", f"unsupported conversion: {from_format} → {to_format}")
149
+
150
+
151
+ def _print_cortex_to_hcortex_summary(args, doc, hcortex_md, errors, warnings):
152
+ print(f"converted CORTEX → HCORTEX: {args.input} → {args.out}")
153
+ print(f" sections: {len(doc.sections)}")
154
+ print(f" entries: {sum(len(s.entries) for s in doc.sections)}")
155
+ print(f" bytes: {len(hcortex_md.encode('utf-8'))}")
156
+ print(f" errors: {len(errors)}")
157
+ print(f" warnings: {len(warnings)}")
158
+ for d in errors[:10]:
159
+ print(f" [{d.severity}] {d.code}: {d.message}")
160
+ for d in warnings[:10]:
161
+ print(f" [{d.severity}] {d.code}: {d.message}")
162
+
163
+
164
+ def _print_cortex_to_hcortex_skip(args, doc, hcortex_md, errors, warnings):
165
+ print(f"converted CORTEX → HCORTEX: {args.input} (NOT written — {len(errors)} errors)", file=sys.stderr)
166
+ print(f" sections: {len(doc.sections)}")
167
+ print(f" entries: {sum(len(s.entries) for s in doc.sections)}")
168
+ print(f" bytes: {len(hcortex_md.encode('utf-8'))} (would-be)")
169
+ print(f" errors: {len(errors)}")
170
+ print(f" warnings: {len(warnings)}")
171
+ for d in errors[:10]:
172
+ print(f" [{d.severity}] {d.code}: {d.message}")
173
+ for d in warnings[:10]:
174
+ print(f" [{d.severity}] {d.code}: {d.message}")
175
+ print(" Use --force-write-on-error to write --out anyway.", file=sys.stderr)
176
+
177
+
178
+ def _print_diags_to_stderr(errors, warnings):
179
+ print(f"\n<!-- {len(errors)} errors, {len(warnings)} warnings -->", file=sys.stderr)
180
+ for d in errors[:10]:
181
+ print(f" [{d.severity}] {d.code}: {d.message}", file=sys.stderr)
182
+ for d in warnings[:10]:
183
+ print(f" [{d.severity}] {d.code}: {d.message}", file=sys.stderr)
@@ -0,0 +1,97 @@
1
+ """``cortex explain-loss`` — explain loss, omission, or non-reversible content.
2
+
3
+ Canonical name: ``explain-loss`` (since v0.3.2).
4
+ Deprecated alias: ``v2-explain-loss`` (still accepted).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+
12
+ from ...core.errors import CortexError
13
+ from ...v2.parser import parse_cortex_v2
14
+ from ...v2.view import parse_view_entries_from_doc, calculate_view_coverage
15
+ from ...v2.view_renderer import render_hcortex
16
+ from ...v2.hcortex_parser import parse_hcortex
17
+ from ...v2.encoder import encode_cortex_from_ast
18
+ from ...v2.equivalence import compare_ast_equivalent
19
+
20
+
21
+ def run(args) -> int:
22
+ if not os.path.exists(args.input):
23
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
24
+
25
+ with open(args.input, "r", encoding="utf-8") as f:
26
+ text = f.read()
27
+
28
+ is_hcortex = "internal_encoding: HCORTEX" in text
29
+
30
+ losses = []
31
+
32
+ if not is_hcortex:
33
+ # CORTEX input: explain what would be lost in CORTEX → HCORTEX → CORTEX
34
+ doc_orig = parse_cortex_v2(text)
35
+ hcortex_md, _ = render_hcortex(doc_orig)
36
+ hdoc = parse_hcortex(hcortex_md, strict=False)
37
+ doc_reconstructed, _ = encode_cortex_from_ast(hdoc)
38
+
39
+ ast_eq, diffs = compare_ast_equivalent(doc_orig, doc_reconstructed)
40
+ if not ast_eq:
41
+ losses.append({
42
+ "direction": "CORTEX → HCORTEX → CORTEX",
43
+ "type": "AST-equivalence fail",
44
+ "diff_count": len(diffs),
45
+ "diffs": [d.to_dict() for d in diffs[:10]],
46
+ })
47
+
48
+ # Check VIEW coverage
49
+ directives, _ = parse_view_entries_from_doc(doc_orig)
50
+ coverage, uncovered = calculate_view_coverage(doc_orig, directives)
51
+ if coverage < 1.0:
52
+ losses.append({
53
+ "direction": "CORTEX → HCORTEX",
54
+ "type": "incomplete VIEW coverage",
55
+ "coverage_percent": int(coverage * 100),
56
+ "uncovered_count": len(uncovered),
57
+ "uncovered": uncovered[:10],
58
+ })
59
+ else:
60
+ # HCORTEX input
61
+ hdoc = parse_hcortex(text, strict=False)
62
+ losses.append({
63
+ "direction": "HCORTEX parse",
64
+ "type": "diagnostics",
65
+ "diag_count": len(hdoc.diags),
66
+ "diags": [d.to_dict() for d in hdoc.diags[:10]],
67
+ })
68
+ if hdoc.orphan_content:
69
+ losses.append({
70
+ "direction": "HCORTEX parse",
71
+ "type": "orphan content (outside VIEW)",
72
+ "line_count": len(hdoc.orphan_content),
73
+ "sample": hdoc.orphan_content[:5],
74
+ })
75
+
76
+ result = {
77
+ "input": args.input,
78
+ "format": "HCORTEX" if is_hcortex else "CORTEX",
79
+ "loss_count": len(losses),
80
+ "losses": losses,
81
+ }
82
+
83
+ if args.format == "json":
84
+ print(json.dumps(result, indent=2, ensure_ascii=False, default=str))
85
+ else:
86
+ print(f"Input: {args.input} ({result['format']})")
87
+ print(f"Losses detected: {len(losses)}")
88
+ for i, loss in enumerate(losses, 1):
89
+ print(f"\n Loss {i}: {loss['direction']} — {loss['type']}")
90
+ if 'diff_count' in loss:
91
+ print(f" diff_count: {loss['diff_count']}")
92
+ if 'coverage_percent' in loss:
93
+ print(f" coverage: {loss['coverage_percent']}%")
94
+ if 'uncovered_count' in loss:
95
+ print(f" uncovered: {loss['uncovered_count']} entries")
96
+
97
+ return 0 if not losses else 1
@@ -0,0 +1,113 @@
1
+ """``cortex inspect`` — show AST, sections, sigils, VIEW coverage, errors.
2
+
3
+ Canonical name: ``inspect`` (since v0.3.2).
4
+ Deprecated alias: ``v2-inspect`` (still accepted).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from collections import Counter
12
+
13
+ from ...core.errors import CortexError
14
+ from ...v2.parser import parse_cortex_v2
15
+ from ...v2.view import parse_view_entries_from_doc, calculate_view_coverage
16
+
17
+
18
+ def run(args) -> int:
19
+ if not os.path.exists(args.input):
20
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
21
+
22
+ with open(args.input, "r", encoding="utf-8") as f:
23
+ text = f.read()
24
+
25
+ is_hcortex = "internal_encoding: HCORTEX" in text
26
+
27
+ if is_hcortex:
28
+ # For HCORTEX, parse and report blocks
29
+ from ...v2.hcortex_parser import parse_hcortex
30
+ hdoc = parse_hcortex(text, strict=False)
31
+ result = {
32
+ "format": "HCORTEX",
33
+ "header": hdoc.header.__dict__,
34
+ "block_count": len(hdoc.blocks),
35
+ "orphan_lines": len(hdoc.orphan_content),
36
+ "diagnostics": [d.to_dict() for d in hdoc.diags],
37
+ "blocks": [
38
+ {
39
+ "view_name": b.view_name,
40
+ "kind": b.kind,
41
+ "target": b.target,
42
+ "reverse": b.reverse,
43
+ "section": b.section,
44
+ }
45
+ for b in hdoc.blocks
46
+ ],
47
+ }
48
+ else:
49
+ doc = parse_cortex_v2(text)
50
+ directives, diags = parse_view_entries_from_doc(doc)
51
+ coverage, uncovered = calculate_view_coverage(doc, directives)
52
+
53
+ # Sigil distribution
54
+ sigil_counter: Counter = Counter()
55
+ for sec in doc.sections:
56
+ for e in sec.entries:
57
+ sigil_counter[e.sigil] += 1
58
+
59
+ # Entry type distribution
60
+ type_counter: Counter = Counter()
61
+ for sec in doc.sections:
62
+ for e in sec.entries:
63
+ type_counter[e.entry_type] += 1
64
+
65
+ result = {
66
+ "format": "CORTEX",
67
+ "header": doc.header,
68
+ "section_count": len(doc.sections),
69
+ "entry_count": sum(len(s.entries) for s in doc.sections),
70
+ "view_count": len(directives),
71
+ "view_coverage_percent": int(coverage * 100),
72
+ "uncovered_count": len(uncovered),
73
+ "reversible": (coverage == 1.0) and not any(d.severity == "error" for d in diags),
74
+ "sections": [
75
+ {
76
+ "id": s.id,
77
+ "entry_count": len(s.entries),
78
+ }
79
+ for s in doc.sections
80
+ ],
81
+ "sigil_distribution": dict(sigil_counter.most_common()),
82
+ "entry_type_distribution": dict(type_counter.most_common()),
83
+ "diagnostics": [d.to_dict() for d in diags[:20]],
84
+ }
85
+
86
+ if args.format == "json":
87
+ print(json.dumps(result, indent=2, ensure_ascii=False, default=str))
88
+ else:
89
+ print(f"Format: {result['format']}")
90
+ if result['format'] == "CORTEX":
91
+ print(f"Sections: {result['section_count']}")
92
+ print(f"Entries: {result['entry_count']}")
93
+ print(f"VIEW directives: {result['view_count']}")
94
+ print(f"VIEW coverage: {result['view_coverage_percent']}%")
95
+ print(f"Reversible: {result['reversible']}")
96
+ print("\nSections:")
97
+ for s in result['sections']:
98
+ print(f" {s['id']}: {s['entry_count']} entries")
99
+ print("\nSigil distribution:")
100
+ for sig, cnt in list(result['sigil_distribution'].items())[:15]:
101
+ print(f" {sig}: {cnt}")
102
+ print("\nEntry type distribution:")
103
+ for t, cnt in result['entry_type_distribution'].items():
104
+ print(f" {t}: {cnt}")
105
+ else:
106
+ print(f"Header: reversible={result['header']['reversible']}, view_coverage={result['header']['view_coverage']}")
107
+ print(f"Blocks: {result['block_count']}")
108
+ print(f"Orphan lines: {result['orphan_lines']}")
109
+ print("\nBlocks:")
110
+ for b in result['blocks']:
111
+ print(f" {b['view_name']}: kind={b['kind']}, target={b['target']}, reverse={b['reverse']}")
112
+
113
+ return 0
@@ -0,0 +1,104 @@
1
+ """``cortex roundtrip`` — verify CORTEX v2 roundtrip fidelity.
2
+
3
+ Canonical name: ``roundtrip`` (since v0.3.2).
4
+ Deprecated alias: ``v2-roundtrip`` (still accepted).
5
+
6
+ v2.0.1: compares bytes (not text) using read_bytes(), reports stat bytes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+
14
+ from ...core.errors import CortexError
15
+ from ...v2.parser import parse_cortex_v2
16
+ from ...v2.writer import write_cortex_v2
17
+
18
+
19
+ def run(args) -> int:
20
+ if not os.path.exists(args.input):
21
+ raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
22
+
23
+ # v2.0.1: read as BYTES, not text, to catch CRLF and other encoding differences
24
+ with open(args.input, "rb") as f:
25
+ original_bytes = f.read()
26
+
27
+ # Parse from text (UTF-8 decoded) — the parser works on text
28
+ text = original_bytes.decode("utf-8")
29
+ doc = parse_cortex_v2(text)
30
+ reproduced_text = write_cortex_v2(doc)
31
+ reproduced_bytes = reproduced_text.encode("utf-8")
32
+
33
+ # v2.0.1: compare BYTES, not text
34
+ stat_bytes = os.path.getsize(args.input)
35
+ text_chars = len(text)
36
+ repro_bytes = len(reproduced_bytes)
37
+
38
+ if original_bytes == reproduced_bytes:
39
+ if args.format == "json":
40
+ print(json.dumps({
41
+ "ok": True,
42
+ "input": args.input,
43
+ "sections": len(doc.sections),
44
+ "entries": sum(len(s.entries) for s in doc.sections),
45
+ "stat_bytes": stat_bytes,
46
+ "text_chars": text_chars,
47
+ "repro_bytes": repro_bytes,
48
+ "roundtrip": "byte-identical",
49
+ }, indent=2))
50
+ else:
51
+ print(f"✓ Roundtrip byte-identical: {args.input}")
52
+ print(f" sections: {len(doc.sections)}")
53
+ print(f" entries: {sum(len(s.entries) for s in doc.sections)}")
54
+ print(f" stat_bytes: {stat_bytes}")
55
+ print(f" text_chars: {text_chars}")
56
+ print(f" repro_bytes: {repro_bytes}")
57
+ return 0
58
+ else:
59
+ # Find first byte difference
60
+ diff_offset = -1
61
+ for i in range(min(len(original_bytes), len(reproduced_bytes))):
62
+ if original_bytes[i] != reproduced_bytes[i]:
63
+ diff_offset = i
64
+ break
65
+
66
+ # Also find first text line difference for human readability
67
+ orig_lines = text.split('\n')
68
+ repro_lines = reproduced_text.split('\n')
69
+ diff_line = -1
70
+ for i in range(min(len(orig_lines), len(repro_lines))):
71
+ if orig_lines[i] != repro_lines[i]:
72
+ diff_line = i + 1
73
+ break
74
+
75
+ if args.format == "json":
76
+ print(json.dumps({
77
+ "ok": False,
78
+ "input": args.input,
79
+ "stat_bytes": stat_bytes,
80
+ "repro_bytes": repro_bytes,
81
+ "text_chars": text_chars,
82
+ "orig_lines": len(orig_lines),
83
+ "repro_lines": len(repro_lines),
84
+ "first_byte_diff": diff_offset,
85
+ "first_line_diff": diff_line,
86
+ "orig_excerpt": orig_lines[diff_line - 1][:120] if diff_line > 0 else "",
87
+ "repro_excerpt": repro_lines[diff_line - 1][:120] if diff_line > 0 else "",
88
+ }, indent=2))
89
+ else:
90
+ print(f"✗ Roundtrip FAILED: {args.input}")
91
+ print(f" stat_bytes: {stat_bytes}")
92
+ print(f" repro_bytes: {repro_bytes}")
93
+ print(f" text_chars: {text_chars}")
94
+ print(f" orig_lines: {len(orig_lines)}")
95
+ print(f" repro_lines: {len(repro_lines)}")
96
+ if diff_byte := diff_offset:
97
+ print(f" first byte diff at offset {diff_byte}:")
98
+ print(f" orig: 0x{original_bytes[diff_byte]:02x}")
99
+ print(f" repro: 0x{reproduced_bytes[diff_byte]:02x}")
100
+ if diff_line > 0:
101
+ print(f" first line diff at line {diff_line}:")
102
+ print(f" orig: {orig_lines[diff_line - 1][:120]!r}")
103
+ print(f" repro: {repro_lines[diff_line - 1][:120]!r}")
104
+ return 1