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