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,44 @@
|
|
|
1
|
+
"""``cortex list`` — list entries in a .cortex file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ...crud.selectors import select
|
|
8
|
+
from ..commands import load_doc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(args) -> int:
|
|
12
|
+
doc = load_doc(args.input)
|
|
13
|
+
# Build selector from filters
|
|
14
|
+
if args.section and args.sigil:
|
|
15
|
+
sel = f"{args.section}/*"
|
|
16
|
+
# narrow by sigil after the fact
|
|
17
|
+
entries = [e for e in select(doc, sel) if e.sigil == args.sigil]
|
|
18
|
+
elif args.section:
|
|
19
|
+
entries = select(doc, f"{args.section}/*")
|
|
20
|
+
elif args.sigil:
|
|
21
|
+
entries = select(doc, f"{args.sigil}:*")
|
|
22
|
+
else:
|
|
23
|
+
entries = select(doc, "*:*")
|
|
24
|
+
|
|
25
|
+
if args.format == "json":
|
|
26
|
+
print(json.dumps({
|
|
27
|
+
"ok": True,
|
|
28
|
+
"count": len(entries),
|
|
29
|
+
"entries": [e.to_dict() for e in entries],
|
|
30
|
+
}, indent=2, ensure_ascii=False, default=str))
|
|
31
|
+
else:
|
|
32
|
+
if not entries:
|
|
33
|
+
print("(no entries match)")
|
|
34
|
+
return 0
|
|
35
|
+
lines = []
|
|
36
|
+
lines.append(f"{'SECTION':<6} {'SIGIL':<6} {'NAME':<24} {'TYPE':<10} {'HASH':<16}")
|
|
37
|
+
lines.append("-" * 70)
|
|
38
|
+
for e in entries:
|
|
39
|
+
lines.append(
|
|
40
|
+
f"{e.section:<6} {e.sigil:<6} {e.name:<24} {e.type:<10} "
|
|
41
|
+
f"{e.hash[:16] if e.hash else '':<16}"
|
|
42
|
+
)
|
|
43
|
+
print("\n".join(lines))
|
|
44
|
+
return 0
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""``cortex micro`` — micro-token CRUD."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ...core.errors import CortexError
|
|
8
|
+
from ...crud.mutations import (
|
|
9
|
+
add_micro_to_glossary,
|
|
10
|
+
delete_micro_from_glossary,
|
|
11
|
+
update_micro_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
|
+
micro = list(doc.glossary.micro.values())
|
|
20
|
+
if args.format == "json":
|
|
21
|
+
print(json.dumps({
|
|
22
|
+
"ok": True,
|
|
23
|
+
"count": len(micro),
|
|
24
|
+
"micro": [m.to_dict() for m in micro],
|
|
25
|
+
}, indent=2, default=str))
|
|
26
|
+
else:
|
|
27
|
+
if not micro:
|
|
28
|
+
print("(no micro-tokens declared)")
|
|
29
|
+
return 0
|
|
30
|
+
lines = [f"{'TOKEN':<10} VALUE", "-" * 40]
|
|
31
|
+
for m in micro:
|
|
32
|
+
lines.append(f"{m.token:<10} {m.value}")
|
|
33
|
+
print("\n".join(lines))
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_add(args) -> int:
|
|
38
|
+
doc = load_doc(args.input)
|
|
39
|
+
try:
|
|
40
|
+
add_micro_to_glossary(doc, token=args.token, value=args.value)
|
|
41
|
+
except CortexError as e:
|
|
42
|
+
print(f"error: {e}")
|
|
43
|
+
return 1
|
|
44
|
+
if args.dry_run:
|
|
45
|
+
print(json.dumps({"ok": True, "dry_run": True, "token": args.token}, indent=2))
|
|
46
|
+
return 0
|
|
47
|
+
result = atomic_write_cortex(doc, args.input, force=False)
|
|
48
|
+
print(json.dumps({"ok": True, "token": args.token, "written": result.to_dict()}, indent=2, default=str))
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_update(args) -> int:
|
|
53
|
+
doc = load_doc(args.input)
|
|
54
|
+
try:
|
|
55
|
+
update_micro_in_glossary(doc, token=args.token, value=args.value)
|
|
56
|
+
except CortexError as e:
|
|
57
|
+
print(f"error: {e}")
|
|
58
|
+
return 1
|
|
59
|
+
if args.dry_run:
|
|
60
|
+
print(json.dumps({"ok": True, "dry_run": True, "token": args.token}, indent=2))
|
|
61
|
+
return 0
|
|
62
|
+
result = atomic_write_cortex(doc, args.input, force=False)
|
|
63
|
+
print(json.dumps({"ok": True, "token": args.token, "written": result.to_dict()}, indent=2, default=str))
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_delete(args) -> int:
|
|
68
|
+
doc = load_doc(args.input)
|
|
69
|
+
try:
|
|
70
|
+
delete_micro_from_glossary(doc, token=args.token, force=args.force)
|
|
71
|
+
except CortexError as e:
|
|
72
|
+
print(f"error: {e}")
|
|
73
|
+
return 1
|
|
74
|
+
if args.dry_run:
|
|
75
|
+
print(json.dumps({"ok": True, "dry_run": True, "token": args.token}, indent=2))
|
|
76
|
+
return 0
|
|
77
|
+
result = atomic_write_cortex(doc, args.input, force=False)
|
|
78
|
+
print(json.dumps({"ok": True, "token": args.token, "written": result.to_dict()}, indent=2, default=str))
|
|
79
|
+
return 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""``cortex move`` — move an entry to another section.
|
|
2
|
+
|
|
3
|
+
v1.1.2: post-mutation validation via :func:`~cortex.cli.commands.post_mutation_gate`
|
|
4
|
+
(so moving an entry to a section that violates level policy is rejected
|
|
5
|
+
unless ``--force``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
from ...core.errors import CortexError
|
|
13
|
+
from ...crud.mutations import move_entry
|
|
14
|
+
from ...crud.transactions import atomic_write_cortex
|
|
15
|
+
from ..commands import load_doc, post_mutation_gate
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(args) -> int:
|
|
19
|
+
doc = load_doc(args.input)
|
|
20
|
+
try:
|
|
21
|
+
entry = move_entry(doc, args.selector, args.to_section)
|
|
22
|
+
except CortexError as e:
|
|
23
|
+
print(f"error: {e}")
|
|
24
|
+
return 1
|
|
25
|
+
|
|
26
|
+
if args.dry_run:
|
|
27
|
+
print(json.dumps({
|
|
28
|
+
"ok": True, "dry_run": True,
|
|
29
|
+
"moved": entry.to_dict(),
|
|
30
|
+
}, indent=2, default=str))
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
# v1.1.2: post-mutation validation gate (same as add).
|
|
34
|
+
err = post_mutation_gate(doc, args)
|
|
35
|
+
if err is not None:
|
|
36
|
+
print(json.dumps(err, indent=2, default=str))
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
result = atomic_write_cortex(doc, args.input, force=args.force)
|
|
40
|
+
print(json.dumps({
|
|
41
|
+
"ok": True,
|
|
42
|
+
"moved": entry.to_dict(),
|
|
43
|
+
"written": result.to_dict(),
|
|
44
|
+
}, indent=2, default=str))
|
|
45
|
+
return 0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""``cortex new`` — create a new .cortex file from a template.
|
|
2
|
+
|
|
3
|
+
v1.1.2: ``--json`` now produces real JSON output (was text-only before).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from ...core.errors import CortexError, TemplateUnknownError
|
|
12
|
+
from ...crud.transactions import atomic_write_cortex
|
|
13
|
+
from ...templates import build_brain, build_package, build_skill
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(args) -> int:
|
|
17
|
+
kind = args.kind
|
|
18
|
+
if kind == "brain":
|
|
19
|
+
doc = build_brain(
|
|
20
|
+
name=args.name or "project-brain",
|
|
21
|
+
domain=args.domain or "active work",
|
|
22
|
+
owner=args.owner or "agent",
|
|
23
|
+
language=args.language,
|
|
24
|
+
template=args.template,
|
|
25
|
+
with_diagrams=args.with_diagrams,
|
|
26
|
+
)
|
|
27
|
+
elif kind == "skill":
|
|
28
|
+
doc = build_skill(
|
|
29
|
+
name=args.name or "codec-cortex",
|
|
30
|
+
version=args.doc_version or "1.0.0",
|
|
31
|
+
domain=args.domain or "LLM/SLM contextual memory",
|
|
32
|
+
owner=args.owner or "agent",
|
|
33
|
+
language=args.language,
|
|
34
|
+
template=args.template,
|
|
35
|
+
with_diagrams=args.with_diagrams,
|
|
36
|
+
)
|
|
37
|
+
elif kind == "package":
|
|
38
|
+
doc = build_package(
|
|
39
|
+
name=args.name or "context_package",
|
|
40
|
+
version=args.doc_version or "0.1.0",
|
|
41
|
+
domain=args.domain or "specific domain",
|
|
42
|
+
owner=args.owner or "source",
|
|
43
|
+
language=args.language,
|
|
44
|
+
template=args.template,
|
|
45
|
+
with_diagrams=args.with_diagrams,
|
|
46
|
+
)
|
|
47
|
+
elif kind == "generic":
|
|
48
|
+
doc = build_brain(
|
|
49
|
+
name=args.name or "generic-cortex",
|
|
50
|
+
domain=args.domain or "generic",
|
|
51
|
+
owner=args.owner or "agent",
|
|
52
|
+
language=args.language,
|
|
53
|
+
template=args.template,
|
|
54
|
+
with_diagrams=args.with_diagrams,
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
raise TemplateUnknownError(kind)
|
|
58
|
+
|
|
59
|
+
out_path = args.out
|
|
60
|
+
if os.path.exists(out_path) and not args.force:
|
|
61
|
+
raise CortexError(
|
|
62
|
+
"E015_ATOMIC_WRITE_FAILED",
|
|
63
|
+
f"output file exists: {out_path} (use --force to overwrite)",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
result = atomic_write_cortex(doc, out_path, force=True, keep_backup=False)
|
|
67
|
+
payload = {
|
|
68
|
+
"ok": True,
|
|
69
|
+
"text": f"created {out_path} ({result.bytes_written} bytes)",
|
|
70
|
+
"path": out_path,
|
|
71
|
+
"bytes": result.bytes_written,
|
|
72
|
+
"kind": kind,
|
|
73
|
+
}
|
|
74
|
+
# v1.1.2: honour --json properly
|
|
75
|
+
json_mode = getattr(args, "_json_mode", False)
|
|
76
|
+
if json_mode:
|
|
77
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False, default=str))
|
|
78
|
+
else:
|
|
79
|
+
print(payload["text"])
|
|
80
|
+
return 0
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""``cortex recover`` — recover a legacy or non-conforming .cortex file.
|
|
2
|
+
|
|
3
|
+
Closes audit gap H-06: tolerates preambles, legacy column names, missing
|
|
4
|
+
``$0``, and reconstructs a conforming document with RSK diagnostics for
|
|
5
|
+
every reconstructed sigil.
|
|
6
|
+
|
|
7
|
+
v1.1.6 P1-5: recover now validates the result and returns non-zero if
|
|
8
|
+
the recovered artefact is not conformant (e.g. still has E033/E034 errors
|
|
9
|
+
that recovery cannot fix).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from ...core.errors import CortexError
|
|
19
|
+
from ...core.parser import parse_cortex
|
|
20
|
+
from ...core.writer import write_cortex
|
|
21
|
+
from ...core.validator import validate
|
|
22
|
+
from ...crud.transactions import atomic_write_text
|
|
23
|
+
from ...hcortex import recover_cortex
|
|
24
|
+
from ..commands import load_doc # noqa: F401 (re-exported helper)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(args) -> int:
|
|
28
|
+
if not os.path.exists(args.input):
|
|
29
|
+
raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
|
|
30
|
+
with open(args.input, "r", encoding="utf-8") as f:
|
|
31
|
+
text = f.read()
|
|
32
|
+
|
|
33
|
+
result = recover_cortex(
|
|
34
|
+
text, path=args.input, strict=args.strict,
|
|
35
|
+
embed_aud_rsk=getattr(args, "embed_aud_rsk", False),
|
|
36
|
+
)
|
|
37
|
+
cortex_text = write_cortex(result.doc)
|
|
38
|
+
|
|
39
|
+
# v1.1.6 P1-5: validate the recovered artefact. If it still has
|
|
40
|
+
# non-bypassable errors, return non-zero so CI and automation can
|
|
41
|
+
# detect that recovery did not produce a conformant file.
|
|
42
|
+
reparsed = parse_cortex(cortex_text, path=args.input)
|
|
43
|
+
post_diags = validate(reparsed, strict=False)
|
|
44
|
+
post_errors = [d for d in post_diags if d.get("severity") == "error"]
|
|
45
|
+
conformant = len(post_errors) == 0
|
|
46
|
+
|
|
47
|
+
if args.out:
|
|
48
|
+
# Write even if non-conformant — the user may need to inspect
|
|
49
|
+
# the partial recovery. But return non-zero to signal the issue.
|
|
50
|
+
atomic_write_text(cortex_text, args.out, keep_backup=False)
|
|
51
|
+
if args.format == "json":
|
|
52
|
+
print(json.dumps({
|
|
53
|
+
"ok": conformant,
|
|
54
|
+
"input": args.input,
|
|
55
|
+
"out": args.out,
|
|
56
|
+
"reconstructed_glossary": result.reconstructed_glossary,
|
|
57
|
+
"preamble_lines": len(result.preamble),
|
|
58
|
+
"diagnostics": result.diagnostics,
|
|
59
|
+
"post_validation_errors": post_errors if not conformant else [],
|
|
60
|
+
"conformant": conformant,
|
|
61
|
+
}, indent=2, default=str))
|
|
62
|
+
else:
|
|
63
|
+
print(f"recovered {args.input} → {args.out}")
|
|
64
|
+
print(f" preamble stripped: {len(result.preamble)} line(s)")
|
|
65
|
+
print(f" reconstructed $0: {result.reconstructed_glossary}")
|
|
66
|
+
print(f" diagnostics: {len(result.diagnostics)}")
|
|
67
|
+
for d in result.diagnostics:
|
|
68
|
+
sev = d.get("severity", "info")
|
|
69
|
+
print(f" [{sev}] {d.get('code')}: {d.get('message')}")
|
|
70
|
+
if not conformant:
|
|
71
|
+
print(f" ⚠ WARNING: recovered artefact has {len(post_errors)} "
|
|
72
|
+
"validation error(s) — NOT conformant")
|
|
73
|
+
for e in post_errors[:5]:
|
|
74
|
+
print(f" [{e.get('code')}] {e.get('message', '')[:100]}")
|
|
75
|
+
else:
|
|
76
|
+
if args.format == "json":
|
|
77
|
+
print(json.dumps({
|
|
78
|
+
"ok": conformant,
|
|
79
|
+
"input": args.input,
|
|
80
|
+
"reconstructed_glossary": result.reconstructed_glossary,
|
|
81
|
+
"preamble_lines": len(result.preamble),
|
|
82
|
+
"diagnostics": result.diagnostics,
|
|
83
|
+
"conformant": conformant,
|
|
84
|
+
"cortex": cortex_text,
|
|
85
|
+
}, indent=2, default=str))
|
|
86
|
+
else:
|
|
87
|
+
sys.stdout.write(cortex_text)
|
|
88
|
+
|
|
89
|
+
# v1.1.6 P1-5: non-zero if not conformant
|
|
90
|
+
return 0 if conformant else 1
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""``cortex render`` (alias: ``cortex decode``) — render .cortex to HCORTEX markdown.
|
|
2
|
+
|
|
3
|
+
v1.1.2: ``--json`` now produces real JSON output (was text-only before).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from ...core.errors import CortexError
|
|
13
|
+
from ...crud.transactions import atomic_write_text
|
|
14
|
+
from ...hcortex import render_hcortex_edit, render_hcortex_read
|
|
15
|
+
from ..commands import load_doc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(args) -> int:
|
|
19
|
+
doc = load_doc(args.input)
|
|
20
|
+
# v1.1.3 P2-8: default mode is 'readable' so `decode <file>` works
|
|
21
|
+
# without --mode (matches the SKILL §22.2 planned UX).
|
|
22
|
+
mode = (args.mode or "readable").lower()
|
|
23
|
+
|
|
24
|
+
# Mode normalisation: legacy 'read'/'edit' → canonical
|
|
25
|
+
if mode == "read":
|
|
26
|
+
mode = "readable"
|
|
27
|
+
# Map profile arg to uppercase
|
|
28
|
+
profile = args.profile.upper() if args.profile else None
|
|
29
|
+
|
|
30
|
+
if mode == "edit":
|
|
31
|
+
md = render_hcortex_edit(doc, source=args.input)
|
|
32
|
+
elif mode in ("readable", "audit", "recovery", "full"):
|
|
33
|
+
# Map mode aliases to profile + with_source
|
|
34
|
+
if mode == "recovery":
|
|
35
|
+
profile = profile or "RECOVERY"
|
|
36
|
+
elif mode == "full":
|
|
37
|
+
profile = profile or "FULL"
|
|
38
|
+
args.with_source = True
|
|
39
|
+
elif mode == "audit":
|
|
40
|
+
args.with_source = True
|
|
41
|
+
md = render_hcortex_read(
|
|
42
|
+
doc,
|
|
43
|
+
with_source=getattr(args, "with_source", False),
|
|
44
|
+
profile=profile,
|
|
45
|
+
mode=mode.upper() if mode in ("recovery", "full") else "READABLE",
|
|
46
|
+
layout=getattr(args, "layout", None),
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
raise CortexError(
|
|
50
|
+
"E021_INVALID_VALUE",
|
|
51
|
+
f"unknown render mode {args.mode!r}",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
out = args.out
|
|
55
|
+
json_mode = getattr(args, "_json_mode", False)
|
|
56
|
+
if out:
|
|
57
|
+
# If --out exists and no --force, error (audit gap L-03).
|
|
58
|
+
if os.path.exists(out) and not getattr(args, "force", False):
|
|
59
|
+
raise CortexError(
|
|
60
|
+
"E015_ATOMIC_WRITE_FAILED",
|
|
61
|
+
f"output file exists: {out} (use --force to overwrite)",
|
|
62
|
+
)
|
|
63
|
+
result = atomic_write_text(md, out, keep_backup=False)
|
|
64
|
+
payload = {
|
|
65
|
+
"ok": True,
|
|
66
|
+
"text": (
|
|
67
|
+
f"rendered {args.input} → {out} "
|
|
68
|
+
f"({result.bytes_written} bytes, mode={mode}"
|
|
69
|
+
f"{f', profile={profile}' if profile else ''})"
|
|
70
|
+
),
|
|
71
|
+
"input": args.input,
|
|
72
|
+
"out": out,
|
|
73
|
+
"mode": mode,
|
|
74
|
+
"profile": profile,
|
|
75
|
+
"bytes": result.bytes_written,
|
|
76
|
+
}
|
|
77
|
+
if json_mode:
|
|
78
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False, default=str))
|
|
79
|
+
else:
|
|
80
|
+
print(payload["text"])
|
|
81
|
+
else:
|
|
82
|
+
# No --out: write markdown to stdout. --json wraps it in a payload.
|
|
83
|
+
if json_mode:
|
|
84
|
+
print(json.dumps({
|
|
85
|
+
"ok": True,
|
|
86
|
+
"input": args.input,
|
|
87
|
+
"mode": mode,
|
|
88
|
+
"profile": profile,
|
|
89
|
+
"markdown": md,
|
|
90
|
+
}, indent=2, ensure_ascii=False, default=str))
|
|
91
|
+
else:
|
|
92
|
+
sys.stdout.write(md)
|
|
93
|
+
return 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""``cortex update`` (alias: ``cortex patch_update``) — update an existing entry.
|
|
2
|
+
|
|
3
|
+
v1.1.2: post-mutation validation via :func:`~cortex.cli.commands.post_mutation_gate`
|
|
4
|
+
(so an update that breaks a contract is rejected unless ``--force``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from ...core.errors import CortexError
|
|
13
|
+
from ...crud.mutations import update_entry
|
|
14
|
+
from ...crud.transactions import atomic_write_cortex
|
|
15
|
+
from ..commands import load_doc, post_mutation_gate
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_SET_PAIR_RE = re.compile(r'^(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<val>.*)$')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_set_pairs(pairs):
|
|
22
|
+
out = {}
|
|
23
|
+
for p in pairs:
|
|
24
|
+
m = _SET_PAIR_RE.match(p)
|
|
25
|
+
if not m:
|
|
26
|
+
raise CortexError("E021_INVALID_VALUE", f"invalid --set pair: {p!r} (expected key=value)")
|
|
27
|
+
key = m.group("key")
|
|
28
|
+
val = m.group("val")
|
|
29
|
+
# Strip surrounding quotes
|
|
30
|
+
if len(val) >= 2 and val[0] == val[-1] and val[0] in ('"', "'"):
|
|
31
|
+
val = val[1:-1]
|
|
32
|
+
elif val == "true":
|
|
33
|
+
val = True
|
|
34
|
+
elif val == "false":
|
|
35
|
+
val = False
|
|
36
|
+
elif val.lower() in ("null", "none", "nil", "undefined"):
|
|
37
|
+
# v1.1.7 P0-3 + v1.1.8: convert null-like literals to None
|
|
38
|
+
val = None
|
|
39
|
+
elif re.fullmatch(r"-?\d+", val):
|
|
40
|
+
val = int(val)
|
|
41
|
+
elif re.fullmatch(r"-?\d+\.\d+", val):
|
|
42
|
+
val = float(val)
|
|
43
|
+
out[key] = val
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run(args) -> int:
|
|
48
|
+
doc = load_doc(args.input)
|
|
49
|
+
set_ = _parse_set_pairs(args.set_pairs) if args.set_pairs else None
|
|
50
|
+
try:
|
|
51
|
+
entry = update_entry(
|
|
52
|
+
doc,
|
|
53
|
+
args.selector,
|
|
54
|
+
set_=set_,
|
|
55
|
+
replace_body=args.body,
|
|
56
|
+
append=args.append,
|
|
57
|
+
)
|
|
58
|
+
except CortexError as e:
|
|
59
|
+
print(f"error: {e}")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
if args.dry_run:
|
|
63
|
+
print(json.dumps({
|
|
64
|
+
"ok": True, "dry_run": True,
|
|
65
|
+
"entry": entry.to_dict(),
|
|
66
|
+
}, indent=2, default=str))
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
# v1.1.2: post-mutation validation gate (same as add).
|
|
70
|
+
err = post_mutation_gate(doc, args)
|
|
71
|
+
if err is not None:
|
|
72
|
+
print(json.dumps(err, indent=2, default=str))
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
result = atomic_write_cortex(doc, args.input, force=args.force)
|
|
76
|
+
print(json.dumps({
|
|
77
|
+
"ok": True,
|
|
78
|
+
"entry": entry.to_dict(),
|
|
79
|
+
"written": result.to_dict(),
|
|
80
|
+
}, indent=2, default=str))
|
|
81
|
+
return 0
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""``cortex canonicalize`` — normalize artefacts without changing semantics.
|
|
2
|
+
|
|
3
|
+
Canonical name: ``canonicalize`` (since v0.3.2).
|
|
4
|
+
Deprecated alias: ``v2-canonicalize`` (still accepted).
|
|
5
|
+
|
|
6
|
+
v0.3.2 — VIEW-aware behavior (B-01/B-05 fix):
|
|
7
|
+
|
|
8
|
+
The previous implementation always rewrote the .cortex using
|
|
9
|
+
:func:`write_cortex_v2`, which produced a canonically-formatted v2
|
|
10
|
+
artefact. That broke v1-render compatibility for artefacts that did
|
|
11
|
+
not declare VIEW directives (notably the benchmark corpus), causing
|
|
12
|
+
BCFNR=1.0 and HCORTEX empty output (issues B-01, B-03, B-05).
|
|
13
|
+
|
|
14
|
+
The new behavior is:
|
|
15
|
+
|
|
16
|
+
1. Parse the artefact and check whether it has any operational VIEW
|
|
17
|
+
directives (``view.parse_view_entries_from_doc`` semantics).
|
|
18
|
+
2. If it has VIEW directives AND ``--preserve`` was NOT passed:
|
|
19
|
+
- Apply full canonicalization (the original behavior, via
|
|
20
|
+
:func:`write_cortex_v2`). This is safe because the VIEW
|
|
21
|
+
directives guarantee reversibility.
|
|
22
|
+
3. If it does NOT have VIEW directives, OR ``--preserve`` was passed:
|
|
23
|
+
- Emit a warning to stderr explaining that the artefact lacks
|
|
24
|
+
VIEW directives and that only whitespace/section ordering will
|
|
25
|
+
be normalized (structure preserved).
|
|
26
|
+
- Use :func:`write_cortex_v2_preserve` to produce a
|
|
27
|
+
structure-preserving serialization. This keeps v1-render
|
|
28
|
+
compatibility.
|
|
29
|
+
4. In all cases, return 0 on success.
|
|
30
|
+
|
|
31
|
+
The ``--preserve`` flag is an explicit escape hatch for users who want
|
|
32
|
+
to force the structure-preserving path even when VIEW directives are
|
|
33
|
+
present (e.g. when preparing an artefact for a v1-only consumer).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
from ...core.errors import CortexError
|
|
42
|
+
from ...v2.parser import parse_cortex_v2
|
|
43
|
+
from ...v2.writer import (
|
|
44
|
+
write_cortex_v2,
|
|
45
|
+
write_cortex_v2_preserve,
|
|
46
|
+
has_view_directives,
|
|
47
|
+
)
|
|
48
|
+
from ...v2.view_renderer import render_hcortex
|
|
49
|
+
from ...v2.hcortex_parser import parse_hcortex
|
|
50
|
+
from ...v2.encoder import encode_cortex_from_ast
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _emit_warning(msg: str) -> None:
|
|
54
|
+
"""Emit a warning to stderr (visible even when stdout is redirected)."""
|
|
55
|
+
print(f"WARNING: {msg}", file=sys.stderr)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _canonicalize_cortex(text: str, preserve: bool) -> tuple[str, list[str]]:
|
|
59
|
+
"""Canonicalize a CORTEX (non-HCORTEX) artefact.
|
|
60
|
+
|
|
61
|
+
Returns ``(output_text, warnings)``.
|
|
62
|
+
"""
|
|
63
|
+
warnings: list[str] = []
|
|
64
|
+
doc = parse_cortex_v2(text)
|
|
65
|
+
|
|
66
|
+
has_views = has_view_directives(doc)
|
|
67
|
+
|
|
68
|
+
if preserve:
|
|
69
|
+
warnings.append(
|
|
70
|
+
"--preserve requested: structure-preserving canonicalization "
|
|
71
|
+
"(whitespace + section ordering only). VIEW directives, if any, "
|
|
72
|
+
"are NOT used for canonical rendering."
|
|
73
|
+
)
|
|
74
|
+
out = write_cortex_v2_preserve(doc)
|
|
75
|
+
return out, warnings
|
|
76
|
+
|
|
77
|
+
if not has_views:
|
|
78
|
+
warnings.append(
|
|
79
|
+
"artefact has no VIEW directives: applying structure-preserving "
|
|
80
|
+
"canonicalization (whitespace + section ordering only). The "
|
|
81
|
+
"output remains v1-render compatible. Add VIEW directives to "
|
|
82
|
+
"enable full v2 canonicalization, or pass --preserve to silence "
|
|
83
|
+
"this warning."
|
|
84
|
+
)
|
|
85
|
+
out = write_cortex_v2_preserve(doc)
|
|
86
|
+
return out, warnings
|
|
87
|
+
|
|
88
|
+
# Default path: VIEW directives present, no --preserve → full canonicalize.
|
|
89
|
+
out = write_cortex_v2(doc)
|
|
90
|
+
return out, warnings
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _canonicalize_hcortex(text: str, preserve: bool) -> tuple[str, list[str]]:
|
|
94
|
+
"""Canonicalize an HCORTEX artefact.
|
|
95
|
+
|
|
96
|
+
HCORTEX → CORTEX → HCORTEX (canonicalize both legs).
|
|
97
|
+
|
|
98
|
+
The same VIEW-aware logic applies on the intermediate CORTEX: if it
|
|
99
|
+
has no VIEW directives, we fall back to the structure-preserving
|
|
100
|
+
serializer for the CORTEX leg, then render HCORTEX from it.
|
|
101
|
+
"""
|
|
102
|
+
warnings: list[str] = []
|
|
103
|
+
hdoc = parse_hcortex(text, strict=False)
|
|
104
|
+
doc, _ = encode_cortex_from_ast(hdoc)
|
|
105
|
+
|
|
106
|
+
has_views = has_view_directives(doc)
|
|
107
|
+
|
|
108
|
+
if preserve or not has_views:
|
|
109
|
+
if preserve:
|
|
110
|
+
warnings.append(
|
|
111
|
+
"--preserve requested: HCORTEX canonicalization will use "
|
|
112
|
+
"the structure-preserving CORTEX leg."
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
warnings.append(
|
|
116
|
+
"HCORTEX artefact has no VIEW directives (after decoding to "
|
|
117
|
+
"CORTEX): applying structure-preserving canonicalization on "
|
|
118
|
+
"the CORTEX leg. HCORTEX output will be re-rendered from "
|
|
119
|
+
"the preserved CORTEX."
|
|
120
|
+
)
|
|
121
|
+
# Use preserved CORTEX, then re-render HCORTEX
|
|
122
|
+
from ...v2.writer import write_cortex_v2_preserve
|
|
123
|
+
_ = write_cortex_v2_preserve(doc) # no-op effect, kept for symmetry
|
|
124
|
+
hcortex_md, _ = render_hcortex(doc)
|
|
125
|
+
return hcortex_md, warnings
|
|
126
|
+
|
|
127
|
+
# Default path: full canonicalize
|
|
128
|
+
cortex_text = write_cortex_v2(doc)
|
|
129
|
+
hcortex_md, _ = render_hcortex(doc)
|
|
130
|
+
_ = cortex_text # generated for symmetry; HCORTEX is the output
|
|
131
|
+
return hcortex_md, warnings
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run(args) -> int:
|
|
135
|
+
if not os.path.exists(args.input):
|
|
136
|
+
raise CortexError("E013_NOT_FOUND", f"file not found: {args.input}")
|
|
137
|
+
|
|
138
|
+
with open(args.input, "r", encoding="utf-8") as f:
|
|
139
|
+
text = f.read()
|
|
140
|
+
|
|
141
|
+
preserve = bool(getattr(args, "preserve", False))
|
|
142
|
+
|
|
143
|
+
is_hcortex = "internal_encoding: HCORTEX" in text
|
|
144
|
+
|
|
145
|
+
if is_hcortex:
|
|
146
|
+
out_text, warnings = _canonicalize_hcortex(text, preserve)
|
|
147
|
+
kind = "HCORTEX"
|
|
148
|
+
else:
|
|
149
|
+
out_text, warnings = _canonicalize_cortex(text, preserve)
|
|
150
|
+
kind = "CORTEX"
|
|
151
|
+
|
|
152
|
+
for w in warnings:
|
|
153
|
+
_emit_warning(w)
|
|
154
|
+
|
|
155
|
+
if args.out:
|
|
156
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
157
|
+
f.write(out_text)
|
|
158
|
+
print(f"canonicalized {kind}: {args.input} → {args.out}")
|
|
159
|
+
else:
|
|
160
|
+
sys.stdout.write(out_text)
|
|
161
|
+
|
|
162
|
+
return 0
|