diff --git a/.github/workflows/ontology-pages.yml b/.github/workflows/ontology-pages.yml index 8cc734e27..383600baa 100644 --- a/.github/workflows/ontology-pages.yml +++ b/.github/workflows/ontology-pages.yml @@ -5,6 +5,7 @@ on: types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] branches: [main] paths: + - "docs/index.html" - "docs/ontology/**" - "scripts/build_ontology_site.py" - "scripts/publish_ontology_site.py" @@ -12,12 +13,14 @@ on: - "tests/test_ontology.py" - "tests/test_ontology_site.py" - "tests/test_publish_ontology_site.py" + - "tests/test_public_landing.py" - ".github/workflows/ontology-pages.yml" - "pyproject.toml" - "uv.lock" push: branches: [main] paths: + - "docs/index.html" - "docs/ontology/**" - "scripts/build_ontology_site.py" - "scripts/publish_ontology_site.py" @@ -25,6 +28,7 @@ on: - "tests/test_ontology.py" - "tests/test_ontology_site.py" - "tests/test_publish_ontology_site.py" + - "tests/test_public_landing.py" - ".github/workflows/ontology-pages.yml" - "pyproject.toml" - "uv.lock" @@ -41,7 +45,7 @@ jobs: validate: name: Validate ontology publication if: github.event_name == 'pull_request' && github.event.action != 'closed' && github.event.pull_request.draft == false - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 @@ -66,7 +70,7 @@ jobs: run: | uv run --frozen python -m pytest -q tests/test_ontology.py uv run --frozen python -m coverage run --branch \ - -m pytest -q tests/test_ontology_site.py tests/test_publish_ontology_site.py + -m pytest -q tests/test_ontology_site.py tests/test_publish_ontology_site.py tests/test_public_landing.py uv run --frozen python -m coverage report \ --include=scripts/build_ontology_site.py,scripts/publish_ontology_site.py \ --fail-under=100 @@ -80,11 +84,12 @@ jobs: scripts/build_ontology_site.py scripts/publish_ontology_site.py scripts/ontology_site_contract.py tests/test_ontology_site.py tests/test_publish_ontology_site.py + tests/test_public_landing.py publish: name: Publish ontology to GitHub Pages if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pages: write @@ -116,7 +121,7 @@ jobs: run: | uv run --frozen python -m pytest -q tests/test_ontology.py uv run --frozen python -m coverage run --branch \ - -m pytest -q tests/test_ontology_site.py tests/test_publish_ontology_site.py + -m pytest -q tests/test_ontology_site.py tests/test_publish_ontology_site.py tests/test_public_landing.py uv run --frozen python -m coverage report \ --include=scripts/build_ontology_site.py,scripts/publish_ontology_site.py \ --fail-under=100 diff --git a/.github/workflows/prov-o-contract.yml b/.github/workflows/prov-o-contract.yml index a289748c9..008f73405 100644 --- a/.github/workflows/prov-o-contract.yml +++ b/.github/workflows/prov-o-contract.yml @@ -36,7 +36,7 @@ jobs: standards-contract: name: Registry, inference, coverage, PostgreSQL if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 services: postgres: image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a73249f6..b82226a7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: pytest: name: Full test suite if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 services: postgres: image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 @@ -66,7 +66,7 @@ jobs: frontend: name: Frontend lint, test, build if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 diff --git a/README.md b/README.md index 5f1480b40..7c2c10ff6 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,49 @@ # LineageWeave -Reconstructs git-branch-style lineage DAGs from scattered short records -- -turns a flat pile of loosely-grouped, timestamped items into a browsable set -of branching threads, without any explicit "this follows from that" link -existing in the source data. +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/LineageWeave) -``` +**Turn scattered timestamped records into navigable, evidence-bearing lineage.** + +LineageWeave reconstructs git-branch-style lineage DAGs when source records do not contain explicit “this follows from that” links. It helps reviewers and analysts understand how records continue, branch, or remain unrelated while keeping every inferred connection tied to inspectable evidence. + +```text group A-100 - rec-001 Initial site visit and project scope discussion - └─ rec-002 Pricing renegotiation follow-up - ├─ rec-003 Pricing renegotiation: revised quote sent - └─ rec-004 Delivery schedule question raised - └─ rec-005 Delivery schedule confirmed with logistics - rec-006 Unrelated: annual account review (own root -- no forced match) + rec-001 Initial site visit + └─ rec-002 Pricing follow-up + ├─ rec-003 Revised quote + └─ rec-004 Delivery question + └─ rec-005 Delivery confirmed + rec-006 Annual account review ← separate root ``` -This is a **demo prototype**: it ships with synthetic sample data only -(`lineageweave/fixtures.py`) and no connection to any real dataset or -organization. - -The supporting [product requirements](docs/product-requirements.md) define -the product outcomes, non-goals, ecosystem boundaries, and release evidence; -ADRs remain normative for architecture and policy. - -## Why - -Given a pile of records with no native cross-record link, no single cheap -signal reliably tells you which record continues which -- see -[`docs/lineage-bi-research-notes.md`](docs/lineage-bi-research-notes.md) for -the validation numbers and the literature this design follows. LineageWeave -fuses several independent, individually-weak signals (temporal proximity, a -shared grouping key, text similarity, and an optional LLM judgment) instead -of trusting any one of them alone. The normative research-grounding policy is -[ADR 0084](docs/adr/0084-lineage-research-grounding.md); the linked notes -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 -[contextual-orchestrator](https://github.com/ContextualWisdomLab/contextual-orchestrator) -(`lineageweave/adjudication_client.py`). Tree assembly reuses -[ThreadWeave](https://github.com/ContextualWisdomLab/ThreadWeave) (JWZ -message threading) and channel fusion reuses -[RankWeave](https://github.com/ContextualWisdomLab/RankWeave) (weighted -score fusion for reconstruction and the fail-closed Rankings port) -- both real dependencies, not reimplemented here. - -## Run it +The repository provides both a reusable Python reconstruction library and an authenticated API/web product stack. It does not turn inferred lineage into causal truth, replace the source systems that own the original records, or perform psychometric/statistical estimation itself. + +## Why LineageWeave + +Scattered operational records often contain useful continuity without durable parent/child links. Timestamp proximity alone is weak; text similarity alone is weak; a shared grouping key alone is weak. LineageWeave combines independent evidence channels, preserves uncertainty, and keeps unrelated records separate instead of forcing a match. + +| Need | What LineageWeave provides | +| --- | --- | +| Follow record history | Branching Event Lineage instead of a flat timestamp list | +| Inspect why two records connect | Evidence-bearing edge and channel information | +| Explore related entities | Typed semantic/provenance neighborhoods kept distinct from lineage edges | +| Keep uncertainty honest | Missing providers and unsupported semantics remain explicit unknowns | +| Reuse the engine | A standalone Python API with injected external services | +| Operate a product surface | Authenticated REST/web stack backed by PostgreSQL and Keycloak | + +The supporting [product requirements](docs/product-requirements.md) define product outcomes and non-goals. Architecture decisions remain normative for policy and implementation boundaries. + +## Quick start: reconstruction library + +LineageWeave requires Python 3.12 or newer in the current source tree. ```bash +python -m venv .venv +. .venv/bin/activate pip install -e . -python -m lineageweave.server -# -> http://127.0.0.1:8420 ``` -Or use the library directly: +Map your records into `lineageweave.Record` and call `reconstruct()`: ```python from lineageweave import reconstruct @@ -74,150 +54,121 @@ for tree in trees: print(tree.group_key, "branch points:", tree.branch_points()) ``` -## Bring your own data +The package does not assume a particular source schema. External channels such as embeddings or LLM adjudication are injected explicitly; the null/default path does not require provider credentials. -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. +## Run the authenticated product stack -To turn on the embedding or LLM channels, pass a real client instead of the -`Null*` defaults: +For local product evaluation, the Makefile deliberately uses `$HOME/.env` as the Compose environment boundary so provider credentials stay outside the repository. Do not replace or overwrite an existing home credential file. A clean local run needs only an empty owner-readable file because the default development profile already supplies throwaway local-only service defaults: -```python -from lineageweave import reconstruct -from lineageweave.adjudication_client import ContextualOrchestratorAdjudicationClient +```bash +if [ ! -e "$HOME/.env" ]; then + install -m 600 /dev/null "$HOME/.env" +fi -llm = ContextualOrchestratorAdjudicationClient(base_url="http://localhost:8000", api_key="...") -trees = reconstruct(my_records, llm=llm) +make up +KEYCLOAK_ADMIN_PASSWORD=admin_dev_only make seed +make smoke ``` -## Test +If your existing `$HOME/.env` overrides `KEYCLOAK_ADMIN_PASSWORD`, export that same value in the shell before `make seed` instead of using the shown development default. Optional contextual-orchestrator/provider settings also belong in the existing home environment boundary; add only the values you intend to manage and preserve unrelated existing entries. -```bash -pip install -e ".[dev]" -pytest -``` +This starts the repository-owned development stack and verifies the local identity round trip. Demo identities and seeded records are synthetic development fixtures, not production customer data or deployment evidence. -## Local product stack (Docker Compose) +For frontend development, follow [`frontend/README.md`](frontend/README.md). Deployment and environment details belong in the operator documentation rather than this product landing page. -The reconstruction library above is being wrapped in a real product (see -[ARCHITECTURE.md](ARCHITECTURE.md#product-schema-phase-1-of-a-larger-roadmap) -and [ADR 0001](docs/adr/0001-demo-identity-and-data-boundary.md)). Phase 1's -infrastructure -- PostgreSQL, Valkey, and a real Keycloak OIDC realm seeded -with synthetic demo accounts -- runs via Docker Compose: +## How reconstruction works -```bash -make up # docker compose up -d: postgres, valkey, keycloak -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 - # started -make down -``` +A lineage edge is a governed inference, not a hidden heuristic verdict. The engine can combine available signals such as temporal evidence, grouping evidence, text similarity, embeddings, or an optional model adjudication channel. Unavailable channels can be omitted; malformed calibrated inputs fail closed rather than silently receiving guessed weights. -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. - -Postgres and Keycloak are built (`docker/postgres-init/`, `docker/keycloak/`) -rather than bind-mounted, so the keycloak database's init script and the -realm seed ship inside the images themselves -- portable to any Docker host -or CI runner, no assumption about a shared local filesystem layout. - -Demo accounts (`docker/keycloak/realm-export.json`) are synthetic: -`demo.analyst` / `demo.admin`, password `lineageweave-demo-only`, each -carrying `corp_code` / `pu_code` as token claims -- these are throwaway -local-dev credentials in a locally-run realm, never the org's real Keyverse -tenant (see ADR 0001 for why). - -Host ports (15432, 16379, 18080, 18001, 18420) deliberately avoid each service's -own default -- a dev machine commonly already runs its own -Postgres/Redis/local server on those. Override via `.env` (copy -`.env.example`) or inline if even those collide, e.g. -`KEYCLOAK_PORT=28080 make up`. - -Postgres's `POSTGRES_DB` (the "app" database) is migrated automatically on -first boot -- `docker/postgres-init/Dockerfile` bakes in the exact same -`migrations/0001_initial_schema.sql` file `tests/test_schema.py` applies, -no re-typed copy. - -`backend/` is a FastAPI app talking directly to that database (`asyncpg`, -no ORM, no file DB) and to Keycloak's live JWKS for OIDC verification: +The product keeps two concepts separate: -```bash -make up -make seed # scripts/seed_demo_data.py: inserts synthetic corp/account/post - # rows keyed to the *real* Keycloak demo users' subject ids, - # plus Valkey ticket_created events so Activity is not empty -curl http://localhost:18420/healthz -``` +- **Event Lineage** answers “which record plausibly continues which?” and forms the branching thread. +- **Semantic/provenance neighborhoods** answer “which typed people, organizations, projects, events, commitments, or governed constructs relate to this evidence?” -The optional authenticated MCP resource server submits and reads the same -durable Global Ask jobs as REST. Enable it only with quota values established -by the deployment's k6 capacity evidence; the service intentionally has no -guessed request/window defaults: +An inferred lineage edge is not labeled causal or authoritative without separate evidence. The current research basis and validation notes are documented in [`docs/lineage-bi-research-notes.md`](docs/lineage-bi-research-notes.md), with normative research policy in [ADR 0084](docs/adr/0084-lineage-research-grounding.md). -```bash -MCP_RATE_LIMIT_REQUESTS= \ -MCP_RATE_LIMIT_WINDOW_SECONDS= \ -docker compose --profile mcp up mcp -# Streamable HTTP resource: http://localhost:18001/mcp +## Ecosystem integration + +LineageWeave owns reconstruction, evidence-bearing lineage presentation, and its semantic neighborhood projection. Adjacent products retain their own authority: + +| Product | Boundary | +| --- | --- | +| [ThreadWeave](https://github.com/ContextualWisdomLab/ThreadWeave) | Standard message/thread assembly reused where its published contract fits | +| [RankWeave](https://github.com/ContextualWisdomLab/RankWeave) | Retrieval/score fusion used through its owned contract | +| [TEPP](https://github.com/ContextualWisdomLab/TEPP) | Psychometric and statistical estimation; LineageWeave consumes published results rather than reimplementing the mathematics | +| [fast-mlsirm](https://github.com/ContextualWisdomLab/fast-mlsirm) | Measurement/calibration capabilities used by supported reporting paths | +| [contextual-orchestrator](https://github.com/ContextualWisdomLab/contextual-orchestrator) | Provider/model routing and LLM orchestration for optional model-backed channels | +| [Keyverse](https://github.com/ContextualWisdomLab/keyverse) | Ecosystem identity authority; local Keycloak fixtures do not become the production identity source of truth | + +These integrations use explicit APIs/contracts. LineageWeave does not read another product's private tables or copy its numerical/model ownership into this repository. + +## Architecture at a glance + +```text +Source records / imports + │ + ▼ +┌──────────────────────────────┐ +│ LineageWeave │ +├──────────────────────────────┤ +│ record normalization │ +│ evidence channels │ +│ lineage reconstruction │ +│ provenance / semantic view │ +│ authorization-aware API │ +└──────────────┬───────────────┘ + │ + governed integrations + │ + ┌─────────┼──────────┐ + ▼ ▼ ▼ + ThreadWeave RankWeave TEPP / + orchestrator ``` -`GET /api/posts`, `GET /api/posts/{post_id}`, -`GET /api/posts/{post_id}/keymen`, `GET /api/keymen/{person_id}/related`, -`GET /api/posts/{post_id}/affiliate-tree`, -`GET /api/posts/{post_id}/voc-evidence`, -and `POST /api/posts/{post_id}/extract-keymen` -require a real bearer token (RBAC: the account's role must grant -`post_read`; ABAC: a private post is only visible to accounts affiliated -with its owning corporate entity -- `backend/app/main.py`). A Keyman who -is only mentioned on a post the account cannot see is 403, same deny -path. `backend/tests/test_api.py` proves both the allow and the deny -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) -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`; -`post_admin` can rebuild), and the post list / detail popup call the -FastAPI backend over real `fetch()` with the token Keycloak issued. +The full product stack adds PostgreSQL persistence, authenticated application APIs, a browser client, and optional integration services around the same evidence boundary. See [`ARCHITECTURE.md`](ARCHITECTURE.md) for bounded contexts and deployment structure. + +## Security and evidence boundary + +The authenticated product surface is designed so that source visibility and authorization remain part of the evidence contract: + +- API access uses real bearer-token verification in the product stack; +- private evidence is not made visible merely because another visible record mentions the same person or entity; +- external model/provider credentials remain outside portable source and are injected at their owning integration boundary; +- synthetic demo accounts are explicitly local-development fixtures; +- generated semantic or lineage claims remain evidence-linked and do not become source-system truth by inference. + +Threat, authorization, retention, and provenance decisions are documented in [`docs/adr/`](docs/adr/) and [`ARCHITECTURE.md`](ARCHITECTURE.md). + +## Verification + +Install development dependencies and run the repository test suites: ```bash -make up -make seed -cd frontend && cp .env.example .env.local && pnpm install && pnpm run dev -# Repeated chip/close controls: pnpm run storybook -# (Node 24 via frontend/mise.toml; pnpm only) -# Empty a run-bearing registry: insert analysis_run_retention_grant -# for session_user, GRANT analysis_run_retention_admin, then -# select purge_analysis_run_registry('approved-retention-purge'). -# The published token is not a grant (ADR 0020). -# -> http://localhost:5173, click "Log in", redirects through the real -# Keycloak login page for demo.analyst / lineageweave-demo-only +pip install -e '.[dev]' +pytest ``` -`docker compose up` also builds and serves the frontend itself (nginx, -`frontend/Dockerfile`) at `http://localhost:15173` -- the `VITE_*` build -args are wired from the same `.env` ports as every other service. -`frontend/src/App.test.tsx` covers the login-redirect and -fetch-then-render-popup paths (`react-oidc-context`'s `useAuth` mocked -- -the *real* OIDC round-trip is what `scripts/smoke_test_oidc.py` and -`backend/tests/test_api.py` already prove against a live Keycloak). +For the local product stack, use the same `$HOME/.env` boundary described above. After `make up`, make the effective local Keycloak administrator password available to the seed process, then run `make seed`, `make smoke`, and `make down`. + +Hosted checks on the unchanged pull-request head remain the authority for integration. A successful local command, synthetic fixture, benchmark, or predecessor-head check is not promoted into release, deployment, customer, or production-readiness evidence. + +## Documentation map + +| Goal | Start here | +| --- | --- | +| Product requirements and non-goals | [`docs/product-requirements.md`](docs/product-requirements.md) | +| Architecture and responsibility boundaries | [`ARCHITECTURE.md`](ARCHITECTURE.md) | +| Architecture decisions | [`docs/adr/`](docs/adr/) | +| Reconstruction research and validation | [`docs/lineage-bi-research-notes.md`](docs/lineage-bi-research-notes.md) | +| Public documentation landing | [`docs/index.html`](docs/index.html) | +| Frontend development | [`frontend/README.md`](frontend/README.md) | +| Current package/runtime metadata | [`pyproject.toml`](pyproject.toml) | -## Modular / standalone +## Contributing -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. +Keep Event Lineage distinct from ontology/KG relationships, preserve evidence and authorization boundaries, and do not move statistical/psychometric arithmetic into this repository. Public contract changes should update tests, architecture decisions, and customer/operator documentation together. ## License -MIT -- see [LICENSE](LICENSE). +LineageWeave is licensed under the [MIT License](LICENSE). Third-party dependencies and external datasets retain their own licenses and must remain compatible with ContextualWisdomLab's commercial-use policy. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..8fb67aef2 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,55 @@ + + + + + + LineageWeave + + + + + + +
+

