ontosmith 0.1.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 (115) hide show
  1. api/__init__.py +14 -0
  2. api/server.py +440 -0
  3. harness/__init__.py +10 -0
  4. harness/canonicalization.py +298 -0
  5. harness/doc_check.py +113 -0
  6. harness/make_graph_image.py +103 -0
  7. harness/rag_eval.py +116 -0
  8. harness/rag_eval_embedder.py +164 -0
  9. harness/rag_eval_hybrid_search.py +130 -0
  10. harness/render_charts.py +449 -0
  11. harness/verification/__init__.py +33 -0
  12. harness/verification/audit.py +168 -0
  13. harness/verification/cell_runner.py +179 -0
  14. harness/verification/config.py +379 -0
  15. harness/verification/report.py +114 -0
  16. harness/verification/scoring.py +285 -0
  17. harness/verification/verify_cli.py +164 -0
  18. ontosmith-0.1.0.data/data/share/ontosmith/app/app.css +312 -0
  19. ontosmith-0.1.0.data/data/share/ontosmith/app/app.js +409 -0
  20. ontosmith-0.1.0.data/data/share/ontosmith/app/favicon.svg +1 -0
  21. ontosmith-0.1.0.data/data/share/ontosmith/app/index.html +72 -0
  22. ontosmith-0.1.0.data/data/share/ontosmith/app/logo-mark.svg +1 -0
  23. ontosmith-0.1.0.data/data/share/ontosmith/examples/README.md +129 -0
  24. ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-history.txt +1 -0
  25. ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-people.txt +1 -0
  26. ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-products.txt +1 -0
  27. ontosmith-0.1.0.data/data/share/ontosmith/examples/corpus/alder-finch-workshops.txt +1 -0
  28. ontosmith-0.1.0.data/data/share/ontosmith/examples/triples.jsonl +7 -0
  29. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/conformance.json +100 -0
  30. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/fitted_qualifiers.json +33 -0
  31. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/pack.json +114 -0
  32. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/CENSUS.md +139 -0
  33. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/profile.json +316 -0
  34. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/profile_v2.json +816 -0
  35. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/setup_config.json +450 -0
  36. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/setup_config_v3.json +445 -0
  37. ontosmith-0.1.0.data/data/share/ontosmith/packs/bio-medical/profile/setup_config_v4.json +450 -0
  38. ontosmith-0.1.0.data/data/share/ontosmith/packs/conformance.schema.json +1 -0
  39. ontosmith-0.1.0.data/data/share/ontosmith/packs/core/conformance.json +1 -0
  40. ontosmith-0.1.0.data/data/share/ontosmith/packs/core/pack.json +1 -0
  41. ontosmith-0.1.0.data/data/share/ontosmith/packs/core/profile/setup_config.json +27 -0
  42. ontosmith-0.1.0.data/data/share/ontosmith/packs/fetch_btt.py +107 -0
  43. ontosmith-0.1.0.data/data/share/ontosmith/packs/fetch_gutenberg.py +93 -0
  44. ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/conformance.json +60 -0
  45. ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/pack.json +57 -0
  46. ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/profile/profile.json +1525 -0
  47. ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/profile/profile.md +141 -0
  48. ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/profile/setup_config.json +79 -0
  49. ontosmith-0.1.0.data/data/share/ontosmith/packs/legal-contract/role_words.json +1 -0
  50. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/conformance.json +1 -0
  51. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/direction_declarations.json +442 -0
  52. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/pack.json +72 -0
  53. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/profile/profile.json +9 -0
  54. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/profile/setup_config.json +591 -0
  55. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/profile/setup_config_v2.json +1140 -0
  56. ontosmith-0.1.0.data/data/share/ontosmith/packs/open-domain/tn_table.json +101 -0
  57. ontosmith-0.1.0.data/data/share/ontosmith/packs/pack.schema.json +1 -0
  58. ontosmith-0.1.0.dist-info/METADATA +507 -0
  59. ontosmith-0.1.0.dist-info/RECORD +115 -0
  60. ontosmith-0.1.0.dist-info/WHEEL +5 -0
  61. ontosmith-0.1.0.dist-info/entry_points.txt +2 -0
  62. ontosmith-0.1.0.dist-info/licenses/LICENSE +21 -0
  63. ontosmith-0.1.0.dist-info/top_level.txt +3 -0
  64. pipeline/__init__.py +4 -0
  65. pipeline/__main__.py +175 -0
  66. pipeline/analyze_hardened.py +138 -0
  67. pipeline/batch.py +274 -0
  68. pipeline/batch_cli.py +924 -0
  69. pipeline/batch_run.py +2581 -0
  70. pipeline/bindings-template.env +67 -0
  71. pipeline/biored_candidates.py +127 -0
  72. pipeline/calibration.py +58 -0
  73. pipeline/configure.py +540 -0
  74. pipeline/conflict_candidates.py +102 -0
  75. pipeline/connections.py +608 -0
  76. pipeline/decisions.py +409 -0
  77. pipeline/docred.py +113 -0
  78. pipeline/drift_check.py +281 -0
  79. pipeline/embedder.py +157 -0
  80. pipeline/ensemble.py +98 -0
  81. pipeline/er_candidates.py +80 -0
  82. pipeline/evaluate.py +163 -0
  83. pipeline/export_graph.py +68 -0
  84. pipeline/file_registry.py +716 -0
  85. pipeline/gate_cli.py +816 -0
  86. pipeline/graph_store.py +724 -0
  87. pipeline/graph_write.py +91 -0
  88. pipeline/hard_negatives.py +125 -0
  89. pipeline/heartbeat.py +39 -0
  90. pipeline/inspect.py +289 -0
  91. pipeline/neo4j_store.py +719 -0
  92. pipeline/onto_candidates.py +141 -0
  93. pipeline/ontology_builder.py +1192 -0
  94. pipeline/operating_points.py +103 -0
  95. pipeline/pack_schema.py +345 -0
  96. pipeline/pair_proposer.py +839 -0
  97. pipeline/profile_config.py +1106 -0
  98. pipeline/profiler.py +134 -0
  99. pipeline/promotion.py +261 -0
  100. pipeline/proposer.py +557 -0
  101. pipeline/query_stack.py +1330 -0
  102. pipeline/queue.py +256 -0
  103. pipeline/recalibrate.py +77 -0
  104. pipeline/reference_profile.py +67 -0
  105. pipeline/registry.py +284 -0
  106. pipeline/registry_store.py +121 -0
  107. pipeline/relation_map.py +92 -0
  108. pipeline/rer.py +379 -0
  109. pipeline/retrieval_eval.py +186 -0
  110. pipeline/review.py +233 -0
  111. pipeline/roster_pass.py +188 -0
  112. pipeline/search.py +1093 -0
  113. pipeline/setup_service.py +958 -0
  114. pipeline/threshold_stability.py +59 -0
  115. pipeline/turtle_export.py +103 -0
