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,81 @@
|
|
|
1
|
+
"""Markdown model for HCORTEX renderers.
|
|
2
|
+
|
|
3
|
+
Defines the small set of markers used by the EDIT renderer to embed
|
|
4
|
+
structural metadata that the EDIT parser can recover losslessly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Header lines (first line of file)
|
|
15
|
+
HCORTEX_READ_HEADER = "<!-- cortex-render: hcortex-read; roundtrip: false -->"
|
|
16
|
+
HCORTEX_EDIT_HEADER = "<!-- cortex-render: hcortex-edit; roundtrip: structural; source: {source}; hash: {hash} -->"
|
|
17
|
+
|
|
18
|
+
# Section marker: <!-- cortex-section: id="$2" title="ACTIVE WORK" -->
|
|
19
|
+
SECTION_MARKER_RE = re.compile(
|
|
20
|
+
r'<!--\s*cortex-section:\s*id="(?P<id>[^"]+)"(?:\s+title="(?P<title>[^"]*)")?\s*-->'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Entry marker: <!-- cortex-entry: section="$2" sigil="FCS" name="primary" type="attrs" hash="sha256:..." -->
|
|
24
|
+
ENTRY_MARKER_RE = re.compile(
|
|
25
|
+
r'<!--\s*cortex-entry:\s*section="(?P<section>[^"]+)"\s+sigil="(?P<sigil>[^"]+)"\s+'
|
|
26
|
+
r'name="(?P<name>[^"]+)"\s+type="(?P<type>[^"]+)"'
|
|
27
|
+
r'(?:\s+hash="(?P<hash>[^"]+)")?\s*-->'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Glossary block: ```cortex-glossary
|
|
31
|
+
GLOSARY_FENCE_RE = re.compile(r"```cortex-glossary\s*\n(?P<body>.*?)\n```", re.DOTALL)
|
|
32
|
+
|
|
33
|
+
# Verbatim fence: ```puml, ```plantuml, ```code, etc.
|
|
34
|
+
VERBATIM_FENCE_RE = re.compile(r"```(?P<lang>[a-zA-Z0-9_\-]+)\s*\n(?P<body>.*?)\n```", re.DOTALL)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class EditHeader:
|
|
39
|
+
"""Parsed HCORTEX-EDIT first-line header."""
|
|
40
|
+
|
|
41
|
+
source: str
|
|
42
|
+
hash: str
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def parse(cls, line: str) -> Optional["EditHeader"]:
|
|
46
|
+
if "cortex-render: hcortex-edit" not in line:
|
|
47
|
+
return None
|
|
48
|
+
m = re.search(r'source:\s*([^;]+?)\s*;', line)
|
|
49
|
+
h = re.search(r'hash:\s*(\S+)', line)
|
|
50
|
+
if not m or not h:
|
|
51
|
+
return None
|
|
52
|
+
return cls(source=m.group(1).strip(), hash=h.group(1).strip())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_hcortex_read(line: str) -> bool:
|
|
56
|
+
return "cortex-render: hcortex-read" in line
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_hcortex_edit(line: str) -> bool:
|
|
60
|
+
return "cortex-render: hcortex-edit" in line
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_hcortex_read_in_text(text: str, max_lines: int = 5) -> bool:
|
|
64
|
+
"""Check if any of the first ``max_lines`` lines declares hcortex-read.
|
|
65
|
+
|
|
66
|
+
v1.1.3 P1-5: the ``Perfil:`` line is now the first real line, so the
|
|
67
|
+
``cortex-render`` marker moved to the second line. Callers that
|
|
68
|
+
need to detect HCORTEX-READ must scan a few lines, not just the first.
|
|
69
|
+
"""
|
|
70
|
+
for line in text.splitlines()[:max_lines]:
|
|
71
|
+
if is_hcortex_read(line):
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_hcortex_edit_in_text(text: str, max_lines: int = 5) -> bool:
|
|
77
|
+
"""Check if any of the first ``max_lines`` lines declares hcortex-edit."""
|
|
78
|
+
for line in text.splitlines()[:max_lines]:
|
|
79
|
+
if is_hcortex_edit(line):
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""HCORTEX profile resolver and priority classifier.
|
|
2
|
+
|
|
3
|
+
Implements audit gaps B-003, B-004, H-03:
|
|
4
|
+
|
|
5
|
+
- ``ProfileResolver`` maps a profile name to a P-level budget:
|
|
6
|
+
MIN → P0
|
|
7
|
+
RECOVERY → P0 + P1
|
|
8
|
+
WORK → P0 + P1 + P2
|
|
9
|
+
FULL → P0..P5
|
|
10
|
+
- ``PriorityClassifier`` assigns each entry a P-level based on its sigil
|
|
11
|
+
and ``survive``/``severity`` attributes, per Section 11.2 of SKILL.md.
|
|
12
|
+
- ``filter_by_profile()`` returns the entries that survive the profile.
|
|
13
|
+
- ``sort_by_plevel()`` orders entries P0 → P5.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from ..core.ast import CortexDocument, Entry
|
|
22
|
+
from ..core.errors import (
|
|
23
|
+
PLEVEL_ORDER,
|
|
24
|
+
SIGIL_DEFAULT_PLEVEL,
|
|
25
|
+
SURVIVE_TO_PLEVEL,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Profile definitions (Section 11.4 of SKILL.md)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Profile:
|
|
35
|
+
name: str # MIN | RECOVERY | WORK | FULL
|
|
36
|
+
plevels: Tuple[str, ...] # P-levels included
|
|
37
|
+
token_budget: Optional[int] = None # orientative; None = unlimited
|
|
38
|
+
|
|
39
|
+
def includes(self, plevel: str) -> bool:
|
|
40
|
+
return plevel in self.plevels
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
PROFILES: Dict[str, Profile] = {
|
|
44
|
+
"MIN": Profile("MIN", ("P0",), 512),
|
|
45
|
+
"RECOVERY": Profile("RECOVERY", ("P0", "P1"), 1024),
|
|
46
|
+
"WORK": Profile("WORK", ("P0", "P1", "P2"), 3072),
|
|
47
|
+
"FULL": Profile("FULL", tuple(PLEVEL_ORDER), None),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Order of preference for default resolution (Section 9.3 of SKILL.md):
|
|
51
|
+
# explicit > budget > mode > CORTEX-WORK
|
|
52
|
+
DEFAULT_PROFILE = "WORK"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_profile(name: Optional[str] = None) -> Profile:
|
|
56
|
+
"""Resolve a profile name to a :class:`Profile`.
|
|
57
|
+
|
|
58
|
+
Unknown names raise ``ValueError``. ``None`` returns the default
|
|
59
|
+
(``WORK``).
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
if name is None:
|
|
63
|
+
return PROFILES[DEFAULT_PROFILE]
|
|
64
|
+
key = name.upper()
|
|
65
|
+
if key not in PROFILES:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"unknown HCORTEX profile {name!r}; "
|
|
68
|
+
f"expected one of {sorted(PROFILES)}"
|
|
69
|
+
)
|
|
70
|
+
return PROFILES[key]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Priority classifier
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def classify_entry(entry: Entry) -> str:
|
|
78
|
+
"""Return the P-level (``P0``..``P5``) for ``entry``.
|
|
79
|
+
|
|
80
|
+
Rules (Section 11.2 + 11.3 of SKILL.md):
|
|
81
|
+
1. ``survive:min`` → P0
|
|
82
|
+
2. ``survive:recovery``→ P1
|
|
83
|
+
3. ``survive:work`` → P2
|
|
84
|
+
4. ``survive:full`` → P5
|
|
85
|
+
5. ``severity:blocking`` on CNST → P0 (overrides sigil default)
|
|
86
|
+
6. Otherwise: sigil default (see :data:`SIGIL_DEFAULT_PLEVEL`)
|
|
87
|
+
7. Unknown sigil: P5 (lowest priority)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if isinstance(entry.value, dict):
|
|
91
|
+
survive = entry.value.get("survive")
|
|
92
|
+
if isinstance(survive, str) and survive in SURVIVE_TO_PLEVEL:
|
|
93
|
+
# `min` always wins over everything else (it's the most critical)
|
|
94
|
+
return SURVIVE_TO_PLEVEL[survive]
|
|
95
|
+
severity = entry.value.get("severity")
|
|
96
|
+
if severity == "blocking":
|
|
97
|
+
return "P0"
|
|
98
|
+
return SIGIL_DEFAULT_PLEVEL.get(entry.sigil, "P5")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def classify_doc(doc: CortexDocument) -> Dict[str, List[Tuple[Entry, str]]]:
|
|
102
|
+
"""Return ``{section_id: [(entry, plevel), ...]}`` for the whole doc."""
|
|
103
|
+
|
|
104
|
+
out: Dict[str, List[Tuple[Entry, str]]] = {}
|
|
105
|
+
for sec in doc.sections:
|
|
106
|
+
if sec.id == "$0":
|
|
107
|
+
continue
|
|
108
|
+
out[sec.id] = [(e, classify_entry(e)) for e in sec.entries]
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def plevel_rank(plevel: str) -> int:
|
|
113
|
+
"""Return sort rank for a P-level (P0=0, P5=5)."""
|
|
114
|
+
|
|
115
|
+
if plevel in PLEVEL_ORDER:
|
|
116
|
+
return PLEVEL_ORDER.index(plevel)
|
|
117
|
+
return len(PLEVEL_ORDER)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Profile filtering
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ProfileFilterResult:
|
|
126
|
+
"""Result of filtering a document by profile.
|
|
127
|
+
|
|
128
|
+
``kept`` contains the entries that survived; ``omitted`` lists the
|
|
129
|
+
entries that were dropped (with reason) so the renderer can declare
|
|
130
|
+
them explicitly per Section 9.3 of the SKILL.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
kept: List[Tuple[str, Entry, str]] # (section_id, entry, plevel)
|
|
134
|
+
omitted: List[Tuple[str, Entry, str, str]] # (section_id, entry, plevel, reason)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def filter_by_profile(doc: CortexDocument, profile: Profile) -> ProfileFilterResult:
|
|
138
|
+
"""Filter ``doc`` entries by ``profile``, preserving section grouping.
|
|
139
|
+
|
|
140
|
+
The output is flat (list of ``(section_id, entry, plevel)``) but
|
|
141
|
+
keeps section grouping via the ``section_id`` field. The renderer
|
|
142
|
+
decides whether to render per-section or flat.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
kept: List[Tuple[str, Entry, str]] = []
|
|
146
|
+
omitted: List[Tuple[str, Entry, str, str]] = []
|
|
147
|
+
for sec in doc.sections:
|
|
148
|
+
if sec.id == "$0":
|
|
149
|
+
continue
|
|
150
|
+
for entry in sec.entries:
|
|
151
|
+
plevel = classify_entry(entry)
|
|
152
|
+
if profile.includes(plevel):
|
|
153
|
+
kept.append((sec.id, entry, plevel))
|
|
154
|
+
else:
|
|
155
|
+
omitted.append((sec.id, entry, plevel, f"excluded by profile {profile.name}"))
|
|
156
|
+
# Sort kept by P0..P5 (within each section, entries keep original order)
|
|
157
|
+
kept.sort(key=lambda t: plevel_rank(t[2]))
|
|
158
|
+
return ProfileFilterResult(kept=kept, omitted=omitted)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def sort_by_plevel(
|
|
162
|
+
entries: List[Tuple[str, Entry, str]],
|
|
163
|
+
) -> List[Tuple[str, Entry, str]]:
|
|
164
|
+
"""Stable-sort entries by P-level (P0 first, P5 last)."""
|
|
165
|
+
|
|
166
|
+
return sorted(entries, key=lambda t: plevel_rank(t[2]))
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""HCORTEX-READ renderer — human-friendly Markdown with profile support.
|
|
2
|
+
|
|
3
|
+
Per Section 8.1 and 9 of the SKILL:
|
|
4
|
+
- First line declares ``Perfil: CORTEX-<LEVEL>``.
|
|
5
|
+
- Filters entries by P-level/survive according to the profile.
|
|
6
|
+
- Orders entries P0 → P5.
|
|
7
|
+
- In ``AUDIT`` mode, adds a ``source`` column with ``<SIGIL>:<name>``.
|
|
8
|
+
- Declares any omissions by profile budget.
|
|
9
|
+
- Does NOT guarantee reconstruction (roundtrip:false).
|
|
10
|
+
|
|
11
|
+
Modes (Section 9.2):
|
|
12
|
+
- ``READABLE`` — clean executive view, no source column
|
|
13
|
+
- ``AUDIT`` — adds ``source`` column for traceability
|
|
14
|
+
- ``RECOVERY`` — only P0/P1 entries (alias for profile=RECOVERY + READABLE)
|
|
15
|
+
- ``FULL`` — all entries (alias for profile=FULL + AUDIT)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from ..core.ast import CortexDocument, Entry, Glossary
|
|
23
|
+
from ..core.writer import serialize_value
|
|
24
|
+
from .markdown_model import HCORTEX_READ_HEADER
|
|
25
|
+
from .profiles import (
|
|
26
|
+
filter_by_profile,
|
|
27
|
+
resolve_profile,
|
|
28
|
+
sort_by_plevel,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Human-friendly section titles (when the .cortex title is empty)
|
|
33
|
+
SECTION_TITLES = {
|
|
34
|
+
"$0": "Glossary (omitted)",
|
|
35
|
+
"$1": "Identity",
|
|
36
|
+
"$2": "Active Work",
|
|
37
|
+
"$3": "Governance",
|
|
38
|
+
"$4": "Rules",
|
|
39
|
+
"$5": "Risks & Limits",
|
|
40
|
+
"$6": "Diagrams",
|
|
41
|
+
"$7": "Field Contracts",
|
|
42
|
+
"$8": "Survival & Priorities",
|
|
43
|
+
"$9": "Context Profiles",
|
|
44
|
+
"$10": "Degradation Policy",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Human-readable sigil names (English) — for header translation
|
|
48
|
+
SIGIL_NAMES = {
|
|
49
|
+
"IDN": "Identity",
|
|
50
|
+
"DOM": "Domain",
|
|
51
|
+
"AXM": "Axiom",
|
|
52
|
+
"CNST": "Constraint",
|
|
53
|
+
"FCS": "Focus",
|
|
54
|
+
"OBJ": "Objective",
|
|
55
|
+
"WRK": "Work State",
|
|
56
|
+
"STP": "Next Step",
|
|
57
|
+
"NXT": "Queued Action",
|
|
58
|
+
"RSK": "Risk",
|
|
59
|
+
"AUD": "Audit",
|
|
60
|
+
"STAT": "Status",
|
|
61
|
+
"REF": "Reference",
|
|
62
|
+
"KNW": "Knowledge",
|
|
63
|
+
"SES": "Session",
|
|
64
|
+
"LNG": "Lesson",
|
|
65
|
+
"DIAG": "Diagram",
|
|
66
|
+
"CLAIM": "Claim",
|
|
67
|
+
"LIM": "Limit",
|
|
68
|
+
"HDL": "Handler",
|
|
69
|
+
"TAG": "Tag",
|
|
70
|
+
"PFL": "Pitfall",
|
|
71
|
+
"DEP": "Dependency",
|
|
72
|
+
"DESC": "Description",
|
|
73
|
+
"ERR": "Error",
|
|
74
|
+
"!": "Rule",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Mode aliases (Section 9.2 of SKILL.md)
|
|
79
|
+
MODE_PROFILE_ALIAS = {
|
|
80
|
+
"RECOVERY": "RECOVERY",
|
|
81
|
+
"FULL": "FULL",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render_attrs_table(entry: Entry, with_source: bool = False) -> List[str]:
|
|
86
|
+
"""Render an attrs entry as a Markdown key/value table.
|
|
87
|
+
|
|
88
|
+
When ``with_source=True`` (AUDIT mode), a third column ``source``
|
|
89
|
+
is appended with the ``<SIGIL>:<name>`` tag for traceability.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if with_source:
|
|
93
|
+
lines = [
|
|
94
|
+
"| key | value | source |",
|
|
95
|
+
"| --- | --- | --- |",
|
|
96
|
+
]
|
|
97
|
+
else:
|
|
98
|
+
lines = [
|
|
99
|
+
"| key | value |",
|
|
100
|
+
"| --- | --- |",
|
|
101
|
+
]
|
|
102
|
+
if isinstance(entry.value, dict):
|
|
103
|
+
rows = list(entry.value.items())
|
|
104
|
+
else:
|
|
105
|
+
rows = [("value", entry.value)]
|
|
106
|
+
src = f"{entry.sigil}:{entry.name}"
|
|
107
|
+
for k, v in rows:
|
|
108
|
+
if isinstance(v, str):
|
|
109
|
+
val = v.replace("|", "\\|")
|
|
110
|
+
else:
|
|
111
|
+
val = serialize_value(v)
|
|
112
|
+
if with_source:
|
|
113
|
+
lines.append(f"| {k} | {val} | `{src}` |")
|
|
114
|
+
else:
|
|
115
|
+
lines.append(f"| {k} | {val} |")
|
|
116
|
+
return lines
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def render_cuerpo(entry: Entry) -> List[str]:
|
|
120
|
+
text = str(entry.value or "").rstrip()
|
|
121
|
+
return ["```text", text, "```"] if text else []
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def render_bloque(entry: Entry) -> List[str]:
|
|
125
|
+
text = str(entry.value or "").rstrip()
|
|
126
|
+
lang = "text"
|
|
127
|
+
if "@startuml" in text or "@enduml" in text:
|
|
128
|
+
lang = "puml"
|
|
129
|
+
return [f"```{lang}", text, "```"]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def render_entry(
|
|
133
|
+
entry: Entry,
|
|
134
|
+
glossary: Glossary,
|
|
135
|
+
with_source: bool = False,
|
|
136
|
+
plevel: Optional[str] = None,
|
|
137
|
+
) -> List[str]:
|
|
138
|
+
"""Render a single entry to Markdown lines.
|
|
139
|
+
|
|
140
|
+
``plevel`` (when provided) is shown as a small badge next to the
|
|
141
|
+
heading so the reader can see the priority pack assignment.
|
|
142
|
+
|
|
143
|
+
v1.1.3 P1-7: in AUDIT mode (``with_source=True``), cuerpo and bloque
|
|
144
|
+
entries now get a visible ``source: `<SIGIL>:<name>` `` line before
|
|
145
|
+
the block, so traceability is uniform across all entry types.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
out: List[str] = []
|
|
149
|
+
sigil_name = SIGIL_NAMES.get(entry.sigil, entry.sigil)
|
|
150
|
+
header = f"### {sigil_name}: {entry.name}"
|
|
151
|
+
if plevel:
|
|
152
|
+
header += f" <sub>[{plevel}]</sub>"
|
|
153
|
+
if with_source and not plevel:
|
|
154
|
+
header += f" <sub>[{entry.sigil}:{entry.name}]</sub>"
|
|
155
|
+
out.append(header)
|
|
156
|
+
out.append("")
|
|
157
|
+
if entry.type in ("attrs", "attrs-pos"):
|
|
158
|
+
out.extend(render_attrs_table(entry, with_source=with_source))
|
|
159
|
+
elif entry.type == "cuerpo":
|
|
160
|
+
# v1.1.3 P1-7: visible source line for cuerpo in audit mode
|
|
161
|
+
if with_source:
|
|
162
|
+
out.append(f"source: `{entry.sigil}:{entry.name}`")
|
|
163
|
+
out.append("")
|
|
164
|
+
out.extend(render_cuerpo(entry))
|
|
165
|
+
elif entry.type == "bloque":
|
|
166
|
+
# v1.1.3 P1-7: visible source line for bloque in audit mode
|
|
167
|
+
if with_source:
|
|
168
|
+
out.append(f"source: `{entry.sigil}:{entry.name}`")
|
|
169
|
+
out.append("")
|
|
170
|
+
out.extend(render_bloque(entry))
|
|
171
|
+
elif entry.type == "relación":
|
|
172
|
+
if with_source:
|
|
173
|
+
out.append(f"source: `{entry.sigil}:{entry.name}`")
|
|
174
|
+
out.append("")
|
|
175
|
+
out.append(f"```\n{entry.value}\n```")
|
|
176
|
+
else:
|
|
177
|
+
out.append(str(entry.value))
|
|
178
|
+
out.append("")
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def render_hcortex_read(
|
|
183
|
+
doc: CortexDocument,
|
|
184
|
+
with_source: bool = False,
|
|
185
|
+
profile: Optional[str] = None,
|
|
186
|
+
mode: str = "READABLE",
|
|
187
|
+
layout: Optional[str] = None,
|
|
188
|
+
) -> str:
|
|
189
|
+
"""Render ``doc`` as HCORTEX-READ Markdown.
|
|
190
|
+
|
|
191
|
+
``profile`` may be ``MIN``, ``RECOVERY``, ``WORK`` or ``FULL``.
|
|
192
|
+
``mode`` may be ``READABLE``, ``AUDIT``, ``RECOVERY`` or ``FULL``.
|
|
193
|
+
``layout`` may be ``priority`` (global P0→P5), ``section``
|
|
194
|
+
(preserve section grouping), or ``None`` (auto-select).
|
|
195
|
+
|
|
196
|
+
Mode aliases:
|
|
197
|
+
- ``RECOVERY`` ≡ profile=RECOVERY, mode=READABLE, layout=priority
|
|
198
|
+
- ``FULL`` ≡ profile=FULL, mode=AUDIT
|
|
199
|
+
|
|
200
|
+
Layout auto-selection (re-audit H-RA-03):
|
|
201
|
+
- MIN, RECOVERY, AUDIT (default) → ``priority`` (global P0→P5)
|
|
202
|
+
- WORK, FULL, READABLE (default) → ``section`` (preserve sections)
|
|
203
|
+
|
|
204
|
+
The first line of the output declares the profile (Section 9.3 of
|
|
205
|
+
SKILL.md), making it auditable.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
# Resolve mode aliases
|
|
209
|
+
if mode in MODE_PROFILE_ALIAS:
|
|
210
|
+
profile = profile or MODE_PROFILE_ALIAS[mode]
|
|
211
|
+
if mode == "FULL":
|
|
212
|
+
with_source = True
|
|
213
|
+
|
|
214
|
+
prof = resolve_profile(profile)
|
|
215
|
+
audit_mode = (mode.upper() == "AUDIT") or with_source
|
|
216
|
+
# v1.1.3 P1-6: report the actual mode the user requested (not a hardcoded
|
|
217
|
+
# "READABLE" string). AUDIT mode must declare Mode: AUDIT.
|
|
218
|
+
audit_mode_str = "AUDIT" if audit_mode else mode.upper()
|
|
219
|
+
|
|
220
|
+
# Layout auto-selection (re-audit H-RA-03)
|
|
221
|
+
if layout is None:
|
|
222
|
+
if prof.name in ("MIN", "RECOVERY") or audit_mode:
|
|
223
|
+
layout = "priority"
|
|
224
|
+
else:
|
|
225
|
+
layout = "section"
|
|
226
|
+
layout = layout.lower()
|
|
227
|
+
|
|
228
|
+
# Filter entries by profile
|
|
229
|
+
result = filter_by_profile(doc, prof)
|
|
230
|
+
kept = sort_by_plevel(result.kept)
|
|
231
|
+
|
|
232
|
+
out: List[str] = []
|
|
233
|
+
# v1.1.3 P1-5: Perfil MUST be the first real line of HCORTEX output
|
|
234
|
+
# (SKILL.md §9.3). The cortex-render marker goes on the SECOND line
|
|
235
|
+
# so that auditors and parsers see the profile first.
|
|
236
|
+
out.append(f"Perfil: CORTEX-{prof.name}")
|
|
237
|
+
out.append(HCORTEX_READ_HEADER)
|
|
238
|
+
out.append("")
|
|
239
|
+
out.append("# HCORTEX-READ")
|
|
240
|
+
out.append("")
|
|
241
|
+
out.append(
|
|
242
|
+
f"> Non-reversible human view. Profile: {prof.name} "
|
|
243
|
+
f"(P-levels: {', '.join(prof.plevels)}). "
|
|
244
|
+
f"Mode: {audit_mode_str}. Layout: {layout}."
|
|
245
|
+
)
|
|
246
|
+
out.append("")
|
|
247
|
+
|
|
248
|
+
# v1.1.5 P1-5: warn if $0 contains operational entries (hidden from HCORTEX)
|
|
249
|
+
GLOSSARY_ENTRY_SIGILS = frozenset({"GSIG", "GTYP", "GMIC", "GCON"})
|
|
250
|
+
zero_section_ops = []
|
|
251
|
+
for sec in doc.sections:
|
|
252
|
+
if sec.id != "$0":
|
|
253
|
+
continue
|
|
254
|
+
for entry in sec.entries:
|
|
255
|
+
if entry.sigil not in GLOSSARY_ENTRY_SIGILS:
|
|
256
|
+
zero_section_ops.append(f"{entry.sigil}:{entry.name}")
|
|
257
|
+
if zero_section_ops:
|
|
258
|
+
out.append("> ⚠ **WARNING**: $0 contains operational entries that are "
|
|
259
|
+
"hidden from this view:")
|
|
260
|
+
for op in zero_section_ops[:10]:
|
|
261
|
+
out.append(f"> - `{op}`")
|
|
262
|
+
if len(zero_section_ops) > 10:
|
|
263
|
+
out.append(f"> - ... and {len(zero_section_ops) - 10} more")
|
|
264
|
+
out.append("> Run `cortex verify --strict` to fix (E033_ZERO_SECTION_MEMORY_ENTRY).")
|
|
265
|
+
out.append("")
|
|
266
|
+
|
|
267
|
+
if layout == "priority":
|
|
268
|
+
# Global P0→P5 order — re-audit H-RA-03
|
|
269
|
+
# Group entries by P-level, then by section within each P-level
|
|
270
|
+
out.extend(_render_priority_layout(kept, doc, audit_mode))
|
|
271
|
+
else:
|
|
272
|
+
# Section-grouped layout (original behaviour)
|
|
273
|
+
sections_kept: dict = {}
|
|
274
|
+
for sec_id, entry, plevel in kept:
|
|
275
|
+
sections_kept.setdefault(sec_id, []).append((entry, plevel))
|
|
276
|
+
for sec in doc.sections:
|
|
277
|
+
if sec.id == "$0":
|
|
278
|
+
continue
|
|
279
|
+
if sec.id not in sections_kept:
|
|
280
|
+
continue
|
|
281
|
+
title = sec.title or SECTION_TITLES.get(sec.id, sec.id)
|
|
282
|
+
out.append(f"## {sec.id} · {title}")
|
|
283
|
+
out.append("")
|
|
284
|
+
for entry, plevel in sections_kept[sec.id]:
|
|
285
|
+
out.extend(render_entry(
|
|
286
|
+
entry, doc.glossary,
|
|
287
|
+
with_source=audit_mode,
|
|
288
|
+
plevel=plevel,
|
|
289
|
+
))
|
|
290
|
+
|
|
291
|
+
# Declare omissions
|
|
292
|
+
if result.omitted:
|
|
293
|
+
out.append("## Omissions by profile")
|
|
294
|
+
out.append("")
|
|
295
|
+
out.append(
|
|
296
|
+
f"The following entries were omitted by profile **{prof.name}** "
|
|
297
|
+
f"(allowed P-levels: {', '.join(prof.plevels)}):"
|
|
298
|
+
)
|
|
299
|
+
out.append("")
|
|
300
|
+
out.append("| section | sigil | name | plevel | reason |")
|
|
301
|
+
out.append("| --- | --- | --- | --- | --- |")
|
|
302
|
+
for sec_id, entry, plevel, reason in result.omitted:
|
|
303
|
+
out.append(
|
|
304
|
+
f"| {sec_id} | {entry.sigil} | {entry.name} | {plevel} | {reason} |"
|
|
305
|
+
)
|
|
306
|
+
out.append("")
|
|
307
|
+
|
|
308
|
+
return "\n".join(out).rstrip() + "\n"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _render_priority_layout(
|
|
312
|
+
kept: List[Tuple[str, Entry, str]],
|
|
313
|
+
doc: CortexDocument,
|
|
314
|
+
audit_mode: bool,
|
|
315
|
+
) -> List[str]:
|
|
316
|
+
"""Render entries grouped by P-level (P0 first, P5 last).
|
|
317
|
+
|
|
318
|
+
Re-audit H-RA-03: ensures global P0→P5 order regardless of original
|
|
319
|
+
section order.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
from .profiles import PLEVEL_ORDER
|
|
323
|
+
out: List[str] = []
|
|
324
|
+
# Group by P-level preserving the global sort
|
|
325
|
+
by_plevel: dict = {}
|
|
326
|
+
for sec_id, entry, plevel in kept:
|
|
327
|
+
by_plevel.setdefault(plevel, []).append((sec_id, entry))
|
|
328
|
+
|
|
329
|
+
for plevel in PLEVEL_ORDER:
|
|
330
|
+
if plevel not in by_plevel:
|
|
331
|
+
continue
|
|
332
|
+
out.append(f"## Priority {plevel}")
|
|
333
|
+
out.append("")
|
|
334
|
+
for sec_id, entry in by_plevel[plevel]:
|
|
335
|
+
# Include section as a sub-heading for traceability
|
|
336
|
+
out.append(f"<!-- section: {sec_id} · {entry.sigil}:{entry.name} · {plevel} -->")
|
|
337
|
+
out.extend(render_entry(
|
|
338
|
+
entry, doc.glossary,
|
|
339
|
+
with_source=audit_mode,
|
|
340
|
+
plevel=plevel,
|
|
341
|
+
))
|
|
342
|
+
return out
|