LineageWeave

+

Evidence-bound lineage reconstruction and branching history for scattered enterprise records.

+
+
+

Use LineageWeave when related records do not carry reliable explicit cross-record links and you need to inspect how events, claims, posts, and decisions relate over time without turning inferred lineage into source-system truth.

+ +
+

Evidence boundary

+

Published source and release evidence remain authoritative. Lineage inference, retrieval, and model-assisted adjudication do not silently become source-system truth, authentication authority, tenant authority, measurement authority, or provider-routing authority.

+
+
+

ContextualWisdomLab / LineageWeave

+ + diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 3b2deb73d..000000000 --- a/docs/index.md +++ /dev/null @@ -1,30 +0,0 @@ -# LineageWeave - -LineageWeave reconstructs evidence-bound lineage, project journeys, and relationship context from scattered enterprise records so people can inspect how events, claims, posts, and decisions relate over time. - -## Start here - -Use LineageWeave when source systems contain related records without reliable explicit cross-record links and you need a browsable, evidence-preserving view of branching histories rather than a flat search result. - -The product combines deterministic lineage assembly with bounded retrieval/fusion and optional adjudication through shared ContextualWisdomLab components. It keeps source authority, authentication, tenant scope, measurement, and model-provider routing in their owning products and contracts. - -## Product surfaces - -- lineage reconstruction and project journeys; -- operations/dashboard views over governed records; -- evidence-aware Global Ask and related context navigation; -- authenticated API and web client over durable PostgreSQL state; -- integration with ThreadWeave, RankWeave, TEPP, contextual-orchestrator, and shared context contracts through explicit boundaries. - -## Documentation - -- [Repository overview and local stack](../README.md) -- [Architecture](../ARCHITECTURE.md) -- [Product requirements](product-requirements.md) -- [Product/technical gap baseline](product-technical-gap-baseline.md) -- [Architecture decisions](adr/) -- [Research notes](lineage-bi-research-notes.md) - -## Status and evidence - -The repository has progressed beyond its original synthetic reconstruction demo into a broader authenticated product stack, while synthetic fixtures remain important for safe examples and tests. Treat protected-main behavior, current release evidence, and repository verification as authoritative; do not infer production readiness from a documentation page or an open pull request alone. diff --git a/scripts/publish_ontology_site.py b/scripts/publish_ontology_site.py index 677ea4a92..6665084d8 100644 --- a/scripts/publish_ontology_site.py +++ b/scripts/publish_ontology_site.py @@ -29,6 +29,7 @@ PROV_PROFILE_RELATIVE_PATH = Path("docs/ontology/prov-o-support-profile.ttl") COMPATIBILITY_RELATIVE_PATH = Path("docs/ontology/namespace-compatibility.ttl") SHAPES_RELATIVE_PATH = Path("docs/ontology/lineageweave-kg-shapes.ttl") +LANDING_RELATIVE_PATH = Path("docs/index.html") #: ADR 0207: the repository-case namespace is canonical and the #: lowercase form is the deprecated compatibility vocabulary. CANONICAL_NAMESPACE = "https://contextualwisdomlab.github.io/LineageWeave/ontology#" @@ -217,6 +218,7 @@ def publish_site(repository_root: Path, output_dir: Path) -> None: profile = root / PROV_PROFILE_RELATIVE_PATH compatibility_source = root / COMPATIBILITY_RELATIVE_PATH shapes_source = root / SHAPES_RELATIVE_PATH + landing_source = root / LANDING_RELATIVE_PATH if not source.is_file(): raise FileNotFoundError(f"ontology source is missing: {source}") if not profile.is_file(): @@ -227,6 +229,8 @@ def publish_site(repository_root: Path, output_dir: Path) -> None: ) if not shapes_source.is_file(): raise FileNotFoundError(f"SHACL shapes graph is missing: {shapes_source}") + if not landing_source.is_file(): + raise FileNotFoundError(f"public landing source is missing: {landing_source}") output = _validate_output_directory(output_dir, source, profile) renderer = _load_renderer(root) @@ -242,6 +246,7 @@ def publish_site(repository_root: Path, output_dir: Path) -> None: shutil.rmtree(output) try: renderer.build_site(root, output) + shutil.copyfile(landing_source, output / "index.html") except BaseException: shutil.rmtree(output, ignore_errors=True) raise diff --git a/tests/test_github_hosted_runner_contract.py b/tests/test_github_hosted_runner_contract.py new file mode 100644 index 000000000..549fd42b4 --- /dev/null +++ b/tests/test_github_hosted_runner_contract.py @@ -0,0 +1,53 @@ +"""Repository-owned Linux workflows use an explicit supported hosted image.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED_JOB_COUNTS = { + ROOT / ".github" / "workflows" / "tests.yml": 2, + ROOT / ".github" / "workflows" / "ontology-pages.yml": 2, + ROOT / ".github" / "workflows" / "prov-o-contract.yml": 1, +} + + +def _job_level_runners(path: Path) -> dict[str, str]: + """Return literal ``jobs..runs-on`` declarations from one workflow. + + GitHub workflow job keys are two spaces below the top-level ``jobs`` map and + their scalar ``runs-on`` declarations are four spaces below it. Restricting + extraction to those structural levels prevents comments, shell heredocs, and + unrelated strings from satisfying this repository contract without adding a + YAML parser solely for a CI-policy test. + """ + runners: dict[str, str] = {} + current_job: str | None = None + in_jobs = False + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + if raw_line == "jobs:": + in_jobs = True + current_job = None + continue + if not in_jobs: + continue + if raw_line and not raw_line.startswith(" ") and not raw_line.startswith("#"): + break + if raw_line.startswith(" ") and not raw_line.startswith(" "): + stripped = raw_line.strip() + if stripped and not stripped.startswith("#") and stripped.endswith(":"): + current_job = stripped[:-1] + continue + if current_job is not None and raw_line.startswith(" runs-on:"): + value = raw_line.split(":", 1)[1].split("#", 1)[0].strip().strip("'\"") + runners[current_job] = value + + return runners + + +def test_repository_workflows_pin_ubuntu_2404() -> None: + """Require every actual repository-owned Linux job to pin Ubuntu 24.04.""" + for path, expected_count in EXPECTED_JOB_COUNTS.items(): + runners = _job_level_runners(path) + assert len(runners) == expected_count, (path, runners) + assert set(runners.values()) == {"ubuntu-24.04"}, (path, runners) diff --git a/tests/test_public_landing.py b/tests/test_public_landing.py new file mode 100644 index 000000000..ee9a2fcca --- /dev/null +++ b/tests/test_public_landing.py @@ -0,0 +1,56 @@ +"""Tests for the authoritative LineageWeave GitHub Pages landing source.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "publish_ontology_site.py" + + +def _load_publisher(): + spec = importlib.util.spec_from_file_location("publish_ontology_site_landing", SCRIPT) + if spec is None or spec.loader is None: + raise AssertionError("ontology publisher could not be loaded") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_publication_copies_the_authoritative_landing_with_durable_navigation( + tmp_path: Path, +) -> None: + publisher = _load_publisher() + output = tmp_path / "site" + + publisher.publish_site(ROOT, output) + + source = (ROOT / publisher.LANDING_RELATIVE_PATH).read_text(encoding="utf-8") + published = (output / "index.html").read_text(encoding="utf-8") + assert published == source + for target in ( + 'href="ontology/"', + 'href="https://github.com/ContextualWisdomLab/LineageWeave"', + 'href="https://github.com/ContextualWisdomLab/LineageWeave/blob/main/README.md"', + 'href="https://github.com/ContextualWisdomLab/LineageWeave/blob/main/ARCHITECTURE.md"', + 'href="https://github.com/ContextualWisdomLab/LineageWeave/releases"', + 'href="https://deepwiki.com/ContextualWisdomLab/LineageWeave"', + ): + assert target in published + + +def test_publication_fails_closed_without_the_authoritative_landing( + tmp_path: Path, + monkeypatch, +) -> None: + publisher = _load_publisher() + output = tmp_path / "site" + monkeypatch.setattr(publisher, "LANDING_RELATIVE_PATH", Path("docs/missing-index.html")) + + with pytest.raises(FileNotFoundError, match="public landing source"): + publisher.publish_site(ROOT, output) + + assert not output.exists() diff --git a/tests/test_publish_ontology_site.py b/tests/test_publish_ontology_site.py index 99b60d3e3..b62003e90 100644 --- a/tests/test_publish_ontology_site.py +++ b/tests/test_publish_ontology_site.py @@ -54,6 +54,9 @@ def _repository_fixture(tmp_path: Path) -> Path: "lineageweave-kg-shapes.ttl", ): (ontology_dir / name).write_bytes((ROOT / "docs" / "ontology" / name).read_bytes()) + (repository / "docs" / "index.html").write_bytes( + (ROOT / "docs" / "index.html").read_bytes() + ) (scripts_dir / "build_ontology_site.py").write_bytes( (ROOT / "scripts" / "build_ontology_site.py").read_bytes() )