codec-cortex 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. codec_cortex-0.3.2.dist-info/METADATA +183 -0
  2. codec_cortex-0.3.2.dist-info/RECORD +81 -0
  3. codec_cortex-0.3.2.dist-info/WHEEL +5 -0
  4. codec_cortex-0.3.2.dist-info/entry_points.txt +2 -0
  5. codec_cortex-0.3.2.dist-info/licenses/LICENSE +21 -0
  6. codec_cortex-0.3.2.dist-info/scm_file_list.json +124 -0
  7. codec_cortex-0.3.2.dist-info/scm_version.json +8 -0
  8. codec_cortex-0.3.2.dist-info/top_level.txt +1 -0
  9. cortex/__init__.py +22 -0
  10. cortex/__main__.py +7 -0
  11. cortex/_version.py +24 -0
  12. cortex/cli/__init__.py +5 -0
  13. cortex/cli/commands/__init__.py +150 -0
  14. cortex/cli/commands/add.py +89 -0
  15. cortex/cli/commands/compile.py +31 -0
  16. cortex/cli/commands/delete.py +44 -0
  17. cortex/cli/commands/diagram.py +130 -0
  18. cortex/cli/commands/diff.py +135 -0
  19. cortex/cli/commands/doctor.py +74 -0
  20. cortex/cli/commands/format.py +29 -0
  21. cortex/cli/commands/get.py +45 -0
  22. cortex/cli/commands/glossary.py +98 -0
  23. cortex/cli/commands/list.py +44 -0
  24. cortex/cli/commands/micro.py +79 -0
  25. cortex/cli/commands/move.py +45 -0
  26. cortex/cli/commands/new.py +80 -0
  27. cortex/cli/commands/recover.py +90 -0
  28. cortex/cli/commands/render.py +93 -0
  29. cortex/cli/commands/update.py +81 -0
  30. cortex/cli/commands/v2_canonicalize.py +162 -0
  31. cortex/cli/commands/v2_compare.py +74 -0
  32. cortex/cli/commands/v2_convert.py +183 -0
  33. cortex/cli/commands/v2_explain_loss.py +97 -0
  34. cortex/cli/commands/v2_inspect.py +113 -0
  35. cortex/cli/commands/v2_roundtrip.py +104 -0
  36. cortex/cli/commands/v2_roundtrip_bidir.py +128 -0
  37. cortex/cli/commands/v2_verify_view.py +65 -0
  38. cortex/cli/commands/verify.py +119 -0
  39. cortex/cli/main.py +636 -0
  40. cortex/core/__init__.py +85 -0
  41. cortex/core/ast.py +313 -0
  42. cortex/core/compare.py +180 -0
  43. cortex/core/document_kind.py +525 -0
  44. cortex/core/errors.py +399 -0
  45. cortex/core/lexer.py +267 -0
  46. cortex/core/parser.py +671 -0
  47. cortex/core/validator.py +268 -0
  48. cortex/core/writer.py +249 -0
  49. cortex/crud/__init__.py +17 -0
  50. cortex/crud/mutations.py +342 -0
  51. cortex/crud/selectors.py +95 -0
  52. cortex/crud/transactions.py +213 -0
  53. cortex/glossary/__init__.py +21 -0
  54. cortex/glossary/contracts.py +37 -0
  55. cortex/glossary/minimal.py +94 -0
  56. cortex/glossary/model.py +77 -0
  57. cortex/glossary/resolver.py +96 -0
  58. cortex/hcortex/__init__.py +35 -0
  59. cortex/hcortex/edit_parser.py +489 -0
  60. cortex/hcortex/edit_renderer.py +158 -0
  61. cortex/hcortex/markdown_model.py +81 -0
  62. cortex/hcortex/profiles.py +166 -0
  63. cortex/hcortex/read_renderer.py +342 -0
  64. cortex/hcortex/recovery.py +782 -0
  65. cortex/py.typed +0 -0
  66. cortex/templates/__init__.py +8 -0
  67. cortex/templates/brain.py +118 -0
  68. cortex/templates/minimal_glossary.py +42 -0
  69. cortex/templates/package.py +91 -0
  70. cortex/templates/skill.py +91 -0
  71. cortex/v2/__init__.py +30 -0
  72. cortex/v2/diagnostics.py +57 -0
  73. cortex/v2/encoder.py +1106 -0
  74. cortex/v2/equivalence.py +323 -0
  75. cortex/v2/hcortex_parser.py +615 -0
  76. cortex/v2/hcortex_renderer.py +450 -0
  77. cortex/v2/ir.py +223 -0
  78. cortex/v2/parser.py +655 -0
  79. cortex/v2/view.py +425 -0
  80. cortex/v2/view_renderer.py +474 -0
  81. cortex/v2/writer.py +301 -0
@@ -0,0 +1,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