moca-reader 0.6.0__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 (55) hide show
  1. moca_reader/__init__.py +84 -0
  2. moca_reader/_js.py +291 -0
  3. moca_reader/attestations.py +271 -0
  4. moca_reader/audience.py +49 -0
  5. moca_reader/backends/__init__.py +1 -0
  6. moca_reader/backends/bm25.py +118 -0
  7. moca_reader/backends/dense.py +146 -0
  8. moca_reader/backends/hybrid.py +73 -0
  9. moca_reader/backends/lexical.py +112 -0
  10. moca_reader/backends/memory_store.py +122 -0
  11. moca_reader/citation.py +213 -0
  12. moca_reader/codes.py +110 -0
  13. moca_reader/conformance.py +249 -0
  14. moca_reader/content.py +312 -0
  15. moca_reader/diagnostics.py +45 -0
  16. moca_reader/digest.py +92 -0
  17. moca_reader/dsse.py +97 -0
  18. moca_reader/ed25519.py +83 -0
  19. moca_reader/embedders.py +224 -0
  20. moca_reader/evidence_text.py +187 -0
  21. moca_reader/frontmatter.py +151 -0
  22. moca_reader/library.py +176 -0
  23. moca_reader/manifest.py +186 -0
  24. moca_reader/metadata.py +58 -0
  25. moca_reader/profiles.py +5 -0
  26. moca_reader/py.typed +0 -0
  27. moca_reader/reader.py +468 -0
  28. moca_reader/relations.py +46 -0
  29. moca_reader/resolver.py +33 -0
  30. moca_reader/reviews.py +18 -0
  31. moca_reader/schemas/ag-ui-citations.schema.json +36 -0
  32. moca_reader/schemas/citation-record.schema.json +326 -0
  33. moca_reader/schemas/manifest.schema.json +171 -0
  34. moca_reader/schemas/node.schema.json +372 -0
  35. moca_reader/schemas/review-predicate.schema.json +42 -0
  36. moca_reader/schemas/search-hit.schema.json +62 -0
  37. moca_reader/schemas/sidecar-index.schema.json +155 -0
  38. moca_reader/schemas/structure.schema.json +93 -0
  39. moca_reader/schemas/trust-root.schema.json +130 -0
  40. moca_reader/schemas.py +44 -0
  41. moca_reader/search.py +346 -0
  42. moca_reader/sidecar.py +228 -0
  43. moca_reader/source.py +285 -0
  44. moca_reader/state.py +119 -0
  45. moca_reader/strategies/__init__.py +1 -0
  46. moca_reader/strategies/diversify.py +28 -0
  47. moca_reader/strategies/ontology_guided.py +63 -0
  48. moca_reader/structure.py +385 -0
  49. moca_reader/structure_ops.py +160 -0
  50. moca_reader/unicode.py +33 -0
  51. moca_reader-0.6.0.dist-info/METADATA +209 -0
  52. moca_reader-0.6.0.dist-info/RECORD +55 -0
  53. moca_reader-0.6.0.dist-info/WHEEL +4 -0
  54. moca_reader-0.6.0.dist-info/licenses/LICENSE +201 -0
  55. moca_reader-0.6.0.dist-info/licenses/NOTICE +15 -0
