diff --git a/.github/workflows/ontology-pages.yml b/.github/workflows/ontology-pages.yml new file mode 100644 index 000000000..43c5d384c --- /dev/null +++ b/.github/workflows/ontology-pages.yml @@ -0,0 +1,135 @@ +name: Ontology Pages + +on: + pull_request: + branches: [main] + paths: + - "docs/ontology/**" + - "scripts/build_ontology_site.py" + - "scripts/publish_ontology_site.py" + - "tests/test_ontology.py" + - "tests/test_ontology_site.py" + - "tests/test_publish_ontology_site.py" + - ".github/workflows/ontology-pages.yml" + - "pyproject.toml" + - "uv.lock" + push: + branches: [main] + paths: + - "docs/ontology/**" + - "scripts/build_ontology_site.py" + - "scripts/publish_ontology_site.py" + - "tests/test_ontology.py" + - "tests/test_ontology_site.py" + - "tests/test_publish_ontology_site.py" + - ".github/workflows/ontology-pages.yml" + - "pyproject.toml" + - "uv.lock" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + name: Validate ontology publication + if: github.event_name == 'pull_request' + concurrency: + group: ontology-pages-validation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up locked Python dependency manager + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Install committed dependencies + run: uv sync --frozen --extra dev + + - name: Verify ontology and publication contracts + 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 + uv run --frozen python -m coverage report \ + --include=scripts/build_ontology_site.py,scripts/publish_ontology_site.py \ + --fail-under=100 + + - name: Build static ontology site + run: uv run --frozen python scripts/publish_ontology_site.py --output-dir _site + + - name: Compile owned Python surface + run: >- + uv run --frozen python -m compileall -q + scripts/build_ontology_site.py scripts/publish_ontology_site.py + tests/test_ontology_site.py tests/test_publish_ontology_site.py + + publish: + name: Publish ontology to GitHub Pages + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + concurrency: + group: ontology-pages-publication + cancel-in-progress: false + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up locked Python dependency manager + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Install committed dependencies + run: uv sync --frozen --extra dev + + - name: Verify exact protected source before publication + 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 + uv run --frozen python -m coverage report \ + --include=scripts/build_ontology_site.py,scripts/publish_ontology_site.py \ + --fail-under=100 + + - name: Build deterministic publication artifact + run: uv run --frozen python scripts/publish_ontology_site.py --output-dir _site + + - name: Configure GitHub Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: _site + + - name: Deploy GitHub Pages artifact + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/CHANGELOG.d/2.22.0-ontology-pages.md b/CHANGELOG.d/2.22.0-ontology-pages.md new file mode 100644 index 000000000..ba7d73518 --- /dev/null +++ b/CHANGELOG.d/2.22.0-ontology-pages.md @@ -0,0 +1,12 @@ +## Added + +- Added a deterministic GitHub Pages publication pipeline for the public + ontology documentation URL, with fragment-addressable terms and Turtle, + JSON-LD, N-Triples, PROV-O profile, and source-digest artifacts. +- Added semantic round-trip, byte-determinism, fail-closed source, CLI, and + 100% statement/branch coverage tests for the ontology site renderer. +- Added a fail-closed publication boundary that prevents duplicate public + fragments, unsafe linked IRI schemes, symlink or source-overlapping outputs, + and deletion of output directories not marked as generated. +- Restricted Pages deployment to `main`, preserved non-cancelling publication + concurrency, and kept all third-party Actions pinned by full commit SHA. diff --git a/CHANGELOG.d/naruon-calendar-projection-contract.md b/CHANGELOG.d/naruon-calendar-projection-contract.md new file mode 100644 index 000000000..1ae13df82 --- /dev/null +++ b/CHANGELOG.d/naruon-calendar-projection-contract.md @@ -0,0 +1,16 @@ +# Naruon calendar projection contract + +## Added + +- Add a strict, bounded v1 consumer contract for calendar occurrences already authorized and policy-filtered by Naruon, including occurrence identity, provider revision, timezone/all-day semantics, disclosure level, and observed provenance. +- Export the projection parser, immutable result types, media type, schema version, and read client through the public LineageWeave package surface. +- Add a reusable bounded JSON response read so oversized pages are rejected before allocation and parsing. + +## Changed + +- Clarify that LineageWeave owns post-grounded commitments and issue/todo records, while Naruon owns provider CalDAV synchronization, revisions, writeback, retry, and reconciliation. +- Replace the misleading CalDAV label on the earlier custom JSON `/events` feed with an explicit pseudo-CalDAV correction. + +## Security + +- Reject unsafe base URLs, whitespace/control-bearing service tokens, unbounded response bodies, invalid numeric controls, oversized pages/windows, naive timestamps, duplicate occurrences, unknown fields/vocabularies, and URL-shaped opaque references. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ed1a099..7ce55b42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ All notable changes to this project are documented here. Format follows - `make smoke` and `make seed` now run through the locked project `uv` environment, so local OIDC and synthetic-data workflows resolve the same pinned dependencies as CI. +- Provider response parsing now rejects malformed chat envelopes without + exposing provider response bodies, exception text, or secrets through + buyer-facing APIs and persisted ingestion failure details. RankWeave, OIDC, + TEPP, structured VISION, summaries, chat, and extraction channels now retain + stable next-action-safe failure messages. ## [2.12.6] - 2026-08-20 diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py index 2387d940b..324c6cd67 100644 --- a/backend/app/analysis_run_start.py +++ b/backend/app/analysis_run_start.py @@ -104,7 +104,7 @@ def transport(payload: dict[str, Any]) -> dict[str, Any]: headers = {"authorization": f"Bearer {api_key}"} if api_key.strip() else {} return post_json(url, payload, headers=headers, timeout=30.0) except (HttpClientError, OSError, ValueError, TypeError) as exc: - raise TeppNotAvailable(str(exc)) from exc + raise TeppNotAvailable("TEPP transport unavailable") from exc return TeppClient(transport=transport) diff --git a/backend/app/auth.py b/backend/app/auth.py index 155974d52..695a43a40 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -56,7 +56,7 @@ def _jwks(settings: Settings, *, force_refresh: bool = False) -> dict: except (HttpClientError, OSError, ValueError) as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, - f"could not fetch OIDC JWKS for {settings.oidc_issuer}: {exc}", + "could not fetch OIDC JWKS: identity provider unavailable", ) from exc _jwks_cache[cache_key] = cached return cached @@ -134,7 +134,7 @@ def _decode_access_token(token: str, settings: Settings) -> dict: except HTTPException: raise except jwt.PyJWTError as exc: - raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"invalid token: {exc}") from exc + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid access token") from exc subject = claims.get("sub") if not isinstance(subject, str) or not subject.strip(): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "access token has no subject") diff --git a/backend/app/main.py b/backend/app/main.py index fb943315f..38be0ff93 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2650,7 +2650,7 @@ async def ask_agent( except (HttpClientError, KeyError, OSError, ValueError) as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, - f"Ask Agent is unavailable: {exc}", + "Ask Agent is unavailable: contextual-orchestrator returned no complete evidence object", ) from exc cited_ids = list(answer.cited_post_ids) return { diff --git a/backend/app/post_content_worker.py b/backend/app/post_content_worker.py index 458b9021f..2ee5bea82 100644 --- a/backend/app/post_content_worker.py +++ b/backend/app/post_content_worker.py @@ -37,6 +37,7 @@ _RECOVERY_INTERVAL_SECONDS = 30.0 _INCOMPLETE_FAILURE_CODE = "post_content_ingestion_incomplete" _ATTEMPT_LIMIT_FAILURE_CODE = "post_content_ingestion_attempt_limit" +_UNEXPECTED_FAILURE_DETAIL = "post-content provider operation failed; retry the ingestion job" async def _stream_tail(client: redis.Redis) -> str: @@ -267,7 +268,7 @@ async def process_post_content_job( pool, post_id, failure_code="post_content_ingestion_failed", - detail_text=str(exc)[:1000], + detail_text=_UNEXPECTED_FAILURE_DETAIL, expected_attempt_count=attempt_count, ) return diff --git a/backend/tests/test_auth_jwks.py b/backend/tests/test_auth_jwks.py index 709d2c16e..5830731ef 100644 --- a/backend/tests/test_auth_jwks.py +++ b/backend/tests/test_auth_jwks.py @@ -9,7 +9,8 @@ import pytest from fastapi import HTTPException -import backend.app.auth as auth +from backend.app import auth +from lineageweave.http_client import HttpClientError def _segment(value: dict) -> str: @@ -173,3 +174,42 @@ def test_decode_rejects_missing_subject(monkeypatch: pytest.MonkeyPatch) -> None with pytest.raises(HTTPException) as error: auth._decode_access_token("token", settings) assert error.value.status_code == 401 + + +def test_decode_hides_raw_jwt_provider_error(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(auth, "_signing_key", lambda settings, token: "signing-key") + + def fail_decode(*args: object, **kwargs: object) -> dict: + raise auth.jwt.InvalidTokenError("provider secret") + + monkeypatch.setattr(auth.jwt, "decode", fail_decode) + settings = SimpleNamespace( + oidc_issuer="https://id.example", + oidc_audience="lineageweave-api", + oidc_clock_skew_seconds=5, + ) + + with pytest.raises(HTTPException) as error: + auth._decode_access_token("token", settings) + assert error.value.detail == "invalid access token" + assert "provider secret" not in str(error.value.detail) + + +def test_jwks_hides_raw_identity_provider_error(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + auth, + "get_json", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + HttpClientError("identity provider secret") + ), + ) + settings = SimpleNamespace( + oidc_issuer="https://id.example", + oidc_discovery_uri="https://id.example/.well-known/openid-configuration", + oidc_jwks_uri_override="", + ) + + with pytest.raises(HTTPException) as error: + auth._jwks(settings) + assert error.value.detail == "could not fetch OIDC JWKS: identity provider unavailable" + assert "identity provider secret" not in str(error.value.detail) diff --git a/docs/adr/0030-external-llm-gateway-environment.md b/docs/adr/0030-external-llm-gateway-environment.md index e518ee114..4b2550866 100644 --- a/docs/adr/0030-external-llm-gateway-environment.md +++ b/docs/adr/0030-external-llm-gateway-environment.md @@ -52,12 +52,19 @@ runtime, with masking enabled; the repository contains no provider secret. The contextual-orchestrator service remains the only LLM boundary. LineageWeave does not call the provider gateway directly and does not create a fallback local score, summary, extraction, or answer when the gateway is unavailable. +Provider response envelopes and exceptions are also a trust boundary: clients +extract only the expected content and expose stable, next-action-safe error +messages. Raw provider response bodies, exception text, prompts, and secrets +must never be returned through a buyer-facing API or persisted failure detail. ## Consequences - Provider changes are deployment configuration, not source changes. - A missing or invalid gateway credential fails at the orchestrator boundary; it must not be replaced by a fabricated channel result. +- A provider failure is logged with internal correlation context where + operational logging permits, but the buyer receives a stable unavailable + message that tells them to retry or restore the provider configuration. - `CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS` must explicitly allow the hostname selected by `LLM_GATEWAY_API_URL`; wildcard allowlists are forbidden. - Local Compose development permits only the explicitly enumerated diff --git a/docs/adr/0038-calendar-source-contract.md b/docs/adr/0038-calendar-source-contract.md index b9e89d51b..0052cf029 100644 --- a/docs/adr/0038-calendar-source-contract.md +++ b/docs/adr/0038-calendar-source-contract.md @@ -1,37 +1,53 @@ -# ADR 0038: Separate CalDAV events from internal commitments +# ADR 0038: Separate external calendar events from internal commitments -- Status: Accepted +- Status: Superseded in part by ADR 0123 - Date: 2026-08-18 ## Context -The buyer Calendar destination needs both external calendar events and +The buyer Calendar destination needs both external calendar observations and actionable records derived from LineageWeave posts. They have different -ownership and evidence boundaries. PR #251 defines CalDAV as an independent -consumer port, while the current application already stores authorized -commitments and issue tickets. +ownership and evidence boundaries. The application already stores authorized +commitments and issue tickets, while the first external adapter read a custom +JSON `GET {CALDAV_BASE_URL}/events` feed. -## Decision +That feed was not a CalDAV client or server contract. It did not implement RFC +4791 WebDAV discovery/REPORT, RFC 5545 recurrence and timezone semantics, RFC +6578 synchronization, provider revisions, or provider authorization. Product +and code language must not represent it as shipped CalDAV interoperability. -`GET /api/calendar` returns two independent collections: +## Original decision retained -- `events`: events read from `CALDAV_BASE_URL/events` through - `lineageweave.caldav_client`; malformed external rows are ignored. +The Buyer Calendar returns two independent collections: + +- `events`: externally observed calendar occurrences; and - `commitments`: the existing authorized internal commitment projection, filtered by the requesting account's `post_read` RBAC and post ABAC rules. -When CalDAV is unset or temporarily unavailable, `events` is empty and the -response includes a next action in `calendar_sources`; the internal -commitments remain available. The backend never invents an external event. +When the external calendar channel is unset or temporarily unavailable, +`events` is empty and the internal commitments remain available. The backend +never invents an external event. + +LineageWeave does not add a second calendar database, CalDAV server, provider +credential store, or writeback engine. + +## Superseding decision + +ADR 0123 replaces the custom `/events` transport and CalDAV naming with a +versioned, read-only Naruon calendar projection contract. Naruon is the authority +for customer-owned provider access, source registry, synchronization, provider +revisions, writeback, retries, and reconciliation. LineageWeave consumes only +bounded, already-authorized `observed` occurrence projections. -This checkpoint does not add a second calendar database. A persistent event -store and sync history may be added when offline access, change tracking, or -CalDAV write-back becomes a product requirement. +The original separation between `events` and `commitments` remains mandatory. +An external event is not converted to an internal issue or commitment without a +separate source-grounded LineageWeave decision and evidence trail. ## Consequences -- The Calendar screen is useful with the existing synthetic commitment data, - even without an external calendar server. -- External events cannot be mistaken for post-grounded commitments. -- CalDAV transport failures do not turn the entire buyer surface into a - fail-closed blank screen. +- The Calendar remains useful with authorized commitment data when Naruon is + unavailable. +- External observations cannot be mistaken for post-grounded commitments. +- Product documentation no longer represents a custom JSON feed as CalDAV. +- Runtime activation waits for Naruon's matching read endpoint and service + audience; absence continues to fail closed rather than fabricate events. diff --git a/docs/adr/0123-naruon-calendar-projection-boundary.md b/docs/adr/0123-naruon-calendar-projection-boundary.md new file mode 100644 index 000000000..239a9517f --- /dev/null +++ b/docs/adr/0123-naruon-calendar-projection-boundary.md @@ -0,0 +1,161 @@ +# ADR 0123: Consume calendar observations through Naruon + +- Status: Accepted +- Date: 2026-08-21 +- Issue: #336 +- Related authority: `ContextualWisdomLab/naruon#978`, `#998`, and `#1437` + +## Context + +LineageWeave derives customer commitments from authorized post evidence and +stores them as issue tickets with due dates. The Buyer Calendar can therefore +show two different kinds of records: + +1. LineageWeave-authoritative commitments and To Do records; and +2. external calendar events observed in a customer-owned provider. + +ADR 0038 correctly separated these collections but named a custom JSON +`GET {CALDAV_BASE_URL}/events` feed as CalDAV. That endpoint does not implement +RFC 4791 discovery, WebDAV REPORT, iCalendar recurrence or VTIMEZONE, RFC 6578 +synchronization, ETag reconciliation, scheduling, or provider authorization. +The name therefore overstates the shipped product. + +Naruon is the CWL authority for customer-owned mail, calendar, contact, and file +provider interaction. Its scheduling boundary owns typed event/commitment +semantics, DAV capability discovery, synchronization, provider revisions, +writeback, retries, and reconciliation. Reimplementing those responsibilities +inside LineageWeave would duplicate credentials and provider state and would +turn LineageWeave into a second calendar product. + +## Decision + +LineageWeave consumes a **read-only, versioned Naruon calendar projection**. It +does not connect to a CalDAV provider directly. + +The contract is implemented by: + +- `lineageweave.naruon_calendar_projection`; +- `docs/contracts/naruon-calendar-projection-v1.schema.json`; and +- strict parser, transport, byte-bound, and public-package tests in + `tests/test_naruon_calendar_projection.py` and `tests/test_http_client.py`. + +The projection endpoint is conceptually: + +```text +GET {Naruon base}/api/calendar/events + ?window_start= + &window_end= + &limit=<1..200> + [&cursor=] +``` + +The request uses an audience-scoped service credential configured for the +LineageWeave deployment. It does not forward a browser or end-user bearer token, +and it never receives provider credentials. The credential must be a bounded +single token without whitespace or control characters. The consumer admits at +most a 1 MiB response body before JSON parsing. + +Each occurrence carries only: + +```text +event_reference +occurrence_reference +source_reference +provider_revision +display_text +starts_at +ends_at +all_day +time_zone +status_code +disclosure_code +truth_status_code = observed +observed_at +``` + +Naruon applies tenant, source, participant, and disclosure policy before the +response crosses the service boundary. `busy_only` rows contain only safe +Naruon-supplied display text. Attendees, descriptions, provider URLs, private +conflict reasons, access tokens, and raw DAV payloads are outside this contract. + +LineageWeave keeps the two truth domains separate: + +```text +LineageWeave commitment +- authoritative post-derived work record +- issue/todo identity +- source-post evidence and ontology/provenance + +Naruon event projection +- observed provider occurrence +- opaque Naruon source/event/occurrence identity +- provider revision and observation time +``` + +An observed external event is never promoted into an internal commitment merely +because it appears in the same Calendar screen. + +## Validation and failure posture + +The LineageWeave consumer rejects: + +- non-HTTP(S), userinfo-bearing, query-bearing, fragment-bearing, whitespace, or + control-bearing base URLs; +- missing, control-bearing, or whitespace-bearing service credentials; +- windows longer than 366 days; +- pages larger than 200 events or response bodies larger than 1 MiB; +- unknown fields, schema versions, status, disclosure, or truth vocabularies; +- naive timestamps, invalid intervals, and duplicate occurrence references; +- URL-shaped or whitespace-bearing opaque references and cursors; +- boolean, fractional, or out-of-range page limits and invalid timeouts. + +The adapter follows no redirects through the current shared HTTP client. Errors +identify the configured host when necessary but never include the service +credential or response body. + +Until Naruon ships the matching read endpoint and service-audience contract, +LineageWeave runtime wiring remains disabled and fail-closed. Existing internal +commitments remain available even when the external event channel is absent. + +## Consequences + +### Positive + +- Product language no longer implies CalDAV interoperability that does not + exist. +- Provider credentials, sync cursors, ETags, recurrence reconciliation, and + scheduling remain in one authority. +- LineageWeave gains a strict, bounded ontology/provenance-compatible event + observation contract without creating another event store. +- Calendar commitments and external observations remain auditable and cannot be + silently conflated. +- The contract may merge and be released independently while runtime activation + remains disabled. + +### Costs and limitations + +- The Buyer Calendar will not show external events until Naruon implements and + releases the corresponding read projection. +- The two repositories require provider/consumer contract tests before runtime + activation. +- This decision does not claim provider interoperability, CalDAV conformance, or + a completed Naruon connector. +- Contract v1 does not include attendees, recurrence rules, or provider URLs; + broader disclosure requires a new reviewed contract version. + +## Runtime activation gate + +Runtime activation requires all of the following: + +1. Naruon publishes the matching endpoint, media type, service audience, and + conformance fixtures; +2. LineageWeave wires configuration and the Buyer API without forwarding an + end-user token; +3. provider/consumer fixtures pass against immutable released artifacts; +4. degraded, timeout, retry, revision, and reconciliation behavior is tested; +5. exact-head security, coverage, review, and protected merge gates pass in both + repositories. + +## References + +See `docs/doctoring/NARUON_CALENDAR_PROJECTION_REFERENCES.md`. diff --git a/docs/adr/0131-published-ontology-pages.md b/docs/adr/0131-published-ontology-pages.md new file mode 100644 index 000000000..51f4e7da3 --- /dev/null +++ b/docs/adr/0131-published-ontology-pages.md @@ -0,0 +1,109 @@ +# ADR 0131 — Publish the ontology namespace as a deterministic GitHub Pages artifact + +**Decision status:** Accepted +**Date:** 2026-08-21 + +## Context + +ADR 0004 established `docs/ontology/lineageweave-kg.ttl` as the formal, +machine-validated OWL 2 / RDF Schema / SKOS vocabulary for LineageWeave. The +repository already verifies that the ontology and relational controlled +vocabulary do not drift. However, the product-facing URL +`https://contextualwisdomlab.github.io/LineageWeave/ontology#` returned no +published resource, so ontology terms shown to buyers and external consumers +did not lead to a documentation endpoint. + +Publishing the authenticated LineageWeave application itself is not the right +fix. The ontology is a public specification artifact. It must remain usable +without tenant credentials, runtime APIs, PostgreSQL, contextual-orchestrator, +or any private source data. + +A second concern is namespace identity. The knowledge-graph Turtle and runtime +lookup predicate use the lowercase semantic namespace +`https://contextualwisdomlab.github.io/lineageweave/ontology#`, while the +committed PROV-O support profile and its contract test use the repository-case +namespace `https://contextualwisdomlab.github.io/LineageWeave/ontology#`. +GitHub Pages paths are case-sensitive. Silently rewriting either form would be +a breaking ontology migration, not a deployment repair. Issue #372 therefore +owns the inventory, canonical-namespace decision, compatibility vocabulary, +and consumer migration plan. + +## Decision + +1. Add a deterministic Python renderer, `scripts/build_ontology_site.py`, that + reads the authoritative Turtle source and emits a static Pages tree. +2. Publish a fragment-addressable HTML vocabulary at + `https://contextualwisdomlab.github.io/LineageWeave/ontology`, with one + stable anchor for every documented class, property, concept scheme, and + concept. A resource with more than one documented RDF type is rendered once + with one anchor. +3. Publish equivalent machine-readable artifacts beside the HTML: + `ontology.ttl`, `ontology.jsonld`, `ontology.nt`, the PROV-O support profile, + and a source-digest manifest. +4. Preserve `lineageweave-kg.ttl` byte-for-byte as the published Turtle + artifact. JSON-LD and N-Triples are generated from a canonicalized RDF graph + and are tested for semantic isomorphism with the source. +5. Do not add a build timestamp. The same source tree must produce the same + artifact bytes. The manifest records the source SHA-256 instead. +6. Run publication through `scripts/publish_ontology_site.py`, a fail-closed + boundary that rejects duplicate HTML fragments, non-HTTP(S) linked IRIs, + symlink outputs, source-overlapping outputs, and replacement of directories + that do not contain the generator marker. This prevents ontology data from + becoming executable links and prevents a misconfigured output path from + deleting unrelated files. +7. Validate publication behavior on pull requests, including 100% statement + and branch coverage for both the renderer and publication boundary. Deploy + only from `main`; a manual dispatch from any other ref is not a publication + path. +8. Pin every third-party GitHub Action by full commit SHA and grant Pages and + OIDC permissions only to the deployment job. Pull-request validation may + cancel superseded runs, while the single publication concurrency group does + not cancel an in-progress deployment. +9. Keep existing semantic IRIs unchanged in this deployment PR. The Pages + document distinguishes the public documentation endpoint from the semantic + identifier. Issue #372 and a future versioned ADR must govern any namespace + migration, compatibility mappings, deprecation interval, and stored-data + migration. +10. The repository must have Pages source set to **GitHub Actions** once. After + that administrative enablement, publication is entirely workflow-driven. + +## Consequences + +- The requested URL becomes a stable public specification surface after this + change reaches `main`, the repository Pages source is configured for GitHub + Actions, and the Pages environment completes successfully. +- External consumers can inspect human-readable terms or download equivalent + RDF serializations without running LineageWeave. +- A changed ontology cannot publish if its lookup-code contract, semantic + round-trip, deterministic-build contract, public-link safety, unique-fragment + contract, filesystem replacement boundary, or coverage gate fails. +- GitHub Pages remains a static documentation host; it does not provide HTTP + content negotiation or become a graph database, SPARQL endpoint, or source + of runtime truth. +- No private tenant data, runtime secrets, model output, or authenticated UI is + present in the artifact. +- The existing case-distinct namespace forms remain a tracked interoperability + gap rather than being hidden by this deployment change. + +## Related decisions and work + +- [ADR 0004](0004-knowledge-graph-ontology.md): ontology and relational + vocabulary contract. +- [ADR 0011](0011-prov-o-standard-relations.md): standard PROV-O relations. +- [ADR 0065](0065-prov-o-provenance-boundary.md): provenance authority + boundary. +- Issue #372: reconcile lowercase and repository-case public namespace IRIs. +- PR #349: authenticated Ontology Explorer consumer surface. + +## References — APA 7th + +GitHub. (2026). *Using custom workflows with GitHub Pages*. +https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages + +Sauermann, L., & Cyganiak, R. (2008). *Cool URIs for the Semantic Web*. +World Wide Web Consortium. https://www.w3.org/TR/cooluris/ + +Villazón-Terrazas, B., Vilches-Blázquez, L. M., Corcho, O., & Gómez-Pérez, A. +(2011). Methodological guidelines for publishing government linked data. In +D. Wood (Ed.), *Linking government data* (pp. 27–49). Springer. +https://doi.org/10.1007/978-1-4614-1767-5_2 diff --git a/docs/contracts/naruon-calendar-projection-v1.example.json b/docs/contracts/naruon-calendar-projection-v1.example.json new file mode 100644 index 000000000..caa5f0e82 --- /dev/null +++ b/docs/contracts/naruon-calendar-projection-v1.example.json @@ -0,0 +1,22 @@ +{ + "schema_version": "1.0", + "projection_revision": "projection_fixture_001", + "events": [ + { + "event_reference": "event_fixture_001", + "occurrence_reference": "occurrence_fixture_001", + "source_reference": "source_fixture_001", + "provider_revision": "revision_fixture_007", + "display_text": "Customer review", + "starts_at": "2026-08-24T09:00:00+09:00", + "ends_at": "2026-08-24T10:00:00+09:00", + "all_day": false, + "time_zone": "Asia/Seoul", + "status_code": "confirmed", + "disclosure_code": "summary_visible", + "truth_status_code": "observed", + "observed_at": "2026-08-21T00:00:00Z" + } + ], + "next_cursor": null +} diff --git a/docs/contracts/naruon-calendar-projection-v1.schema.json b/docs/contracts/naruon-calendar-projection-v1.schema.json new file mode 100644 index 000000000..c873180f4 --- /dev/null +++ b/docs/contracts/naruon-calendar-projection-v1.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contextualwisdomlab.org/schemas/naruon-calendar-projection-v1.schema.json", + "title": "Naruon Calendar Projection v1", + "description": "A bounded, policy-filtered event page observed by Naruon and consumed read-only by LineageWeave. This is not a CalDAV provider response.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "projection_revision", "events"], + "properties": { + "schema_version": { + "const": "1.0" + }, + "projection_revision": { + "$ref": "#/$defs/opaque_reference" + }, + "events": { + "type": "array", + "maxItems": 200, + "items": { + "$ref": "#/$defs/calendar_occurrence" + } + }, + "next_cursor": { + "oneOf": [ + { + "$ref": "#/$defs/cursor" + }, + { + "type": "null" + } + ] + } + }, + "$defs": { + "opaque_reference": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^(?!.*://)[^\\s\\u0000-\\u001F\\u007F]+$" + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 1024, + "pattern": "^(?!.*://)[^\\s\\u0000-\\u001F\\u007F]+$" + }, + "rfc3339": { + "type": "string", + "format": "date-time", + "maxLength": 64 + }, + "calendar_occurrence": { + "type": "object", + "additionalProperties": false, + "required": [ + "event_reference", + "occurrence_reference", + "source_reference", + "provider_revision", + "display_text", + "starts_at", + "ends_at", + "all_day", + "time_zone", + "status_code", + "disclosure_code", + "truth_status_code", + "observed_at" + ], + "properties": { + "event_reference": { + "$ref": "#/$defs/opaque_reference" + }, + "occurrence_reference": { + "$ref": "#/$defs/opaque_reference" + }, + "source_reference": { + "$ref": "#/$defs/opaque_reference" + }, + "provider_revision": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^(?!\\s)(?!.*\\s$)(?!.*://)[^\\u0000-\\u001F\\u007F]+$" + }, + "display_text": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001F\\u007F]+$" + }, + "starts_at": { + "$ref": "#/$defs/rfc3339" + }, + "ends_at": { + "$ref": "#/$defs/rfc3339" + }, + "all_day": { + "type": "boolean" + }, + "time_zone": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[^\\s\\u0000-\\u001F\\u007F]+$" + }, + "status_code": { + "enum": ["confirmed", "tentative", "desired", "cancelled"] + }, + "disclosure_code": { + "enum": ["busy_only", "summary_visible"] + }, + "truth_status_code": { + "const": "observed" + }, + "observed_at": { + "$ref": "#/$defs/rfc3339" + } + } + } + } +} diff --git a/docs/doctoring/NARUON_CALENDAR_PROJECTION_REFERENCES.md b/docs/doctoring/NARUON_CALENDAR_PROJECTION_REFERENCES.md new file mode 100644 index 000000000..ef9b83535 --- /dev/null +++ b/docs/doctoring/NARUON_CALENDAR_PROJECTION_REFERENCES.md @@ -0,0 +1,23 @@ +# Naruon calendar projection references + +## Product traceability + +| Source | Product decision | Evidence | +|---|---|---| +| RFC 4791 | Do not call the legacy JSON `/events` feed CalDAV; provider DAV behavior belongs to Naruon. | ADR 0038; ADR 0123; issue #336 | +| RFC 6578 | Sync tokens and collection reconciliation are provider-authority concerns, not LineageWeave read-model fields. | ADR 0123; Naruon #978/#998 | +| RFC 5545 | Recurrence occurrence identity, timezone, and all-day semantics must survive the Naruon projection. | Projection v1 schema and parser tests | +| PROV-O | External rows remain `observed`; LineageWeave commitments retain separate authoritative post provenance. | `truth_status_code`; ADR 0123 | +| OWASP API4:2023 | Limit page size, date window, timeout, and response bytes before parsing to constrain resource consumption. | Client bounds; bounded HTTP response tests | + +## APA 7th references + +Daboo, C., Desruisseaux, B., & Dusseault, L. M. (2007). *Calendaring extensions to WebDAV (CalDAV)* (RFC 4791). RFC Editor. https://doi.org/10.17487/RFC4791 + +Daboo, C., & Quillaud, A. (2012). *Collection synchronization for Web Distributed Authoring and Versioning (WebDAV)* (RFC 6578). RFC Editor. https://doi.org/10.17487/RFC6578 + +Desruisseaux, B. (2009). *Internet calendaring and scheduling core object specification (iCalendar)* (RFC 5545). RFC Editor. https://doi.org/10.17487/RFC5545 + +OWASP Foundation. (2023). *OWASP API Security Top 10—API4:2023 unrestricted resource consumption*. https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/ + +World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.org/TR/prov-o/ diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e65883463..ee22b0e6d 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -12,7 +12,7 @@ - **Multiple Project Extraction**: (Resolved) LLM prompt updated to request key_events as objects with project_name, separating events correctly. - **5W1H Missing**: (Resolved) LLM prompt updated to explicitly request 5W1H evidence items in the JSON output array. - **R&R and Keyman Missing**: (Resolved) LLM prompt updated to explicitly instruct using actual stated names rather than collective titles. -- **Entity Resolution / Searxng**: Abbreviations like "한전" and "한국전력" are not mapped properly using Searxng and KG corroboration. +- **Entity Resolution / Searxng**: Abbreviations like "한전" and "한국전력" are not mapped properly using Searxng and KG corroboration. - **Meso-level Team Mapping**: (Resolved) Checked extraction logic; `team` mapping logic is present and correct, but LLM needed better explicit instruction which is covered by R&R resolution. - **Base64 Image Omni-modal**: Current text-only embedding fails on images. Omni-modal LLM processing is required for images to capture layout, font size, colors, and spatial meaning. @@ -20,7 +20,15 @@ - **DB Architecture**: Ensure PostgreSQL is strictly used (no file DBs), 3rd normal form is maintained, and Hot Partitions are handled. DB locks must be managed (or use read/write replicas). - **Zotero Integration**: Papers and standards referenced by TEPP must be synced via Local Zotero API (http://localhost:23119/api/) and cited using APA 7th edition in docstrings. - **Testing**: We need actual testing of Psychometrics (Fast-MLSIRM parameter calibration, RMSE of estimates, Fixed-Item Parameter Calibration, CAT) against synthetic/demo data. -- **Security & Compliance**: PII masking cannot break the system. Need SOC 2 and CSAP compliance alternatives to blind PII masking. +- **Security & Compliance**: PII masking cannot break the system. Need SOC 2 and CSAP compliance alternatives to blind PII masking. - **LLM Orchestration**: Ensure ALL LLM calls route through `contextual-orchestrator` utilizing API keys (BYTEZ, NVIDIA, OPENROUTER, OPENAI) with auto model discovery and optimal reasoning effort allocation (Fugu/Conductor/TRINITY research). +## 4. Public Ontology Publication Gap +- **Observed gap**: `https://contextualwisdomlab.github.io/LineageWeave/ontology#` has no deployed public resource even though the authoritative OWL/RDFS/SKOS Turtle ontology already exists in `docs/ontology/lineageweave-kg.ttl`. +- **Active remediation — PR #371**: Add a deterministic GitHub Pages renderer, fail-closed publication boundary, and protected deployment workflow that publishes fragment-addressable HTML, byte-identical Turtle, isomorphic JSON-LD and N-Triples, the PROV-O support profile, and a source-digest manifest. +- **Publication safety**: The deployment path rejects duplicate term fragments, linked RDF IRIs outside HTTP(S), symlink outputs, source-overlapping output paths, and replacement of directories not marked as generated. Pull requests validate only; only `main` may publish, and an in-progress deployment is not cancelled by a newer run. +- **Namespace boundary**: The knowledge-graph ontology/runtime use a lowercase `lineageweave` namespace while the PROV-O support profile uses repository-case `LineageWeave`. PR #371 does not silently rewrite either semantic identity. Issue #372 owns the inventory, canonical namespace decision, compatibility vocabulary, deprecation window, stored-data migration, and downstream consumer verification. +- **Completion criteria**: Exact-head ontology/publication tests pass; owned renderer and publication-boundary statement/branch coverage is 100%; required security and repository Checks reach terminal success; an independent approval exists; the repository Pages source is GitHub Actions; the protected `main` deployment succeeds; and the requested URL resolves with stable anchors such as `#Post`. +- **Current truth**: Until PR #371 is merged and the `main` Pages deployment is verified, the URL remains unbuilt and must not be reported as live. + *This document is continuously updated by the hourly automated agent loop.* diff --git a/docs/superpowers/plans/2026-08-21-naruon-calendar-projection-contract.md b/docs/superpowers/plans/2026-08-21-naruon-calendar-projection-contract.md new file mode 100644 index 000000000..975572f19 --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-naruon-calendar-projection-contract.md @@ -0,0 +1,72 @@ +# Naruon Calendar Projection Contract Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement each task with test-first verification. + +**Goal:** Publish a strict read-only LineageWeave consumer contract for calendar observations authorized and projected by Naruon, without making LineageWeave a CalDAV/provider authority. + +**Architecture:** Keep commitments and issue/todo records authoritative in LineageWeave. Consume Naruon event occurrences through one bounded service-to-service HTTP contract, reject schema or policy drift fail-closed, and leave provider synchronization, revisions, credentials, writeback, retry, and reconciliation in Naruon. + +**Tech Stack:** Python 3.12+, dataclasses, JSON Schema Draft 2020-12, RFC 3339, pytest, coverage.py, shared bounded HTTP client. + +**Spec:** `docs/adr/0123-naruon-calendar-projection-boundary.md` + +## Global Constraints + +- No provider or end-user credential enters this contract. +- External occurrences are `observed`; LineageWeave commitments remain a separate authority. +- Windows are at most 366 days, pages at most 200 events, and response bodies at most 1 MiB. +- Unknown versions, fields, vocabularies, URL-shaped references, duplicate occurrences, and invalid clocks fail closed. +- Runtime wiring remains disabled until Naruon publishes an immutable provider contract and audience. +- Changed production statement/branch coverage and public docstrings must reach 100%. + +--- + +### Task 1: Write strict parser and transport RED tests + +**Files:** +- Create: `tests/test_naruon_calendar_projection.py` +- Create: `tests/test_naruon_calendar_projection_edges.py` +- Modify: `tests/test_http_client.py` +- Modify: `tests/test_http_client_edges.py` + +**Interfaces:** +- Produces expected public API: `parse_naruon_calendar_page` and `NaruonCalendarProjectionClient`. + +- [ ] Add failing tests for strict fields, timestamps, closed vocabularies, duplicate occurrence identity, cursor/base URL safety, service-token whitespace, numeric bounds, response-byte bounds, and public exports. +- [ ] Run the focused tests and confirm failure is caused by the missing contract and byte-bound transport. + +### Task 2: Implement bounded package contract + +**Files:** +- Create: `lineageweave/naruon_calendar_projection.py` +- Modify: `lineageweave/http_client.py` +- Modify: `lineageweave/__init__.py` + +**Interfaces:** +- Produces: `parse_naruon_calendar_page(payload, *, maximum_events=200) -> NaruonCalendarPage`. +- Produces: `NaruonCalendarProjectionClient.list_events(...) -> NaruonCalendarPage`. +- Extends: `get_json(..., maximum_response_bytes=None)`. + +- [ ] Implement immutable occurrence/page models and exact parser validation. +- [ ] Implement service-credential transport with 366-day, 200-row, 1 MiB, and 30-second ceilings. +- [ ] Reject whitespace/control-bearing tokens and ambiguous numeric values. +- [ ] Export the supported package surface. +- [ ] Run focused tests until green, then run branch coverage at 100%. + +### Task 3: Record truth, standards, and activation boundary + +**Files:** +- Modify: `docs/adr/0038-calendar-source-contract.md` +- Create: `docs/adr/0123-naruon-calendar-projection-boundary.md` +- Create: `docs/contracts/naruon-calendar-projection-v1.schema.json` +- Create: `docs/doctoring/NARUON_CALENDAR_PROJECTION_REFERENCES.md` +- Create: `CHANGELOG.d/naruon-calendar-projection-contract.md` + +**Interfaces:** +- Produces one immutable provider/consumer schema for Naruon conformance fixtures. + +- [ ] Correct the pseudo-CalDAV product claim while preserving events-versus-commitments separation. +- [ ] Record the Naruon/LineageWeave authority and runtime activation gate. +- [ ] Add RFC 4791, RFC 5545, RFC 6578, PROV-O, and bounded-resource traceability. +- [ ] Validate JSON syntax, ADR uniqueness, documentation hygiene, compileall, Ruff, and diff hygiene. +- [ ] Open a clean PR from current protected `main`, supersede the inherited stacked PR, and keep runtime integration disabled. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..8765205c6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4610,7 +4610,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
- {destination === "admin" ? : null} + {destination === "admin" && accessToken ? : null}