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
cortex/v2/view.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
r"""VIEW directives — declarative reversible render for CORTEX ⇄ HCORTEX-R.
|
|
2
|
+
|
|
3
|
+
Implements the VIEW sigilo: a declarative contract inside CORTEX that
|
|
4
|
+
specifies how entries should be rendered as human Markdown (HCORTEX-R)
|
|
5
|
+
and how to reconstruct them back.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
CORTEX + VIEW → CortexV2Document → SkillIR → render with VIEW → HCORTEX-R
|
|
9
|
+
HCORTEX-R → parse VIEW markers → reverse strategies → CORTEX + VIEW
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Enums
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class ViewKind(str, Enum):
|
|
24
|
+
TABLE = "table"
|
|
25
|
+
KV_TABLE = "kv_table"
|
|
26
|
+
MATRIX = "matrix"
|
|
27
|
+
LIST = "list"
|
|
28
|
+
NUMBERED_LIST = "numbered_list"
|
|
29
|
+
CHECKLIST = "checklist"
|
|
30
|
+
PROSE = "prose"
|
|
31
|
+
QUOTE = "quote"
|
|
32
|
+
PUML = "puml"
|
|
33
|
+
CODE = "code"
|
|
34
|
+
CALLOUT = "callout"
|
|
35
|
+
SECTION = "section"
|
|
36
|
+
RAW = "raw"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReverseStrategy(str, Enum):
|
|
40
|
+
ROWS_TO_ENTRIES = "rows_to_entries"
|
|
41
|
+
ROW_TO_ATTRS = "row_to_attrs"
|
|
42
|
+
COLUMNS_TO_ATTRS = "columns_to_attrs"
|
|
43
|
+
ITEMS_TO_ENTRIES = "items_to_entries"
|
|
44
|
+
ITEMS_TO_ORDERED_ENTRIES = "items_to_ordered_entries"
|
|
45
|
+
ITEMS_TO_STATUS_ENTRIES = "items_to_status_entries"
|
|
46
|
+
BODY_TO_CUERPO = "body_to_cuerpo"
|
|
47
|
+
VERBATIM_TO_BLOQUE = "verbatim_to_bloque"
|
|
48
|
+
CALLOUT_TO_RISK = "callout_to_risk"
|
|
49
|
+
CALLOUT_TO_LIMIT = "callout_to_limit"
|
|
50
|
+
PRESERVE_HUMAN_BLOCK = "preserve_human_block"
|
|
51
|
+
IGNORE_WITH_WARNING = "ignore_with_warning"
|
|
52
|
+
MANUAL_REVIEW = "manual_review"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Compatibility matrix: which reverse strategies are valid for each kind
|
|
56
|
+
KIND_REVERSE_COMPAT: Dict[ViewKind, Set[ReverseStrategy]] = {
|
|
57
|
+
ViewKind.TABLE: {ReverseStrategy.ROWS_TO_ENTRIES, ReverseStrategy.COLUMNS_TO_ATTRS},
|
|
58
|
+
ViewKind.KV_TABLE: {ReverseStrategy.ROW_TO_ATTRS},
|
|
59
|
+
ViewKind.MATRIX: {ReverseStrategy.COLUMNS_TO_ATTRS, ReverseStrategy.ROWS_TO_ENTRIES},
|
|
60
|
+
ViewKind.LIST: {ReverseStrategy.ITEMS_TO_ENTRIES, ReverseStrategy.BODY_TO_CUERPO},
|
|
61
|
+
ViewKind.NUMBERED_LIST: {ReverseStrategy.ITEMS_TO_ORDERED_ENTRIES},
|
|
62
|
+
ViewKind.CHECKLIST: {ReverseStrategy.ITEMS_TO_STATUS_ENTRIES},
|
|
63
|
+
ViewKind.PROSE: {ReverseStrategy.BODY_TO_CUERPO, ReverseStrategy.PRESERVE_HUMAN_BLOCK},
|
|
64
|
+
ViewKind.QUOTE: {ReverseStrategy.BODY_TO_CUERPO},
|
|
65
|
+
ViewKind.PUML: {ReverseStrategy.VERBATIM_TO_BLOQUE},
|
|
66
|
+
ViewKind.CODE: {ReverseStrategy.VERBATIM_TO_BLOQUE},
|
|
67
|
+
ViewKind.CALLOUT: {ReverseStrategy.CALLOUT_TO_RISK, ReverseStrategy.CALLOUT_TO_LIMIT, ReverseStrategy.PRESERVE_HUMAN_BLOCK},
|
|
68
|
+
ViewKind.SECTION: {ReverseStrategy.PRESERVE_HUMAN_BLOCK},
|
|
69
|
+
ViewKind.RAW: {ReverseStrategy.VERBATIM_TO_BLOQUE, ReverseStrategy.PRESERVE_HUMAN_BLOCK},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# ViewDirective
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ViewDirective:
|
|
79
|
+
"""A VIEW directive parsed from CORTEX.
|
|
80
|
+
|
|
81
|
+
Fields match the contract: ``kind|target|fields|order|reverse|scope|title|status``
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
kind: ViewKind
|
|
86
|
+
target: str
|
|
87
|
+
reverse: ReverseStrategy
|
|
88
|
+
status: str = "cur"
|
|
89
|
+
fields: Optional[str] = None
|
|
90
|
+
order: Optional[str] = None
|
|
91
|
+
title: Optional[str] = None
|
|
92
|
+
scope: Optional[str] = None
|
|
93
|
+
section: Optional[str] = None
|
|
94
|
+
source_section: Optional[str] = None
|
|
95
|
+
preserve: Optional[str] = None
|
|
96
|
+
hash: Optional[str] = None
|
|
97
|
+
fallback: Optional[str] = None
|
|
98
|
+
raw: str = ""
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
return {
|
|
102
|
+
"name": self.name,
|
|
103
|
+
"kind": self.kind.value,
|
|
104
|
+
"target": self.target,
|
|
105
|
+
"reverse": self.reverse.value,
|
|
106
|
+
"status": self.status,
|
|
107
|
+
"fields": self.fields,
|
|
108
|
+
"order": self.order,
|
|
109
|
+
"title": self.title,
|
|
110
|
+
"scope": self.scope,
|
|
111
|
+
"preserve": self.preserve,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class ViewDiagnostic:
|
|
117
|
+
"""Diagnostic from VIEW parsing/validation."""
|
|
118
|
+
|
|
119
|
+
code: str
|
|
120
|
+
message: str
|
|
121
|
+
view_name: Optional[str] = None
|
|
122
|
+
severity: str = "warning" # warning | error | info
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict:
|
|
125
|
+
return {"code": self.code, "message": self.message, "view_name": self.view_name, "severity": self.severity}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Parser
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
VALID_KINDS = frozenset(k.value for k in ViewKind)
|
|
133
|
+
VALID_REVERSES = frozenset(r.value for r in ReverseStrategy)
|
|
134
|
+
VALID_VIEW_STATUSES = frozenset({"cur", "planned", "deprecated", "human_only", "generated", "edited"})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def parse_view_entry(name: str, attrs: Dict[str, Any], raw: str = "") -> Tuple[Optional[ViewDirective], List[ViewDiagnostic]]:
|
|
138
|
+
"""Parse a VIEW entry from its attrs dict.
|
|
139
|
+
|
|
140
|
+
Returns (directive, diagnostics). If the directive is invalid,
|
|
141
|
+
returns (None, diagnostics).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
diags: List[ViewDiagnostic] = []
|
|
145
|
+
|
|
146
|
+
# Required fields
|
|
147
|
+
kind_str = attrs.get("kind")
|
|
148
|
+
target = attrs.get("target")
|
|
149
|
+
reverse_str = attrs.get("reverse")
|
|
150
|
+
status = attrs.get("status", "cur")
|
|
151
|
+
|
|
152
|
+
# Validate kind
|
|
153
|
+
if not kind_str:
|
|
154
|
+
diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_KIND", f"VIEW:{name} missing required field 'kind'", name, "error"))
|
|
155
|
+
return None, diags
|
|
156
|
+
if kind_str not in VALID_KINDS:
|
|
157
|
+
diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_KIND", f"VIEW:{name} has unknown kind '{kind_str}'", name, "error"))
|
|
158
|
+
return None, diags
|
|
159
|
+
|
|
160
|
+
# Validate target
|
|
161
|
+
if not target:
|
|
162
|
+
diags.append(ViewDiagnostic("E_VIEW_EMPTY_TARGET", f"VIEW:{name} missing required field 'target'", name, "error"))
|
|
163
|
+
return None, diags
|
|
164
|
+
|
|
165
|
+
# Validate reverse
|
|
166
|
+
if not reverse_str:
|
|
167
|
+
diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_REVERSE", f"VIEW:{name} missing required field 'reverse'", name, "error"))
|
|
168
|
+
return None, diags
|
|
169
|
+
if reverse_str not in VALID_REVERSES:
|
|
170
|
+
diags.append(ViewDiagnostic("E_VIEW_UNKNOWN_REVERSE", f"VIEW:{name} has unknown reverse '{reverse_str}'", name, "error"))
|
|
171
|
+
return None, diags
|
|
172
|
+
|
|
173
|
+
kind = ViewKind(kind_str)
|
|
174
|
+
reverse = ReverseStrategy(reverse_str)
|
|
175
|
+
|
|
176
|
+
# Validate kind/reverse compatibility
|
|
177
|
+
if reverse not in KIND_REVERSE_COMPAT.get(kind, set()):
|
|
178
|
+
diags.append(ViewDiagnostic(
|
|
179
|
+
"E_VIEW_INCOMPATIBLE_REVERSE",
|
|
180
|
+
f"VIEW:{name} kind='{kind_str}' is incompatible with reverse='{reverse_str}'",
|
|
181
|
+
name, "error"
|
|
182
|
+
))
|
|
183
|
+
return None, diags
|
|
184
|
+
|
|
185
|
+
# Validate status
|
|
186
|
+
if status not in VALID_VIEW_STATUSES:
|
|
187
|
+
diags.append(ViewDiagnostic(
|
|
188
|
+
"W_VIEW_UNKNOWN_STATUS",
|
|
189
|
+
f"VIEW:{name} has unknown status '{status}', defaulting to 'cur'",
|
|
190
|
+
name, "warning"
|
|
191
|
+
))
|
|
192
|
+
status = "cur"
|
|
193
|
+
|
|
194
|
+
directive = ViewDirective(
|
|
195
|
+
name=name,
|
|
196
|
+
kind=kind,
|
|
197
|
+
target=target,
|
|
198
|
+
reverse=reverse,
|
|
199
|
+
status=status,
|
|
200
|
+
fields=attrs.get("fields"),
|
|
201
|
+
order=attrs.get("order"),
|
|
202
|
+
title=attrs.get("title"),
|
|
203
|
+
scope=attrs.get("scope"),
|
|
204
|
+
section=attrs.get("section"),
|
|
205
|
+
source_section=attrs.get("source_section"),
|
|
206
|
+
preserve=attrs.get("preserve"),
|
|
207
|
+
hash=attrs.get("hash"),
|
|
208
|
+
fallback=attrs.get("fallback"),
|
|
209
|
+
raw=raw,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return directive, diags
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def parse_view_entries_from_doc(doc) -> Tuple[List[ViewDirective], List[ViewDiagnostic]]:
|
|
216
|
+
"""Extract and parse all VIEW entries from a CortexV2Document.
|
|
217
|
+
|
|
218
|
+
v2.2.1: VIEW entries in $0 with type/risk/cortex/desc are sigil declarations,
|
|
219
|
+
NOT operational directives. Only VIEW entries outside $0 (or in $0 without
|
|
220
|
+
sigil_decl characteristics) are treated as directives.
|
|
221
|
+
|
|
222
|
+
Returns (directives, diagnostics).
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
directives: List[ViewDirective] = []
|
|
226
|
+
all_diags: List[ViewDiagnostic] = []
|
|
227
|
+
seen_names: Set[str] = set()
|
|
228
|
+
|
|
229
|
+
for sec in doc.sections:
|
|
230
|
+
for entry in sec.entries:
|
|
231
|
+
if entry.sigil != "VIEW":
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# v2.2.1 P0-2: In $0, if entry has type/risk/cortex/desc, it's a sigil declaration
|
|
235
|
+
if sec.id == "$0" and isinstance(entry.value, dict):
|
|
236
|
+
if any(k in entry.value for k in ("type", "risk", "cortex", "desc")):
|
|
237
|
+
# This is a sigil declaration, not a directive — skip
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
# Outside $0 (or $0 without sigil_decl characteristics): parse as directive
|
|
241
|
+
directive, diags = parse_view_entry(entry.name, entry.value, entry.raw)
|
|
242
|
+
all_diags.extend(diags)
|
|
243
|
+
if directive is not None:
|
|
244
|
+
if directive.name in seen_names:
|
|
245
|
+
all_diags.append(ViewDiagnostic(
|
|
246
|
+
"E_VIEW_DUPLICATE_NAME",
|
|
247
|
+
f"VIEW:{directive.name} is defined more than once",
|
|
248
|
+
directive.name, "error"
|
|
249
|
+
))
|
|
250
|
+
else:
|
|
251
|
+
seen_names.add(directive.name)
|
|
252
|
+
directives.append(directive)
|
|
253
|
+
|
|
254
|
+
return directives, all_diags
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Target resolution
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def resolve_target(target: str, doc) -> List:
|
|
262
|
+
"""Resolve a VIEW target selector against a CortexV2Document.
|
|
263
|
+
|
|
264
|
+
Returns a list of matching V2Entry objects.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
from .parser import V2Entry
|
|
268
|
+
|
|
269
|
+
results: List[V2Entry] = []
|
|
270
|
+
|
|
271
|
+
# $0:canonical_sigils — sigil declarations in $0
|
|
272
|
+
if target == "$0:canonical_sigils":
|
|
273
|
+
sec0 = doc.get_section("$0")
|
|
274
|
+
if sec0:
|
|
275
|
+
for e in sec0.entries:
|
|
276
|
+
if e.entry_type == "sigil_decl":
|
|
277
|
+
results.append(e)
|
|
278
|
+
return results
|
|
279
|
+
|
|
280
|
+
# $0:contracts — contract entries in $0
|
|
281
|
+
if target == "$0:contracts":
|
|
282
|
+
sec0 = doc.get_section("$0")
|
|
283
|
+
if sec0:
|
|
284
|
+
for e in sec0.entries:
|
|
285
|
+
if e.sigil == "$0" and e.name.startswith("contract_"):
|
|
286
|
+
results.append(e)
|
|
287
|
+
return results
|
|
288
|
+
|
|
289
|
+
# $0:microtokens — micro-token entries in $0
|
|
290
|
+
if target == "$0:microtokens":
|
|
291
|
+
sec0 = doc.get_section("$0")
|
|
292
|
+
if sec0:
|
|
293
|
+
for e in sec0.entries:
|
|
294
|
+
if e.sigil == "$0" and e.name.startswith("micro_"):
|
|
295
|
+
results.append(e)
|
|
296
|
+
return results
|
|
297
|
+
|
|
298
|
+
# $0:type_decls — type declaration entries in $0
|
|
299
|
+
if target == "$0:type_decls":
|
|
300
|
+
sec0 = doc.get_section("$0")
|
|
301
|
+
if sec0:
|
|
302
|
+
for e in sec0.entries:
|
|
303
|
+
if e.sigil == "$0" and e.name.startswith("type_"):
|
|
304
|
+
results.append(e)
|
|
305
|
+
return results
|
|
306
|
+
|
|
307
|
+
# $N:NAME — specific entry by name within section $N (v2.2.2: handles $0:enum_state, $0:delimiters, etc.)
|
|
308
|
+
# Single-colon, starts with $ → match by entry name within the section
|
|
309
|
+
if ":" in target and target.count(":") == 1 and target.startswith("$"):
|
|
310
|
+
parts = target.split(":", 1)
|
|
311
|
+
sec_id = parts[0]
|
|
312
|
+
name = parts[1]
|
|
313
|
+
sec = doc.get_section(sec_id)
|
|
314
|
+
if sec:
|
|
315
|
+
for e in sec.entries:
|
|
316
|
+
if e.name == name:
|
|
317
|
+
results.append(e)
|
|
318
|
+
return results
|
|
319
|
+
|
|
320
|
+
# $N — entire section
|
|
321
|
+
if target.startswith("$") and ":" not in target:
|
|
322
|
+
sec = doc.get_section(target)
|
|
323
|
+
if sec:
|
|
324
|
+
results.extend(sec.entries)
|
|
325
|
+
return results
|
|
326
|
+
|
|
327
|
+
# $N:SIGIL:* — all entries of a sigil within a section
|
|
328
|
+
# $N:SIGIL:name — specific entry within a section (v2.2.2: fix 3-part name selector)
|
|
329
|
+
if ":" in target and target.count(":") == 2 and target.startswith("$"):
|
|
330
|
+
parts = target.split(":")
|
|
331
|
+
sec_id = parts[0]
|
|
332
|
+
sigil = parts[1]
|
|
333
|
+
third = parts[2]
|
|
334
|
+
sec = doc.get_section(sec_id)
|
|
335
|
+
if sec:
|
|
336
|
+
if third == "*":
|
|
337
|
+
for e in sec.entries:
|
|
338
|
+
if e.sigil == sigil:
|
|
339
|
+
results.append(e)
|
|
340
|
+
else:
|
|
341
|
+
# specific entry name within section
|
|
342
|
+
for e in sec.entries:
|
|
343
|
+
if e.sigil == sigil and e.name == third:
|
|
344
|
+
results.append(e)
|
|
345
|
+
return results
|
|
346
|
+
|
|
347
|
+
# SIGIL:* — all entries of a sigil (any section)
|
|
348
|
+
if ":" in target and target.endswith(":*"):
|
|
349
|
+
sigil = target.split(":")[0]
|
|
350
|
+
for sec in doc.sections:
|
|
351
|
+
for e in sec.entries:
|
|
352
|
+
if e.sigil == sigil:
|
|
353
|
+
results.append(e)
|
|
354
|
+
return results
|
|
355
|
+
|
|
356
|
+
# SIGIL:name — exact entry (2-part, no section qualifier)
|
|
357
|
+
if ":" in target and "*" not in target and not target.startswith("$"):
|
|
358
|
+
parts = target.split(":", 1)
|
|
359
|
+
sigil = parts[0]
|
|
360
|
+
name = parts[1]
|
|
361
|
+
for sec in doc.sections:
|
|
362
|
+
for e in sec.entries:
|
|
363
|
+
if e.sigil == sigil and e.name == name:
|
|
364
|
+
results.append(e)
|
|
365
|
+
return results
|
|
366
|
+
|
|
367
|
+
# HUMAN_BLOCK:name — no CORTEX entries, returns empty
|
|
368
|
+
if target.startswith("HUMAN_BLOCK:"):
|
|
369
|
+
return results
|
|
370
|
+
|
|
371
|
+
# group:name — resolve via group (not yet implemented)
|
|
372
|
+
if target.startswith("group:"):
|
|
373
|
+
return results
|
|
374
|
+
|
|
375
|
+
return results
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Coverage
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
def calculate_view_coverage(doc, directives: List[ViewDirective]) -> Tuple[float, List[str]]:
|
|
383
|
+
"""Calculate VIEW coverage: what fraction of eligible entries are covered.
|
|
384
|
+
|
|
385
|
+
v2.3.1 P0-5: 0 eligible entries + 0 directives = 0% (not 100%).
|
|
386
|
+
A document with no content cannot be "fully covered" — it's empty.
|
|
387
|
+
|
|
388
|
+
Returns (coverage_fraction, uncovered_entry_descriptions).
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
# Collect all eligible entries (exclude VIEW entries and $0 metadata)
|
|
392
|
+
eligible: List[str] = []
|
|
393
|
+
covered: Set[str] = set()
|
|
394
|
+
|
|
395
|
+
for sec in doc.sections:
|
|
396
|
+
for entry in sec.entries:
|
|
397
|
+
if entry.sigil == "VIEW":
|
|
398
|
+
continue
|
|
399
|
+
if entry.sigil == "$0" and entry.entry_type == "meta":
|
|
400
|
+
continue
|
|
401
|
+
desc = f"{entry.section}/{entry.sigil}:{entry.name}"
|
|
402
|
+
eligible.append(desc)
|
|
403
|
+
|
|
404
|
+
# v2.3.1 P0-5: If there are no eligible entries, coverage is 0% (not 100%)
|
|
405
|
+
# unless there are also no directives AND no sections (truly empty doc).
|
|
406
|
+
# A document with content but no VIEW directives must report 0%.
|
|
407
|
+
if len(eligible) == 0:
|
|
408
|
+
if len(directives) == 0:
|
|
409
|
+
# Truly empty document
|
|
410
|
+
return 0.0, []
|
|
411
|
+
# Directives exist but nothing to cover (shouldn't happen)
|
|
412
|
+
return 0.0, []
|
|
413
|
+
|
|
414
|
+
# Resolve each directive's target and mark covered entries
|
|
415
|
+
for directive in directives:
|
|
416
|
+
matched = resolve_target(directive.target, doc)
|
|
417
|
+
for entry in matched:
|
|
418
|
+
desc = f"{entry.section}/{entry.sigil}:{entry.name}"
|
|
419
|
+
covered.add(desc)
|
|
420
|
+
|
|
421
|
+
uncovered = [desc for desc in eligible if desc not in covered]
|
|
422
|
+
total = len(eligible)
|
|
423
|
+
coverage = (total - len(uncovered)) / total if total > 0 else 0.0
|
|
424
|
+
|
|
425
|
+
return coverage, uncovered
|