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
|
@@ -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
|