From 8acc0107ec0160f974c6cdd7a7914d2bfa36bb15 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:30:17 -0400 Subject: [PATCH] feat: functional-service.v1 lab surface (offline smoke, SourceOS carry) Normalize ocrlab to the Holmes functional-lab standard: manifest, deterministic offline smoke, validator, and SourceOS carry profile. No model downloads. --- .github/workflows/validate.yml | 6 ++- Makefile | 24 +++------ README.md | 25 ++++++++- evidence/smoke-receipt.example.json | 37 +++++++++++++ examples/smoke-input.json | 17 ++++++ examples/smoke-output.json | 22 ++++++++ manifests/functional-service.json | 54 +++++++++++++++++++ tools/emit_sourceos_carry.py | 71 +++++++++++++++++++++++++ tools/smoke.py | 50 ++++++++++++++++++ tools/validate.py | 80 +++++++++++++++++++++++++++++ 10 files changed, 367 insertions(+), 19 deletions(-) create mode 100644 evidence/smoke-receipt.example.json create mode 100644 examples/smoke-input.json create mode 100644 examples/smoke-output.json create mode 100644 manifests/functional-service.json create mode 100644 tools/emit_sourceos_carry.py create mode 100644 tools/smoke.py create mode 100644 tools/validate.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ed16bce..16247cb 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -9,5 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Validate + - name: Validate ocrlab run: make validate + - name: Offline smoke test + run: make smoke + - name: Emit SourceOS carry fixture + run: make carry diff --git a/Makefile b/Makefile index bfbb035..e897b13 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,10 @@ - .PHONY: ocr-bootstrap ocr-rebuild ocr-verify ocr-smoketest ocr-shell +.PHONY: validate smoke carry - ocr-bootstrap: - ./scripts/bootstrap_brew_ocr.sh - - ocr-rebuild: - ./scripts/rebuild_ocr_env.sh - - ocr-verify: - ./scripts/verify_ocr_env.sh - - ocr-smoketest: - source .venv-ocr/bin/activate && ./scripts/smoketest_ocr.sh +validate: + python3 tools/validate.py - ocr-shell: - ./scripts/ocr_shell.sh +smoke: + python3 tools/smoke.py -.PHONY: validate -validate: - ./tools/validate.sh +carry: + python3 tools/emit_sourceos_carry.py > examples/sourceos-carry.ocrlab.json diff --git a/README.md b/README.md index 6fbf5b5..5d30e77 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ # ocrlab -Linux-native capability repo for SourceOS (Linux-first). +Linux-native lab workspace for OCR and document-text extraction feature experiments for SocioProphet services. + +## Role in the Holmes lab family + +`ocrlab` is the first governed pilot lab for the Holmes document-intelligence stack. It emits a `functional-service.v1` manifest that can be consumed by: + +- `SocioProphet/holmes` as the document-intelligence product surface; +- `SocioProphet/functional-model-surfaces` as the normative service contract spine; +- `SocioProphet/sociosphere` as workspace registry and maturity controller; +- `SourceOS-Linux/sourceos-model-carry` as disabled-by-default SourceOS carry metadata; +- `SociOS-Linux/socios` as opt-in training/tuning orchestration; +- `SocioProphet/model-governance-ledger` and `SocioProphet/model-router` for promotion and routing. + +This repository does not store model weights, mutable adapters, personal data, secrets, or promotion authority. + +## Local validation + +```bash +make validate +make smoke +make carry +``` + +The smoke path is deterministic and offline-only. diff --git a/evidence/smoke-receipt.example.json b/evidence/smoke-receipt.example.json new file mode 100644 index 0000000..7ea8beb --- /dev/null +++ b/evidence/smoke-receipt.example.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "ocrlab.smoke-receipt.v0", + "kind": "SmokeReceipt", + "serviceId": "ocrlab.holmes.ocr-pilot", + "runId": "urn:socioprophet:ocrlab:smoke:offline-lexical-v0", + "mode": "offline-deterministic", + "networkRequired": false, + "modelWeightsIncluded": false, + "mutableAdapterIncluded": false, + "inputRef": "examples/smoke-input.json", + "outputRef": "examples/smoke-output.json", + "assertions": [ + { + "id": "top-result-holmes", + "description": "Holmes candidate ranks first for governed semantic search query.", + "pass": true + }, + { + "id": "no-network", + "description": "Smoke runner uses only Python standard library and local fixtures.", + "pass": true + }, + { + "id": "sourceos-no-mutable-state", + "description": "SourceOS carry path carries no model weights or mutable adapters.", + "pass": true + } + ], + "governanceRefs": [ + "SocioProphet/holmes", + "SocioProphet/functional-model-surfaces", + "SocioProphet/model-governance-ledger", + "SocioProphet/model-router", + "SourceOS-Linux/sourceos-model-carry", + "SocioProphet/sociosphere" + ] +} diff --git a/examples/smoke-input.json b/examples/smoke-input.json new file mode 100644 index 0000000..065b59b --- /dev/null +++ b/examples/smoke-input.json @@ -0,0 +1,17 @@ +{ + "query": "governed semantic search", + "candidates": [ + { + "id": "holmes", + "text": "Holmes investigates evidence with semantic search and governed retrieval." + }, + { + "id": "weather", + "text": "The weather report predicts rain and wind tomorrow." + }, + { + "id": "sourceos", + "text": "SourceOS carries signed service references and local-only model profiles." + } + ] +} diff --git a/examples/smoke-output.json b/examples/smoke-output.json new file mode 100644 index 0000000..e3f7cb8 --- /dev/null +++ b/examples/smoke-output.json @@ -0,0 +1,22 @@ +{ + "serviceId": "ocrlab.holmes.ocr-pilot", + "query": "governed semantic search", + "ranking": [ + { + "id": "holmes", + "score": 3, + "matchedTerms": ["governed", "search", "semantic"] + }, + { + "id": "sourceos", + "score": 0, + "matchedTerms": [] + }, + { + "id": "weather", + "score": 0, + "matchedTerms": [] + } + ], + "pass": true +} diff --git a/manifests/functional-service.json b/manifests/functional-service.json new file mode 100644 index 0000000..7644483 --- /dev/null +++ b/manifests/functional-service.json @@ -0,0 +1,54 @@ +{ + "schemaVersion": "functional-service.v1", + "service": { + "id": "ocrlab.holmes.ocr-pilot", + "name": "OCRLab Holmes OCR Pilot", + "ownerRepository": "SociOS-Linux/ocrlab", + "status": "experimental", + "description": "Offline deterministic OCR-feature and ranking pilot for Holmes document-intelligence and SourceOS carry integration." + }, + "function": "ocr", + "model": { + "modelRef": "urn:socioprophet:functional-model:ocrlab:deterministic-ocr:v0", + "adapterRefs": [], + "runtime": "python-stdlib-offline-smoke", + "mutableStatePolicy": "forbidden-in-sourceos" + }, + "inputs": [ + "image/document", + "text/corpus" + ], + "outputs": [ + "ocr/features", + "ranking/candidates", + "evidence/smoke-receipt" + ], + "evals": { + "required": true, + "references": [ + "examples/smoke-input.json", + "examples/smoke-output.json", + "evidence/smoke-receipt.example.json" + ], + "minimumPromotionGate": "offline-smoke-similarity-ordering" + }, + "governance": { + "ledgerRequired": true, + "guardrailRequired": true, + "routingRequired": true, + "policyRefs": [ + "SocioProphet/model-governance-ledger", + "SocioProphet/guardrail-fabric", + "SocioProphet/model-router", + "SocioProphet/holmes" + ] + }, + "sourceosCarry": { + "allowed": true, + "carriesMutableModelState": false, + "clientRefRequired": true, + "launchProfileRefs": [ + "urn:srcos:model-carry:functional-service:ocrlab-holmes-pilot" + ] + } +} diff --git a/tools/emit_sourceos_carry.py b/tools/emit_sourceos_carry.py new file mode 100644 index 0000000..23407d3 --- /dev/null +++ b/tools/emit_sourceos_carry.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "manifests" / "functional-service.json" + + +def main() -> int: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + service = manifest["service"] + carry = { + "schemaVersion": "functional-service-carry.v0", + "kind": "FunctionalServiceCarry", + "carryId": "urn:srcos:model-carry:functional-service:ocrlab-holmes-pilot", + "displayName": "OCRLab Holmes Pilot Carry Profile", + "service": { + "id": service["id"], + "ownerRepository": service["ownerRepository"], + "function": manifest["function"], + "manifestRef": "SociOS-Linux/ocrlab:manifests/functional-service.json" + }, + "sourceos": { + "includeInImageBuilds": True, + "enabledByDefault": False, + "activationRequiresOptIn": True, + "carriesMutableModelState": False, + "carriesModelWeights": False, + "clientRefRequired": True + }, + "runtime": { + "mode": "local-client-reference", + "networkDefault": "denied", + "launchProfileRef": "urn:srcos:launch-profile:ocrlab-holmes-pilot:disabled-v0", + "healthCheck": "make smoke" + }, + "policy": { + "localOnlyDefault": True, + "sendPromptOffDeviceDefault": False, + "allowToolUse": False, + "allowNetwork": False, + "allowModelDownload": False, + "requiresSignedServiceReference": True, + "requiresLedgerForPromotion": True, + "requiresSignedIntentForPersonalTuning": True + }, + "evidence": { + "emitRuntimeHealth": True, + "emitRoutingDecision": True, + "emitSmokeReceipt": True, + "emitPromptHashOnly": True, + "smokeReceiptRef": "SociOS-Linux/ocrlab:evidence/smoke-receipt.example.json" + }, + "integrations": { + "productSurface": "SocioProphet/holmes", + "workspaceController": "SocioProphet/sociosphere", + "contractSpine": "SocioProphet/functional-model-surfaces", + "carryAuthority": "SourceOS-Linux/sourceos-model-carry", + "optInAutomation": "SociOS-Linux/socios", + "governanceLedger": "SocioProphet/model-governance-ledger", + "router": "SocioProphet/model-router" + } + } + print(json.dumps(carry, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/smoke.py b/tools/smoke.py new file mode 100644 index 0000000..a5a43c4 --- /dev/null +++ b/tools/smoke.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +INPUT = ROOT / "examples" / "smoke-input.json" +EXPECTED = ROOT / "examples" / "smoke-output.json" + + +def terms(text: str) -> set[str]: + return set(re.findall(r"[a-z0-9]+", text.lower())) + + +def rank(payload: dict) -> dict: + query = payload["query"] + query_terms = terms(query) + rows = [] + for candidate in payload.get("candidates", []): + matched = sorted(query_terms & terms(candidate.get("text", ""))) + rows.append({ + "id": candidate["id"], + "score": len(matched), + "matchedTerms": matched, + }) + rows.sort(key=lambda row: (-row["score"], row["id"])) + return { + "serviceId": "ocrlab.holmes.ocr-pilot", + "query": query, + "ranking": rows, + "pass": bool(rows and rows[0]["id"] == "holmes"), + } + + +def main() -> int: + payload = json.loads(INPUT.read_text(encoding="utf-8")) + actual = rank(payload) + expected = json.loads(EXPECTED.read_text(encoding="utf-8")) + if actual != expected: + print(json.dumps({"ok": False, "actual": actual, "expected": expected}, indent=2), file=sys.stderr) + return 1 + print(json.dumps({"ok": True, "serviceId": actual["serviceId"], "top": actual["ranking"][0]}, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate.py b/tools/validate.py new file mode 100644 index 0000000..9d7debc --- /dev/null +++ b/tools/validate.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +REQUIRED_FILES = [ + "README.md", + "Makefile", + "manifests/functional-service.json", + "examples/smoke-input.json", + "examples/smoke-output.json", + "evidence/smoke-receipt.example.json", + "tools/smoke.py", + "tools/emit_sourceos_carry.py", +] + +EXPECTED_SERVICE_ID = "ocrlab.holmes.ocr-pilot" +EXPECTED_FUNCTION = "ocr" + + +def fail(message: str) -> int: + print(f"ERROR: {message}", file=sys.stderr) + return 1 + + +def load_json(path: str) -> dict: + return json.loads((ROOT / path).read_text(encoding="utf-8")) + + +def main() -> int: + for rel in REQUIRED_FILES: + if not (ROOT / rel).exists(): + return fail(f"missing required file: {rel}") + + manifest = load_json("manifests/functional-service.json") + if manifest.get("schemaVersion") != "functional-service.v1": + return fail("manifest must use functional-service.v1") + if manifest.get("function") != EXPECTED_FUNCTION: + return fail("manifest function must be ocr") + service = manifest.get("service", {}) + if service.get("id") != EXPECTED_SERVICE_ID: + return fail("manifest service.id mismatch") + if service.get("ownerRepository") != "SociOS-Linux/ocrlab": + return fail("manifest ownerRepository mismatch") + + sourceos = manifest.get("sourceosCarry", {}) + if sourceos.get("allowed") is not True: + return fail("sourceosCarry.allowed must be true") + if sourceos.get("carriesMutableModelState") is not False: + return fail("SourceOS carry must not carry mutable model state") + if sourceos.get("clientRefRequired") is not True: + return fail("SourceOS carry must require a client ref") + + output = load_json("examples/smoke-output.json") + if output.get("serviceId") != EXPECTED_SERVICE_ID: + return fail("smoke output serviceId mismatch") + ranking = output.get("ranking", []) + if not ranking or ranking[0].get("id") != "holmes": + return fail("smoke output must rank holmes first") + if output.get("pass") is not True: + return fail("smoke output pass must be true") + + receipt = load_json("evidence/smoke-receipt.example.json") + if receipt.get("networkRequired") is not False: + return fail("smoke receipt must be offline") + if receipt.get("modelWeightsIncluded") is not False: + return fail("smoke receipt must not include model weights") + if receipt.get("mutableAdapterIncluded") is not False: + return fail("smoke receipt must not include mutable adapters") + + print("OK: ocrlab manifest and fixtures validated") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())