diff --git a/.env.example b/.env.example index 7282c5a2e..b702bd2c7 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,8 @@ -# Copy to .env to override. Every value below already has a working -# default baked into docker-compose.yml (see ${VAR:-default} references) -- -# `docker compose up` succeeds from a clean checkout with no .env file at -# all for the default profile. The optional MCP profile requires measured -# quota inputs below. Other defaults are throwaway local-dev-only credentials, not -# production secrets; see docs/adr/0001-demo-identity-and-data-boundary.md. +# Copy to .env to override. Every value below is either a working local-dev +# default or an explicitly optional integration setting. The optional MCP +# profile requires measured quota inputs below. Local defaults are throwaway +# development credentials, not production secrets; see +# docs/adr/0001-demo-identity-and-data-boundary.md. # Host ports deliberately avoid each service's own default (5432, 6379, # 8080) -- a dev machine commonly already runs its own Postgres/Redis/local @@ -34,24 +33,14 @@ MCP_ALLOWED_ORIGINS= MCP_RATE_LIMIT_REQUESTS= MCP_RATE_LIMIT_WINDOW_SECONDS= -# Optional. Empty = every LLM/vision channel is unavailable (Null client, -# dropped and renormalized -- never a placeholder score). Point these at a -# running contextual-orchestrator to turn the channels on. +# Optional contextual-orchestrator consumer contract. Empty keeps every +# LLM/vision channel unavailable (Null client, dropped and renormalized -- never +# a placeholder score). Provider endpoints, provider credentials, model-agent +# configuration, discovery and fallback belong to the separately deployed +# contextual-orchestrator and are intentionally not LineageWeave settings. ORCHESTRATOR_BASE_URL= ORCHESTRATOR_API_KEY= -# GitHub workflows inject the canonical provider names from masked secrets. -# Non-GitHub Compose runs also accept the operator's ~/.env compatibility -# names below; docker-compose maps them to the canonical names without -# exposing them to the frontend or committing them. -# Canonical provider endpoint for contextual-orchestrator. -LLM_GATEWAY_API_URL= -# Compatibility alias; LLM_GATEWAY_API_URL wins when both are set. -LLM_GATEWAY_URL= -LLM_GATEWAY_API_KEY= -LLM_GATEWAY_EMBEDDING_MODEL= -LLM_API_GATEWAY= -LLM_API_KEY= CALDAV_BASE_URL= # Optional Naruon calendar projection consume (ADR 0203 step 2 / #336). # Empty keeps observed events fail-closed. Never put an end-user bearer here. @@ -64,4 +53,3 @@ RANKWEAVE_DISABLED= # Empty keeps truncated-without-cursor. Do not reuse OIDC or orchestrator secrets. # Must be at least 32 bytes when source paging is enabled. ONTOLOGY_SOURCE_CURSOR_SECRET= -CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS= diff --git a/AGENTS.md b/AGENTS.md index 3af4169bc..ca766e5be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,9 +70,10 @@ does it (`gh repo list ContextualWisdomLab`). - All LLM, VISION, embedding, and structured-output traffic crosses `contextual-orchestrator`. This repository never calls a provider API directly and never uses a monkey patch to repair an upstream capability. -- Compose loads provider transport credentials from `~/.env` into the - orchestrator service. Never copy those values into this repository, an - image, a fixture, a log, or a committed agent configuration. +- LineageWeave accepts only the deployed orchestrator's + `ORCHESTRATOR_BASE_URL` and `ORCHESTRATOR_API_KEY`. Provider transport + credentials stay in the contextual-orchestrator deployment; never copy them + into this repository, an image, a fixture, a log, or agent configuration. - `LLM_GATEWAY_MODEL`, `VISION_MODEL`, and provider-specific model selectors are not LineageWeave configuration. Model discovery, capability selection, reasoning effort, protocol negotiation, and VISION selection belong to @@ -98,16 +99,16 @@ evidence for model quality, routing, reasoning effort, agent count, synthesis, or VISION selection. If the papers do not support a policy, leave it undecided or unavailable rather than inventing a heuristic. -The canonical provider credentials are runtime-only from `~/.env` through the -Compose `env_file` boundary. Never copy `~/.env` into the repository or image, -print its values, or persist them. Do not add `LLM_GATEWAY_MODEL`; the upstream +Provider credentials remain runtime-only in the contextual-orchestrator +deployment. Never copy them into this repository or image, print them, or +persist them. Do not add `LLM_GATEWAY_MODEL`; the upstream contextual-orchestrator owns model discovery and selection. ## LLM and VISION boundary -- Use `LLM_GATEWAY_API_KEY` and `LLM_GATEWAY_API_URL` from the user's `~/.env` - at runtime. Keep compatibility aliases only at the process boundary; do not - introduce a second credential source or a repository-local secret. +- Use `ORCHESTRATOR_BASE_URL` and `ORCHESTRATOR_API_KEY` to consume the + deployed contextual-orchestrator. Do not introduce a provider credential + source, compatibility alias, or repository-local secret. - Every LLM and VISION operation goes through contextual-orchestrator. This includes adjudication, summaries, Keyman/entity extraction, post chat, paragraph structure, image region recognition, OCR, image descriptions, and @@ -128,7 +129,8 @@ contextual-orchestrator owns model discovery and selection. instruction roles at the orchestrator boundary. The orchestrator owns the translation and provider capability handling; do not fork prompts per transport in this repository. -- Treat `LLM_GATEWAY_API_URL` as an opaque OpenAI-compatible gateway endpoint. +- Treat `ORCHESTRATOR_BASE_URL` as the deployed contextual-orchestrator + endpoint. Do not add MLX/local-server URL schemes, port lists, local defaults, chat-template injection, or vendor-specific bootstrap exceptions in LineageWeave. Provider-specific capability translation belongs upstream. diff --git a/CHANGELOG.d/2.29.0-contextual-orchestrator-owner-boundary.md b/CHANGELOG.d/2.29.0-contextual-orchestrator-owner-boundary.md new file mode 100644 index 000000000..05b8006e0 --- /dev/null +++ b/CHANGELOG.d/2.29.0-contextual-orchestrator-owner-boundary.md @@ -0,0 +1,5 @@ +## Changed + +- LineageWeave now consumes contextual-orchestrator only through `ORCHESTRATOR_BASE_URL` and `ORCHESTRATOR_API_KEY`; provider endpoints, provider credentials, model-agent bootstrap, discovery and fallback remain owned by contextual-orchestrator. +- The default Compose stack no longer embeds or starts a LineageWeave-owned contextual-orchestrator runtime. Missing orchestration configuration leaves model-backed channels unavailable instead of falling back to a provider gateway. +- Added architectural fitness coverage that rejects provider-boundary configuration and an embedded orchestrator runtime in LineageWeave. diff --git a/Makefile b/Makefile index b6764b72f..9f49d172b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ .PHONY: up down logs smoke seed ps load-http load-mcp -# Keep provider credentials outside the repository. Compose interpolation must -# read the same home env file as the orchestrator container's env_file. -COMPOSE := docker compose --env-file "$$HOME/.env" +# Compose reads the repository-local .env convention when present. Provider +# credentials belong to the separately deployed contextual-orchestrator and +# are not an input to the LineageWeave stack. +COMPOSE := docker compose up: $(COMPOSE) up -d diff --git a/README.md b/README.md index 5f1480b40..d70ab688c 100644 --- a/README.md +++ b/README.md @@ -37,19 +37,29 @@ retain the supporting bibliography and aggregate evidence. ## How it fits with the rest of the ecosystem -LineageWeave is a thin orchestration/BI layer. It does not do its own -psychometric or statistical estimation -- that stays inside -[TEPP](https://github.com/ContextualWisdomLab/TEPP) (Rust), consumed here -purely through TEPP's own published wire contract -(`lineageweave/tepp_client.py`, `AnalysisRunRequest` v1), never by reading -TEPP's tables or reimplementing TEPP's model. See -[ARCHITECTURE.md](ARCHITECTURE.md) for why the "computation layer must be -Rust + GPU/CPU multithreaded" rule that applies to TEPP does not apply to -this repo. - -The optional LLM-adjudication channel calls +LineageWeave owns lineage product policy, source-evidence binding, instrument +and rubric administration, pilot lifecycle, interpretation, and audit. It does +not own reusable model routing or reusable psychometric numerical kernels. + +Every production LLM-backed capability calls [contextual-orchestrator](https://github.com/ContextualWisdomLab/contextual-orchestrator) -(`lineageweave/adjudication_client.py`). Tree assembly reuses +through its published consumer contract. Provider/model discovery, routing, +fallback, structured-output compatibility, multi-agent orchestration, +reasoning-effort allocation, usage/cost provenance, and provider credentials +stay in contextual-orchestrator. LineageWeave receives versioned observations +and provenance; it never treats an LLM judgment as truth and never falls back +to a provider endpoint directly. See [ADR 0300](docs/adr/0300-contextual-orchestrator-owner-boundary.md). + +Reusable psychometric numerical/statistical kernels and their recovery +evidence belong to +[fast-mlsirm](https://github.com/ContextualWisdomLab/fast-mlsirm). Temporal, +event, multilevel, cross-classified, and multiple-membership measurement +semantics belong to [TEPP](https://github.com/ContextualWisdomLab/TEPP), +consumed through TEPP's published wire contract +(`lineageweave/tepp_client.py`, `AnalysisRunRequest` v1). LineageWeave does +not read either owner's tables or copy their model implementations. + +Tree assembly reuses [ThreadWeave](https://github.com/ContextualWisdomLab/ThreadWeave) (JWZ message threading) and channel fusion reuses [RankWeave](https://github.com/ContextualWisdomLab/RankWeave) (weighted @@ -80,14 +90,15 @@ Map your records into `lineageweave.Record` (see `lineageweave/models.py` for the field docs) and call `reconstruct()` directly -- nothing in this package assumes any particular source schema. -To turn on the embedding or LLM channels, pass a real client instead of the -`Null*` defaults: +To turn on the embedding or LLM channels, use clients backed by a running +contextual-orchestrator. A provider endpoint or provider credential is not a +LineageWeave integration contract: ```python from lineageweave import reconstruct from lineageweave.adjudication_client import ContextualOrchestratorAdjudicationClient -llm = ContextualOrchestratorAdjudicationClient(base_url="http://localhost:8000", api_key="...") +llm = ContextualOrchestratorAdjudicationClient(base_url="https://orchestrator.example", api_key="...") trees = reconstruct(my_records, llm=llm) ``` @@ -107,7 +118,7 @@ infrastructure -- PostgreSQL, Valkey, and a real Keycloak OIDC realm seeded with synthetic demo accounts -- runs via Docker Compose: ```bash -make up # docker compose up -d: postgres, valkey, keycloak +make up # docker compose up -d: postgres, valkey, keycloak, backend, frontend make smoke # real login as the synthetic demo user + JWT signature # verification against Keycloak's live JWKS -- proves the # OIDC round-trip actually works, not just that containers @@ -115,13 +126,12 @@ make smoke # real login as the synthetic demo user + JWT signature make down ``` -Outside GitHub, `make up` reads `~/.env` through Compose's `--env-file`. -Configure the contextual-orchestrator provider there with -`LLM_GATEWAY_API_URL` and `LLM_GATEWAY_API_KEY`; the key is never committed or -printed. `LLM_GATEWAY_URL`, `LLM_API_GATEWAY`, and `LLM_API_KEY` remain -compatibility aliases only. `ORCHESTRATOR_BASE_URL` and -`ORCHESTRATOR_API_KEY` are separate, internal -LineageWeave-to-orchestrator settings. +The local stack does not build or start contextual-orchestrator and does not +load provider credentials. If model-backed channels are required, deploy or +reach contextual-orchestrator through its canonical owner path and set only +`ORCHESTRATOR_BASE_URL` and `ORCHESTRATOR_API_KEY` in LineageWeave's local +`.env`. Leaving either empty keeps model-backed channels unavailable/fail-closed; +there is no direct-provider fallback. Postgres and Keycloak are built (`docker/postgres-init/`, `docker/keycloak/`) rather than bind-mounted, so the keycloak database's init script and the @@ -182,7 +192,7 @@ path against a live Keycloak + throwaway Postgres database, including that a private post scoped to a *different* corporate entity is excluded from the list and 403s on direct fetch. -`frontend/` (React + Vite + TypeScript, `docker compose`'s fourth service) +`frontend/` (React + Vite + TypeScript, `docker compose`'s frontend service) is a real client, not mocked or static: `react-oidc-context` drives an actual Authorization Code redirect through Keycloak, the home page draws the reconstructed lineage as a git-branch SVG (`GET /api/lineage`; @@ -214,9 +224,10 @@ the *real* OIDC round-trip is what `scripts/smoke_test_oidc.py` and ## Modular / standalone This repo runs standalone (own server, own tests, own CI) and is equally -usable as a library module (`import lineageweave`) inside a larger service --- no global state, no required environment variables, every external -dependency (embeddings, LLM adjudication, TEPP) is injected, not hardcoded. +usable as a library module (`import lineageweave`) inside a larger service. +Deterministic reconstruction remains available without any model service; +model-backed channels are injected through contextual-orchestrator's consumer +contract and fail closed when that contract is unavailable. ## License diff --git a/docker-compose.yml b/docker-compose.yml index d0a2422aa..ea503745b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,45 +97,6 @@ services: postgres: condition: service_healthy - orchestrator: - # Consume the paper-grounded orchestration service from main; inference - # remains behind its authenticated OpenAI-compatible boundary. - build: - context: ./docker/contextual-orchestrator - dockerfile: Dockerfile - env_file: - - ${HOME}/.env - environment: - AGENTS_FILE: /app/agents.json - PORT: 8000 - CONTEXTUAL_ORCHESTRATOR_TOKEN: ${CONTEXTUAL_ORCHESTRATOR_TOKEN:-${ORCHESTRATOR_API_KEY:-lineageweave-orchestrator-dev-only}} - # Gateway credentials and URL are supplied only by env_file (${HOME}/.env). - # Do not repeat them under environment:, where Compose interpolation can - # overwrite env_file values with an empty host-shell value. - # The upstream default remains 64 KiB for ordinary text APIs. Buyer - # image blocks are base64 data URIs, so the multimodal boundary gets an - # explicit bounded 8 MiB limit rather than an unbounded request size. - CONTEXTUAL_ORCHESTRATOR_MAX_BODY_BYTES: ${CONTEXTUAL_ORCHESTRATOR_MAX_BODY_BYTES:-8388608} - CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS: ${CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS:-host.docker.internal} - OTEL_SERVICE_NAME: ${OTEL_ORCHESTRATOR_SERVICE_NAME:-contextual-orchestrator} - # Do not set OTEL_EXPORTER_OTLP_ENDPOINT here. An empty - # ${OTEL_EXPORTER_OTLP_ENDPOINT:-} interpolation would wipe a value from - # env_file (${HOME}/.env). Export stays opt-in from that file or the host. - command: ["python", "/app/start.py"] - ports: - - "${ORCHESTRATOR_PORT:-18000}:8000" - healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2)"] - interval: 5s - timeout: 3s - retries: 10 - # Warm-up window: failures inside start_period do not consume the retry - # budget, so a booting orchestrator that becomes healthy within 50s is - # never counted against retries. A dead service trips the gate at - # ~100s (start_period + 10 x 5s), matching the previous retries: 20 - # budget exactly; the win is boot tolerance, not faster dead detection. - start_period: 50s - backend: build: context: . @@ -167,12 +128,11 @@ services: VALKEY_URL: redis://valkey:6379/0 OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-lineageweave} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} - # Empty by default: every LLM/vision channel stays the Null client - # (dropped, not faked). Set these to a running contextual-orchestrator - # to turn the channels on. Provider credentials use LLM_GATEWAY_API_URL / - # LLM_GATEWAY_API_KEY in the orchestrator's private env file. - ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-http://orchestrator:8000} - ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-${CONTEXTUAL_ORCHESTRATOR_TOKEN:-lineageweave-orchestrator-dev-only}} + # Empty by default: every LLM/vision channel stays unavailable. Set only + # the published contextual-orchestrator consumer endpoint and bearer to + # enable them. Provider configuration belongs to contextual-orchestrator. + ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-} + ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-} SEARXNG_BASE_URL: http://searxng:8080 TEPP_TRANSPORT_URL: ${TEPP_TRANSPORT_URL:-} TEPP_API_KEY: ${TEPP_API_KEY:-} @@ -190,8 +150,6 @@ services: condition: service_healthy database_migration: condition: service_completed_successfully - orchestrator: - condition: service_healthy keycloak: condition: service_started valkey: @@ -222,8 +180,8 @@ services: OIDC_JWKS_URI: ${OIDC_JWKS_URI:-} OIDC_CLOCK_SKEW_SECONDS: ${OIDC_CLOCK_SKEW_SECONDS:-5} VALKEY_URL: redis://valkey:6379/0 - ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-http://orchestrator:8000} - ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-${CONTEXTUAL_ORCHESTRATOR_TOKEN:-lineageweave-orchestrator-dev-only}} + ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-} + ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-} # Local Keycloak mints this exact fixed audience. Production Keyverse # deployments configure both values together outside this demo stack. MCP_RESOURCE_URL: http://localhost:18001/mcp @@ -242,8 +200,6 @@ services: condition: service_healthy database_migration: condition: service_completed_successfully - orchestrator: - condition: service_healthy keycloak: condition: service_started valkey: diff --git a/docker/contextual-orchestrator/Dockerfile b/docker/contextual-orchestrator/Dockerfile deleted file mode 100644 index 0af60f58c..000000000 --- a/docker/contextual-orchestrator/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM python:3.12-slim@sha256:423ed6ab25b1921a477529254bfeeabf5855151dc2c3141699a1bfc852199fbf - -WORKDIR /app - -# Reuse the upstream implementation without copying it into LineageWeave. -# Pin the runtime to a reviewed immutable upstream commit; model selection, -# structured synthesis, and reasoning policy stay in contextual-orchestrator. -ADD https://github.com/ContextualWisdomLab/contextual-orchestrator/archive/1a40e0f7ad10d1a24137d69d20e44fc9a5dcdd89.tar.gz /tmp/contextual-orchestrator.tar.gz -RUN mkdir /tmp/contextual-orchestrator \ - && tar -xzf /tmp/contextual-orchestrator.tar.gz --strip-components=1 -C /tmp/contextual-orchestrator \ - && cp -R /tmp/contextual-orchestrator/contextual_orchestrator /app/contextual_orchestrator \ - && cp -R /tmp/contextual-orchestrator/examples /app/examples \ - && rm -rf /tmp/contextual-orchestrator /tmp/contextual-orchestrator.tar.gz \ - && python -m pip install --no-cache-dir \ - 'opentelemetry-api>=1.30.0' \ - 'opentelemetry-sdk>=1.30.0' \ - 'opentelemetry-exporter-otlp-proto-http>=1.30.0' \ - && useradd --uid 10001 --no-create-home orchestrator - -COPY agents.json /app/agents.json -COPY start.py /app/start.py - -ENV AGENTS_FILE=/app/agents.json \ - PORT=8000 - -USER orchestrator -EXPOSE 8000 -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ - CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2)"] -CMD ["python", "/app/start.py"] diff --git a/docker/contextual-orchestrator/agents.json b/docker/contextual-orchestrator/agents.json deleted file mode 100644 index 6eed030a5..000000000 --- a/docker/contextual-orchestrator/agents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "agents": [ - { - "id": "multimodal_reasoning_agent", - "model": "", - "provider_protocol": "auto", - "base_url": "https://integrate.api.nvidia.com/v1", - "credential_key": "LLM_GATEWAY_API_KEY", - "tags": [ - "reasoning", - "writing", - "planning", - "verification", - "extraction", - "vision" - ], - "priority": 1 - } - ] -} diff --git a/docker/contextual-orchestrator/start.py b/docker/contextual-orchestrator/start.py deleted file mode 100644 index 01dc5d189..000000000 --- a/docker/contextual-orchestrator/start.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Bootstrap the local NIM credential, then start contextual-orchestrator. - -The provider key is transport-only: it is registered in the orchestrator's -process-local credential store before the server starts and removed from the -process environment before request handling begins. -""" - -from __future__ import annotations - -import os -import sys -import json -from pathlib import Path - - -def _pop_first_env(*names: str) -> str: - """Read the first configured alias without leaving credentials in the environment.""" - first = "" - for name in names: - value = os.environ.pop(name, "").strip() - if value and not first: - first = value - return first - - -def main() -> None: - """Register the provider credential and delegate to the upstream server.""" - gateway_key = _pop_first_env("LLM_GATEWAY_API_KEY", "LLM_API_KEY") - provider_credentials = { - name: value - for name in ( - "OPENAI_API_KEY", - "OPENROUTER_API_KEY", - "NVIDIA_NIM_API_KEY", - "NVIDIA_NIM_API_KEY_SUB", - "BYTEZ_API_KEY", - ) - if (value := os.environ.pop(name, "").strip()) - } - if not gateway_key: - raise SystemExit("LLM_GATEWAY_API_KEY or LLM_API_KEY is required to start the real LLM service") - auth_token = os.environ.get("CONTEXTUAL_ORCHESTRATOR_TOKEN", "").strip() - if not auth_token: - raise SystemExit("CONTEXTUAL_ORCHESTRATOR_TOKEN is required to start the authenticated LLM service") - - provider_url = _pop_first_env("LLM_GATEWAY_API_URL", "LLM_GATEWAY_URL", "LLM_API_GATEWAY") - if not provider_url: - raise SystemExit("LLM_GATEWAY_API_URL or LLM_GATEWAY_URL is required to start the gateway") - if not provider_url.rstrip("/").endswith("/v1"): - provider_url = provider_url.rstrip("/") + "/v1" - raw_limit = os.environ.pop("LLM_GATEWAY_MAX_OUTPUT_TOKENS", "4096").strip() - try: - max_output_tokens = int(raw_limit) - except ValueError as exc: - raise SystemExit("LLM_GATEWAY_MAX_OUTPUT_TOKENS must be an integer") from exc - if not 64 <= max_output_tokens <= 4096: - raise SystemExit("LLM_GATEWAY_MAX_OUTPUT_TOKENS must be between 64 and 4096") - raw_body_limit = os.environ.pop("CONTEXTUAL_ORCHESTRATOR_MAX_BODY_BYTES", str(8 * 1024 * 1024)).strip() - try: - max_body_bytes = int(raw_body_limit) - except ValueError as exc: - raise SystemExit("CONTEXTUAL_ORCHESTRATOR_MAX_BODY_BYTES must be an integer") from exc - if not 64 * 1024 <= max_body_bytes <= 64 * 1024 * 1024: - raise SystemExit("CONTEXTUAL_ORCHESTRATOR_MAX_BODY_BYTES must be between 65536 and 67108864") - agents_path = Path("/tmp/lineageweave-agents.json") - agents = json.loads(Path("/app/agents.json").read_text(encoding="utf-8")) - for agent in agents["agents"]: - agent["base_url"] = provider_url - agent["credential_key"] = "LLM_GATEWAY_API_KEY" - agent.setdefault("provider_protocol", "auto") - os.environ.pop("LLM_GATEWAY_EMBEDDING_MODEL", None) - agents_path.write_text(json.dumps(agents), encoding="utf-8") - - from contextual_orchestrator.credentials import register_credential - - register_credential("LLM_GATEWAY_API_KEY", gateway_key) - for credential_name, credential_value in provider_credentials.items(): - register_credential(credential_name, credential_value) - del gateway_key - del provider_credentials - sys.argv = [ - "contextual_orchestrator", - "--serve", - "--agents", - str(agents_path), - "--auto-discover-model-agents", - "--allow-discovery-failures", - "--host", - "0.0.0.0", - "--port", - "8000", - "--allow-public-bind", - "--auth-token", - auth_token, - "--max-output-tokens", - str(max_output_tokens), - "--max-body-bytes", - str(max_body_bytes), - ] - del provider_url - del auth_token - from contextual_orchestrator.__main__ import main as serve - - serve() - - -if __name__ == "__main__": - main() diff --git a/docs/adr/0300-contextual-orchestrator-owner-boundary.md b/docs/adr/0300-contextual-orchestrator-owner-boundary.md new file mode 100644 index 000000000..d5aa8d058 --- /dev/null +++ b/docs/adr/0300-contextual-orchestrator-owner-boundary.md @@ -0,0 +1,59 @@ +# ADR 0300: Contextual-orchestrator owns the provider runtime boundary + +- Status: Accepted +- Date: 2026-09-01 +- Supersedes: the LineageWeave-owned provider deployment and credential portions of ADR 0030, ADR 0045, ADR 0072, ADR 0076, and ADR 0083 +- Preserves: ADR 0070's upstream-contract-only principle +- Related: `ContextualWisdomLab/contextual-orchestrator`, `ContextualWisdomLab/fast-mlsirm`, `ContextualWisdomLab/TEPP` + +## Context + +LineageWeave owns measurement policy, source-evidence binding, instrument and rubric administration, pilot lifecycle, interpretation, and audit. It consumes LLM judgments as fallible observations. It does not own provider selection or an LLM serving runtime. + +The repository nevertheless accumulated an embedded contextual-orchestrator container bootstrap, a LineageWeave-owned model-agent file, provider endpoint and credential variables, and operator-script fallbacks that could treat a provider gateway as though it were the contextual-orchestrator service. Those paths create two sources of truth for provider discovery and credentials and allow a consumer to bypass the orchestration layer accidentally. + +The canonical owner is `ContextualWisdomLab/contextual-orchestrator`. Its published versioned contract owns provider/model discovery, routing and fallback, structured-output compatibility, test-time-compute allocation, role-specific reasoning effort, multi-agent orchestration, usage/cost provenance, provider credentials, and provider protocol translation. + +## Decision + +LineageWeave is only a contextual-orchestrator consumer. + +Production and operator code in this repository may configure the LLM boundary only with: + +- `ORCHESTRATOR_BASE_URL`: the published contextual-orchestrator service endpoint. +- `ORCHESTRATOR_API_KEY`: the consumer credential accepted by that service. + +LineageWeave MUST NOT accept provider endpoints, provider API keys, provider-specific model-agent configuration, or contextual-orchestrator owner-process credentials as substitutes for those consumer settings. In particular, provider-gateway aliases and provider API-key names are not LineageWeave runtime configuration. + +The default Compose stack MUST NOT build or start contextual-orchestrator. Operators deploy or select contextual-orchestrator through its canonical owner path and then point LineageWeave at the published service contract. With no configured orchestrator endpoint or consumer credential, LLM, VISION, embedding, judge, explanation, extraction, and other model-backed channels remain unavailable or fail closed according to their existing Null-client contracts. There is no direct-provider fallback. + +All LineageWeave LLM capabilities follow this dependency direction: + +`LineageWeave measurement policy/evidence -> contextual-orchestrator judge orchestration -> versioned judge observations/provenance -> fast-mlsirm psychometric computation -> optional TEPP temporal/multilevel analysis -> LineageWeave interpretation/admin/audit`. + +Provider DTOs and orchestration internals are outside the LineageWeave domain model. Wire compatibility is isolated behind LineageWeave's contextual-orchestrator client adapters. Reusable psychometric numerical kernels remain owned by fast-mlsirm; temporal/event/multilevel measurement semantics remain owned by TEPP. + +## Architectural fitness + +Repository tests enforce that: + +1. `docker/contextual-orchestrator` is absent. +2. production/runtime Python does not accept provider credential or provider-gateway configuration names. +3. `.env.example` exposes only the contextual-orchestrator consumer endpoint and consumer credential for LLM integration. +4. `docker-compose.yml` does not own an orchestrator service or provider configuration and passes only `ORCHESTRATOR_BASE_URL` / `ORCHESTRATOR_API_KEY` to LineageWeave processes. + +These tests are ownership checks, not a ban on historical or normative documentation naming upstream provider variables when explaining why they are outside this bounded context. + +## Consequences + +- Provider configuration and credentials have one canonical owner. +- Updating contextual-orchestrator no longer requires a LineageWeave-local provider bootstrap or model-agent copy. +- Local development that needs LLM behavior must run or reach contextual-orchestrator separately. The deterministic LineageWeave stack remains usable with model-backed channels unavailable. +- A contextual-orchestrator outage or missing contract configuration is visible as an unavailable model-backed capability, never as a provider fallback. +- Existing documentation that describes LineageWeave as loading provider credentials into a local orchestrator container is stale under this ADR and must migrate to the consumer-only contract. + +## Migration and rollback + +Migration removes the embedded orchestrator Docker bootstrap and provider-owned environment variables, narrows operator scripts to the published consumer contract, and adds architectural fitness tests. Consumer behavior otherwise remains unchanged: requests continue to target contextual-orchestrator's versioned HTTP contract. + +Rollback of a contextual-orchestrator deployment is performed in the contextual-orchestrator owner environment by selecting a previously reviewed owner release. Reintroducing provider credentials, provider endpoints, copied model-agent configuration, or an embedded orchestration runtime into LineageWeave is not an acceptable rollback because it recreates the ownership defect. diff --git a/docs/context-map.md b/docs/context-map.md new file mode 100644 index 000000000..81e5e84dd --- /dev/null +++ b/docs/context-map.md @@ -0,0 +1,88 @@ +# LineageWeave Context Map + +Status: current architectural contract for the LineageWeave repository. Accepted ADRs remain authoritative when a specific decision is more detailed. + +## LineageWeave bounded contexts + +### Measurement Policy + +Owns the product decision about what is being evaluated and under which versioned measurement contract. It defines intended constructs, instrument lifecycle rules, model-family eligibility, activation criteria, evidence requirements, and the rule that unavailable or invalid observations do not become scores. + +It does **not** implement reusable psychometric estimators or select LLM providers. + +### Instrument Administration + +Owns versioned instruments, items, instructions, rubrics, evidence rules, factor declarations, anchors, judge-policy references, pilot sampling plans, scoring-model references, release state, and immutable published instrument versions. + +It may refer to a psychometric model family such as `rasch`, `irt_2plm`, `irt_3plm`, or `irt_4plm`; the reusable numerical implementation belongs to fast-mlsirm. + +### Evidence & Adjudication + +Owns source-evidence binding, admissibility and provenance of evidence presented for an item or lineage decision, and the product contract for observations returned by an adjudicator. + +LLM-backed adjudication is an Anti-Corruption Layer over contextual-orchestrator. A judge observation is evidence from a fallible rater/method facet, not truth. LineageWeave preserves enough provenance to distinguish judge model, provider observation identity when supplied by the orchestrator, orchestration/prompt policy revision, language, occasion, and agent role when those conditions are scientifically material. + +### Reporting & Interpretation + +Owns buyer-facing projections, explanation of accepted measurement evidence, audit views, lineage/product interpretation, and the distinction between pilot evidence and operationally activated scoring. It consumes versioned scientific results; it does not recompute an upstream owner's model privately. + +### Lineage Reconstruction + +Owns LineageWeave-specific candidate/evidence assembly and the product lineage graph semantics that are not generic retrieval or message threading. Generic ranking/fusion is delegated to RankWeave and generic message threading to ThreadWeave through their published contracts. + +## External owner contexts + +```mermaid +flowchart LR + MP[LineageWeave\nMeasurement Policy] + IA[LineageWeave\nInstrument Administration] + EA[LineageWeave\nEvidence & Adjudication] + RI[LineageWeave\nReporting & Interpretation] + LR[LineageWeave\nLineage Reconstruction] + + CO[contextual-orchestrator\nLLM orchestration] + FM[fast-mlsirm\npsychometric kernels] + TP[TEPP\ntemporal/multilevel measurement] + RW[RankWeave\nretrieval/fusion/evaluation] + TW[ThreadWeave\nmessage threading] + KV[Keyverse\nidentity/federation] + EW[EgressWeave\ngeneric outbound security] + GH[.github\norganization governance] + + MP --> IA + IA --> EA + EA -->|published judge-orchestration contract| CO + EA -->|versioned observations| MP + MP -->|versioned psychometric request/result contract| FM + FM --> RI + MP -->|optional temporal/multilevel contract| TP + TP --> RI + LR -->|published ranking/fusion contract| RW + LR -->|published threading contract| TW + KV -->|identity claims/contracts| IA + RI --> GH + LR --> GH + EA -. outbound-policy adapter when required .-> EW +``` + +The dependency direction is contract-first. A LineageWeave module may depend on its local port/Anti-Corruption Layer for an owner service; it must not import or copy that owner's provider, estimator, persistence, or control-plane internals. + +## Required orchestration and measurement flow + +For any model-backed measurement or evaluation capability, the normative flow is: + +`LineageWeave Measurement Policy / Evidence -> contextual-orchestrator Judge Orchestration -> versioned judge observations/provenance -> fast-mlsirm psychometric computation -> optional TEPP temporal/multilevel analysis -> LineageWeave Reporting & Interpretation`. + +Skipping contextual-orchestrator for a production LLM call, copying a fast-mlsirm numerical kernel, or performing TEPP-owned temporal/multilevel estimation locally is an ownership defect, not an optimization. + +## Anti-Corruption Layers + +- `lineageweave/adjudication_client.py`, embedding/vision/extraction/chat clients, and equivalent model-backed adapters translate LineageWeave evidence/policy into contextual-orchestrator's published contract. Provider credentials and provider SDKs are forbidden here. +- `lineageweave/tepp_client.py` translates to TEPP's published analysis contract. LineageWeave does not read TEPP tables. +- fast-mlsirm is consumed through its published package/API contracts for reusable psychometric numerics. LineageWeave may assemble product inputs and persist owner results, but must not fork an estimator. +- RankWeave and ThreadWeave are consumed through their public contracts; their generic algorithms are not copied into LineageWeave. +- Identity-provider internals stay behind the Keyverse/OIDC boundary. LineageWeave owns only its application authorization and product account linkage. + +## Transitional debt + +Historical modules and documentation may still use technical-layer or pre-boundary names. An accepted historical ADR remains useful evidence but does not override a later superseding ADR. Each migration must preserve unique LineageWeave behavior as explicit owner-contract requirements, add contract/parity evidence, migrate consumers, then remove the duplicate implementation and stale path rather than keeping two live sources of truth. diff --git a/docs/doctoring/ACTUAL_RUNTIME_EVIDENCE_2026-08-19_VISION.md b/docs/doctoring/ACTUAL_RUNTIME_EVIDENCE_2026-08-19_VISION.md index 3e3bb9ac6..d57352244 100644 --- a/docs/doctoring/ACTUAL_RUNTIME_EVIDENCE_2026-08-19_VISION.md +++ b/docs/doctoring/ACTUAL_RUNTIME_EVIDENCE_2026-08-19_VISION.md @@ -14,10 +14,12 @@ access token, API key, source title, organization name, post id, or image body. | Vision model bootstrap | Vision requests omit a model; contextual-orchestrator selects the registered vision-capable agent. | | local VLM executable | `mlx-vlm 0.6.15` served `gemma-4-e4b-it-4bit` on `http://host.docker.internal:18082/v1`. | -These observations are historical and do not define the current provider -contract. Current configuration must provide the generic -`LLM_GATEWAY_API_URL` and `LLM_GATEWAY_API_KEY`; no MLX-specific endpoint, -scheme, port, or chat-template field is required or injected. Vision is +These observations are historical and do not define the current provider or +consumer contract. Under ADR 0300, LineageWeave configures only the deployed +contextual-orchestrator consumer boundary with `ORCHESTRATOR_BASE_URL` and +`ORCHESTRATOR_API_KEY`. Provider endpoints, provider credentials, model-agent +bootstrap, and provider-specific Vision configuration belong to the +contextual-orchestrator deployment and are not LineageWeave settings. Vision is considered available only after a real multimodal request succeeds through -`contextual-orchestrator`. A text-only gateway failure is not converted into +`contextual-orchestrator`. A text-only provider failure is not converted into OCR, a caption, or a placeholder success. diff --git a/docs/ubiquitous-language.md b/docs/ubiquitous-language.md new file mode 100644 index 000000000..cdc136b7d --- /dev/null +++ b/docs/ubiquitous-language.md @@ -0,0 +1,106 @@ +# LineageWeave Ubiquitous Language + +These terms are the product/domain vocabulary for code, schemas, API contracts, tests, UI, and current architecture documents. Historical documents keep their original wording as evidence; new work uses the terms below unless a superseding ADR changes the domain model. + +## Measurement policy and administration + +**Instrument** +A governed collection of items, instructions, evidence rules, response semantics, scoring policy, and intended interpretation for a defined use. An instrument is not interchangeable with one fitted model. + +**Instrument Version** +An immutable published measurement contract. Draft and pilot revisions may change; a published version is never silently rewritten. + +**Item** +One governed evaluative prompt/criterion with an explicit response contract and evidence rule. An item is not an LLM prompt implementation. + +**Rubric** +A versioned rule describing what evidence supports each admissible observation. For a dichotomous item, `0` and `1` mean criterion not-supported/supported under the rubric; they are not compressed ordinal scores. + +**Evidence Rule** +The admissibility rule connecting source evidence to an item response. It specifies required evidence and invalidating conditions before scoring. + +**Observation** +A recorded response produced under a known instrument version and judging/measurement condition. Missing, not-observable, abstain, invalid-evidence, and unresolved-adjudication states are not coerced into an observed category. + +**Dichotomous Observation** +A binary `0`/`1` observation under an explicit criterion. New importance, significance, actionability, evidence, decision, and related evaluative indicators use this as the default unless empirical evidence and an accepted ADR justify a polytomous contract. + +**Ordinal Observation** +A response with ordered categories under its own versioned contract. Historical/shipped ordinal observations are preserved as ordinal and are not mechanically dichotomized. + +**Paired-Comparison Observation** +A separate dichotomous comparison/ranking channel in which one alternative is preferred over another. Its Bradley-Terry/Thurstone-style semantics must not be conflated with a binary item response. + +**Pilot** +A non-operational instrument state in which observations may be collected for simulation, recovery, calibration, dimensionality, local-dependence, linking, DIF/invariance, rater-facet, and external-validity evidence. Pilot output is not a production score merely because a model converges. + +**Activation** +The governed transition from pilot to operational scoring after the instrument's declared evidence criteria are satisfied. Activation is fail-closed: insufficient evidence preserves observations without issuing a latent score. + +## Model families + +**Rasch** +A Rasch-family measurement model with its own measurement-theoretic requirements, including the intended invariance/specific-objectivity interpretation, common discrimination as part of the model, targeting, and Rasch-specific fit expectations. `rasch` is never an alias, label, or shorthand for generic one-parameter logistic IRT. + +**2PLM (`irt_2plm`)** +A logistic IRT family allowing item discrimination to vary. It is the default logistic IRT candidate for psychology/SEM-lineage measurement when varying discrimination is substantively allowed. + +**3PLM (`irt_3plm`)** +A dichotomous IRT family with a lower asymptote. It is considered only when a substantive lower-asymptote/guessing mechanism is justified and identifiable; better likelihood alone is insufficient. + +**4PLM (`irt_4plm`)** +A dichotomous IRT family with lower and upper asymptotes. It is considered where both mechanisms are theoretically meaningful and recoverable, including gambling/gaming-risk or analogous domains when supported by evidence; better likelihood alone is insufficient. + +**Generic 1PL Logistic IRT** +Not a normal production model choice in this ecosystem. It must not be exposed as `Rasch`, `Rasch/1PL`, or `Rasch (1PL)`. A future use requires an explicit scientific ADR and a distinct identifier such as `irt_1pl_logistic`. + +**Measurement Model Family** +The versioned scientific family selected for scoring. The family identifier is distinct from a judge policy, provider/model identity, item rubric, and fitted parameter artifact. + +## LLM-as-rater vocabulary + +**Judge Observation** +A fallible observation returned through contextual-orchestrator under a versioned judging policy. It is never ground truth simply because one or several models agree. + +**Judge Policy** +The LineageWeave-facing reference to a contextual-orchestrator orchestration/prompt policy used to obtain judge observations. Provider routing, model discovery, fallback, role allocation, and credentials are not part of the LineageWeave policy implementation. + +**Judge Facet** +A reproducible judging condition that may affect observations: judge model, provider observation identity/provenance, prompt/policy revision, language, occasion, agent role, and other declared method conditions. Scientifically material facets are retained for severity/leniency, interaction, repeatability, calibration, and DIF/invariance analysis. + +**Adjudication** +The governed use of evidence and one or more fallible observations to support a product decision. Adjudication is not synonymous with accepting an LLM answer. + +**Disagreement** +A reproducible difference among judge observations or between judge and independent criterion evidence. Disagreement is retained/analyzed; it is not automatically resolved by majority vote. + +## Evidence, provenance, and interpretation + +**Source Evidence** +The authorized source material bound to an item, lineage decision, claim, or interpretation. Source evidence is distinct from a generated explanation. + +**Evidence Binding** +The immutable/auditable link between a product observation or interpretation and the authorized source evidence that justified it at a known revision/cutoff. + +**Provenance** +The identities and versions needed to reproduce or audit an observation/result: source revision, instrument/rubric version, judge policy and scientifically material judge facets, owner-service result/model version, timestamps/occasion, and applicable processing receipts. + +**Scientific Result** +A versioned result returned by the canonical computation owner, such as fast-mlsirm or TEPP, together with the diagnostics/evidence required by its contract. LineageWeave stores/projects it but does not privately refit the same reusable model. + +**Interpretation** +A buyer-facing meaning assigned to validated evidence/results within the instrument's intended-use boundary. Interpretation must distinguish observed evidence, model estimates, uncertainty, pilot status, and generated explanation. + +## Ownership terms + +**Consumer Contract** +A published versioned package/API/event schema through which LineageWeave consumes another bounded context. Internal modules, provider SDKs, private tables, or copied source are not consumer contracts. + +**Anti-Corruption Layer (ACL)** +A LineageWeave adapter that translates between this domain's concepts and a foreign bounded context without importing that context's internal model. Model-backed adapters target contextual-orchestrator; temporal/multilevel adapters target TEPP; reusable psychometric numerics target fast-mlsirm. + +**Canonical Owner** +The CWL repository/bounded context with stable responsibility for a reusable capability. Duplicate production implementations in LineageWeave are migration defects even when they are convenient. + +**Fail-Closed** +When required evidence, owner service, authorization, measurement validity, or contract verification is unavailable, the product reports an unavailable/unscored state rather than manufacturing a response, estimate, fallback provider call, or placeholder success. diff --git a/scripts/backfill_post_keymen.py b/scripts/backfill_post_keymen.py index 59e2efd6c..feec3e797 100644 --- a/scripts/backfill_post_keymen.py +++ b/scripts/backfill_post_keymen.py @@ -36,15 +36,15 @@ from lineageweave.post_content_normalization import normalize_post_body -def _first_env(*names: str) -> str: - return next((os.environ.get(name, "").strip() for name in names if os.environ.get(name, "").strip()), "") - - def _orchestrator_config() -> tuple[str, str]: - base_url = _first_env("ORCHESTRATOR_BASE_URL", "LLM_GATEWAY_API_URL", "LLM_GATEWAY_URL") - api_key = _first_env("ORCHESTRATOR_API_KEY", "LLM_GATEWAY_API_KEY") + """Return the published contextual-orchestrator consumer endpoint and bearer.""" + base_url = os.environ.get("ORCHESTRATOR_BASE_URL", "").strip() + api_key = os.environ.get("ORCHESTRATOR_API_KEY", "").strip() if not base_url or not api_key: - raise RuntimeError("contextual-orchestrator gateway configuration is unavailable") + raise RuntimeError( + "set ORCHESTRATOR_BASE_URL and ORCHESTRATOR_API_KEY to reach " + "contextual-orchestrator" + ) return base_url, api_key diff --git a/scripts/estimate_llm_channel_weights.py b/scripts/estimate_llm_channel_weights.py index 1613ceb90..7e760726a 100644 --- a/scripts/estimate_llm_channel_weights.py +++ b/scripts/estimate_llm_channel_weights.py @@ -54,27 +54,13 @@ def _orchestrator_config() -> tuple[str, str]: - """Base URL and bearer key for the batch routing API, from the environment.""" - base_url = next( - ( - os.environ[name].strip() - for name in ("ORCHESTRATOR_BASE_URL", "LLM_GATEWAY_API_URL") - if os.environ.get(name, "").strip() - ), - "", - ) - api_key = next( - ( - os.environ[name].strip() - for name in ("ORCHESTRATOR_API_KEY", "CONTEXTUAL_ORCHESTRATOR_TOKEN") - if os.environ.get(name, "").strip() - ), - "", - ) + """Return the published contextual-orchestrator consumer endpoint and bearer.""" + base_url = os.environ.get("ORCHESTRATOR_BASE_URL", "").strip() + api_key = os.environ.get("ORCHESTRATOR_API_KEY", "").strip() if not base_url or not api_key: raise RuntimeError( - "set ORCHESTRATOR_BASE_URL and ORCHESTRATOR_API_KEY (or " - "CONTEXTUAL_ORCHESTRATOR_TOKEN) to reach the batch routing API" + "set ORCHESTRATOR_BASE_URL and ORCHESTRATOR_API_KEY to reach " + "the contextual-orchestrator batch routing API" ) return base_url.rstrip("/"), api_key diff --git a/tests/test_contextual_orchestrator_owner_boundary.py b/tests/test_contextual_orchestrator_owner_boundary.py new file mode 100644 index 000000000..f131d2a15 --- /dev/null +++ b/tests/test_contextual_orchestrator_owner_boundary.py @@ -0,0 +1,134 @@ +"""Architecture fitness for the contextual-orchestrator ownership boundary. + +LineageWeave is a consumer of contextual-orchestrator. Provider credentials, +provider endpoints, model-agent bootstrap, provider SDKs, and provider +discovery belong to the orchestrator deployment and must never leak back into +this repository's production/runtime configuration. +""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_FORBIDDEN_PROVIDER_CONFIGURATION = { + "OPENAI_API_KEY", + "OPENROUTER_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "BYTEZ_API_KEY", + "LLM_GATEWAY_API_KEY", + "LLM_GATEWAY_API_URL", + "LLM_GATEWAY_URL", + "LLM_API_GATEWAY", + "LLM_API_KEY", + "CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS", +} +_FORBIDDEN_PROVIDER_SDK_PREFIXES = ( + "anthropic", + "cohere", + "google.genai", + "google.generativeai", + "groq", + "litellm", + "mistralai", + "openai", + "together", +) + + +def _python_runtime_paths() -> list[Path]: + paths: list[Path] = [] + for root_name in ("backend", "lineageweave", "scripts"): + root = _REPOSITORY_ROOT / root_name + paths.extend(path for path in root.rglob("*.py") if "tests" not in path.parts) + return sorted(paths) + + +def _syntax_tree(path: Path) -> ast.AST: + """Parse one runtime module so policy checks inspect syntax, not comments.""" + return ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + + +def _string_literals(tree: ast.AST) -> set[str]: + """Return exact string literals used by one Python runtime module.""" + return { + node.value + for node in ast.walk(tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + } + + +def _imported_modules(tree: ast.AST) -> set[str]: + """Return imported module paths without interpreting source text or comments.""" + modules: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + modules.update(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + modules.add(node.module) + return modules + + +def test_lineageweave_does_not_embed_an_orchestrator_runtime() -> None: + """The provider/orchestration runtime is deployed by its canonical owner.""" + assert not (_REPOSITORY_ROOT / "docker" / "contextual-orchestrator").exists() + + +def test_runtime_python_does_not_accept_provider_boundary_configuration() -> None: + """Consumer code accepts only the published orchestrator service contract.""" + violations: dict[str, list[str]] = {} + for path in _python_runtime_paths(): + tree = _syntax_tree(path) + used = sorted(_FORBIDDEN_PROVIDER_CONFIGURATION & _string_literals(tree)) + if used: + violations[str(path.relative_to(_REPOSITORY_ROOT))] = used + assert violations == {} + + +def test_runtime_python_does_not_import_provider_sdks() -> None: + """Provider SDKs stay behind contextual-orchestrator's published contract.""" + violations: dict[str, list[str]] = {} + for path in _python_runtime_paths(): + imported = _imported_modules(_syntax_tree(path)) + forbidden = sorted( + module + for module in imported + if any( + module == prefix or module.startswith(prefix + ".") + for prefix in _FORBIDDEN_PROVIDER_SDK_PREFIXES + ) + ) + if forbidden: + violations[str(path.relative_to(_REPOSITORY_ROOT))] = forbidden + assert violations == {} + + +def test_lineageweave_environment_example_exposes_only_consumer_credentials() -> None: + """The sample environment must not teach operators to configure providers here.""" + sample = (_REPOSITORY_ROOT / ".env.example").read_text(encoding="utf-8") + for name in _FORBIDDEN_PROVIDER_CONFIGURATION: + assert f"{name}=" not in sample + assert "ORCHESTRATOR_BASE_URL=" in sample + assert "ORCHESTRATOR_API_KEY=" in sample + + +def test_compose_does_not_own_contextual_orchestrator_or_provider_config() -> None: + """Compose consumes an external orchestrator instead of becoming its owner.""" + compose = (_REPOSITORY_ROOT / "docker-compose.yml").read_text(encoding="utf-8") + assert "\n orchestrator:\n" not in compose + assert "condition: service_healthy\n orchestrator:" not in compose + assert "context: ./docker/contextual-orchestrator" not in compose + for name in _FORBIDDEN_PROVIDER_CONFIGURATION: + assert f"{name}:" not in compose + assert "ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-}" in compose + assert "ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-}" in compose + + +def test_compose_launcher_does_not_load_the_orchestrator_owner_environment() -> None: + """LineageWeave launch commands must not import a provider secret bundle.""" + makefile = (_REPOSITORY_ROOT / "Makefile").read_text(encoding="utf-8") + assert '--env-file "$$HOME/.env"' not in makefile + assert "COMPOSE := docker compose" in makefile diff --git a/tests/test_contextual_orchestrator_start.py b/tests/test_contextual_orchestrator_start.py deleted file mode 100644 index ad65a4c95..000000000 --- a/tests/test_contextual_orchestrator_start.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Bootstrap delegates model and provider protocol behavior to the orchestrator.""" - -from __future__ import annotations - -import importlib.util -import json -import os -import sys -import types -from pathlib import Path - -import pytest - - -def _load_start_module(): - stubs = {} - previous = {name: sys.modules.get(name) for name in stubs} - sys.modules.update(stubs) - try: - path = Path(__file__).parents[1] / "docker" / "contextual-orchestrator" / "start.py" - spec = importlib.util.spec_from_file_location("lineageweave_contextual_start", path) - assert spec and spec.loader - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - finally: - for name, previous_module in previous.items(): - if previous_module is None: - sys.modules.pop(name, None) - else: - sys.modules[name] = previous_module - - -def test_bootstrap_does_not_patch_upstream_model_classes() -> None: - module = _load_start_module() - assert "ModelClient" not in module.__dict__ - assert "_apply_provider_models" not in module.__dict__ - - -def test_provider_api_url_is_canonical_over_compatibility_aliases(monkeypatch) -> None: - module = _load_start_module() - monkeypatch.setenv("LLM_GATEWAY_API_URL", "https://canonical.example/v1") - monkeypatch.setenv("LLM_GATEWAY_URL", "https://legacy.example/v1") - monkeypatch.setenv("LLM_API_GATEWAY", "https://local-alias.example/v1") - - assert module._pop_first_env("LLM_GATEWAY_API_URL", "LLM_GATEWAY_URL", "LLM_API_GATEWAY") == ( - "https://canonical.example/v1" - ) - - -def test_gateway_api_key_accepts_local_compatibility_alias(monkeypatch) -> None: - module = _load_start_module() - monkeypatch.setenv("LLM_API_KEY", "compatibility-key") - - assert module._pop_first_env("LLM_GATEWAY_API_KEY", "LLM_API_KEY") == "compatibility-key" - - -def test_provider_key_is_not_aliased_as_gateway_transport(monkeypatch) -> None: - module = _load_start_module() - for name in ("LLM_GATEWAY_API_KEY", "LLM_API_KEY"): - monkeypatch.delenv(name, raising=False) - monkeypatch.setenv("NVIDIA_NIM_API_KEY", "provider-only-key") - - with pytest.raises(SystemExit, match="LLM_GATEWAY_API_KEY or LLM_API_KEY"): - module.main() - - -def test_bootstrap_leaves_embedding_selection_to_the_orchestrator(monkeypatch) -> None: - module = _load_start_module() - captured: dict[str, object] = {} - - class FakePath: - def __init__(self, value: str) -> None: - self.value = value - - def read_text(self, *, encoding: str) -> str: - assert self.value == "/app/agents.json" - assert encoding == "utf-8" - return json.dumps({"agents": [{}]}) - - def write_text(self, value: str, *, encoding: str) -> None: - assert self.value == "/tmp/lineageweave-agents.json" - assert encoding == "utf-8" - captured["agents"] = json.loads(value) - - credentials = types.ModuleType("contextual_orchestrator.credentials") - - def register_credential(name: str, value: str) -> None: - captured.setdefault("credentials", []).append((name, value)) - - credentials.register_credential = register_credential - server = types.ModuleType("contextual_orchestrator.__main__") - - def serve() -> None: - captured["argv"] = list(sys.argv) - - server.main = serve - package = types.ModuleType("contextual_orchestrator") - package.__path__ = [] - monkeypatch.setitem(sys.modules, "contextual_orchestrator", package) - monkeypatch.setitem(sys.modules, "contextual_orchestrator.credentials", credentials) - monkeypatch.setitem(sys.modules, "contextual_orchestrator.__main__", server) - monkeypatch.setattr(module, "Path", FakePath) - monkeypatch.setattr(sys, "argv", ["start.py"]) - monkeypatch.setenv("LLM_GATEWAY_API_KEY", "provider-key") - monkeypatch.setenv("LLM_API_KEY", "legacy-provider-key") - monkeypatch.setenv("OPENAI_API_KEY", "openai-key") - monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-key") - monkeypatch.setenv("NVIDIA_NIM_API_KEY", "nim-key") - monkeypatch.setenv("NVIDIA_NIM_API_KEY_SUB", "nim-sub-key") - monkeypatch.setenv("BYTEZ_API_KEY", "bytez-key") - monkeypatch.setenv("CONTEXTUAL_ORCHESTRATOR_TOKEN", "orchestrator-token") - monkeypatch.setenv("LLM_GATEWAY_API_URL", "https://gateway.example") - monkeypatch.setenv("LLM_GATEWAY_EMBEDDING_MODEL", "embedding-model") - - module.main() - - argv = captured["argv"] - assert isinstance(argv, list) - assert "--embedding-provider-url" not in argv - assert "--embedding-model" not in argv - assert captured["credentials"] == [ - ("LLM_GATEWAY_API_KEY", "provider-key"), - ("OPENAI_API_KEY", "openai-key"), - ("OPENROUTER_API_KEY", "openrouter-key"), - ("NVIDIA_NIM_API_KEY", "nim-key"), - ("NVIDIA_NIM_API_KEY_SUB", "nim-sub-key"), - ("BYTEZ_API_KEY", "bytez-key"), - ] - assert not { - "LLM_GATEWAY_API_KEY", - "LLM_API_KEY", - "OPENAI_API_KEY", - "OPENROUTER_API_KEY", - "NVIDIA_NIM_API_KEY", - "NVIDIA_NIM_API_KEY_SUB", - "BYTEZ_API_KEY", - } & os.environ.keys() - agents = captured["agents"] - assert isinstance(agents, dict) - assert not [agent for agent in agents["agents"] if "embedding" in agent.get("tags", [])] - assert "LLM_GATEWAY_EMBEDDING_MODEL" not in os.environ diff --git a/tests/test_ddd_architecture_fitness.py b/tests/test_ddd_architecture_fitness.py new file mode 100644 index 000000000..58fc33e92 --- /dev/null +++ b/tests/test_ddd_architecture_fitness.py @@ -0,0 +1,89 @@ +"""Machine-checkable DDD invariants that do not depend on one framework layout. + +These checks protect domain ownership and scientific vocabulary. They avoid a +blanket folder template: directory moves still require an ADR/context-map +reason and consumer migration evidence. +""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +_ROOT = Path(__file__).resolve().parents[1] +_RUNTIME_ROOTS = ("backend", "lineageweave", "scripts") +_FORBIDDEN_RASCH_ALIASES = { + "Rasch/1PL", + "Rasch = 1PL", + "Rasch (1PL)", + "rasch/1pl", + "rasch = 1pl", + "rasch (1pl)", +} + + +def _runtime_python_files() -> list[Path]: + paths: list[Path] = [] + for root_name in _RUNTIME_ROOTS: + root = _ROOT / root_name + paths.extend(path for path in root.rglob("*.py") if "tests" not in path.parts) + return sorted(paths) + + +def _runtime_string_literals(path: Path) -> set[str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + return { + node.value + for node in ast.walk(tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + } + + +def test_context_map_names_canonical_owner_boundaries() -> None: + """The repository must have one discoverable map of its owner contracts.""" + context_map = (_ROOT / "docs" / "context-map.md").read_text(encoding="utf-8") + for owner in ( + "contextual-orchestrator", + "fast-mlsirm", + "TEPP", + "RankWeave", + "ThreadWeave", + "Keyverse", + "EgressWeave", + ): + assert owner in context_map + assert "Measurement Policy" in context_map + assert "Instrument Administration" in context_map + assert "Evidence & Adjudication" in context_map + assert "Reporting & Interpretation" in context_map + + +def test_ubiquitous_language_keeps_rasch_distinct_from_generic_1pl() -> None: + """Rasch and generic one-parameter logistic IRT remain different concepts.""" + vocabulary = (_ROOT / "docs" / "ubiquitous-language.md").read_text(encoding="utf-8") + assert "**Rasch**" in vocabulary + assert "**2PLM (`irt_2plm`)**" in vocabulary + assert "**3PLM (`irt_3plm`)**" in vocabulary + assert "**4PLM (`irt_4plm`)**" in vocabulary + assert "never an alias" in vocabulary + + +def test_runtime_does_not_alias_rasch_to_generic_1pl() -> None: + """No production Python literal may encode the forbidden Rasch=1PL shorthand.""" + violations: dict[str, list[str]] = {} + for path in _runtime_python_files(): + literals = _runtime_string_literals(path) + matched = sorted(alias for alias in _FORBIDDEN_RASCH_ALIASES if alias in literals) + if matched: + violations[str(path.relative_to(_ROOT))] = matched + assert violations == {} + + +def test_generic_1pl_identifier_is_not_a_normal_runtime_choice() -> None: + """A future generic 1PL use requires a deliberate ADR and fitness-test change.""" + violations: list[str] = [] + for path in _runtime_python_files(): + if "irt_1pl_logistic" in _runtime_string_literals(path): + violations.append(str(path.relative_to(_ROOT))) + assert violations == [] diff --git a/tests/test_documentation_hygiene.py b/tests/test_documentation_hygiene.py index b287d3470..ee04c448e 100644 --- a/tests/test_documentation_hygiene.py +++ b/tests/test_documentation_hygiene.py @@ -114,20 +114,3 @@ def test_role_catalog_identity_migration_is_wired() -> None: assert "having count(*) = 1" in migration_0025 assert "distinct on" not in migration_0019.lower() assert "distinct on" not in migration_0025.lower() - - -def test_orchestrator_runtime_pin_matches_adr() -> None: - """The image pin and ADR must describe the same immutable upstream commit.""" - expected_embedding_contract_commit = "1a40e0f7ad10d1a24137d69d20e44fc9a5dcdd89" - dockerfile = ( - _ROOT / "docker" / "contextual-orchestrator" / "Dockerfile" - ).read_text(encoding="utf-8") - adr = (_ADR_DIRECTORY / "0083-orchestrator-runtime-commit-pin.md").read_text( - encoding="utf-8" - ) - docker_match = re.search(r"archive/([0-9a-f]{40})\.tar\.gz", dockerfile) - adr_match = re.search(r"commit `([0-9a-f]{40})`", adr) - assert docker_match is not None - assert adr_match is not None - assert docker_match.group(1) == adr_match.group(1) - assert docker_match.group(1) == expected_embedding_contract_commit