@@ -0,0 +1,213 @@
1
+ """Citation records and the default retrieval policy (Reader contract §7 and §8).
2
+
3
+ A citation record is a dict valid against citation-record.schema.json: the same JSON in every Reader.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from datetime import UTC, datetime
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from ._js import effective_window, get, js_string, timestamp, truthy
13
+ from .audience import audience_of
14
+ from .content import ContentNode, Representation
15
+ from .digest import utf8
16
+ from .reviews import review_status_of
17
+
18
+ if TYPE_CHECKING:
19
+ from .reader import PackageResult
20
+
21
+ CitationRecord = dict[str, Any]
22
+ RelationIndex = tuple[dict[str, list[str]], dict[str, list[str]]] # superseded, contested
23
+
24
+
25
+ def passes_default_policy(record: CitationRecord) -> bool:
26
+ """In force, not superseded, not deprecated (Reader contract §8)."""
27
+ trust = record["trust"]
28
+ return bool(trust.get("inForce")) and not trust.get("superseded") and trust.get("status") != "deprecated"
29
+
30
+
31
+ def pick_representation(node: ContentNode, locale: str | None) -> Representation:
32
+ """The representation package spec §5 selects for a locale: exact, then a parent, then a child, then the default."""
33
+ if not locale:
34
+ return node.representations[0]
35
+ want = locale.lower()
36
+ reps = node.representations
37
+ # Package spec §5.4: exact; then the longest tag the request starts with (RFC 4647 Lookup); then the lowest
38
+ # tag, in code-point order, that starts with the request.
39
+ tagged = [r for r in reps if r.locale]
40
+ parents = sorted(
41
+ (r for r in tagged if want.startswith(f"{(r.locale or '').lower()}-")), key=lambda r: -len(r.locale or "")
42
+ )
43
+ children = sorted(
44
+ (r for r in tagged if (r.locale or "").lower().startswith(f"{want}-")), key=lambda r: r.locale or ""
45
+ )
46
+ return (
47
+ next((r for r in tagged if (r.locale or "").lower() == want), None)
48
+ or (parents[0] if parents else None)
49
+ or (children[0] if children else None)
50
+ or reps[0]
51
+ )
52
+
53
+
54
+ def _declared(value: Any) -> dict[str, str]:
55
+ out = {"by": js_string(value.get("by"), "by" in value) if isinstance(value, dict) else "undefined"}
56
+ at = get(value, "at")
57
+ if truthy(at):
58
+ out["at"] = js_string(at)
59
+ return out
60
+
61
+
62
+ def _date_or_none(value: Any) -> Any:
63
+ """A frontmatter or manifest date that parses, or None."""
64
+ if value is None or value == "":
65
+ return None
66
+ return None if math.isnan(timestamp(value)) else value
67
+
68
+
69
+ def _has_actor(entry: Any) -> bool:
70
+ """An OKF ``generated`` or ``verified`` entry that names its actor."""
71
+ return isinstance(entry, dict) and entry.get("by") is not None and js_string(entry["by"]) != ""
72
+
73
+
74
+ def citation_for(
75
+ result: PackageResult,
76
+ node: ContentNode,
77
+ rep: Representation,
78
+ now: datetime | None = None,
79
+ index: RelationIndex | None = None,
80
+ text: str | None = None,
81
+ ) -> CitationRecord:
82
+ """Builds one citation record."""
83
+ manifest = result.manifest
84
+ fm = rep.frontmatter
85
+ moca = fm["moca"] if isinstance(fm.get("moca"), (dict, list)) else {}
86
+ moca = moca if isinstance(moca, dict) else {}
87
+ key = f"{js_string(manifest.get('id'))}@{js_string(manifest.get('version'))}"
88
+ # Package spec §4.4: the node's window is the intersection of the package's and its own. A bound that does not
89
+ # parse, or a window whose end is not after its start, is ignored: it never takes a node out of force.
90
+ valid_from, valid_until, from_at, until_at = effective_window(
91
+ manifest.get("validFrom"), manifest.get("validUntil"), moca.get("valid_from"), moca.get("valid_until")
92
+ )
93
+ t = (now or datetime.now(UTC)).timestamp() * 1000
94
+ superseded_by = list(index[0].get(key, [])) if index else []
95
+ contested_by = [
96
+ *(moca["contested_by"] if isinstance(moca.get("contested_by"), list) else []),
97
+ *(index[1].get(key, []) if index else []),
98
+ ]
99
+ declared_sources = fm.get("sources")
100
+ sources: dict[Any, Any] = {}
101
+ for s in declared_sources if isinstance(declared_sources, list) else []:
102
+ sid = get(s, "id")
103
+ if truthy(sid) and isinstance(sid, (str, int, float, bool)):
104
+ sources[sid] = s
105
+
106
+ package: dict[str, Any] = {
107
+ "id": manifest.get("id"),
108
+ "version": manifest.get("version"),
109
+ "digest": result.digest,
110
+ "signed": len(result.signers) > 0,
111
+ }
112
+ if result.signers:
113
+ package["signers"] = list(result.signers)
114
+
115
+ node_part: dict[str, Any] = {
116
+ "id": f"{js_string(manifest.get('id'))}#{node.path}",
117
+ "ref": f"{key}#{node.path}",
118
+ "path": node.path,
119
+ }
120
+ if rep.locale:
121
+ node_part["locale"] = rep.locale
122
+ elif truthy(manifest.get("language")):
123
+ node_part["locale"] = manifest["language"]
124
+ node_part["type"] = fm.get("type")
125
+ if isinstance(fm.get("title"), str):
126
+ node_part["title"] = fm["title"]
127
+ if isinstance(fm.get("description"), str):
128
+ node_part["description"] = fm["description"]
129
+ if isinstance(fm.get("tags"), list):
130
+ node_part["tags"] = [x for x in fm["tags"] if isinstance(x, str)]
131
+ if rep.sha256:
132
+ node_part["digest"] = f"sha256:{rep.sha256}"
133
+
134
+ verified = fm.get("verified")
135
+ verified_list = verified if isinstance(verified, list) else [verified] if truthy(verified) else []
136
+ generated = fm.get("generated")
137
+ trust: dict[str, Any] = {
138
+ "status": fm["status"] if fm.get("status") in ("draft", "stable", "deprecated") else "stable",
139
+ # An entry with no actor (`by`) is dropped rather than reported as "undefined".
140
+ "generated": _declared(generated) if _has_actor(generated) else None,
141
+ "declaredVerified": [_declared(v) for v in verified_list if _has_actor(v)],
142
+ "attestedReviews": [
143
+ {**_review_json(r), "packageVerified": bool(result.signers)}
144
+ for r in result.reviews_by_file.get(rep.file, [])
145
+ ],
146
+ "reviewStatus": review_status_of(result.reviews_by_file.get(rep.file), bool(result.signers)),
147
+ }
148
+ stale_after = _date_or_none(fm.get("stale_after"))
149
+ if truthy(stale_after):
150
+ trust["staleAfter"] = js_string(stale_after)
151
+ if truthy(valid_from):
152
+ trust["validFrom"] = js_string(valid_from)
153
+ if truthy(valid_until):
154
+ trust["validUntil"] = js_string(valid_until)
155
+ trust["stale"] = t >= timestamp(stale_after) if truthy(stale_after) else False
156
+ trust["inForce"] = (from_at is None or t >= from_at.timestamp() * 1000) and (
157
+ until_at is None or t < until_at.timestamp() * 1000
158
+ )
159
+ trust["superseded"] = len(superseded_by) > 0
160
+ if superseded_by:
161
+ trust["supersededBy"] = superseded_by
162
+ trust["contested"] = len(contested_by) > 0
163
+ if contested_by:
164
+ trust["contestedBy"] = contested_by
165
+
166
+ evidence: list[dict[str, Any]] = []
167
+ for i, e in enumerate(moca["evidence"] if isinstance(moca.get("evidence"), list) else []):
168
+ if not truthy(e) or not isinstance(e, (dict, list)):
169
+ continue
170
+ source_key = get(e, "source")
171
+ hashable = isinstance(source_key, (str, int, float, bool))
172
+ if hashable and source_key in sources:
173
+ source = sources[source_key]
174
+ else:
175
+ source = {"id": source_key} if isinstance(e, dict) and "source" in e else {}
176
+ entry: dict[str, Any] = {"source": source}
177
+ if truthy(get(e, "selector")):
178
+ entry["selector"] = get(e, "selector")
179
+ if truthy(get(e, "note")):
180
+ entry["note"] = get(e, "note")
181
+ matched = rep.evidence_checks[i].matched if i < len(rep.evidence_checks) else None
182
+ if matched is not None:
183
+ entry["matched"] = matched
184
+ evidence.append(entry)
185
+
186
+ record: CitationRecord = {
187
+ "package": package,
188
+ "node": node_part,
189
+ "span": {"start": rep.body_offset, "end": rep.body_offset + len(utf8(rep.body))},
190
+ "trust": trust,
191
+ "evidence": evidence,
192
+ }
193
+ if rep.concepts:
194
+ record["concepts"] = list(rep.concepts)
195
+ label, invalid = audience_of(node, rep)
196
+ if label is not None:
197
+ record["audience"] = label
198
+ if invalid:
199
+ record["audienceInvalid"] = True
200
+ if result.profiles:
201
+ record["profiles"] = list(result.profiles)
202
+ if text is not None:
203
+ record["text"] = text
204
+ return record
205
+
206
+
207
+ def _review_json(review: Any) -> dict[str, Any]:
208
+ out: dict[str, Any] = {"reviewer": review.reviewer, "reviewedAt": review.reviewed_at, "outcome": review.outcome}
209
+ if review.signer is not None:
210
+ out["signer"] = review.signer
211
+ if review.scope is not None:
212
+ out["scope"] = review.scope
213
+ return out
moca_reader/codes.py ADDED
@@ -0,0 +1,110 @@
1
+ """Diagnostic codes. This table mirrors Reader contract §11, which is normative: codes are the contract,
2
+ message prose is not. A change here without the matching spec change is a bug."""
3
+
4
+ from __future__ import annotations
5
+
6
+ ERROR = "error"
7
+ WARNING = "warning"
8
+ INFO = "info"
9
+
10
+ CODES: dict[str, tuple[str, str]] = {
11
+ "T001_TARGET_NOT_FOUND": (ERROR, "The target does not exist."),
12
+ "T002_MANIFEST_NOT_FOUND": (ERROR, "The target exists but has no moca.json at its root."),
13
+ "T003_ARCHIVE_REJECTED": (ERROR, "An archive or host source broke a safety rule or limit."),
14
+ "P001_UNSUPPORTED_ENTRY": (ERROR, "The package contains a symbolic link or other non-regular file."),
15
+ "P002_UNREADABLE_ENTRY": (ERROR, "A package entry could not be read."),
16
+ "P003_PATH_COLLISION": (ERROR, "Two package paths are equal after Unicode NFC normalisation or case folding."),
17
+ "M001_MANIFEST_INVALID_JSON": (ERROR, "moca.json is not valid UTF-8 JSON."),
18
+ "M002_MANIFEST_SCHEMA": (ERROR, "moca.json does not validate against the manifest schema."),
19
+ "M003_NO_CONTENT": (ERROR, "The package has neither a content node nor a member."),
20
+ "M004_UNKNOWN_KEY": (
21
+ INFO,
22
+ "A top-level manifest key is neither defined by the spec nor a vendor x-<vendor>-<key> key.",
23
+ ),
24
+ "M005_DUPLICATE_MEMBER": (ERROR, "The same member id appears more than once."),
25
+ "M006_VALIDITY_WINDOW_INVALID": (WARNING, "validUntil is not later than validFrom."),
26
+ "M007_UNREGISTERED_URN": (INFO, "The package id uses a URN namespace that is not registered with IANA."),
27
+ "M008_DIGEST_V1_PACKAGE": (
28
+ INFO,
29
+ "The package was written for a mocaVersion before 0.4; digests, pins and attestations made then will not match moca-digest-v2.",
30
+ ),
31
+ "M009_MOCA_VERSION_NEWER": (
32
+ WARNING,
33
+ "The package was written for a newer mocaVersion than this Reader implements; it is read on a best-effort basis.",
34
+ ),
35
+ "C001_FRONTMATTER_MISSING": (ERROR, "A concept document has no YAML frontmatter (OKF MUST)."),
36
+ "C002_FRONTMATTER_INVALID": (ERROR, "Frontmatter is not parseable YAML or is not a mapping."),
37
+ "C003_TYPE_MISSING": (ERROR, "Frontmatter has no non-empty type (OKF MUST)."),
38
+ "C004_RESERVED_FILE_INVALID": (ERROR, "index.md or log.md breaks the OKF reserved-file rules."),
39
+ "C005_FIELD_INVALID": (WARNING, "A frontmatter field does not match the node schema."),
40
+ "C006_EVIDENCE_SOURCE_UNKNOWN": (WARNING, "moca.evidence names a source id that sources[] does not declare."),
41
+ "C007_LINK_UNRESOLVED": (WARNING, "An internal Markdown link does not resolve."),
42
+ "C008_UNSAFE_PATH": (ERROR, "A package-relative path is absolute or escapes the package root."),
43
+ "C009_LOCALE_ORPHAN": (WARNING, "A locale variant has no default-language file."),
44
+ "C010_VALIDITY_WINDOW_INVALID": (WARNING, "moca.valid_until is not later than moca.valid_from."),
45
+ "C011_EVIDENCE_SELECTOR_UNMATCHED": (
46
+ WARNING,
47
+ "An evidence selector does not match the text source file it cites inside the package.",
48
+ ),
49
+ "A001_ATTESTATION_MALFORMED": (ERROR, "An attestation file is not a parseable envelope and statement."),
50
+ "A002_ATTESTATION_INVALID": (ERROR, "An attestation failed verification or does not match the package."),
51
+ "A003_ATTESTATION_UNVERIFIABLE": (
52
+ WARNING,
53
+ "An attestation could not be checked because no trust root was supplied.",
54
+ ),
55
+ "A004_ATTESTATION_INDETERMINATE": (WARNING, "Online verification was requested but could not complete."),
56
+ "A006_REVIEW_OUTDATED": (
57
+ WARNING,
58
+ "A review attestation covers a file whose bytes have since changed or been removed.",
59
+ ),
60
+ "A007_REVIEW_DATE_FUTURE": (WARNING, "A review is dated later than the host clock, so it does not count."),
61
+ "A008_PACKAGE_DIGEST_MISMATCH": (
62
+ ERROR,
63
+ "A package attestation names this id and version with a different digest: the content changed after it was signed.",
64
+ ),
65
+ "A009_SIGNATURE_REQUIRED": (ERROR, "The host requires a valid package attestation and the package has none."),
66
+ "R001_MEMBER_UNRESOLVED": (WARNING, "A member could not be resolved by the host resolver."),
67
+ "R002_MEMBER_DIGEST_MISMATCH": (ERROR, "A resolved member does not match its pinned digest."),
68
+ "R003_MEMBER_CYCLE": (ERROR, "Members form a cycle."),
69
+ "L001_IDENTITY_CONFLICT": (
70
+ ERROR,
71
+ "A library was given a second package with the same id and version but a different digest; the second is refused.",
72
+ ),
73
+ "R004_RELATION_UNAUTHORISED": (
74
+ INFO,
75
+ "A supersedes or conflictsWith relation is not authorised by a signer of the package, so it has no effect.",
76
+ ),
77
+ "F001_PROFILE_UNRECOGNISED": (INFO, "A declared profile is not recognised by this reader."),
78
+ "O001_ONTOLOGY_UNPARSEABLE": (
79
+ WARNING,
80
+ "A structure or overlay file is over the size or triple limit, or is not parseable Turtle.",
81
+ ),
82
+ "O002_CONCEPT_UNDECLARED": (
83
+ WARNING,
84
+ "A concept is used in a relation or node binding but not declared as a skos:Concept.",
85
+ ),
86
+ "O003_REMOTE_REFERENCE": (
87
+ WARNING,
88
+ "An ontology file or concept binding relies on something a Reader would have to fetch or resolve.",
89
+ ),
90
+ "O005_STRUCTURE_CYCLE": (WARNING, "requires, broader or hasPart forms a cycle."),
91
+ "S001_SIDECAR_INVALID": (ERROR, "index.json does not validate against the sidecar schema."),
92
+ "S002_SIDECAR_TARGET_MISMATCH": (ERROR, "The sidecar is bound to a different package id or version."),
93
+ "S003_SIDECAR_STALE": (WARNING, "The sidecar digest does not match the package; the sidecar is ignored."),
94
+ "S004_SIDECAR_ITEM_INVALID": (ERROR, "A payload item breaks the addressing rules."),
95
+ "S005_SIDECAR_FORMAT_UNKNOWN": (INFO, "The payload format is not recognised; the sidecar is ignored."),
96
+ "S006_MODEL_MISMATCH": (WARNING, "The host's embedder does not match the index's model; dense search is refused."),
97
+ "S007_NO_VECTORS": (
98
+ WARNING,
99
+ "A loaded package has no vectors a dense backend can search; that package is not searched.",
100
+ ),
101
+ }
102
+
103
+ # Codes that are no longer emitted. Codes are never reused.
104
+ RETIRED_CODES = ("A005_SKILLS_WITHHELD", "K001_SKILL_INVALID", "O004_STRUCTURE_VIEW_MISMATCH")
105
+
106
+
107
+ def default_severity(code: str) -> str:
108
+ if code not in CODES:
109
+ raise KeyError(f"unknown diagnostic code {code}")
110
+ return CODES[code][0]
@@ -0,0 +1,249 @@
1
+ """moca-spec's runner protocol (conformance/README.md): one JSON object ``{root, case}`` on stdin, one JSON result
2
+ on stdout. What this Reader concludes for one case of conformance/cases.json. It ships with the package, so the
3
+ corpus runs against the Reader you installed. From a moca-spec checkout:
4
+
5
+ node scripts/run-conformance.mjs --reader "python -m moca_reader.conformance"
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ from collections.abc import Callable
14
+ from datetime import datetime
15
+ from typing import Any
16
+
17
+ from . import (
18
+ DenseBackend,
19
+ HybridBackend,
20
+ LexicalBackend,
21
+ Library,
22
+ MemoryStoreBackend,
23
+ Overlay,
24
+ Search,
25
+ SearchHooks,
26
+ StoreScope,
27
+ bind_sidecar,
28
+ directory_resolver,
29
+ ontology_guided,
30
+ read_package,
31
+ )
32
+ from .state import package_state
33
+
34
+ UNLOADED = "sha256:" + "0" * 64
35
+
36
+
37
+ class ConformanceEmbedder:
38
+ """The identity a case names, and [1, 0, 0, ...] for every text, whatever the role."""
39
+
40
+ def __init__(self, spec: dict[str, Any]) -> None:
41
+ self.name = spec["name"]
42
+ self.version = spec.get("version")
43
+ self.dimensions = spec["dimensions"]
44
+ self.query_prefix = spec.get("queryPrefix")
45
+ self.document_prefix = spec.get("documentPrefix")
46
+
47
+ def embed(self, texts: list[str], role: str) -> list[list[float]]:
48
+ return [[1.0 if i == 0 else 0.0 for i in range(self.dimensions)] for _ in texts]
49
+
50
+
51
+ class EveryHitBackend:
52
+ """Ignores every filter, declares no features, and returns every representation plus one unknown digest."""
53
+
54
+ features: tuple[str, ...] = ()
55
+
56
+ def __init__(self, library: Library) -> None:
57
+ self.library = library
58
+
59
+ def search(self, query: str, limit: int, filters: dict[str, Any]) -> list[dict[str, Any]]:
60
+ hits: list[dict[str, Any]] = []
61
+ for p in self.library.packages:
62
+ for node in p.result.nodes:
63
+ for rep in node.representations:
64
+ hit = {
65
+ "digest": p.result.digest,
66
+ "node": f"{p.result.manifest['id']}#{node.path}",
67
+ "path": node.path,
68
+ }
69
+ if rep.locale:
70
+ hit["locale"] = rep.locale
71
+ hits.append({**hit, "score": 1})
72
+ hits.append({"digest": UNLOADED, "node": "https://example.com/not-loaded#x.md", "path": "x.md", "score": 2})
73
+ return hits
74
+
75
+
76
+ def hostile_hooks() -> SearchHooks:
77
+ """An egress hook that tries to return everything, plus a record from a package that is not loaded."""
78
+
79
+ def egress(candidates: list[dict[str, Any]], ctx: Any) -> list[dict[str, Any]]:
80
+ everything = ctx.library.citations(include_text=True)
81
+ fake = [{**everything[0], "package": {**everything[0]["package"], "digest": UNLOADED}}] if everything else []
82
+ return [*candidates, *everything, *fake]
83
+
84
+ return SearchHooks(egress=egress)
85
+
86
+
87
+ def hostile_ingress_hooks() -> SearchHooks:
88
+ """An ingress hook that asks for more than the caller did: the gate must follow the caller."""
89
+ return SearchHooks(ingress=lambda query, ctx: {"includeAll": True, "limit": 1000, "locale": "fr"})
90
+
91
+
92
+ def concept_item(x: dict[str, Any] | None) -> dict[str, Any] | None:
93
+ if x is None:
94
+ return None
95
+ out = {"iri": x["iri"]}
96
+ if "label" in x:
97
+ out["label"] = x["label"]
98
+ out["layer"] = x["layer"]
99
+ return out
100
+
101
+
102
+ def actual_for(root: str, c: dict[str, Any]) -> dict[str, Any]:
103
+ o = c.get("options") or {}
104
+ read_options: dict[str, Any] = {
105
+ "trust_root": os.path.join(root, o["trustRoot"]) if o.get("trustRoot") else None,
106
+ "resolve_member": directory_resolver([os.path.join(root, m) for m in o["members"]])
107
+ if o.get("members")
108
+ else None,
109
+ "limits": o.get("limits"),
110
+ # The host clock applies to reading too: keys expire (attestations §6).
111
+ "clock": (lambda: datetime.fromisoformat(o["now"])) if o.get("now") else None,
112
+ "require_signature": o.get("requireSignature") is True,
113
+ "strict": o.get("strict") is True,
114
+ }
115
+ overlays = [Overlay(v["id"], v["layer"], path=os.path.join(root, v["path"])) for v in o.get("overlays") or []]
116
+
117
+ if c.get("kind") == "structure":
118
+ library = Library(overlays=overlays)
119
+ for target in c["packages"]:
120
+ result = read_package(os.path.join(root, target), **read_options)
121
+ if result.valid:
122
+ library.add(result)
123
+ s = library.structure
124
+ a = c.get("args") or {}
125
+ iri = c["iri"]
126
+ ops: dict[str, Callable[[], Any]] = {
127
+ "concept": lambda: s.concept(iri),
128
+ "requires": lambda: [concept_item(x) for x in s.requires(iri, transitive=a.get("transitive", False))],
129
+ "requiredBy": lambda: [concept_item(x) for x in s.required_by(iri)],
130
+ "parts": lambda: [concept_item(x) for x in s.parts(iri)],
131
+ "narrower": lambda: [concept_item(x) for x in s.narrower(iri, transitive=a.get("transitive", False))],
132
+ "broader": lambda: [concept_item(x) for x in s.broader(iri)],
133
+ "related": lambda: [concept_item(x) for x in s.related(iri)],
134
+ "sequence": lambda: None if (seq := s.sequence(iri)) is None else [concept_item(x) for x in seq],
135
+ "nodes": lambda: [r["node"]["ref"] for r in s.nodes(iri, include=a.get("include", "none"))],
136
+ }
137
+ return {"codes": [d.code for d in library.diagnostics.items], "result": ops[c["op"]]()}
138
+
139
+ if c.get("kind") == "search":
140
+ now = datetime.fromisoformat(o["now"])
141
+ library = Library(
142
+ clock=lambda: now, overlays=overlays, allow_unsigned_relations=o.get("allowUnsignedRelations") is True
143
+ )
144
+ codes: list[str] = []
145
+ for target in c["packages"]:
146
+ result = read_package(os.path.join(root, target), **read_options)
147
+ if not result.valid:
148
+ continue
149
+ chunks, index = None, None
150
+ if o.get("sidecar"):
151
+ bound = bind_sidecar(os.path.join(root, o["sidecar"]), result)
152
+ codes.extend(d.code for d in bound.diagnostics)
153
+ if bound.usable:
154
+ chunks, index = bound.chunks, bound.index
155
+ library.add(result, chunks, index)
156
+ hooks = (
157
+ hostile_hooks()
158
+ if o.get("hooks") == "hostile"
159
+ else hostile_ingress_hooks()
160
+ if o.get("hooks") == "hostile-ingress"
161
+ else ontology_guided()
162
+ if o.get("hooks") == "ontology-guided"
163
+ else None
164
+ )
165
+ s = c["search"]
166
+ options = {
167
+ "limit": s.get("limit", 50),
168
+ "include_all": s.get("includeAll", False),
169
+ "locale": s.get("locale"),
170
+ "concepts": s.get("concepts"),
171
+ "scope": s.get("scope"),
172
+ }
173
+ if o.get("backend") == "store":
174
+ # Everything listed is ingested (each package on its own with `ingestSeparately`); only the packages
175
+ # in `allow` are "loaded", and the search gets their package states (contract §9.4).
176
+ store = MemoryStoreBackend(filter_pushdown=False)
177
+ if o.get("ingestSeparately"):
178
+ for p in library.packages:
179
+ single = Library(clock=lambda: now)
180
+ single.add(p.result)
181
+ store.ingest(single)
182
+ else:
183
+ store.ingest(library)
184
+ allow = {os.path.join(root, t) for t in o.get("allow") or []}
185
+ allowed = [
186
+ package_state(p.result)
187
+ for p in library.packages
188
+ if p.result.source and p.result.source.location in allow
189
+ ]
190
+ records = Search(
191
+ StoreScope(allowed, lambda: now, o.get("allowUnsignedRelations") is True),
192
+ store,
193
+ audiences=o.get("audiences"),
194
+ allow_unlabelled=o.get("allowUnlabelled") is True,
195
+ ).search(s["query"], **options)
196
+ return {"codes": codes, "records": records}
197
+
198
+ def dense() -> DenseBackend:
199
+ return DenseBackend(library, ConformanceEmbedder(o["embedder"]))
200
+
201
+ backend: Any
202
+ if o.get("backend") == "all":
203
+ backend = EveryHitBackend(library)
204
+ elif o.get("backend") == "dense":
205
+ backend = dense()
206
+ elif o.get("backend") == "hybrid":
207
+ h = o.get("hybrid") or {}
208
+ backend = HybridBackend(LexicalBackend(library), dense(), k=h.get("k", 60), weights=h.get("weights"))
209
+ else:
210
+ backend = LexicalBackend(library)
211
+ codes.extend(d.code for d in getattr(backend, "diagnostics", None) or [])
212
+ # Library diagnostics (L): for example two digests for one id@version.
213
+ codes.extend(d.code for d in library.diagnostics.items if d.code.startswith("L"))
214
+ records = Search(
215
+ library,
216
+ backend,
217
+ audiences=o.get("audiences"),
218
+ hooks=hooks,
219
+ allow_unlabelled=o.get("allowUnlabelled") is True,
220
+ ).search(s["query"], **options)
221
+ return {"codes": codes, "records": records}
222
+
223
+ result = read_package(os.path.join(root, c["target"]), **read_options)
224
+ codes = [d.code for d in result.diagnostics]
225
+ sidecar_usable = None
226
+ if o.get("sidecar"):
227
+ bound = bind_sidecar(os.path.join(root, o["sidecar"]), result)
228
+ codes.extend(d.code for d in bound.diagnostics)
229
+ sidecar_usable = bound.usable
230
+ evidence_matched: dict[str, list[Any]] = {}
231
+ if result.valid:
232
+ for r in Library().add(result).citations():
233
+ if r["evidence"]:
234
+ evidence_matched[r["node"]["path"]] = [e.get("matched") for e in r["evidence"]]
235
+ out: dict[str, Any] = {
236
+ "valid": result.valid,
237
+ "codes": codes,
238
+ "capabilities": result.capabilities,
239
+ "digest": result.digest,
240
+ "evidenceMatched": evidence_matched,
241
+ }
242
+ if sidecar_usable is not None:
243
+ out["sidecarUsable"] = sidecar_usable
244
+ return out
245
+
246
+
247
+ if __name__ == "__main__":
248
+ request = json.loads(sys.stdin.read())
249
+ sys.stdout.write(json.dumps(actual_for(request["root"], request["case"]), ensure_ascii=False))