api/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """The sample API package.
2
+
3
+ A reference HTTP API over the search surface and the
4
+ built graph — search, entity lookup, neighborhood, and
5
+ triple inspection with evidence. It
6
+ is the backend the hands-on guide and the demo app
7
+ run against.
8
+
9
+ The implementation lives in :mod:`api.server` (standard
10
+ library only); importing this package pulls nothing heavy.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ __all__ = ["server"]
api/server.py ADDED
@@ -0,0 +1,440 @@
1
+ """Sample API server: ``ontosmith serve``.
2
+
3
+ A reference HTTP API over the hybrid search
4
+ (:mod:`pipeline.search`) and the built graph:
5
+
6
+ GET /health store identity + inventory
7
+ GET /search hybrid search — the same result
8
+ dict the ``ontosmith search`` CLI
9
+ prints (``?q=...&k=...``)
10
+ GET /entity one entity and its facts
11
+ (``?id=...`` or ``?name=...``)
12
+ GET /neighborhood REL neighborhood of an entity
13
+ (``?id=...&hops=1..4``)
14
+ GET /triple one triple with its evidence spans
15
+ and provenance — the inspection report as
16
+ JSON (``?subject=&relation=&object=``
17
+ or ``?index=N``)
18
+ GET / the demo app: the
19
+ static page in ``app/`` — search box,
20
+ triple/evidence inspector, entity
21
+ report + neighborhood walker
22
+ GET /app/<file> the app's static assets
23
+
24
+ Framework: the Python standard library
25
+ (``http.server.ThreadingHTTPServer``) — a deliberate
26
+ choice for a *reference* server: the product's dependency
27
+ discipline keeps the core install to numpy +
28
+ requests, and a sample API should be readable end-to-end
29
+ in one file, not framework-shaped. Anything heavier
30
+ belongs to the deployment, not the sample.
31
+
32
+ Errors are JSON, named, and shaped like the product's
33
+ refusals: ``{"error": {"code": ..., "message": ...}}`` —
34
+ 400 for bad input (missing/invalid parameters), 404 for
35
+ an unknown entity or unmatched triple, 500 only for an
36
+ unexpected server fault (message named, no traceback).
37
+ The server binds localhost by default and takes its store
38
+ from the same surfaces as the CLI (``--source`` /
39
+ ``--batch`` / ``--store`` / ``NEO4J_*``); with no source
40
+ it serves the shipped example build, so a stranger can
41
+ start it and query the demo graph immediately.
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import argparse
46
+ import json
47
+ import sys
48
+ import sysconfig
49
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
50
+ from pathlib import Path
51
+ from urllib.parse import parse_qs, urlparse
52
+
53
+ from pipeline import connections as _connections
54
+ from pipeline import inspect as _inspect
55
+ from pipeline import search as _search
56
+
57
+
58
+ class ApiError(Exception):
59
+ """A named, shaped API refusal: HTTP status + code."""
60
+
61
+ def __init__(self, status: int, code: str, message: str):
62
+ super().__init__(message)
63
+ self.status = status
64
+ self.code = code
65
+ self.message = message
66
+
67
+
68
+ class SearchService:
69
+ """The endpoint logic over one opened serving store.
70
+ Transport-free: the handler below is a thin shell, so
71
+ the contract is testable without a socket."""
72
+
73
+ def __init__(self, store, embedder=None):
74
+ self.store = store
75
+ self.embedder = embedder
76
+
77
+ # -- endpoints -------------------------------------------
78
+ def health(self) -> dict:
79
+ """Store identity + inventory (the store's own
80
+ describe(), plus a status line)."""
81
+ return {"status": "ok", "store": self.store.describe()}
82
+
83
+ def search(self, params: dict) -> dict:
84
+ """GET /search?q=...&k=... — the hybrid_search
85
+ result dict, unchanged."""
86
+ q = _required(params, "q")
87
+ k = _int_param(params, "k", 10, lo=1, hi=100)
88
+ try:
89
+ return _search.hybrid_search(
90
+ self.store, q, k=k, embedder=self.embedder)
91
+ except ValueError as e:
92
+ raise ApiError(400, "bad_query", str(e)) from e
93
+
94
+ def entity(self, params: dict) -> dict:
95
+ """GET /entity?id=... | ?name=... — the entity
96
+ record plus every attested fact it endpoints."""
97
+ ent = self._resolve_entity(params)
98
+ facts = [t for t in self.store.triples()
99
+ if t["subject_id"] == ent["entity_id"]
100
+ or t["object_id"] == ent["entity_id"]]
101
+ return {
102
+ **ent,
103
+ "facts_as_subject": sum(
104
+ 1 for t in facts
105
+ if t["subject_id"] == ent["entity_id"]),
106
+ "facts_as_object": sum(
107
+ 1 for t in facts
108
+ if t["object_id"] == ent["entity_id"]),
109
+ "facts": facts,
110
+ }
111
+
112
+ def neighborhood(self, params: dict) -> dict:
113
+ """GET /neighborhood?id=...&hops=1..4 — the
114
+ store's neighborhood contract, with the seed
115
+ entity echoed."""
116
+ ent = self._resolve_entity(params)
117
+ hops = _int_param(params, "hops", 1, lo=1, hi=4)
118
+ result = self.store.neighborhood(
119
+ [ent["entity_id"]], hops=hops)
120
+ return {"entity": ent, "hops": hops, **result}
121
+
122
+ def triple(self, params: dict) -> dict:
123
+ """GET /triple?subject=&relation=&object=&index=
124
+ — one triple as the inspection report."""
125
+ records = self.store.triple_records()
126
+ index = params.get("index")
127
+ index = int(index) if index is not None else None
128
+ try:
129
+ rec, position, total = _inspect.select(
130
+ records, index=index,
131
+ subject=params.get("subject"),
132
+ relation=params.get("relation"),
133
+ object_=params.get("object"))
134
+ except ValueError as e:
135
+ raise ApiError(404, "triple_not_found",
136
+ str(e)) from e
137
+ return _inspect.build_report(rec, "api", position,
138
+ total)
139
+
140
+ # -- helpers -----------------------------------------------
141
+ def _resolve_entity(self, params: dict) -> dict:
142
+ """Entity by id (exact) or by name (the store's
143
+ deterministic link, best hit). 400 when neither
144
+ parameter is given, 404 when nothing resolves."""
145
+ entity_id = params.get("id")
146
+ name = params.get("name")
147
+ if entity_id:
148
+ ent = self.store.entity(entity_id)
149
+ if ent is None:
150
+ raise ApiError(
151
+ 404, "entity_not_found",
152
+ f"no entity with id {entity_id!r}")
153
+ return ent
154
+ if name:
155
+ hits = self.store.find_entities(name, limit=1)
156
+ if not hits:
157
+ raise ApiError(
158
+ 404, "entity_not_found",
159
+ f"no entity matches name {name!r}")
160
+ return hits[0]
161
+ raise ApiError(
162
+ 400, "missing_parameter",
163
+ "entity lookup needs ?id= or ?name=")
164
+
165
+
166
+ API_ROUTES = frozenset(
167
+ {"/health", "/search", "/entity", "/neighborhood",
168
+ "/triple"})
169
+
170
+
171
+ def api_route(service: SearchService, path: str,
172
+ params: dict):
173
+ """The API route table: a zero-arg callable producing
174
+ the response payload for ``path``, or None when the
175
+ path is not an API route. ``API_ROUTES`` is exactly
176
+ this table's key set (pinned by test) — the demo app
177
+ pins the endpoints it calls against it."""
178
+ routes = {
179
+ "/health": lambda: service.health(),
180
+ "/search": lambda: service.search(params),
181
+ "/entity": lambda: service.entity(params),
182
+ "/neighborhood":
183
+ lambda: service.neighborhood(params),
184
+ "/triple": lambda: service.triple(params),
185
+ }
186
+ return routes.get(path)
187
+
188
+
189
+ def _required(params: dict, name: str) -> str:
190
+ """A required non-empty query parameter."""
191
+ val = params.get(name)
192
+ if val is None or not str(val).strip():
193
+ raise ApiError(400, "missing_parameter",
194
+ f"missing required parameter: {name}")
195
+ return str(val)
196
+
197
+
198
+ def _int_param(params: dict, name: str, default: int,
199
+ lo: int, hi: int) -> int:
200
+ """An optional integer parameter, range-checked; the
201
+ refusal names the parameter and the allowed range."""
202
+ raw = params.get(name)
203
+ if raw is None:
204
+ return default
205
+ try:
206
+ val = int(raw)
207
+ except (TypeError, ValueError):
208
+ raise ApiError(
209
+ 400, "invalid_parameter",
210
+ f"parameter {name} must be an integer, "
211
+ f"got {raw!r}") from None
212
+ if not lo <= val <= hi:
213
+ raise ApiError(
214
+ 400, "invalid_parameter",
215
+ f"parameter {name} must be {lo}..{hi}, "
216
+ f"got {val}")
217
+ return val
218
+
219
+
220
+ # ----------------------------------------------------------------------
221
+ # Demo app — static assets, served by the same shell
222
+ # ----------------------------------------------------------------------
223
+ APP_CONTENT_TYPES = {
224
+ ".html": "text/html; charset=utf-8",
225
+ ".css": "text/css; charset=utf-8",
226
+ ".js": "text/javascript; charset=utf-8",
227
+ ".svg": "image/svg+xml",
228
+ ".png": "image/png",
229
+ ".ico": "image/x-icon",
230
+ ".json": "application/json; charset=utf-8",
231
+ ".txt": "text/plain; charset=utf-8",
232
+ }
233
+
234
+
235
+ def app_candidates() -> list[Path]:
236
+ """Where the demo app's static files may live, in
237
+ resolution order — the shipped example's candidate
238
+ discipline: the source tree, the pip
239
+ ``--target`` layout (data files beside the packages),
240
+ the regular install layout (the interpreter's data
241
+ path)."""
242
+ pkg_parent = Path(__file__).resolve().parent.parent
243
+ return [
244
+ pkg_parent / "app",
245
+ pkg_parent / "share" / "ontosmith" / "app",
246
+ Path(sysconfig.get_path("data")) / "share"
247
+ / "ontosmith" / "app",
248
+ ]
249
+
250
+
251
+ def app_dir() -> Path | None:
252
+ """The resolved demo-app directory (the first
253
+ candidate holding ``index.html``), or None when no
254
+ candidate resolves — the server then serves the JSON
255
+ API only and says so at startup."""
256
+ for cand in app_candidates():
257
+ if (cand / "index.html").is_file():
258
+ return cand
259
+ return None
260
+
261
+
262
+ def app_asset(name: str) -> tuple[bytes, str] | None:
263
+ """One app asset by bare file name: ``(bytes,
264
+ content type)``, or None. The name must be a single
265
+ path segment — no directories, no traversal, no
266
+ dotfiles — with a known content type, and the file
267
+ must exist in the resolved app directory."""
268
+ if not name or name != Path(name).name \
269
+ or name.startswith("."):
270
+ return None
271
+ root = app_dir()
272
+ if root is None:
273
+ return None
274
+ path = root / name
275
+ ctype = APP_CONTENT_TYPES.get(path.suffix.lower())
276
+ if ctype is None or not path.is_file():
277
+ return None
278
+ return path.read_bytes(), ctype
279
+
280
+
281
+ # ----------------------------------------------------------------------
282
+ # HTTP transport — a thin shell over SearchService
283
+ # ----------------------------------------------------------------------
284
+ class _Handler(BaseHTTPRequestHandler):
285
+ """Routes GETs: ``/`` and ``/app/<file>`` serve the
286
+ demo app; the API routes answer JSON. No other
287
+ verbs: the sample API is read-only."""
288
+
289
+ service: SearchService # set by build_server
290
+ protocol_version = "HTTP/1.1"
291
+
292
+ def log_message(self, *args): # silence access logs
293
+ pass
294
+
295
+ def do_GET(self):
296
+ parsed = urlparse(self.path)
297
+ if parsed.path == "/" \
298
+ or parsed.path.startswith("/app/"):
299
+ self._serve_app(parsed.path)
300
+ return
301
+ params = {k: v[0] for k, v in
302
+ parse_qs(parsed.query).items()}
303
+ route = api_route(self.service, parsed.path,
304
+ params)
305
+ if route is None:
306
+ self._send(404, {"error": {
307
+ "code": "not_found",
308
+ "message": f"no such endpoint: "
309
+ f"{parsed.path}"}})
310
+ return
311
+ try:
312
+ self._send(200, route())
313
+ except ApiError as e:
314
+ self._send(e.status, {"error": {
315
+ "code": e.code, "message": e.message}})
316
+ except Exception as e: # named, never a traceback
317
+ self._send(500, {"error": {
318
+ "code": "internal",
319
+ "message": f"{type(e).__name__}: {e}"}})
320
+
321
+ def _serve_app(self, path: str) -> None:
322
+ """The demo app: ``/`` is its index page,
323
+ ``/app/<file>`` one of its assets. A miss is the
324
+ API's JSON 404, never an HTML error page."""
325
+ name = "index.html" if path == "/" \
326
+ else path[len("/app/"):]
327
+ asset = app_asset(name)
328
+ if asset is None:
329
+ self._send(404, {"error": {
330
+ "code": "not_found",
331
+ "message": f"no such app asset: {path}"}})
332
+ return
333
+ body, ctype = asset
334
+ self.send_response(200)
335
+ self.send_header("Content-Type", ctype)
336
+ self.send_header("Content-Length", str(len(body)))
337
+ self.end_headers()
338
+ self.wfile.write(body)
339
+
340
+ def _send(self, status: int, payload: dict) -> None:
341
+ body = json.dumps(payload, ensure_ascii=False,
342
+ ).encode("utf-8")
343
+ self.send_response(status)
344
+ self.send_header("Content-Type",
345
+ "application/json; charset=utf-8")
346
+ self.send_header("Content-Length", str(len(body)))
347
+ self.end_headers()
348
+ self.wfile.write(body)
349
+
350
+
351
+ def build_server(host: str, port: int,
352
+ service: SearchService) -> ThreadingHTTPServer:
353
+ """A bound (not yet serving) server for ``service``.
354
+ Port 0 picks an ephemeral port — the test suite's
355
+ route, and a safe default for embedding."""
356
+ handler = type("BoundHandler", (_Handler,),
357
+ {"service": service})
358
+ return ThreadingHTTPServer((host, port), handler)
359
+
360
+
361
+ def main(argv=None) -> int:
362
+ """``ontosmith serve [--source PATH] [--batch ID]
363
+ [--store auto|file|neo4j] [--host H] [--port N]
364
+ [--embed]`` — serve the sample API (default:
365
+ localhost:8765, the shipped example build). The
366
+ demo app is served at ``/`` on the same port
367
+ when its assets resolve."""
368
+ ap = argparse.ArgumentParser(
369
+ prog="ontosmith serve",
370
+ description="Sample API server over the "
371
+ "search/graph surface: "
372
+ "/search, /entity, /neighborhood, "
373
+ "/triple (with evidence)")
374
+ ap.add_argument("--source", default=None,
375
+ help="batch directory, records "
376
+ "directory, or records file "
377
+ "(default: the shipped example "
378
+ "build)")
379
+ ap.add_argument("--batch", default=None,
380
+ help="batch id under the state root's "
381
+ "batches/ (alternative to --source)")
382
+ ap.add_argument("--store", default="auto",
383
+ choices=("auto", "file", "neo4j"),
384
+ help="serving store: auto = Neo4j when "
385
+ "NEO4J_URL is configured, else the "
386
+ "file store (default: auto)")
387
+ ap.add_argument("--host", default="127.0.0.1",
388
+ help="bind host (default: 127.0.0.1)")
389
+ ap.add_argument("--port", type=int, default=8765,
390
+ help="bind port (default: 8765)")
391
+ ap.add_argument("--embed", action="store_true",
392
+ help="activate the search vector arm "
393
+ "via the connection layer's "
394
+ "default embedder (refuses if the "
395
+ "embedder seat's key is unset)")
396
+ args = ap.parse_args(argv)
397
+
398
+ try:
399
+ store = _search.open_store(source=args.source,
400
+ batch=args.batch,
401
+ store=args.store)
402
+ embedder = None
403
+ if args.embed:
404
+ from pipeline.embedder import default_embedder
405
+ embedder = default_embedder()
406
+ else:
407
+ # Construction only — no call is made here,
408
+ # and hybrid_search never embeds against a
409
+ # store that ships no chunk embeddings.
410
+ embedder = _search.try_embedder()
411
+ except (ValueError, _connections.BindingKeyError) as e:
412
+ print(f"serve refused: {e}", file=sys.stderr)
413
+ return 1
414
+ service = SearchService(store, embedder=embedder)
415
+ server = build_server(args.host, args.port, service)
416
+ host, port = server.server_address[:2]
417
+ desc = store.describe()
418
+ print(f"ontosmith serve: {desc['kind']} store "
419
+ f"({desc['triples']} triples, "
420
+ f"{desc['documents']} documents) at "
421
+ f"http://{host}:{port} — /search /entity "
422
+ f"/neighborhood /triple /health")
423
+ app = app_dir()
424
+ if app is not None:
425
+ print(f"ontosmith serve: demo app at "
426
+ f"http://{host}:{port}/ (assets: {app})")
427
+ else:
428
+ print("ontosmith serve: demo app assets not "
429
+ "found — JSON API only")
430
+ try:
431
+ server.serve_forever()
432
+ except KeyboardInterrupt:
433
+ pass
434
+ finally:
435
+ server.server_close()
436
+ return 0
437
+
438
+
439
+ if __name__ == "__main__":
440
+ sys.exit(main())
harness/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """OntoSmith verification harness.
2
+
3
+ The harness is the measurement layer that ships with the
4
+ product in a supporting role: the tooling adopters
5
+ use to audit their own gold and score their own builds, and
6
+ the same tooling that produces the public quality record. The
7
+ reusable cores live in :mod:`harness.verification`; the
8
+ standalone evaluation scripts beside this package are the
9
+ retrieval-evaluation surface.
10
+ """