diff --git a/.github/workflows/ontology-pages.yml b/.github/workflows/ontology-pages.yml new file mode 100644 index 000000000..05e7000bb --- /dev/null +++ b/.github/workflows/ontology-pages.yml @@ -0,0 +1,138 @@ +name: Ontology Pages + +on: + pull_request: + branches: [main] + paths: + - "docs/ontology/**" + - "scripts/build_ontology_site.py" + - "scripts/publish_ontology_site.py" + - "scripts/ontology_site_contract.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" + - "scripts/ontology_site_contract.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 + scripts/ontology_site_contract.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/AGENTS.md b/AGENTS.md index 1728f9e61..b34194c8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,8 @@ reimplementing them: multi-channel score fusion (`weighted_convex_fuse` in `reconstruct.py`) and the buyer-facing Rankings port (`rankweave_client.py`) -- never invent a fused score or a theta. + Rankings disclose Cormack RRF channel contributions from owned + rank lists (ADR 0167). - [TEPP](https://github.com/ContextualWisdomLab/TEPP)'s published wire contract for calibrated measurement (`tepp_client.py`) -- never reimplement TEPP's model here. @@ -148,6 +150,10 @@ contextual-orchestrator owns model discovery and selection. from derived semantic text, while retaining the source body and meaningful list/heading nesting. A buyer-facing post view must render semantic paragraphs, not the authoring application's spacing workaround. +- Quantity HTML ``/`` and caret exponents such as `m^3` become + Unicode in derived units and React ``/`` in the post view + (ADR 0165). Never assign the body to `innerHTML`. Do not treat + `qty < 50` or a leading footnote `^1` as an exponent. - Image descriptions, OCR text, and region evidence are analysis artifacts, not buyer-facing prompt instructions. Buyer UI shows the source content and useful captions/evidence only, with provenance where appropriate. @@ -186,12 +192,15 @@ in the same spirit) -- never against real data, per the hard rule above. against a live local stack (`make up`) and self-skip without one -- see [README.md](README.md#local-product-stack-docker-compose). -Period leftover pairs (ADR 0017 / 0018) are computed in +Period leftover pairs (ADR 0048 / 0049 / 0119 / 0162 / 0163 / 0164) are computed in `lineageweave/leftover_pairs.py` from the residual after a real -GRM/GPCM score, never invented. Missing cells stay out of the -Gabriel factorization. Closest and farthest post–criterion pairs -persist to `report_leftover_pair` and sit above the member list so -a click opens that post. +GRM/GPCM score, never invented. Distances are Euclidean on the +two-dimensional Gabriel leftover map; missing cells stay out of +the factorization. Closest and farthest post–criterion pairs +persist to `report_leftover_pair` with signed residual `R`, observed +`Y`, and expected `E[Y|θ, item]` so `R = Y − E` remains auditable, +plus leftover-map rank so rank 0 is not read as structure. They sit +above the member list so a click opens that post. `frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`, pnpm via Corepack -- do not add a second Node package manager or a diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d0280ff97..fe6a94941 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -122,7 +122,10 @@ flowchart LR `rankweave_client.py`'s default transport raises `RankWeaveNotAvailable`. `GET /api/rankings` then returns `rankweave_not_available` and an empty ranking list. Hidden posts - are omitted from every channel. See ADR 0024. + are omitted from every channel. Accepted hits include + `channel_evidence` computed from owned temporal/lexical ranks + (Cormack weighted RRF contribution); RankWeave extra fields are + ignored and no theta is invented. See ADR 0024 and ADR 0167. ## Standards and citations @@ -195,7 +198,9 @@ it fetches a genuine access token from a live Keycloak, verifies the allow/deny ABAC boundary against a throwaway migrated Postgres database (a private post scoped to a *different* corporate entity is proven excluded from the list and 403s on direct fetch), and proves a forged -token is rejected. `scripts/seed_demo_data.py` populates the docker-compose +token is rejected. Its dev-only FastAPI `TestClient` uses Starlette with the +project's official `httpx` dev dependency; no alternate transport package is +introduced. `scripts/seed_demo_data.py` populates the docker-compose stack itself with the same shape of synthetic data for manual/frontend use. `CORSMiddleware` (`backend/app/main.py`) allows exactly the frontend's origin(s) (`FRONTEND_ORIGINS`), `GET` and `POST` (the extract-keymen @@ -588,8 +593,10 @@ on those same fixed parameters (Kim, 2006 FIPC). After scoring, `information_polytomous` ranks the shared-bank items by Fisher information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, -residual SVD leftover pairs (Jeon et al., 2021; ADR 0017) persist to -`report_leftover_pair`. Results persist to +residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; +ADR 0048 / 0119 / 0162 / 0163 / 0164) persist to `report_leftover_pair` with +signed residual `R`, observed `Y`, expected `E[Y|θ, item]`, and full +leftover-map rank. Results persist to `report_period_score` / `report_member_score`. `GET /api/reports/{grouping}` lists the trend; `GET /api/reports/{grouping}/{period}` is ABAC-filtered; @@ -601,7 +608,9 @@ bank as the dummy high/low band rows, so comparison-strip click through opens those DAG posts. Report members include the earliest open ticket title, status lookup label, and due date when one exists. The home page renders the actual mean θ, the FIPC delta, the CAT-selected item, leftover -closest/farthest pairs above the member list, and the +closest/farthest pairs (signed residual `R`, observed `Y`, expected +`E`, full rank, and two-axis leftover-map distance `d` after IRT main +effects) above the member list, and the PU / corp / thread comparison -- never a placeholder. TEPP is unchanged. ## Phase 6b: Knowledge Graph as a real Ontology + Semantic Layer diff --git a/CHANGELOG.d/2.12.13-leftover-map-two-dimensional-distance.md b/CHANGELOG.d/2.12.13-leftover-map-two-dimensional-distance.md new file mode 100644 index 000000000..974f1614a --- /dev/null +++ b/CHANGELOG.d/2.12.13-leftover-map-two-dimensional-distance.md @@ -0,0 +1,8 @@ +## 2.12.13 — Two-dimensional leftover-map distances + +- Measure closest and farthest leftover post–criterion distances on + the two-axis Jeon / Gabriel interaction map (ADR 0119). Hidden SVD + axes after the second no longer change `leftover_distance`. Rank-0 + and rank-1 maps still pad unused axes with zero. After `make seed`, + leftover pairs above the member list still open that post. Never + invent a leftover score or a theta. diff --git a/CHANGELOG.d/2.12.14-leftover-observed-expected.md b/CHANGELOG.d/2.12.14-leftover-observed-expected.md new file mode 100644 index 000000000..b3236e0d2 --- /dev/null +++ b/CHANGELOG.d/2.12.14-leftover-observed-expected.md @@ -0,0 +1,8 @@ +## 2.12.14 — Leftover observed Y and expected E + +- Persist observed `Y` and expected `E[Y|θ, item]` on leftover + post–criterion pairs (ADR 0163). Residual stays `R = Y − E`. After + `make seed`, closest and farthest leftover pairs sit above the member + list with `Y` and `E` next to leftover-map distance `d`; click opens + that post. Omit the badge when either value is missing. Never invent + a leftover score or a theta. diff --git a/CHANGELOG.d/2.12.15-idempotent-migration-replay.md b/CHANGELOG.d/2.12.15-idempotent-migration-replay.md new file mode 100644 index 000000000..002ed55f6 --- /dev/null +++ b/CHANGELOG.d/2.12.15-idempotent-migration-replay.md @@ -0,0 +1,3 @@ +- Replay every idempotent migration from 0012 onward on existing PostgreSQL + volumes through a POSIX `/bin/sh` filename boundary, including tenant settings + and leftover evidence migrations, while stopping startup on the first error. diff --git a/CHANGELOG.d/2.12.15-leftover-map-rank.md b/CHANGELOG.d/2.12.15-leftover-map-rank.md new file mode 100644 index 000000000..9f2e670c2 --- /dev/null +++ b/CHANGELOG.d/2.12.15-leftover-map-rank.md @@ -0,0 +1,8 @@ +## 2.12.15 — Leftover-map rank + +- Persist leftover-map rank on leftover post–criterion pairs (ADR 0164). + Rank is the number of Gabriel singular values above the floor. After + `make seed`, closest and farthest leftover pairs sit above the member + list with `rank {n}` next to leftover-map distance `d`; click opens + that post. Rank 0 names no leftover structure. Never invent a leftover + score or a theta. diff --git a/CHANGELOG.d/2.12.16-leftover-residual-disclosure.md b/CHANGELOG.d/2.12.16-leftover-residual-disclosure.md new file mode 100644 index 000000000..b18326ce2 --- /dev/null +++ b/CHANGELOG.d/2.12.16-leftover-residual-disclosure.md @@ -0,0 +1,6 @@ +# 2.12.16 leftover residual disclosure + +After `make seed`, closest and farthest leftover pairs sit above the +period-report member list with signed residual `R`, observed `Y`, +expected `E`, full leftover-map rank, and two-axis distance `d`. +Read the named evidence, then click the pair to open that post. diff --git a/CHANGELOG.d/2.12.17-rankweave-ranking-channel-evidence.md b/CHANGELOG.d/2.12.17-rankweave-ranking-channel-evidence.md new file mode 100644 index 000000000..13231f708 --- /dev/null +++ b/CHANGELOG.d/2.12.17-rankweave-ranking-channel-evidence.md @@ -0,0 +1,11 @@ +# 2.12.17 — RankWeave ranking channel evidence + +Rankings now shows why a fused hit landed. + +- `GET /api/rankings` attaches owned-channel evidence: 1-based + newest-first and title-overlap ranks plus Cormack RRF contribution + `weight / (η + rank)` with η = 60. +- A channel the post is missing from is omitted. RankWeave extra + fields are ignored. No fused score or theta is invented. +- The Rankings list discloses that evidence under each hit. Copy + states this is not a calibrated score. Click still opens the post. diff --git a/CHANGELOG.d/2.12.18-quantity-superscripts.md b/CHANGELOG.d/2.12.18-quantity-superscripts.md new file mode 100644 index 000000000..21505d37b --- /dev/null +++ b/CHANGELOG.d/2.12.18-quantity-superscripts.md @@ -0,0 +1,8 @@ +# 2.12.18 — Quantity superscript display + +## Fixed + +- Post popups now show cubic metres and similar quantities as superscripts + and subscripts (`12 m³`, `H₂O`) instead of flattened `m^3` or `m3`. + Semantic units store Unicode so embeddings keep the exponent. Comparison + operators and leading footnote carets stay literal (ADR 0165). diff --git a/CHANGELOG.d/2.12.6-api-route-contract-tests.md b/CHANGELOG.d/2.12.6-api-route-contract-tests.md new file mode 100644 index 000000000..525d150fa --- /dev/null +++ b/CHANGELOG.d/2.12.6-api-route-contract-tests.md @@ -0,0 +1,10 @@ +# Verify buyer API failure and authorization boundaries + +Exercise Ask Agent, RankWeave rankings, and saved locale preferences through +their authenticated HTTP routes. Rankings now have deterministic accepted and +unavailable contract evidence, including proof that another entity's private +post does not enter a buyer's channels. Ask Agent's explicit empty-question +response now uses FastAPI's current `HTTP_422_UNPROCESSABLE_CONTENT` status +name without changing the wire status. Backend integration tests keep +Starlette's TestClient on the project's official `httpx` dev dependency; no +alternate look-alike transport package is introduced. diff --git a/CHANGELOG.d/2.12.6-lineage-dag-stories.md b/CHANGELOG.d/2.12.6-lineage-dag-stories.md new file mode 100644 index 000000000..fc15bc8c1 --- /dev/null +++ b/CHANGELOG.d/2.12.6-lineage-dag-stories.md @@ -0,0 +1,6 @@ +# Verify lineage DAG states before changing graph CSS + +Open `Lineage/LineageDag` in Storybook and compare the empty, grouped/forked, +ungrouped, and long-title states. The graph now omits a relationship from both +the SVG and its count unless both endpoints are visible in the same reconstruct +group. diff --git a/CHANGELOG.d/2.12.7-non-identifying-gap-baseline.md b/CHANGELOG.d/2.12.7-non-identifying-gap-baseline.md new file mode 100644 index 000000000..b012a9e46 --- /dev/null +++ b/CHANGELOG.d/2.12.7-non-identifying-gap-baseline.md @@ -0,0 +1,28 @@ +# Restore a non-identifying product and technical gap baseline + +- Rewrites `docs/product-technical-gap-baseline.md` without identifying + post identifiers, organization names, or production record keys (ADR 0001). +- Binds remaining gaps to the live PR and issue inventory, Figma File ID + `1Su3lDRmiZdcUs47t1QwIX` (ADR 0002), and the org coverage/hourly callers + ContextualWisdomLab/.github#1258 and #1259. +- Does not change login or frontend typecheck; LineageWeave#426 owns the + shared unauthenticated AdminPanel/OIDC repair, ontology Pages stack, and this + baseline because #497 merged into #426 rather than protected `main`. + LineageWeave#494 remains an overlap audit, not a second dependency. Repeated + concurrent add/revert oscillation was not chased; its exact `5d9728a` head + instead merge-restacks current #426, has a four-file optional-only diff, and + passes Frontend while Full suite remains in progress. #426 must land first + to retain that dependency order. +- Records #496's accepted-receipt recheck dependency, #505's non-default #490 + merge, #509's subsequent merge into that same unprotected stack, and #507's + clean protected-main restack without claiming any as protected-release + evidence. #507's exact remote tree matches its validated local tree, with 41 + focused and 770 parent tests passing; hosted Frontend, Full suite, and + OpenCode are green, all 12 threads are resolved, and only Strix remains in + progress, but zero approvals still require independent review. #509 remains + a narrow changelog/parser/test/live-schema diff; + its all-green checks and resolved thread do not make #490 protected delivery. + #499 remains hidden-stack evidence. +- Records that a closed unmerged PR and pre-existing public history require an + ADR 0001 security/privacy-owner remediation process, without reproducing or + hinting at the private runtime source-table identifier involved. diff --git a/CHANGELOG.d/2.12.7-ontology-pages.md b/CHANGELOG.d/2.12.7-ontology-pages.md new file mode 100644 index 000000000..8f57aeab3 --- /dev/null +++ b/CHANGELOG.d/2.12.7-ontology-pages.md @@ -0,0 +1,13 @@ +## 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 complete manifest inventory for every published ontology artifact. +- 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/2.12.7-optional-extra-collection.md b/CHANGELOG.d/2.12.7-optional-extra-collection.md new file mode 100644 index 000000000..7e9361b7b --- /dev/null +++ b/CHANGELOG.d/2.12.7-optional-extra-collection.md @@ -0,0 +1,15 @@ +# Skip optional-extra suites when the sandbox did not install them + +- OpenCode coverage-evidence supplies pytest and coverage but not + LineageWeave's optional backend extras. Collection no longer fails + with ``ModuleNotFoundError`` for ``asyncpg``, ``psycopg2``, ``redis``, + ``fast_mlsirm``, or its ``numpy`` runtime dependency when those extras + are absent. The repository-root hook also covers sibling ``backend/tests`` + and tests that import an optional dependency through a local module, + including ``period_report`` when only NumPy is unavailable. +- Kept paths defer to pytest's remaining ignore hooks, preserving built-in + ``--ignore`` and project collection rules. +- Hosted CI still installs ``dev`` and ``backend`` extras, so every + suite continues to collect and run there. +- Login typecheck and the ADR 0001 gap baseline remain owned by + LineageWeave#426; this PR keeps only optional-extra collection work. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ed1a099..a46223352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,94 @@ All notable changes to this project are documented here. Format follows ## [Unreleased] +### Added + +- ADR 0157 and its exact-head inventory choose the existing lowercase public + ontology namespace as canonical and define the compatibility, publication, + and migration evidence required by issue #372 without rewriting identifiers. +- The ontology Pages artifact now publishes the deprecated repository-case + compatibility vocabulary after validating every mapping's term kind. +- The PROV-O support profile now mints its product class mappings only in the + canonical lowercase namespace while importing the legacy compatibility map. + ### Fixed +- `GET /healthz`: a stray decorator had stacked this route onto + `read_tenant_settings`, so the liveness probe silently required auth and + hit Postgres instead of returning `{"status": "ok"}`, and the real + `healthz()` handler had no route at all. Restored the decorator to the + correct handler. +- Closed the repository-wide docstring-coverage gap: added the 35 missing + public docstrings the AST audit found across `lineageweave/` and + `backend/app/`. +- The product-gap baseline now records private-runtime findings only as + aggregate synthetic-fixture contracts and identifies the existing + post-scoped lineage DAG without retaining post or organization identifiers. +- The public ontology now states its OWL 2 Full/RDF-Based semantics for the + ADR 0036 RDF-reified project evidence, and the PROV-O support profile uses + its canonical lowercase deployed IRI. - `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. +## [2.12.18] - 2026-08-24 + +### Fixed + +- Quantity superscripts and subscripts (`m³`, `H₂O`) now render as + text-level runs in the post popup and persist as Unicode in semantic + units. Comparison operators and leading footnote carets stay literal. + +## [2.12.17] - 2026-08-24 + +### Added + +- Rankings now names the newest-first and title-overlap ranks that + RankWeave fused, with each channel's Cormack RRF contribution. + Missing channels stay omitted. This is ranking evidence, not a + calibrated score. Click still opens that post. + +## [2.12.16] - 2026-08-24 + +### Added + +- Period leftover pair rows now name signed residual `R` beside observed + `Y`, expected `E`, leftover-map rank, and distance `d`, then open that + post from the reusable `LeftoverPairList` Storybook component + (Jeon et al., 2021, eq. 3; ADR 0162). + +## [2.12.15] - 2026-08-24 + +### Added + +- Period leftover pair rows now name leftover-map rank after IRT main + effects next to leftover-map distance `d`, then open that post + (Jeon et al., 2021, eq. 3; ADR 0164). Rank 0 names no leftover + structure rather than inventing a leftover score. When observed `Y` + and expected `E` are also present, one localized next action names all + three measurements instead of hiding either amendment. + +## [2.12.14] - 2026-08-24 + +### Added + +- Period leftover pair rows now name observed `Y` and expected + `E[Y|θ, item]` after IRT main effects next to leftover-map distance + `d`, then open that post (Jeon et al., 2021, eq. 3; ADR 0163). Residual + stays `R = Y − E`. Missing or non-finite `Y` / `E` omit the badge + rather than inventing a leftover score. + +## [2.12.13] - 2026-08-24 + +### Fixed + +- Closest and farthest leftover post–criterion distances are now + Euclidean on the two-axis Jeon / Gabriel interaction map (ADR 0119). + Hidden SVD axes after the second no longer change `leftover_distance`. + Rank-0 and rank-1 maps still pad unused axes with zero, so seed + leftover pairs above the member list still open that post. Never + invent a leftover score or a theta. + ## [2.12.6] - 2026-08-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 1bcf50763..42b091e42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,9 @@ chip name contains `Corporate entity: Demo Corp` and the persisted mean θ. The period-report panel says Demo Corp is the opened grouping and to read its mean θ and member posts, then open a post. Those members land immediately under that next action, ahead of Other Corp -and the week strip. Opening Public post names the next action: read +and the week strip. After `make seed`, leftover closest/farthest pairs +sit above the member list with leftover-map rank; rank 0 names no +leftover structure. Opening Public post names the next action: read Event Lineage, Keyman, and evaluation on that post. The popup Event Lineage DAG marks that post current. After that current node, the popup names Keyman and evaluation as the next read. After landed diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py index 2387d940b..8ebc41649 100644 --- a/backend/app/analysis_run_start.py +++ b/backend/app/analysis_run_start.py @@ -100,6 +100,7 @@ def configured_tepp_client(transport_url: str = "", api_key: str = "") -> TeppCl return TeppClient() def transport(payload: dict[str, Any]) -> dict[str, Any]: + """POST the TEPP wire payload to `url`, raising TeppNotAvailable on any transport failure.""" try: headers = {"authorization": f"Bearer {api_key}"} if api_key.strip() else {} return post_json(url, payload, headers=headers, timeout=30.0) diff --git a/backend/app/config.py b/backend/app/config.py index 02dc8dc34..9baf7d983 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -10,6 +10,8 @@ @dataclass(frozen=True) class Settings: + """Immutable snapshot of the backend's environment-driven configuration.""" + database_url: str # Reachable *from this backend process* -- used only to fetch JWKS # signing keys. Inside docker-compose this is the internal service DNS diff --git a/backend/app/lineage_ingestion.py b/backend/app/lineage_ingestion.py index f1e76d495..cffb96bd8 100644 --- a/backend/app/lineage_ingestion.py +++ b/backend/app/lineage_ingestion.py @@ -10,8 +10,11 @@ from __future__ import annotations +import math +import re +from collections.abc import Mapping from datetime import datetime -from typing import Any, Mapping +from typing import Any import asyncpg @@ -19,6 +22,10 @@ from lineageweave.lineage_persistence import lineage_edge_specs from lineageweave.models import Edge, Record +# ADR 0145 rejected the unanchored estimator. A future accepted ADR must add +# its independently validated method code here before persisted weights can run. +_SUPPORTED_ANCHOR_METHOD_CODES: frozenset[str] = frozenset() + def _occurred_at(value: datetime) -> datetime: """Reconstruct expects naive datetimes; asyncpg returns timestamptz.""" @@ -72,6 +79,80 @@ async def persist_lineage_edges(conn: asyncpg.Connection, edges: list[Edge]) -> ) +async def load_estimated_channel_weights( + conn: asyncpg.Connection, active_channels: set[str] +) -> dict[str, float] | None: + """Load only a complete vector from an independently anchored method. + + ADR 0145 currently authorizes no anchor method. A partial or invalid vector + returns ``None`` rather than being repaired. A database that has not applied + migration 0135 is likewise an unavailable state, detected without issuing a + statement that would abort the caller's outer PostgreSQL transaction. + """ + table_exists = await conn.fetchval( + "select to_regclass('public.lineage_channel_weight') is not null" + ) + if not table_exists: + return None + rows = await conn.fetch( + "select channel_code, weight_value, estimation_run_id, " + "estimation_method_code, estimator_version, anchor_method_code, " + "source_snapshot_sha256, sample_pair_count, knowledge_cutoff " + "from lineage_channel_weight" + ) + persisted = {row["channel_code"]: float(row["weight_value"]) for row in rows} + if not persisted or set(persisted) != active_channels: + return None + if any( + not math.isfinite(weight) or weight <= 0 or weight > 1 + for weight in persisted.values() + ): + return None + if not math.isclose(sum(persisted.values()), 1.0, rel_tol=0.0, abs_tol=1e-9): + return None + provenance = { + ( + row["estimation_run_id"], + row["estimation_method_code"], + row["estimator_version"], + row["anchor_method_code"], + row["source_snapshot_sha256"], + row["sample_pair_count"], + row["knowledge_cutoff"], + ) + for row in rows + } + if len(provenance) != 1: + return None + run = next(iter(provenance)) + ( + run_id, + estimation_method, + estimator_version, + anchor_method, + snapshot_digest, + sample_pair_count, + knowledge_cutoff, + ) = run + if ( + run_id is None + or not isinstance(estimation_method, str) + or not estimation_method.strip() + or not isinstance(estimator_version, str) + or not estimator_version.strip() + or not isinstance(anchor_method, str) + or anchor_method not in _SUPPORTED_ANCHOR_METHOD_CODES + or not isinstance(snapshot_digest, str) + or re.fullmatch(r"[0-9a-f]{64}", snapshot_digest) is None + or not isinstance(sample_pair_count, int) + or isinstance(sample_pair_count, bool) + or sample_pair_count < 200 + or not isinstance(knowledge_cutoff, datetime) + ): + return None + return persisted + + async def rebuild_lineage(conn: asyncpg.Connection) -> list[Edge]: """Reconstruct lineage for every ``source_post`` and persist the edges.""" rows = await conn.fetch( @@ -79,7 +160,13 @@ async def rebuild_lineage(conn: asyncpg.Connection) -> list[Edge]: "process_unit_id, thread_group_key, secondary_grouping_key " f"from source_post where {SOURCE_POST_ELIGIBILITY_SQL.format(alias='source_post')}" ) - edges = lineage_edge_specs(records_from_source_posts(rows)) + # No adjudication client is wired on this path, so the active channel + # set is the three deterministic channels (reconstruct drops llm when + # unavailable rather than faking it). + weights = await load_estimated_channel_weights( + conn, {"temporal", "secondary_key", "text"} + ) + edges = lineage_edge_specs(records_from_source_posts(rows), weights=weights) await persist_lineage_edges(conn, edges) return edges diff --git a/backend/app/main.py b/backend/app/main.py index fb943315f..0f083310d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -583,13 +583,12 @@ async def _post_filter_options( ) -@app.get("/healthz") - @app.get("/api/settings", response_model=dict) async def read_tenant_settings( account: CurrentAccount = Depends(get_current_account), pool: asyncpg.Pool = Depends(get_pool), ): + """Return the tenant's current brand name, defaulting to "LineageWeave" if unset.""" async with pool.acquire() as conn: row = await conn.fetchrow("SELECT brand_name FROM tenant_settings WHERE id = 1") if not row: @@ -602,6 +601,7 @@ async def update_tenant_settings( account: CurrentAccount = Depends(get_current_account), pool: asyncpg.Pool = Depends(get_pool), ): + """Admin-only: upsert the tenant's brand name and return the stored value.""" # Only admins can change settings _require_post_admin(account) brand_name = payload.get("brandName", "LineageWeave") @@ -614,6 +614,7 @@ async def update_tenant_settings( return {"brandName": brand_name} +@app.get("/healthz") async def healthz() -> dict[str, str]: """Liveness probe: the process is up. Does not touch Postgres.""" return {"status": "ok"} @@ -658,10 +659,14 @@ async def read_me( class LocalePreferenceRequest(BaseModel): + """Body of a PATCH /api/me/preferences request.""" + preferred_locale: Literal["en", "ko", "zh", "ja", "vi"] class CustomerHintResolveRequest(BaseModel): + """Body of a POST /api/customer-master/resolve-hint request.""" + hint_code: str @@ -2621,7 +2626,7 @@ async def ask_agent( """Answer a buyer question from authorized post and graph evidence.""" question = request.question.strip() if not question: - raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "question is required") + raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, "question is required") _require_post_read(account) client = _post_chat_client() if not client.available: @@ -2663,6 +2668,8 @@ async def ask_agent( class PostBookmarkRequest(BaseModel): + """Body of a POST /api/posts/{post_id}/bookmark request.""" + bookmarked: bool @@ -2672,6 +2679,7 @@ async def read_post_bookmark( account: CurrentAccount = Depends(get_current_account), pool: asyncpg.Pool = Depends(get_pool), ) -> dict[str, Any]: + """Report whether the current account has bookmarked this post.""" await _load_visible_post(post_id, account, pool) async with pool.acquire() as conn: row = await conn.fetchrow( @@ -2689,6 +2697,7 @@ async def write_post_bookmark( account: CurrentAccount = Depends(get_current_account), pool: asyncpg.Pool = Depends(get_pool), ) -> dict[str, Any]: + """Set or clear the current account's bookmark on this post.""" await _load_visible_post(post_id, account, pool) async with pool.acquire() as conn: if request.bookmarked: @@ -3123,11 +3132,12 @@ async def read_rankings( account: CurrentAccount = Depends(get_current_account), pool: asyncpg.Pool = Depends(get_pool), ) -> dict[str, Any]: - """RankWeave fusion of ABAC-visible posts (ADR 0024). + """RankWeave fusion of ABAC-visible posts (ADR 0024 / ADR 0167). Hidden posts are omitted from every channel. Never invents a fused - score or a theta. Fail-closed when RankWeave is disabled or the - library is missing. + score or a theta. Channel evidence is computed from owned rank + lists. Fail-closed when RankWeave is disabled or the library is + missing. """ _require_post_read(account) async with pool.acquire() as conn: diff --git a/backend/app/post_content_queue.py b/backend/app/post_content_queue.py index dae640240..29547cfad 100644 --- a/backend/app/post_content_queue.py +++ b/backend/app/post_content_queue.py @@ -23,6 +23,8 @@ @dataclass(frozen=True) class PostContentJobRequest: + """One queued or running post-content ingestion job.""" + post_id: str source_body_sha256: str status_code: str @@ -35,6 +37,7 @@ def source_body_sha256(body: str) -> str: def post_content_api_status(status_code: str | None, *, content_present: bool) -> str: + """Map a job's internal status code to the API-facing status string.""" if status_code in _ACTIVE: return "processing" if status_code == FAILED: diff --git a/backend/app/post_content_worker.py b/backend/app/post_content_worker.py index 458b9021f..8b7ef3202 100644 --- a/backend/app/post_content_worker.py +++ b/backend/app/post_content_worker.py @@ -210,6 +210,13 @@ async def process_post_content_job( embedding_factory: Callable[[], EmbeddingClient], structure_factory: Callable[[], PostStructureClient], ) -> None: + """Claim, run, and record the outcome of one post-content ingestion job. + + Claims the job for `post_id`/`source_body_digest` (a no-op if it is + already claimed, stale, or superseded), normalizes and persists the + post body through the given provider clients, then marks the job + succeeded or durably failed for retry. + """ settings = load_settings() row = await _claim_job( pool, @@ -283,6 +290,12 @@ async def consume_post_content_stream_once( embedding_factory: Callable[[], EmbeddingClient], structure_factory: Callable[[], PostStructureClient], ) -> str: + """Process one batch of the Valkey wake-up stream and return the new cursor. + + Reads up to 10 entries after `last_id`, runs `process_post_content_job` + for each, and returns the last-seen entry id so the caller can resume + from there on the next poll. + """ batches = await client.xread({POST_CONTENT_STREAM_KEY: last_id}, count=10, block=1000) for _stream_name, entries in batches: for entry_id, fields in entries: diff --git a/backend/app/post_eligibility.py b/backend/app/post_eligibility.py index 41473d9da..54e0a551b 100644 --- a/backend/app/post_eligibility.py +++ b/backend/app/post_eligibility.py @@ -1,4 +1,4 @@ -"""Shared source-post eligibility SQL for buyer evidence reads.""" +"""Shared source-post eligibility SQL for analysis-facing evidence reads.""" SOURCE_CONTEXT_COLUMNS = ( "source_author_code", @@ -17,12 +17,14 @@ def source_context_present_sql(alias: str) -> str: + """SQL fragment: true if any source-context column on `alias` is non-blank.""" return " or ".join( f"nullif(btrim({alias}.{column}), '') is not null" for column in SOURCE_CONTEXT_COLUMNS ) def source_context_missing_sql(alias: str) -> str: + """SQL fragment: true if every source-context column on `alias` is blank.""" return " and ".join( f"nullif(btrim({alias}.{column}), '') is null" for column in SOURCE_CONTEXT_COLUMNS ) diff --git a/backend/app/post_evaluation_ingestion.py b/backend/app/post_evaluation_ingestion.py index c290a7bd3..af6ea3993 100644 --- a/backend/app/post_evaluation_ingestion.py +++ b/backend/app/post_evaluation_ingestion.py @@ -15,6 +15,8 @@ @dataclass(frozen=True) class PersistedEvaluation: + """One persisted per-criterion LLM-as-a-Judge response for a post.""" + criterion_code: str criterion_label: str | None response_category: int @@ -50,6 +52,7 @@ async def ingest_post_evaluation( async def fetch_post_evaluation(conn: asyncpg.Connection, post_id: str) -> list[PersistedEvaluation]: + """Load this post's persisted per-criterion evaluation responses, ordered by criterion code.""" rows = await conn.fetch( """ select e.criterion_code, v.lookup_label as criterion_label, diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index 50614b0ad..b714f3667 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -443,8 +443,9 @@ async def persist_period_report( """ insert into report_leftover_pair ( grouping_kind, grouping_key, period_code, rubric_version, - pair_kind, post_id, criterion_code, leftover_distance, leftover_residual - ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9) + pair_kind, post_id, criterion_code, leftover_distance, leftover_residual, + observed_response, expected_response, leftover_map_rank + ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) """, grouping_kind, grouping_key, @@ -455,6 +456,9 @@ async def persist_period_report( pair.criterion_code, pair.leftover_distance, pair.leftover_residual, + pair.observed_response, + pair.expected_response, + pair.leftover_map_rank, ) @@ -601,7 +605,8 @@ async def fetch_period_reports( leftover = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli f""" select lp.grouping_key, lp.pair_kind, lp.post_id, lp.criterion_code, - lp.leftover_distance, lp.leftover_residual, p.post_title, + lp.leftover_distance, lp.leftover_residual, + lp.observed_response, lp.expected_response, lp.leftover_map_rank, p.post_title, p.visibility_code, p.corporate_entity_id, ({_SOURCE_CONTEXT_PRESENT_SQL}) as has_real_source_context from report_leftover_pair lp @@ -698,6 +703,21 @@ async def fetch_period_reports( "criterion_code": str(row["criterion_code"]), "leftover_distance": float(row["leftover_distance"]), "leftover_residual": float(row["leftover_residual"]), + "observed_response": ( + None + if row["observed_response"] is None + else float(row["observed_response"]) + ), + "expected_response": ( + None + if row["expected_response"] is None + else float(row["expected_response"]) + ), + "leftover_map_rank": ( + None + if row["leftover_map_rank"] is None + else int(row["leftover_map_rank"]) + ), "visibility_code": row["visibility_code"], "corporate_entity_id": str(row["corporate_entity_id"]), "has_real_source_context": bool(row["has_real_source_context"]), diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 438b4786a..cef8eafb8 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -14,6 +14,7 @@ import os import uuid +from contextlib import closing from pathlib import Path import jwt @@ -113,6 +114,24 @@ / "migrations" / "0102_project_bound_summary_event.sql" ) +_TENANT_SETTINGS_MIGRATION = ( + Path(__file__).resolve().parents[2] / "migrations" / "0103_tenant_settings.sql" +) +_CHANNEL_WEIGHT_MIGRATION = ( + Path(__file__).resolve().parents[2] + / "migrations" + / "0135_lineage_channel_weight.sql" +) +_LEFTOVER_OBSERVED_EXPECTED_MIGRATION = ( + Path(__file__).resolve().parents[2] + / "migrations" + / "0163_report_leftover_observed_expected.sql" +) +_LEFTOVER_MAP_RANK_MIGRATION = ( + Path(__file__).resolve().parents[2] + / "migrations" + / "0164_report_leftover_map_rank.sql" +) def _postgres_available() -> bool: @@ -226,6 +245,10 @@ def seeded_db(demo_analyst_token): cur.execute(_MAJOR_EVENT_ACTION_MIGRATION.read_text()) cur.execute(_PROJECT_BOUND_ACTION_MIGRATION.read_text()) cur.execute(_PROJECT_BOUND_EVENT_MIGRATION.read_text()) + cur.execute(_TENANT_SETTINGS_MIGRATION.read_text()) + cur.execute(_CHANNEL_WEIGHT_MIGRATION.read_text()) + cur.execute(_LEFTOVER_OBSERVED_EXPECTED_MIGRATION.read_text()) + cur.execute(_LEFTOVER_MAP_RANK_MIGRATION.read_text()) cur.execute( "insert into common_lookup_value (lookup_category, lookup_code, lookup_label) values " "('corporate_entity_level', 'group', 'Group'), " @@ -1138,6 +1161,117 @@ def test_me_reflects_the_authenticated_account(client, demo_analyst_token) -> No ) +def test_update_me_preferences_persists_a_supported_locale( + client, demo_analyst_token, seeded_db +) -> None: + """A supported Buyer locale round-trips without leaking into later tests.""" + headers = {"Authorization": f"Bearer {demo_analyst_token}"} + me_before = client.get("/api/me", headers=headers).json() + original = me_before["preferred_locale"] + updated = "en" if original == "ko" else "ko" + try: + response = client.patch( + "/api/me/preferences", + json={"preferred_locale": updated}, + headers=headers, + ) + assert response.status_code == 200 + assert response.json() == {"preferred_locale": updated} + + me_response = client.get("/api/me", headers=headers) + assert me_response.json()["preferred_locale"] == updated + finally: + with closing(psycopg2.connect(seeded_db["dsn"])) as conn: + with conn, conn.cursor() as cur: + cur.execute( + "update user_account set preferred_locale = %s where user_account_id = %s", + (original, me_before["user_account_id"]), + ) + + +def test_update_me_preferences_rejects_an_unsupported_locale(client, demo_analyst_token) -> None: + """An unsupported locale is rejected and cannot change the saved preference.""" + headers = {"Authorization": f"Bearer {demo_analyst_token}"} + original = client.get("/api/me", headers=headers).json()["preferred_locale"] + response = client.patch( + "/api/me/preferences", + json={"preferred_locale": "fr"}, + headers=headers, + ) + assert response.status_code == 422 + assert client.get("/api/me", headers=headers).json()["preferred_locale"] == original + + +def test_update_me_preferences_requires_authentication(client) -> None: + """Anonymous callers cannot write an account-scoped locale preference.""" + response = client.patch("/api/me/preferences", json={"preferred_locale": "ko"}) + assert response.status_code in (401, 403) + + +def test_rankings_fail_closed_payload_is_exact( + client, demo_analyst_token, seeded_db, monkeypatch +) -> None: + """A missing RankWeave transport is unavailable, never ambiguous success.""" + from lineageweave.rankweave_client import RankWeaveClient + + monkeypatch.setattr("backend.app.main._rankweave_client", RankWeaveClient) + response = client.get("/api/rankings", headers={"Authorization": f"Bearer {demo_analyst_token}"}) + assert response.status_code == 200 + assert response.json() == { + "port": "rankweave", + "status": "unavailable", + "status_reason": "rankweave_not_available", + "rankings": [], + } + + +def test_rankings_accept_only_abac_visible_posts( + client, demo_analyst_token, seeded_db, monkeypatch +) -> None: + """A deterministic RankWeave adapter ranks visible synthetic posts only.""" + from lineageweave.rankweave_client import RankWeaveClient + + captured_channels: dict[str, list[str]] = {} + + def fuse_visible( + channels: dict[str, list[str]], _weights: dict[str, float] + ) -> list[dict[str, str]]: + captured_channels.update(channels) + return [{"item_id": post_id} for post_id in channels["temporal"]] + + monkeypatch.setattr( + "backend.app.main._rankweave_client", + lambda: RankWeaveClient(transport=fuse_visible), + ) + response = client.get( + "/api/rankings", + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["port"] == "rankweave" + assert body["status"] == "accepted" + assert body["status_reason"] is None + assert [row["fused_rank"] for row in body["rankings"]] == list( + range(1, len(body["rankings"]) + 1) + ) + visible_by_id = {row["post_id"]: row for row in body["rankings"]} + assert visible_by_id[seeded_db["own_private_post_id"]]["post_title"] == ( + "Own-corp private post" + ) + assert visible_by_id[seeded_db["public_post_id"]]["post_title"] == "Public post" + assert seeded_db["other_private_post_id"] not in visible_by_id + assert seeded_db["other_private_post_id"] not in captured_channels["temporal"] + assert "theta" not in str(body).lower() + + +def test_rankings_requires_authentication(client) -> None: + """Anonymous callers cannot enumerate the buyer's visible ranking corpus.""" + response = client.get("/api/rankings") + assert response.status_code in (401, 403) + + def test_customer_master_returns_authorized_catalog_contract(client, demo_analyst_token, seeded_db) -> None: admin_conn = psycopg2.connect(seeded_db["dsn"]) try: @@ -1961,13 +2095,62 @@ def test_nonexistent_post_is_not_found(client, demo_analyst_token) -> None: assert response.status_code == 404 +def test_settings_get_returns_the_seeded_brand_name( + client, demo_analyst_token, seeded_db +) -> None: + """An authenticated reader receives the persisted synthetic brand.""" + + response = client.get( + "/api/settings", + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + assert response.status_code == 200 + assert response.json() == {"brandName": "LineageWeave"} + + +def test_update_settings_requires_post_admin(client, demo_analyst_token, seeded_db) -> None: + """A non-admin reader cannot mutate tenant presentation settings.""" + + response = client.patch( + "/api/settings", + json={"brandName": "Someone Else's Brand"}, + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + assert response.status_code == 403 + + +def test_update_settings_as_admin_changes_the_brand_name( + client, demo_analyst_token, seeded_db +) -> None: + """A post admin can persist and subsequently read a synthetic brand.""" + + _grant_post_admin(seeded_db["dsn"]) + patch_response = client.patch( + "/api/settings", + json={"brandName": "Renamed Corp"}, + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + assert patch_response.status_code == 200 + assert patch_response.json() == {"brandName": "Renamed Corp"} + + get_response = client.get( + "/api/settings", + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + assert get_response.json() == {"brandName": "Renamed Corp"} + + def test_missing_token_is_unauthorized(client) -> None: response = client.get("/api/posts") assert response.status_code in (401, 403) def test_forged_token_is_rejected(client) -> None: - forged = jwt.encode({"sub": "not-a-real-subject", "iss": f"{_KEYCLOAK_BASE_URL}/realms/{_REALM}"}, key="wrong-key", algorithm="HS256") + forged = jwt.encode( + {"sub": "not-a-real-subject", "iss": f"{_KEYCLOAK_BASE_URL}/realms/{_REALM}"}, + key="synthetic-wrong-signing-key-32bytes", + algorithm="HS256", + ) response = client.get("/api/posts", headers={"Authorization": f"Bearer {forged}"}) assert response.status_code == 401 @@ -4137,6 +4320,37 @@ def test_derive_commitment_unavailable_without_orchestrator( assert response.status_code == 503 +def test_ask_rejects_an_empty_question(client, demo_analyst_token, seeded_db) -> None: + """Whitespace is not a buyer question and is rejected before orchestration.""" + response = client.post( + "/api/ask", + json={"question": " "}, + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + assert response.status_code == 422 + + +def test_ask_is_unavailable_without_orchestrator_credentials( + client, demo_analyst_token, seeded_db, monkeypatch +) -> None: + """Null chat client must 503, not invent an answer.""" + from lineageweave.post_chat import NullPostChatClient + + monkeypatch.setattr("backend.app.main._post_chat_client", lambda: NullPostChatClient()) + response = client.post( + "/api/ask", + json={"question": "What happened with the public post?"}, + headers={"Authorization": f"Bearer {demo_analyst_token}"}, + ) + assert response.status_code == 503 + + +def test_ask_requires_authentication(client) -> None: + """Anonymous callers cannot ask across the buyer's authorized post corpus.""" + response = client.post("/api/ask", json={"question": "Any question"}) + assert response.status_code in (401, 403) + + def test_derive_commitment_uses_post_created_at_and_does_not_duplicate( client, demo_analyst_token, seeded_db, monkeypatch ) -> None: @@ -4518,6 +4732,13 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, assert leftover_kinds <= {"closest", "farthest"} assert all(pair["post_title"] for pair in high_report.get("leftover_pairs", [])) assert all(pair["leftover_distance"] >= 0 for pair in high_report.get("leftover_pairs", [])) + for pair in high_report.get("leftover_pairs", []): + assert pair["leftover_map_rank"] >= 0 + observed = pair.get("observed_response") + expected = pair.get("expected_response") + if observed is None or expected is None: + continue + assert abs(pair["leftover_residual"] - (observed - expected)) < 1e-6 week3 = client.get( "/api/reports/process_unit/2026-W03", @@ -4973,3 +5194,14 @@ def test_post_search_matches_source_record_key_and_one_character_typo( fuzzy = client.get("/api/posts", params={"search": typo}, headers=headers) assert fuzzy.status_code == 200, fuzzy.text assert any(post["post_id"] == seeded_db["own_private_post_id"] for post in fuzzy.json()["posts"]) + + +def test_healthz_is_a_public_liveness_probe_not_tenant_settings(client) -> None: + """/healthz must stay the plain liveness probe, never the tenant-settings route it once + collided with when a stray decorator stacked onto read_tenant_settings.""" + health = client.get("/healthz") + assert health.status_code == 200 + assert health.json() == {"status": "ok"} + + unauthenticated_settings = client.get("/api/settings") + assert unauthenticated_settings.status_code in (401, 403) diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..8873b7c2c --- /dev/null +++ b/conftest.py @@ -0,0 +1,21 @@ +"""Repository-wide pytest collection hooks for optional backend extras.""" + +from __future__ import annotations + +from pathlib import Path + +from lineageweave.optional_extra_collection import ( + collection_path_requires_missing_extras, + missing_optional_extra_modules, +) + + +def pytest_ignore_collect(collection_path: Path, config: object) -> bool | None: + """Skip files that import optional extras the sandbox did not install.""" + del config + if collection_path_requires_missing_extras( + collection_path, + missing_optional_extra_modules(), + ): + return True + return None diff --git a/docker/postgres-init/migrate.sh b/docker/postgres-init/migrate.sh index f329117d6..4105008fe 100644 --- a/docker/postgres-init/migrate.sh +++ b/docker/postgres-init/migrate.sh @@ -12,13 +12,19 @@ until pg_isready -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d sleep 1 done -# ponytail: gate at the existing 0012 boundary; replace with a migration ledger -# when a new non-idempotent migration family is introduced. +# ponytail: gate at the existing 0012 boundary; replace with a migration +# ledger when a new non-idempotent migration family is introduced. +# +# This used to be an explicit allowlist of every file number from 0012 up. +# It silently fell behind at 0103. Keep one fixed lower-bound pattern instead; +# shell arithmetic would treat leading zeroes as octal, and base#value is not +# portable under this script's POSIX /bin/sh contract (ADR 0166). for migration in /opt/lineageweave/migrations/*.sql; do + [ -f "$migration" ] || continue migration_name=${migration##*/} case "$migration_name" in - 0012_*|0013_*|0014_*|0015_*|0016_*|0017_*|0018_*|0019_*|0020_*|0021_*|0022_*|0023_*|0024_*|0025_*|0026_*|0027_*|0028_*|0029_*|0030_*|0031_*|0032_*|0033_*|0034_*|0035_*|0036_*|0037_*|0038_*|0039_*|0040_*|0041_*|0042_*|0043_*|0044_*|0045_*|0046_*|0047_*|0048_*|0049_*|0050_*) ;; - 0060_*|0100_*|0101_*|0102_*) ;; + 000[0-9]_*|001[01]_*) continue ;; + [0-9][0-9][0-9][0-9]_*) ;; *) continue ;; esac printf 'Applying %s\n' "$migration_name" diff --git a/docs/adr/0024-rankweave-fusion-fail-closed.md b/docs/adr/0024-rankweave-fusion-fail-closed.md index af1de902e..c03f73b45 100644 --- a/docs/adr/0024-rankweave-fusion-fail-closed.md +++ b/docs/adr/0024-rankweave-fusion-fail-closed.md @@ -35,6 +35,11 @@ tables, and does not bind the demo IdP to production Keyverse. 4. After login, Rankings sits above Calendar. Unavailable copy is **Rankings · RankWeave not available**. An accepted hit lists the title; click opens that `source_post`. +5. Accepted hits also disclose owned-channel evidence (ADR 0167): + 1-based `channel_rank` and Cormack contribution + `weight / (η + rank)` for each channel the post actually appears + in. Missing channels are omitted. RankWeave extra fields are + ignored. Copy states this is not a calibrated score. ## Consequences diff --git a/docs/adr/0036-semantic-project-and-keyman-evidence.md b/docs/adr/0036-semantic-project-and-keyman-evidence.md index 1c28c5b15..253664cc4 100644 --- a/docs/adr/0036-semantic-project-and-keyman-evidence.md +++ b/docs/adr/0036-semantic-project-and-keyman-evidence.md @@ -30,7 +30,12 @@ Keyman. 4. Extract semantic project mentions through the existing post-summary orchestrator path. Persist and expose canonical key, display name, evidence phrase, confidence, ontology IRI, and extraction method in - `post_project_mention` and the summary API. + `post_project_mention` and the summary API. The RDF projection models each + mention as an `rdf:Statement`: `rdf:subject` is the post, `rdf:predicate` is + `:mentionsProject`, and `rdf:object` is the project. + Because those RDF reification terms are reserved identifiers in OWL 2 DL, + the ontology explicitly uses OWL 2 Full with RDF-Based Semantics; it does + not claim the OWL 2 DL profile. 5. Project period reports union non-empty imported project keys with semantic project mentions whose confidence is at least `0.7`. A post may belong to multiple project groups. Lower-confidence candidates remain visible as diff --git a/docs/adr/0048-persist-lsirm-leftover-pairs.md b/docs/adr/0048-persist-lsirm-leftover-pairs.md index 8e87383f0..bc79cb17e 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -2,6 +2,9 @@ **Decision status:** Accepted **Date:** 2026-08-17 +**Amended by:** [ADR 0119](0119-leftover-map-two-dimensional-distance.md) (two leftover-map axes); +[ADR 0163](0163-leftover-observed-expected.md) (observed Y and expected E); +[ADR 0164](0164-leftover-map-rank.md) (full map rank) ## Context @@ -30,7 +33,11 @@ and one `farthest` observed cell per period report in `report_leftover_pair` (3NF, two-or-more-word `snake_case`). The biplot lives in `lineageweave/leftover_pairs.py` so leftover -tests do not import `period_report` or `fast_mlsirm`. +tests do not import `period_report` or `fast_mlsirm`. Distances are +Euclidean on the two leftover-map axes (ADR 0119). Each leftover row +also names observed `Y` and expected `E[Y|θ, item]` so residual +reconciles to `Y − E` (ADR 0163), and names the full singular-value +rank while distance remains on the first two axes (ADR 0164). Cascade the rows with `report_period_score`. A leftover post must also be a `report_member_score` row, and the leftover criterion diff --git a/docs/adr/0049-leftover-pair-report-ui.md b/docs/adr/0049-leftover-pair-report-ui.md index a93985b40..3b6e4ce43 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -2,6 +2,9 @@ **Decision status:** Accepted **Date:** 2026-08-17 +**Amended by:** [ADR 0162](0162-leftover-residual-disclosure.md) (signed residual R); +[ADR 0163](0163-leftover-observed-expected.md) (observed Y and expected E); +[ADR 0164](0164-leftover-map-rank.md) (full map rank) ## Context @@ -17,10 +20,15 @@ second navigation surface. On each period-report group, render leftover pairs **above** the member list. Each pair is a button: closest or farthest label, post -title, criterion short label, leftover-map distance, and the next -action (“Open this post to read the criterion it sat closest to / -farthest from after main effects.”). Clicking the button opens that -post with the same handler as a member row. +title, criterion short label, signed residual `R`, two-axis leftover-map +distance, full map rank, observed `Y`, and expected `E` when finite. +The next action names every available measurement before opening the +post; no amendment hides another, and rank 0 explicitly names no +leftover structure. +Clicking the button opens that post with the same handler as a member +row. Residual naming is [ADR 0162](0162-leftover-residual-disclosure.md), +observed/expected naming is [ADR 0163](0163-leftover-observed-expected.md), +rank naming is [ADR 0164](0164-leftover-map-rank.md). After `make seed`, closest and farthest leftover pairs sit above the member list. Click a pair to open that post. diff --git a/docs/adr/0078-focused-lineage-graph-on-post-open.md b/docs/adr/0078-focused-lineage-graph-on-post-open.md index 74a3f38b7..76ccee5b2 100644 --- a/docs/adr/0078-focused-lineage-graph-on-post-open.md +++ b/docs/adr/0078-focused-lineage-graph-on-post-open.md @@ -24,6 +24,19 @@ global graph only as the initial loading fallback. The graph is an interaction-time projection; it does not alter stored lineage edges or analysis-run snapshots. +The layout boundary independently keeps only edges whose source and target +nodes are both present in the same visible reconstruct group. A dangling edge +or an edge crossing visible groups is omitted from both the SVG and its edge +count. This preserves the backend's eligibility/ABAC decision when a partial +or stale client payload reaches the renderer; an invisible relationship must +not survive as buyer-facing aggregate evidence. + +Within one visible group, the layout may receive a converging DAG or a cyclic +import. It retains every visible edge, positions a shared child once on the +first deterministic walk, and excludes already-positioned or active-path +children from recursive re-entry. This prevents non-termination and repeated +placement without rewriting the stored graph. + ## Consequences - Opening a related post shows all visible nodes in its connected lineage @@ -33,6 +46,10 @@ analysis-run snapshots. lineage" state rather than an unrelated DAG. - A focused component can be larger than the landing limit, so the endpoint remains authenticated and ABAC-filtered. +- Captions count the same authorized, renderable edges that the buyer can see; + a relationship to an omitted node cannot leak through a count. +- A converging child has one stable SVG position while every authorized parent + edge remains visible. ## Alternatives rejected diff --git a/docs/adr/0119-leftover-map-two-dimensional-distance.md b/docs/adr/0119-leftover-map-two-dimensional-distance.md new file mode 100644 index 000000000..417d585c5 --- /dev/null +++ b/docs/adr/0119-leftover-map-two-dimensional-distance.md @@ -0,0 +1,53 @@ +# ADR 0119 — Measure leftover-map distances on two Gabriel axes + +**Decision status:** Accepted +**Date:** 2026-08-24 + +Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md). + +## Context + +ADR 0048 persists closest and farthest leftover post–criterion pairs +after IRT main effects. Jeon et al. (2021, eq. 3) place the leftover +interaction `−γ‖ξ_p − ζ_i‖` on a two-dimensional person–item map. +Gabriel (1971) supplies those coordinates from a residual biplot. + +`leftover_pairs.py` previously measured Euclidean distance on every +kept SVD axis. A rank-3 residual therefore reported a leftover +distance that a buyer cannot read on the 2D interaction map, and +that would disagree with stored two-axis coordinates if those later +persist. Rank-1 seed fixtures still passed because unused axes were +absent, not because the estimator was two-dimensional. + +## Decision + +Closest and farthest leftover distances are Euclidean on **exactly +two** leftover-map axes. `_leftover_map_positions` may still return +the full Gabriel factorization; `_pad_map_axes` pads a rank-0 or +rank-1 map with zeros and truncates hidden axes after the second. +Missing cells stay out of the factorization. Rank-0 residuals still +emit a stable pair with distance zero. Do not invent a leftover +score. Do not invent a theta. + +This slice does not persist map coordinates or change leftover UI. +Those remain ADR 0048 / 0049, and persist-map tables stay on their +own PR stack. + +## Consequences + +`leftover_distance` on `report_leftover_pair` matches the 2D Jeon +map. After `make seed`, closest and farthest pairs above the member +list still open that post. A rank-3 synthetic residual proves the +stored distance equals the two-axis hypot and is not the full-rank +norm. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 diff --git a/docs/adr/0145-psychometric-channel-weight-estimation.md b/docs/adr/0145-psychometric-channel-weight-estimation.md new file mode 100644 index 000000000..8bb45e40d --- /dev/null +++ b/docs/adr/0145-psychometric-channel-weight-estimation.md @@ -0,0 +1,87 @@ +# ADR 0145 — Channel-weight estimation remains unavailable without an independent anchor + +**Decision status:** Rejected proposal +**Date:** 2026-08-23 +**Reconciles with:** [ADR 0003](0003-fast-mlsirm-report-integration.md) + +## Context + +`lineageweave.reconstruct` currently uses hand-picked convex weights for its +temporal, secondary-key, text-similarity, and optional LLM channels. Those +constants are an explicitly ungrounded historical fallback; a citation does +not turn them into calibrated measurement. + +The rejected proposal treated channels as 2PL items, candidate pairs as +respondents, and normalized item discriminations as fusion weights. That does +not establish the product construct. An unanchored IRT fit can describe common +response structure, but it provides no independent evidence that its latent +factor is “these posts are genuinely related.” Birnbaum's 2PL item information +is also conditional on trait location and item difficulty, +`I_j(theta) = a_j^2 P_j(theta) (1 - P_j(theta))`; it is not a global constant +proportional only to `a_j`. Normalizing discriminations therefore is not an +information-optimal convex fusion rule. + +The official pinned `fast-mlsirm` contract makes two further boundaries +explicit: `factor_id` assigns items to latent dimensions, while `cluster_id` +represents respondent nesting; and a `FitResult` exposes +`convergence_status` plus package diagnostics that callers must inspect. Those +contracts can validate a fit's execution, but cannot supply the missing +criterion validity. + +Accepted ADR 0003 assigns temporal/event measurement to TEPP and limits this +repository's fast-mlsirm integration to the approved LLM-judge/report path. +This proposed lineage-weight path cannot silently expand that boundary. + +## Decision + +1. **No unanchored estimate.** LineageWeave does not run the proposed + candidate-pair IRT fit and does not persist or activate weights from it. + Estimation reports unavailable until an independent lineage anchor and its + upstream contract exist. +2. **No scientific claim for fallback constants.** Existing constants remain + unchanged for compatibility, but are neither calibrated nor + paper-grounded. This ADR does not promote them to measurement evidence. +3. **A future proposal must be ADR-first.** It must amend ADR 0003, identify an + independent outcome/anchor (for example, an accepted TEPP contract rather + than a local proxy), use official fast-mlsirm diagnostics, reject every + non-converged fit, and prove criterion validity before product activation. +4. **Future persisted vectors must be self-consistent and reproducible.** The + schema requires a known channel vocabulary, finite positive weights, an + exact sum of one at runtime, one estimation run identity, estimator and + anchor method versions, sample size, immutable source-snapshot digest, and + knowledge cutoff. Current code has no supported anchor method and therefore + loads no vector. +5. **Grouping repair is independent of measurement.** Source row identifiers + that were mapped as grouping values are normalized only in derived + reconstruction fields. Their caller-mapped raw values remain preserved in + separate source-provenance columns across backfill and re-import. + +## Consequences + +- `scripts/estimate_channel_weights.py` exits without writing because no + scientifically authorized anchor exists. +- Missing migration 0135 remains a normal rollout state and is detected via a + non-error PostgreSQL catalog probe, so an outer rebuild transaction is not + aborted. +- The persistence contract is fail-closed: malformed, mixed-provenance, or + unsupported-anchor rows are ignored rather than renormalized or repaired. +- Parameter-recovery tests cannot substitute for criterion validity; they may + return only with a future accepted anchored estimator. + +## References + +Birnbaum, A. (1968). Some latent trait models and their use in inferring an +examinee's ability. In F. M. Lord & M. R. Novick, *Statistical theories of +mental test scores* (pp. 397–479). Addison-Wesley. + +Fox, J.-P., & Glas, C. A. W. (2001). Bayesian estimation of a multilevel IRT +model using Gibbs sampling. *Psychometrika, 66*(2), 271–288. +https://doi.org/10.1007/BF02294839 + +McNeish, D., & Wolf, M. G. (2020). Thinking twice about sum scores. +*Behavior Research Methods, 52*(6), 2287–2305. +https://doi.org/10.3758/s13428-020-01398-0 + +ContextualWisdomLab. (2026). *fast-mlsirm*, pinned LineageWeave dependency +contract at commit `5006c38286a4fa1d81bcf57eeed5ce27ae743f50`. +https://github.com/ContextualWisdomLab/fast-mlsirm/tree/5006c38286a4fa1d81bcf57eeed5ce27ae743f50 diff --git a/docs/adr/0157-public-ontology-namespace-identity.md b/docs/adr/0157-public-ontology-namespace-identity.md new file mode 100644 index 000000000..3ed6a9a7d --- /dev/null +++ b/docs/adr/0157-public-ontology-namespace-identity.md @@ -0,0 +1,153 @@ +# ADR 0157 — Keep the lowercase public ontology namespace canonical + +**Decision status:** Accepted +**Date:** 2026-08-23 +**Issue:** [#372](https://github.com/ContextualWisdomLab/LineageWeave/issues/372) + +## Context + +LineageWeave has minted two case-distinct HTTP IRI families. The knowledge- +graph ontology, runtime resolver, API fixtures, and frontend fixtures use +`https://contextualwisdomlab.github.io/lineageweave/ontology#`. The PROV-O +support profile uses +`https://contextualwisdomlab.github.io/LineageWeave/ontology#`. RDF treats +these as different identifiers, and GitHub Pages treats the project paths as +different paths. + +Both forms must be treated as externally durable. The lowercase form can be +stored in `post_project_mention.ontology_iri`, returned by APIs, and copied +into RDF or downstream graph stores. The repository-case form is shipped in a +public RDF support profile. There is no repository evidence that either form +is unused outside LineageWeave. The exact inventory and current-head evidence +are recorded in +[`ONTOLOGY_NAMESPACE_INVENTORY.md`](../doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md). + +W3C guidance favors stable, manageable HTTP identifiers. Changing the wider +deployed namespace merely to match a hosting path would make infrastructure +spelling determine semantic identity. OWL equivalence is also term-kind +specific: class, property, individual, and SKOS-concept mappings are not +interchangeable. + +## Decision + +1. The canonical namespace for existing and future LineageWeave ontology + terms is the lowercase + `https://contextualwisdomlab.github.io/lineageweave/ontology#` namespace. + New runtime values, RDF exports, database rows, examples, and API payloads + must not mint repository-case term IRIs. +2. The repository-case namespace is a deprecated compatibility namespace. It + remains dereferenceable and is never reused for different meanings. +3. The future publication slice must serve both namespace documents with + `200 OK`. The lowercase document is authoritative. The repository-case + document is a compatibility vocabulary that identifies the canonical + document and carries only validated mappings. A redirect alone is + insufficient because it neither proves RDF equivalence nor reliably + communicates fragment-level term mappings. +4. Compatibility mappings are generated from the two parsed RDF graphs and + emitted only when local-name uniqueness, term kind, and defining semantics + match: + - class to class: `owl:equivalentClass`; + - object/datatype/annotation property to the same property kind: + `owl:equivalentProperty`; + - SKOS concept to SKOS concept: `skos:exactMatch` only after concept meaning + is verified; + - individuals: `owl:sameAs` only with evidence of identical identity. + A term without sufficient evidence receives no equivalence assertion. +5. Existing repository-case class IRIs in the PROV-O support profile are + compatibility inputs, not authority to mint more repository-case terms. + Until their migration, the profile imports the term-kind-validated + compatibility vocabulary so its subclass assertions also apply to the + canonical terms. Their direct migration is a later implementation slice + with RDF-isomorphism and term-kind tests. +6. Historical RDF, provenance bundles, and evidence rows are immutable. + Migration tooling may create a versioned canonical projection and retain a + source-to-target mapping, but must not silently rewrite the historical + artifact. Relational migrations must be deterministic, idempotent, + transactional, and reversible from that mapping. +7. Producers stop minting repository-case IRIs in the migration release. + Compatibility documentation and resolution remain available indefinitely. + The repository-case vocabulary is marked deprecated for at least 180 days + and two minor releases, whichever is later, before support can be reduced; + dereferenceability and mappings are not removed at the end of that window. +8. Deployment of the lowercase namespace requires an owned route at that + exact path. Repository rename, client-side case folding, and undocumented + hosting redirects are not substitutes. Until both paths are verified in + production, publication and migration remain incomplete. + +## Considered options + +### Keep lowercase canonical — chosen + +This preserves the ontology's existing identifier, runtime constant, API +contract, and wider set of serialized examples. It follows the principle that +published semantic identifiers should not change with hosting implementation. +It does require an organization-site route in addition to the repository-case +GitHub Pages project path. + +### Make repository-case canonical + +This matches the current repository name and the Pages project path, but would +replace the more widely used semantic identifier for a deployment convenience +and require migration of runtime, API, database, frontend, and graph consumers. + +### Treat both namespaces as canonical + +Rejected because RDF consumers correctly treat the IRIs as different +resources. Maintaining two authorities would preserve the interoperability +defect and make every new term ambiguous. + +## Consequences + +- Existing lowercase identifiers remain stable and new producers have one + unambiguous namespace. +- The repository-case support-profile terms remain resolvable through a + compatibility vocabulary rather than disappearing or being silently folded. +- A separate implementation PR is required for publication routing, validated + mappings, stored-value migration, rollback, consumer fixtures, and exact HTTP + tests. This ADR intentionally performs no namespace rewrite. +- Hosting is more involved than a single Pages project deployment. That cost is + accepted to keep semantic identity independent of repository-case spelling. +- Unknown downstream graphs are treated as real compatibility obligations; an + absence of repository evidence is not evidence that they do not exist. + +## Verification required for the implementation slice + +- Exact `200` responses for both namespace documents and representative + fragments, with the lowercase document identified as canonical. +- RDF graph isomorphism and term-kind tests for every emitted compatibility + mapping; no duplicate local fragments. +- Consumer fixtures proving old graphs still resolve and new serialization + mints only lowercase IRIs. +- Transactional migration tests proving idempotency, rollback, and preservation + of historical provenance bundles. +- Synchronized runtime constants, Turtle/JSON-LD/N-Triples, support profile, + API/frontend contracts, database seeds, generated Pages artifacts, and + changelog. + +## Related decisions + +- [ADR 0004](0004-knowledge-graph-ontology.md): ontology vocabulary authority. +- [ADR 0011](0011-prov-o-standard-relations.md): PROV-O support profile. +- [ADR 0065](0065-prov-o-provenance-boundary.md): provenance/navigation + separation. +- Issue #372 owns the implementation and migration verification. +- PR #349 is an open ontology consumer and PR #426 is an open publication + implementation; neither is protected-main evidence. + +## References — APA 7th + +Miles, A., & Bechhofer, S. (Eds.). (2009). *SKOS simple knowledge +organization system reference* (W3C Recommendation). World Wide Web +Consortium. https://www.w3.org/TR/skos-reference/ + +Sauermann, L., & Cyganiak, R. (2008). *Cool URIs for the Semantic Web* (W3C +Interest Group Note). World Wide Web Consortium. +https://www.w3.org/TR/cooluris/ + +World Wide Web Consortium. (2008). *Best practice recipes for publishing RDF +vocabularies* (W3C Working Group Note). +https://www.w3.org/TR/swbp-vocab-pub/ + +W3C OWL Working Group. (2012). *OWL 2 web ontology language quick reference +guide* (2nd ed., W3C Recommendation). World Wide Web Consortium. +https://www.w3.org/TR/owl2-quick-reference/ diff --git a/docs/adr/0159-published-ontology-pages.md b/docs/adr/0159-published-ontology-pages.md new file mode 100644 index 000000000..30caec60b --- /dev/null +++ b/docs/adr/0159-published-ontology-pages.md @@ -0,0 +1,110 @@ +# ADR 0159 — 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 and the complete + published ontology-directory inventory 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/adr/0162-leftover-residual-disclosure.md b/docs/adr/0162-leftover-residual-disclosure.md new file mode 100644 index 000000000..fc790b7ca --- /dev/null +++ b/docs/adr/0162-leftover-residual-disclosure.md @@ -0,0 +1,64 @@ +# ADR 0162 — Disclose leftover residual on period-report pair rows + +**Decision status:** Accepted +**Date:** 2026-08-23 + +## Context + +ADR 0048 already persists `leftover_distance` (Euclidean leftover-map +gap from the Gabriel biplot of `R`) and `leftover_residual` +(`R = Y − E[Y|θ, item]`) on `report_leftover_pair`. ADR 0049 already +renders closest and farthest pairs above the member list and opens the +named post. The pair button showed only `d`, so a buyer could not tell +a large leftover response from a merely distant map pair. + +Jeon et al. (2021, eq. 3) leftover interaction is +`−γ‖ξ_p − ζ_i‖`. Distance is that map gap. Residual is the observed +leftover *after IRT main effects* that entered the biplot. They are +different quantities. Hiding residual would keep the persisted column +as an unpublished measurement. + +This increment does not persist leftover-map coordinates (ADR 0121 / +PR #481) and does not land Post quality on the leftover criterion +(ADR 0125 / PR #485). + +## Decision + +Each leftover pair button shows: + +1. closest or farthest label, post title, and criterion short label; +2. leftover residual `R` with an explicit sign, two decimal places; +3. observed `Y`, expected `E`, full leftover-map rank, and two-axis + distance `d` when those later ADR 0163/0164 fields are available; +4. one next action that retains every available measurement before + opening the post to read the named criterion. + +Missing leftover rows still render nothing. A non-finite residual +renders an em dash rather than a fabricated leftover score. Click +still uses the same post-open handler as ADR 0049. + +## Consequences + +`GET /api/reports/{grouping}/{period}` already returns +`leftover_residual`. The frontend now names that value. After +`make seed`, closest and farthest leftover pairs sit above the member +list with `R`, `Y`, `E`, rank, and `d`; click opens that post. + +## Related + +Amends [ADR 0049](0049-leftover-pair-report-ui.md). Depends on +[ADR 0048](0048-persist-lsirm-leftover-pairs.md) and +[ADR 0003](0003-fast-mlsirm-report-integration.md). Independent of +leftover interaction-map persistence and leftover-criterion evaluation +landing. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 diff --git a/docs/adr/0163-leftover-observed-expected.md b/docs/adr/0163-leftover-observed-expected.md new file mode 100644 index 000000000..54b9e2fcc --- /dev/null +++ b/docs/adr/0163-leftover-observed-expected.md @@ -0,0 +1,71 @@ +# ADR 0163 — Persist observed Y and expected E on leftover pairs + +**Decision status:** Accepted +**Date:** 2026-08-24 + +Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and +[ADR 0049](0049-leftover-pair-report-ui.md). + +## Context + +ADR 0048 already persists leftover-map distance and leftover residual +`R = Y − E[Y|θ, item]` on `report_leftover_pair`. ADR 0049 already +renders closest and farthest pairs above the member list and opens the +named post. Residual disclosure without naming `Y` and `E` leaves a +buyer unable to tell whether a leftover cell is a high observed +response or a low expected category after IRT main effects. + +Jeon et al. (2021, eq. 3) leftover interaction is `−γ‖ξ_p − ζ_i‖`. +Gabriel (1971) supplies the leftover-map coordinates from a residual +biplot of `R`. `R` is not an invented leftover score: it is the +observed category minus the already-fitted expected category. Those +two inputs must travel with the pair row. + +This increment does not persist leftover-map coordinates, does not +change leftover-map axis count, and does not land Post quality on the +leftover criterion. + +## Decision + +Each leftover pair names: + +1. `observed_response` — the observed category `Y` for that + post–criterion cell; +2. `expected_response` — `E[Y|θ, item]` from the already-fitted + GRM/GPCM main effects; +3. `leftover_residual`, which must equal `Y − E` within `1e-6`. + +Migration `0163` is the single source of both columns on every +install path, fresh or existing -- shipped migrations (`0001`/`0012`) +are never edited after the fact. It adds them as nullable so older +leftover rows keep distance and residual without fabricating `Y` or +`E`. The pair button shows +`Y {observed} · E {expected}` next to leftover-map distance `d` when +both values are finite. The next action is: read observed `Y` and +expected `E` after IRT main effects, then open this post. Omit the +`Y` / `E` badge when either value is missing or non-finite. Do not +invent a leftover score. Do not invent a theta. + +## Consequences + +`GET /api/reports/{grouping}/{period}` returns `observed_response` +and `expected_response`. After `make seed`, closest and farthest +leftover pairs sit above the member list with named `Y` and `E`; +click opens that post. Hidden posts stay hidden. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual UI extraction, and two-axis +leftover-map distance. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 diff --git a/docs/adr/0164-leftover-map-rank.md b/docs/adr/0164-leftover-map-rank.md new file mode 100644 index 000000000..139ed215a --- /dev/null +++ b/docs/adr/0164-leftover-map-rank.md @@ -0,0 +1,63 @@ +# ADR 0164 — Name leftover-map rank on leftover pairs + +**Decision status:** Accepted +**Date:** 2026-08-24 + +Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and +[ADR 0049](0049-leftover-pair-report-ui.md). + +## Context + +ADR 0048 already persists leftover-map distance and leftover residual +`R = Y − E[Y|θ, item]` on `report_leftover_pair`. A rank-0 residual +still emits a stable closest/farthest pair so `make seed` is not empty; +the stored distance is then zero, not a fabricated interaction. ADR 0049 +renders those pairs above the member list. Without leftover-map rank, a +buyer cannot tell a Gabriel biplot with leftover structure (Jeon et al., +2021, eq. 3) from an origin collapse that still shows `d 0.00`. + +This increment does not persist leftover-map coordinates, does not name +observed `Y` / expected `E`, does not change leftover-map axis count, and +does not land Post quality on the leftover criterion. + +## Decision + +Each leftover pair names `leftover_map_rank`: the number of Gabriel +singular values above the leftover singular floor on the complete-case +residual rectangle. Closest and farthest pairs on one period report share +that rank. A fallback pair that is not placed on a leftover map stores +rank `0`. Migration `0164` is the single source of the column on every +install path; shipped migrations (`0001` / `0012`) are never rewritten. +It adds a nullable column so older leftover rows keep distance and residual +without fabricating a rank. + +The pair button shows `rank {n}` when the value is a finite +non-negative integer. Rank `0` next action: leftover map has no leftover +structure after IRT main effects; open this post. Rank `≥ 1` next action: +read leftover map rank after IRT main effects, then open this post. Omit +the rank badge when the value is missing. Do not invent a leftover score. +Do not invent a theta. + +## Consequences + +`GET /api/reports/{grouping}/{period}` returns `leftover_map_rank`. After +`make seed`, closest and farthest leftover pairs sit above the member +list with leftover-map rank; click opens that post. Hidden posts stay +hidden. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual UI extraction, two-axis leftover-map +distance, and leftover observed `Y` / expected `E`. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 diff --git a/docs/adr/0165-quantity-script-display.md b/docs/adr/0165-quantity-script-display.md new file mode 100644 index 000000000..9b38bb6f8 --- /dev/null +++ b/docs/adr/0165-quantity-script-display.md @@ -0,0 +1,55 @@ +# ADR 0165: Render quantity superscripts as text runs, Unicode in units + +- Status: Accepted +- Date: 2026-08-22 +- Depends on: [0061](0061-post-body-character-reference-decoding.md), [0062](0062-semantic-unit-embedding.md), [0102](0102-semantic-source-unit-boundaries.md) + +## Context + +Imported posts write cubic metres and similar quantities as HTML +``/`` or as caret exponents (`m^3`). The display path converted +`` into a caret and then rendered the paragraph as a React text node, +so buyers saw `m^3` instead of a superscript. The DOM chunker dropped the +tags entirely, so embeddings received concatenated `m3`, which is a +different quantity. Comparison operators such as `qty < 50` must remain +plain text, and a leading footnote caret (`^1 …`) is not a unit exponent. + +## Decision + +- Preserve the source body. Derived semantic text maps HTML ``/`` + and quantity caret exponents onto Unicode Super/Subscript characters + (The Unicode Consortium, 2024, §22.4) so search and embeddings can tell + `m³` from `m3` without keeping markup in the unit (Cai, Yu, Wen, & Ma, + 2003). +- The buyer post view splits those Unicode (or leftover caret) runs and + renders them as React ``/`` elements. The body is never + assigned to `innerHTML` (ADR 0061). +- Only a letter, digit, or closing `)` immediately followed by `^` and a + short numeric/`n` exponent is treated as a quantity. A leading `^1` + footnote marker and comparison operators stay literal. +- Unmapped script runs keep a caret or underscore so they remain visible + rather than silently concatenating. Full formula ontology remains out of + scope; this decision covers quantity display and unit-level text. + +## Consequences + +- The post popup shows `12 m³` and `H₂O` as superscripts and subscripts. +- Newly persisted `post_content_unit` text stores Unicode quantities, so + later retrieval does not depend on HTML surviving the chunker. +- Existing concatenated `m3` units stay until re-ingestion; caret-form + units still render through the display splitter. + +## References + +Cai, D., Yu, S., Wen, J.-R., & Ma, W.-Y. (2003). *VIPS: A vision-based +page segmentation algorithm* (Microsoft Research Technical Report +MSR-TR-2003-79). Microsoft Research. + +International Organization for Standardization. (2022). *Quantities and +units — Part 1: General* (ISO 80000-1:2022). + +The Unicode Consortium. (2024). *The Unicode Standard* (Version 16.0.0). +https://www.unicode.org/versions/Unicode16.0.0/ + +WHATWG. (n.d.). *HTML living standard: The `sub` and `sup` elements*. +https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-sub-and-sup-elements diff --git a/docs/adr/0166-idempotent-migration-replay-window.md b/docs/adr/0166-idempotent-migration-replay-window.md new file mode 100644 index 000000000..d28402453 --- /dev/null +++ b/docs/adr/0166-idempotent-migration-replay-window.md @@ -0,0 +1,56 @@ +# ADR 0166 — Replay every idempotent migration after the bootstrap boundary + +**Decision status:** Accepted +**Date:** 2026-08-24 + +## Context + +The PostgreSQL Official Image runs `/docker-entrypoint-initdb.d` only for an +empty data directory. A Compose service that reuses an existing volume therefore +needs a separate replay path for schema changes shipped after initialization. +LineageWeave originally replayed an explicit filename allowlist. That list +stopped at migration 0102, so migration 0103 could ship while the application +depended on `tenant_settings` and existing volumes never created the table. + +Migrations 0001–0011 are the non-idempotent image bootstrap. Migrations from +0012 onward are the replay family. The replay script declares `/bin/sh`, so its +filename gate must use POSIX shell syntax. POSIX.1-2024 requires C decimal, +octal, and hexadecimal constants in arithmetic expansion; the `base#value` +notation is an optional extension and cannot be required by this script. + +## Decision + +- Keep the existing 0012 boundary. Accept migration filenames with exactly four + decimal digits followed by `_`, skip 0000–0011, and replay every later file in + the shell glob's sorted order. Do not maintain another per-file allowlist. +- Keep the gate POSIX `/bin/sh` compatible. A fixed lower-bound filename pattern + avoids both leading-zero arithmetic and non-standard `base#value` syntax. +- Every migration numbered 0012 or later must be safe to replay. Prefer native + PostgreSQL idempotency such as `IF NOT EXISTS` and `ON CONFLICT`; a migration + that cannot be made idempotent requires a migration ledger ADR before it is + added. +- Execute each accepted file with `psql -X -v ON_ERROR_STOP=1`. A failed + migration stops startup instead of leaving a healthy-looking partial schema. +- Tests must cover the stable 0012 boundary and the idempotency of any changed + replayed migration. Application code must not compensate for a missing table. + +## Consequences + +Existing volumes receive migrations such as 0103, 0163, and 0164 without a +whitelist edit. Invalidly named files and the non-idempotent bootstrap family do +not replay. This remains a bounded no-ledger design; introduce a durable +migration ledger before any post-0011 migration needs exactly-once semantics. + +## References + +Docker Library. (n.d.). *Postgres Docker Official Image README*. GitHub. +https://github.com/docker-library/docs/blob/master/postgres/README.md + +IEEE & The Open Group. (2024). *Shell command language* (POSIX.1-2024). +https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html + +PostgreSQL Global Development Group. (2026). *CREATE TABLE* (PostgreSQL 18 +documentation). https://www.postgresql.org/docs/current/sql-createtable.html + +PostgreSQL Global Development Group. (2026). *INSERT* (PostgreSQL 18 +documentation). https://www.postgresql.org/docs/current/sql-insert.html diff --git a/docs/adr/0167-rankweave-ranking-channel-evidence.md b/docs/adr/0167-rankweave-ranking-channel-evidence.md new file mode 100644 index 000000000..d7fe2e30b --- /dev/null +++ b/docs/adr/0167-rankweave-ranking-channel-evidence.md @@ -0,0 +1,67 @@ +# ADR 0167 — Disclose RankWeave ranking channel evidence + +**Decision status:** Accepted +**Date:** 2026-08-23 + +## Context + +ADR 0024 already fuses ABAC-visible posts through RankWeave weighted +reciprocal-rank fusion (Cormack et al., 2009, η = 60; Samuel et al., +2025, unequal weights). `GET /api/rankings` returned only `post_id`, +`post_title`, and 1-based `fused_rank`. A reader could open the hit +but could not see which owned channel ranked it, or how much that +rank contributed. + +Event Lineage channel evidence (ADR 0124 on #387) explains +reconstructed parent→child edges from persisted convex-fusion +scores. Rankings is a different surface: two rank-only channels +(`temporal`, `lexical`) fused in-process at GET time. There is no +persisted ranking table and no TEPP theta. RankWeave extra fields +must not be trusted; a missing channel stays missing. + +This ADR does not replace reconstruction fusion, leftover pairs, +TEPP receipts, Allen interval labels, or Event Lineage isolation +reasons. + +## Decision + +1. Compute ranking channel evidence from LineageWeave-owned rank + lists, never from RankWeave payload extras. For each fused hit + and each channel with a positive weight, take the 1-based rank + of that `post_id` in the ordered id list. Skip a channel the + post is absent from. Do not invent a rank. +2. Contribution is Cormack weighted RRF: + `weight / (η + rank)` with η = 60. Sort by contribution + descending, then `signal_code`. `rank` on the evidence row is + that 1-based evidence order. `channel_rank` is the 1-based + position in that channel. +3. Labels: `temporal` = **Newest first**, `lexical` = **Title + overlap**. The payload never includes a fused score or a theta. +4. Rankings lists the evidence under each accepted hit as an + accessible sibling list, not hover-only. Copy states that + RankWeave fused newest-first and title-overlap ranks, and that + this is not a calibrated score. Click still opens that post. +5. Unavailable RankWeave stays empty (`rankweave_not_available`). + Hidden posts remain omitted from every channel. + +## Consequences + +Buyers can see why a Rankings hit landed without treating RRF as +measurement. A later channel or weight change recomputes evidence +on the next GET; nothing is persisted. Event Lineage evidence +remains the reconstruct/convex path. + +## References + +Cormack, G. V., Clarke, C. L. A., & Buettcher, S. (2009). Reciprocal +rank fusion outperforms Condorcet and individual rank learning +methods. In *Proceedings of the 32nd international ACM SIGIR +conference on Research and development in information retrieval* +(pp. 758–759). ACM. https://doi.org/10.1145/1571941.1572114 + +Samuel, D., MacAvaney, S., Yates, A., Zhang, E., Zhang, S., +Macdonald, C., & Ounis, I. (2025). *Weighted reciprocal rank fusion +for multi-channel retrieval* [Preprint]. + +ADR 0024 (RankWeave fusion fail-closed) +ADR 0124 (Event Lineage channel evidence; separate surface) diff --git a/docs/adr/README.md b/docs/adr/README.md index 762f1c051..d048dea29 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,10 +10,12 @@ decision from them. | Supporting document | Normative ADR | |---|---| | [`product-technical-gap-baseline.md`](../product-technical-gap-baseline.md) | Product/technical traceability projection across the ADR set; ADRs remain normative | -| [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md) | +| [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md) | | [`PROV_O_IMPLEMENTATION.md`](../PROV_O_IMPLEMENTATION.md) | [0065](0065-prov-o-provenance-boundary.md) | | [`PROV_O_IMPLEMENTATION_MATRIX.md`](../PROV_O_IMPLEMENTATION_MATRIX.md) | [0065](0065-prov-o-provenance-boundary.md) | +| [`ONTOLOGY_NAMESPACE_INVENTORY.md`](../doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md) | [0157](0157-public-ontology-namespace-identity.md) | | [`image-content-schema.md`](../image-content-schema.md) | [0066](0066-position-preserving-image-content.md) | -Runtime evidence under `docs/doctoring/` is not converted into an ADR: it -records observed results for already-decided behavior. +Files under `docs/doctoring/` remain non-normative supporting evidence even +when this map links them to an ADR. Runtime-evidence files record observed +results for already-decided behavior. diff --git a/docs/doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md b/docs/doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md new file mode 100644 index 000000000..e797324cf --- /dev/null +++ b/docs/doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md @@ -0,0 +1,83 @@ +# Public ontology namespace inventory + +**Observed repository head:** `ef6f5a5f` (`origin/main`, 2026-08-23) +**Decision:** [ADR 0157](../adr/0157-public-ontology-namespace-identity.md) +**Issue:** [#372](https://github.com/ContextualWisdomLab/LineageWeave/issues/372) + +This is a non-identifying repository inventory. It does not inspect or copy +private runtime records. An unobserved downstream store is classified as +unknown, not empty. + +## Protected-main inventory + +| Surface | Lowercase namespace | Repository-case namespace | Compatibility risk | +|---|---|---|---| +| Ontology source | `docs/ontology/lineageweave-kg.ttl` prefix and ontology IRI | — | Lowercase terms are the formal vocabulary. | +| Runtime resolver | `lineageweave/ontology.py` (`LW`, `LOOKUP_CODE`) | — | Every generated lookup IRI is lowercase. | +| PROV-O support profile | `docs/ontology/prov-o-support-profile.ttl` now mints its four class mappings in the canonical namespace | The imported compatibility vocabulary retains the four repository-case mappings | Public RDF can already be copied into external graphs. | +| Relational persistence | `post_project_mention.ontology_iri` accepts runtime-generated text | `provenance_resource.resource_iri` accepts arbitrary external IRIs | Existing private rows are not inspected; both columns are migration surfaces. | +| Backend serialization | Project and ontology annotations return `ontology_iri` | — | API consumers may persist emitted lowercase IRIs. | +| Frontend | Typed API fields consume `ontology_iri`; tests use representative lowercase class IRIs | — | UI links and exports must stay synchronized with the migration. | +| Tests/examples | Backend, frontend, ontology, and post-chat tests assert lowercase strings | PROV-O contract test asserts repository-case class IRIs | Tests currently preserve the split. | +| Generated publication | Not present on protected main | Not present on protected main | No Pages artifact is protected-main evidence yet. | + +The tracked exact-string occurrences on protected main are: + +- lowercase: `docs/ontology/lineageweave-kg.ttl`, + `lineageweave/ontology.py`, `backend/tests/test_api.py`, + `frontend/src/App.test.tsx`, and `tests/test_post_chat.py`; +- repository-case: `docs/ontology/namespace-compatibility.ttl` and the + compatibility assertion in `tests/test_prov_o.py`. + +## Open-PR impact inventory + +Open PR content is migration impact evidence, not protected-main authority. + +| PR | Exact audited head | Additional surface | +|---|---|---| +| #258 | `2e2ddd1998734d6e29dad0ba916053dd8cf27983` | SHACL and interoperability tests add lowercase consumers while retaining the repository-case support profile. | +| #349 | `40286c1f1e3d25b1e28dc6464ebd031d601fa800` | Ontology Explorer stories consume lowercase IRIs. | +| #426 | `4828b3a5e4eb180bb3cb9c5a06d1327e1003065a` | Pages generator and tests publish at the repository-case project path but deliberately do not migrate semantic identifiers. | +| #490 | `87f74c6395b7090421965359222fa29f9dd9a84d` | Knowledge-graph code, SHACL, and a semantic-projection migration add further persisted/serialized namespace surfaces. | + +Re-fetch these heads before using the inventory for implementation. PR #426 +contains the publication work formerly proposed by closed, unmerged PR #371. + +## External-consumer classification + +- **Known export-capable:** RDF ontology/support-profile files and API + `ontology_iri` fields can leave the repository boundary. +- **Known persistence-capable:** `post_project_mention.ontology_iri` and + `provenance_resource.resource_iri` can retain values across releases. +- **Unknown actual population:** customer databases, cached API responses, + generated RDF bundles, and downstream graph stores are not inspectable from + repository evidence. Migration must assume both namespace forms may exist. +- **Not evidence:** repository search cannot prove that a public IRI was never + copied, indexed, cached, or stored elsewhere. + +## Reproducible audit commands + +```bash +git grep -n -E \ + 'contextualwisdomlab\.github\.io/(lineageweave|LineageWeave)/ontology' \ + origin/main + +gh api 'repos/ContextualWisdomLab/LineageWeave/pulls?state=open&per_page=100' \ + --paginate --jq '.[].number' +``` + +The second command supplies the live PR set. Each head was fetched and scanned +with `git grep` rather than inferred from PR titles. + +## ADR number collision audit + +At exact audited heads `87f74c63` (#490), `de7f78c5` (#355), and `2e2ddd19` +(#258), the formerly colliding work has a distinct allocation: PR #490 owns +ADR `0143`, the non-default Customer Master stack owns `0144`, PR #355 owns +`0145`, and PR #258 owns `0146`. ADRs `0150`–`0156` are also occupied by open +PRs. No protected-main or live open-PR file uses `0157`, so the namespace +decision uses ADR 0157. PR #485 now owns ADR `0158` at exact head +`216c960eaccd8afa3d018b6cdc134938bcbacb8b`; no protected-main or live open-PR +file uses `0159`, so the publication decision in this stack uses ADR 0159. +Recheck immediately before integration; neither number is a global allocator +reservation. diff --git a/docs/lineage-bi-research-notes.md b/docs/lineage-bi-research-notes.md index 94f99d73d..a0f202468 100644 --- a/docs/lineage-bi-research-notes.md +++ b/docs/lineage-bi-research-notes.md @@ -207,6 +207,8 @@ Anagnostopoulos, E., Batsakis, S., & Petrakis, E. G. M. (2013). CHRONOS: A reaso Browne, W. J., Goldstein, H., & Rasbash, J. (2001). Multiple membership multiple classification (MMMC) models. *Statistical Modelling*, *1*(2), 103-124. https://doi.org/10.1177/1471082X0100100202 +Cai, D., Yu, S., Wen, J.-R., & Ma, W.-Y. (2003). *VIPS: A vision-based page segmentation algorithm* (Microsoft Research Technical Report MSR-TR-2003-79). Microsoft Research. + Chang, J., & Blei, D. M. (2009). Relational topic models for document networks. In D. van Dyk & M. Welling (Eds.), *Proceedings of the 12th International Conference on Artificial Intelligence and Statistics* (pp. 81-88). PMLR. Christen, P. (2012). *Data matching: Concepts and techniques for record linkage, entity resolution, and duplicate detection*. Springer. https://doi.org/10.1007/978-3-642-31164-2 @@ -225,6 +227,8 @@ Gildea, D., & Jurafsky, D. (2002). Automatic labeling of semantic roles. *Comput Hearst, M. A. (1997). TextTiling: Segmenting text into multi-paragraph subtopic passages. *Computational Linguistics*, *23*(1), 33-64. +International Organization for Standardization. (2022). *Quantities and units — Part 1: General* (ISO 80000-1:2022). + Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S., & Kiela, D. (2020). Retrieval-augmented generation for knowledge-intensive NLP tasks. In H. Larochelle, M. Ranzato, R. Hadsell, M. F. Balcan, & H. Lin (Eds.), *Advances in Neural Information Processing Systems* (Vol. 33, pp. 9459-9474). Curran Associates. Li, M., Lv, T., Chen, J., Cui, L., Lu, Y., Florencio, D., Zhang, C., Li, Z., & Wei, F. (2023). TrOCR: Transformer-based optical character recognition with pre-trained models. *Proceedings of the AAAI Conference on Artificial Intelligence*, *37*(11), 13094-13102. https://doi.org/10.1609/aaai.v37i11.26538 @@ -241,6 +245,8 @@ Sun, Q., Yuan, J., He, S., Guan, X., Yuan, H., Fu, X., Li, J., & Yu, P. S. (2025 Tong, H., Faloutsos, C., & Pan, J.-Y. (2006). Fast random walk with restart and its applications. *Proceedings of the Sixth International Conference on Data Mining (ICDM'06)*, 613-622. https://doi.org/10.1109/ICDM.2006.70 +The Unicode Consortium. (2024). *The Unicode Standard* (Version 16.0.0). https://www.unicode.org/versions/Unicode16.0.0/ + Wang, Q., Fu, Y., Cao, Y., Wang, S., Tian, Z., & Ding, L. (2023). *Recursively summarizing enables long-term dialogue memory in large language models*. arXiv. https://arxiv.org/abs/2308.15022 WHATWG. (2026). *HTML Living Standard — sections 4.3 (sectioning content) and 4.4 (grouping content)*. https://html.spec.whatwg.org/ @@ -379,3 +385,15 @@ summarize-and-replace older turns instead of an unbounded transcript or a hard truncation that silently drops earlier decisions. This is recorded here as the citation this feature would build on, not as a claim that conversation-level compression is implemented today. + +## Quantity scripts in source units (ADR 0165) + +Board exports write cubic metres as HTML `` or as `m^3`. Flattening +those tags concatenates `m3`, which is a different quantity, and leaving +the caret in the buyer view hides the exponent. Derived units map a short +HTML/caret exponent onto Unicode Super/Subscript characters (The Unicode +Consortium, 2024, §22.4) so embeddings keep the unit (Cai, Yu, Wen, & Ma, +2003) while the post view renders React ``/`` instead of +`innerHTML`. ISO 80000-1 treats the exponent on a unit symbol as part of +the quantity, not decoration. Comparison operators and a leading footnote +caret stay literal. Full formula ontology is still open. diff --git a/docs/ontology/lineageweave-kg.ttl b/docs/ontology/lineageweave-kg.ttl index d9156d332..c945b98f7 100644 --- a/docs/ontology/lineageweave-kg.ttl +++ b/docs/ontology/lineageweave-kg.ttl @@ -10,7 +10,7 @@ ################################################################# # LineageWeave Knowledge Graph Ontology # -# The formal OWL 2 / RDFS / SKOS vocabulary for the +# The formal OWL 2 Full / RDFS / SKOS vocabulary for the # `knowledge_graph_edge` table's node/edge types, the # `entity_relationship_type` / `person_side` / `corporate_entity_level` # controlled vocabularies in migrations/0001_initial_schema.sql, and @@ -33,7 +33,7 @@ a owl:Ontology ; rdfs:label "LineageWeave Knowledge Graph Ontology" ; - rdfs:comment "Formal OWL 2 / RDFS / SKOS vocabulary for LineageWeave's knowledge_graph_edge node and edge types, entity_relationship_type, person_side, corporate_entity_level, and post_summary_role.actor_type_code controlled vocabularies." . + rdfs:comment "Formal OWL 2 Full / RDFS / SKOS vocabulary for LineageWeave's knowledge_graph_edge node and edge types, entity_relationship_type, person_side, corporate_entity_level, and post_summary_role.actor_type_code controlled vocabularies. RDF reification for semantic project evidence is interpreted with OWL 2 RDF-Based Semantics rather than OWL 2 DL." . :lookupCode a owl:AnnotationProperty ; rdfs:label "lookup code" ; @@ -266,8 +266,12 @@ rdfs:comment "A business project referred to by a source post."@en . :ProjectMention a owl:Class ; + rdfs:subClassOf rdf:Statement, + [ a owl:Restriction ; owl:onProperty rdf:subject ; owl:allValuesFrom :Post ], + [ a owl:Restriction ; owl:onProperty rdf:predicate ; owl:hasValue :mentionsProject ], + [ a owl:Restriction ; owl:onProperty rdf:object ; owl:allValuesFrom :Project ] ; rdfs:label "Project mention"@en ; - rdfs:comment "An evidence-backed semantic assertion that a post refers to a project."@en . + rdfs:comment "An evidence-backed, RDF-reified assertion that a post refers to a project; rdf:subject identifies the post, rdf:predicate is :mentionsProject, and rdf:object identifies the project."@en . :mentionsProject a owl:ObjectProperty ; rdfs:domain :Post ; @@ -278,4 +282,5 @@ rdfs:range xsd:string . :semanticConfidence a owl:DatatypeProperty ; + rdfs:domain :ProjectMention ; rdfs:range xsd:decimal . diff --git a/docs/ontology/namespace-compatibility.ttl b/docs/ontology/namespace-compatibility.ttl new file mode 100644 index 000000000..95804cd4e --- /dev/null +++ b/docs/ontology/namespace-compatibility.ttl @@ -0,0 +1,20 @@ +@prefix canonical: . +@prefix dcterms: . +@prefix owl: . +@prefix legacy: . +@prefix xsd: . + + + a owl:Ontology ; + owl:deprecated "true"^^xsd:boolean ; + dcterms:isReplacedBy . + +legacy:Post a owl:Class . +legacy:Person a owl:Class . +legacy:CorporateEntity a owl:Class . +legacy:Team a owl:Class . + +canonical:Post owl:equivalentClass legacy:Post . +canonical:Person owl:equivalentClass legacy:Person . +canonical:CorporateEntity owl:equivalentClass legacy:CorporateEntity . +canonical:Team owl:equivalentClass legacy:Team . diff --git a/docs/ontology/prov-o-support-profile.ttl b/docs/ontology/prov-o-support-profile.ttl index 0175dd70b..6a7ec12a3 100644 --- a/docs/ontology/prov-o-support-profile.ttl +++ b/docs/ontology/prov-o-support-profile.ttl @@ -1,15 +1,16 @@ -@prefix : . +@prefix : . @prefix dcterms: . @prefix org: . @prefix owl: . @prefix prov: . @prefix rdfs: . - + a owl:Ontology ; dcterms:title "LineageWeave PROV-O support profile"@en ; dcterms:conformsTo ; - owl:imports ; + owl:imports , + ; rdfs:comment "The runtime supports all 30 PROV-O classes, all 50 normative properties, both qualification tables, and Appendix B inverse names without redefining the W3C vocabulary."@en . :Post rdfs:subClassOf prov:Entity . diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e65883463..0d78064f8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,26 +1,365 @@ # Product & Technical Gap Baseline -## 1. Known Parsing & Frontend Display Gaps -- **Footnote Parsing**: `post=00505695-3e61-1fd1-83c5-263f88a9e77a` fails to recognize footnotes (li/oi level errors). -- **Table Parsing**: `post=00505695-3e61-1fd1-80c6-86bb61c8ddc5` completely fails at parsing tables. -- **Indentation**: Incorrect indentation rendering in `post=00505695-7571-1fd1-83c3-d521b187ad5b` and `post=00505695-3e61-1fd1-83c0-497b3c1c455e`. -- **Image/Table OCR**: `post=00505695-7571-1fd1-83dd-3d22a61a5734` fails text recognition for tables inside images, markdown parsing fails, and image OCR description is too shallow for Ontology & Semantics. -- **Math/Superscripts**: `post=00505695-9612-1fe1-83a7-e30153323f25` fails to parse superscripts like m^3 properly. Needs strict Ontology grammar for math formulas. -- **Missing UI Elements**: DAG (Directed Acyclic Graph) view is currently missing from the frontend for `post=00505695-7571-1fd1-83c5-895ed333cdbc`. - -## 2. LLM Extraction & Knowledge Graph Gaps -- **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. -- **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. - -## 3. General Architecture Gaps -- **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. -- **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). - -*This document is continuously updated by the hourly automated agent loop.* +> Audit snapshot: 2026-08-24 03:09 KST. This repository records synthetic +> fixtures and aggregate, non-identifying runtime evidence only. Open PRs and +> local checks are not protected-default-branch release evidence. +> Identifying post identifiers, organization names, and production record keys +> must never appear in this file. + +## 1. Exact-head and governance evidence + +The protected default branch was +`ef6f5a5ffcb467bd935dc1e53acc0029669b0bd7` when this baseline was refreshed. +The live queue contained 54 open PRs and 19 open issues. The audited open +delivery set (#426, #490, #496, #507, and #515) had zero approving reviews. Branch +protection / rulesets require two independent approvals, resolved review +threads, and last-push approval; the authenticated GitHub identity that authors +these PRs cannot self-approve. + +Protected `main` currently has two defects that poison downstream work: + +1. Unauthenticated login rendered `AdminPanel` with an undefined access token, + so `tsc -b` failed on `main`. LineageWeave#426 owns the shared login repair + (OIDC return-URL helpers; no admin settings before authentication) and the + ontology Pages stack. #494's demonstrably unique optional-extra collection + is merged into #426 only and remains dependent on that parent reaching + protected `main`. +2. This file on `main` listed identifying post identifiers, which ADR 0001 + forbids. This head removes them from the current tree and binds gaps to the + current PR/issue inventory. The identifiers remain reachable in protected + Git history pending an approved incident/history-remediation process. It + does not duplicate #426's login patch. + +Recent protected-default-branch and org-control-plane evidence: + +| PR | Exact observed head | Current gate evidence | +| ---: | --- | --- | +| #347 | merged as `ef6f5a5ffcb467bd935dc1e53acc0029669b0bd7` | Korean UI standards on the current protected head | +| ContextualWisdomLab/.github #1248 | merged | central Strix scope repair is available to subsequent reruns | +| ContextualWisdomLab/.github #1245 | `92624300414b19dbed0f96a0295b1ac516181b4b`; auto-merge armed; blocked on independent review | retry/defer shared GitHub App installation rate limits so OpenCode dispatch is not starved | +| ContextualWisdomLab/.github #1258 | `9b5dba9f558d20dbb651b409ea9fa54a865e3405`; auto-merge armed; blocked on independent review | `--trust-lockfile` only on pnpm 11.3+; Jest keeps native `--coverage`; no invented Vitest instrumenter | +| ContextualWisdomLab/.github #1259 | `6041f2aa9e23af5850cd83fa838a3eb6c45d84b9`; auto-merge armed; blocked on independent review | thin LineageWeave hourly review-repair caller at minute 4; supersedes #1086 stack driver | +| LineageWeave #426 | `8948cdb036eb6f6a041ad97fe7e33b3043893028` (this stack) | open, mergeable but blocked, review required, auto-merge armed; zero approvals, core backend/frontend checks passing, and security/review checks still queued or running | +| LineageWeave #429 | `3763e1335cd3ac38b5e02b964ab49af34c8d73a0` | open, mergeable but blocked, review required, auto-merge armed | +| LineageWeave #494 | `5d9728a16051e7db453ca513cd5baa75be7450cc`; merged as `1ff0cd13b84d5c5f817706ef23dcbd5c3d67a510` into #426 only | unique diff is the four optional-extra collection files; this stack-only merge is not protected-`main` delivery | +| LineageWeave #497 | `07554b238a822e4423f8e6b4c000e5882fe49163`; merged as `250f20e8a6f830479ce904448cd29ab1a106aeef` into #426 only | ADR 0001 baseline is present on this hidden stack, not on protected `main` | +| LineageWeave #498 | `35823d889c5360ebf2152ed5679d7c22d6832545` | `/healthz` + docstring coverage; overlaps #429; blocked on independent review | +| LineageWeave #496 | `78287c08309f614ca1de04612c3e15c555bed1c6` | accepted TEPP receipts remain Running during an unavailable recheck; open and blocked with the exact-head Strix check failed and zero approvals | +| LineageWeave #499 | `a985f820af7a6552bcf32860b35b513e213a498c`; merged as `8f43d7fd17ae7ae9c197fe89ddb4beee82a2886a` into `docs/customer-master-scope-adr` only | channel-weight estimation remains hidden-stack evidence, not protected-`main` evidence; #507 is the clean protected-main restack of the fail-closed repair | +| LineageWeave #505 | `cbc6bd727d613216e8b0bf93b80d476205e2dd37`; merged as `c6d0ae57ca88684f3e7de992891adc2c208f06ed` into #490 only | merged into the non-default, unprotected parent branch rather than protected `main`; all 5 threads, including the 4 latest findings, are resolved; 4 checks passed and zero approvals | +| LineageWeave #506 | closed unmerged at `fd27f2d52766ac6cfe00e0713dcfc3fe938c6078` | its public PR head and pre-existing public history contain a real private runtime source-table identifier; this baseline intentionally neither names nor describes its value | +| LineageWeave #507 | `b8d9ce429f223d43a8639d0e2b2b0777e9105d2b` directly on protected `main` | exact remote tree matches the validated local tree; hosted Frontend, Full suite, and OpenCode are green, but Strix failed and zero approvals leave review required | +| LineageWeave #490 | `63f3231d249f20b0f34f7fc56fbd3f28f62f6d0c` directly on protected `main` | open, mergeable but blocked, review required; core and OpenCode checks pass, Strix is running, Devin failed, and approvals remain zero | +| LineageWeave #515 | `2d11b4b87beed3eaa7e452349a2daedb44cc32f7` stacked on exact #427 `446ceddd2a447a970cfaf2b6b858e79a0efe4b0d` | deterministic semantic row/cell, footnote, and encoded script normalization; local backend 51 and frontend 25 passed with independent adversarial review, while hosted checks are queued and no approval exists | +| LineageWeave #509 | `bba8a8ac43a43db70c563dd9612ab74c3fbe7930`; merged as `e4d692c6e5daede2af7c0e259d3fc5a4c1c7636a` into #490 only | all 4 hosted checks passed, its 1 thread is resolved, and approvals remain zero; unique diff was limited to the changelog, legacy-JSON fail-closed parser/test, and live PostgreSQL schema regression; local focused validation was 108 passed/1 skipped including 12 live PostgreSQL, migration vocabulary 54/55/54, compile and diff checks passing; this is not protected-`main` delivery | + +This documentation is now owned by the open LineageWeave#426 stack because +#497 merged into that branch rather than protected `main`. #426 owns the login +`tsc` repair, ontology Pages, and this non-identifying baseline. #494 is the +login-only overlap and must not receive this file again. #505 is merged only +into #490's non-default branch; #509's isolated fixes also merged only into +#490 as `e4d692c6e5daede2af7c0e259d3fc5a4c1c7636a`, after which #490 advanced to +`63f3231d249f20b0f34f7fc56fbd3f28f62f6d0c`. That stack remains unprotected. +#499 remains hidden-stack evidence; #507 is the clean +protected-main delivery path for its fail-closed repair. Repeated concurrent +add/revert oscillation on #494 was not chased. Exact `5d9728a` changed stack +ownership and is now merged into #426 as `1ff0cd13`; #426 must still land for +that optional-extra collection work to reach protected `main`. If any exact head +changes, re-fetch and recheck the diff, checks, threads, and approvals before +making a lifecycle claim. + +The current protected-`main` and exact #507 trees are clean of the private +runtime source-table identifier present in the closed #506 head and older +public history. Do not reproduce or hint at its value. Historical remediation +requires the ADR 0001 incident process and security/privacy-owner coordination; +never force-push or delete evidence ad hoc. + +The Grok durable hourly loop and the central thin GitHub Actions caller +ContextualWisdomLab/.github#1259 (minute 4, `pr-review-fix-scheduler.yml`) +both target this repository. Do not add a LineageWeave-local duplicate +workflow. OpenCode coverage-evidence currently fails pnpm 9.15.9 heads on +`--trust-lockfile` (a pnpm 11.3 flag) and on a synthesized Vitest `--coverage` +flag; ContextualWisdomLab/.github#1258 (`9b5dba9`) is the exact-head repair +and has auto-merge armed pending independent OpenCode / Strix / Noema. + +Figma design-system boundary (ADR 0002): File ID `1Su3lDRmiZdcUs47t1QwIX`. +The sanitized file now contains synthetic Event Lineage desktop (`5:14`) and +mobile (`5:15`) frames with graph direction, event dates, an inference +boundary, and exact fused-score evidence. Do not copy source-organization +content into this repository. Storybook remains the executable scene and +edge-case inventory for repeated web objects; rendered code-to-Figma parity +still requires same-viewport browser comparison on an exact candidate head. + +## 2. User-visible capability baseline + +Substantially present on protected `main`: + +- PostgreSQL-backed import, normalized provenance, cutoff-aware analysis runs, + source revisions, lineage reconstruction, and explicit unavailable states. +- Authenticated workspace navigation, post detail, localized summaries, 5W1H, + R&R/Keyman, evidence citations, chat, organization hierarchy, and lineage DAG + (`frontend/src/LineageDag.tsx` is on `main`; the old “DAG view missing” + baseline entry is stale). +- Semantic paragraph/list/table/image-region units that preserve the source + representation and provenance instead of flattening it into one body string. +- Contextual-orchestrator boundaries for adjudication, extraction, summaries, + chat, embeddings, and VISION; null channels remain unavailable and are + dropped from score fusion. +- W3C PROV-O projection through normalized provenance tables, with the + knowledge graph retained as an explicit navigation projection. +- Keyverse/Keycloak OIDC, RankWeave fusion port, TEPP measurement client, + ThreadWeave tree assembly. + +These statements describe source capability, not authenticated production +corpus acceptance or protected release. + +## 3. Snapshot open PR inventory + +Heads below are queue evidence; explicitly marked merged rows are lifecycle +evidence and are not protected-main release evidence. Recheck +SHA, checks, unresolved threads, and independent approval immediately before +any merge claim. Do not self-approve, force-push, or transfer stale review +evidence across heads. + +### 3.1 Merge-blocking and shared-gate repairs + +| PR | Observed head | Intent | Gap it closes when merged | +| ---: | --- | --- | --- | +| #426 | `8948cdb036eb6f6a041ad97fe7e33b3043893028` | Login `tsc`, ontology Pages, namespace compatibility, optional-extra collection, and canonical baseline ownership | Shared frontend typecheck and public ontology publication on protected `main`; core checks pass, remaining security/review checks are unsettled, and no independent approval exists | +| #507 | `b8d9ce429f223d43a8639d0e2b2b0777e9105d2b` | Clean fail-closed weighting repair restacked directly on protected `main` | Local focused 41 and parent full 770 passed; hosted Frontend, Full suite, and OpenCode are green, but Strix failed and independent exact-head approval is absent | +| #494 | `5d9728a16051e7db453ca513cd5baa75be7450cc`; merged as `1ff0cd13b84d5c5f817706ef23dcbd5c3d67a510` into #426 only | Optional-extra collection only; four-file unique diff | Stack ownership preserves unique scope; land #426 to deliver it through protected `main` | +| #497 | `07554b238a822e4423f8e6b4c000e5882fe49163` | Non-identifying gap baseline (ADR 0001), merged only into #426 as `250f20e8a6f830479ce904448cd29ab1a106aeef` | Removes identifying post identifiers from the #426 tree; protected history still requires incident remediation and protected `main` has not received it | +| #498 | `35823d889c5360ebf2152ed5679d7c22d6832545` | `/healthz`, public docstring gate, and overlapping login repair | Preserve only value unique from #426 and #429 after their protected merge order is resolved | +| #429 | `3763e1335cd3ac38b5e02b964ab49af34c8d73a0` | `/healthz` routes to the liveness probe | Operability: liveness vs settings mix-up | +| #428 | Not captured | `migrate.sh` whitelist catch-up | Deploy: migrations silently skipped | +| #393 | Not captured | Detach provider parse error context | Honest orchestrator failure, not a poisoned parse | +| #383 | Not captured | Reader-safe OTel server diagnostics | Issue #361: generic 503 must still preserve diagnostics | +| #474 | Not captured | Rename operator-facing terminology + login return | Workspace copy; do not use “Buyer” for internal objects | +| #436 | Not captured | AdminPanel coverage | Frontend coverage 100% bar for admin settings | +| #439 | Not captured | LineageDag tests and stories | Storybook inventory for DAG edge cases | + +### 3.2 User-visible product surfaces + +| PR | Intent | Related issue / ADR | +| ---: | --- | --- | +| #258 | Workspace evidence board and source-grounded ontology | Critical; CHANGES_REQUESTED historically | +| #355 | Naruon event projection contract | Issues #336, #338 | +| #349 | Bounded ontology and provenance explorer | Issue #341 | +| #387 | Persist and explain Event Lineage channel evidence | Issue #274 | +| #484 | Allen interval relations on Event Lineage edges | Temporal modeling; Allen (1983) | +| #480 | Bind corroborated SKOS org aliases to one catalog row | SKOS exact-match / altLabel | +| #482 | SKOS companion caption on organization chips | Same SKOS catalog | +| #405 | Persisted image-region locations | VISION region provenance | +| #427 | Quantity superscripts in post bodies | Formula / unit display | +| #515 | Deterministic semantic rows/cells, safe encoded scripts, and literal escaped markup stacked on exact #427 | Source-unit reader parity; synthetic local tests pass, hosted checks and independent review remain open | +| #481 | Persist leftover LSIRM interaction-map coordinates | fast-mlsirm leftover pairs | +| #485 | Land leftover pair clicks on the named Post quality criterion | Same leftover surface | +| #490 | Wire remaining ADR 0133–0138 surfaces; exact head `63f3231d249f20b0f34f7fc56fbd3f28f62f6d0c` is still open against protected `main` | Consolidated product stack, including the Knowledge Graph token repair; #505/#509 merges here are not protected delivery, Strix is running, and Devin failed | +| #505 | Planned-facility relationship intent merged as `c6d0ae57ca88684f3e7de992891adc2c208f06ed` into #490 only | All review findings resolved, but the merge target is a non-default unprotected branch | +| #509 | Isolated #505 follow-up fixes at `bba8a8ac43a43db70c563dd9612ab74c3fbe7930`, merged as `e4d692c6e5daede2af7c0e259d3fc5a4c1c7636a` into #490 only | All 4 checks passed and 1/1 thread resolved with zero approvals; unique changelog/parser/test/live-schema diff and local validation remain non-protected stack evidence | +| #434 | Wire adjudication client into corpus-wide rebuild | Issue #289 | + +### 3.3 Ask Agent stack (issues #358–#363, #269–#272) + +| PR | Intent | +| ---: | --- | +| #415 | Korean relative-time expressions in Global Ask | +| #418 | Merged `lineage_graph` for every cited post | +| #419 | Cite persisted image evidence for cited posts | +| #421 | Playwright harness for Ask Agent capabilities | +| #422 | ADRs for Ask Agent temporal / lineage / evidence goal | + +### 3.4 Scientific measurement recovery (must remain true-parameter tests) + +| PR | Intent | +| ---: | --- | +| #451 | GRM parameter-recovery (RMSE vs true parameters) | +| #452 | GPCM parameter-recovery | +| #453 | CAT parameter-recovery | +| #454 | FIPC parameter-recovery | +| #468 | Bind fast-mlsirm, Keyverse, orchestrator, and TEPP | +| #417 | TEPP topic-lineage consumption boundary (TRSL-TM + CHRONOS/TDT) | +| #496 | Durable accepted TEPP receipts and recheck continuity; exact head `78287c08309f614ca1de04612c3e15c555bed1c6` | +| #499 | Psychometric channel-weight estimation merged only into a hidden docs stack; #507 is its clean fail-closed protected-main restack | + +### 3.5 Gap-baseline documentation queue (superseded by this file) + +PRs #440–#450, #455, and #463 rewrite documentation slices of this baseline. +PRs #368 and #479 also rewrite the baseline but are not docs-only: both modify +`frontend/src/App.tsx`. #479 carries the same login-fix blob as exact #426; +#368 carries the same login behavior with an indentation-only difference. +After #426 and this non-identifying inventory land on protected `main`, those +mixed and docs-only heads have no independently demonstrated source value and +should be closed as superseded rather than merged as conflicting rewrites. Do +not merge an identifying baseline over this file. #494 was limited to value +independently verified as unique from #426. Repeated concurrent add/revert +oscillation was not chased; exact `5d9728a` changed stack ownership before its +optional-only diff merged into #426 as `1ff0cd13`. #426 must land for that value +to reach protected `main`. + +## 4. Open issues (product acceptance remaining on `main`) + +| Issue | User-visible gap | Active PR | +| ---: | --- | --- | +| #79 | Milestone 2: port verified direct-PostgreSQL analysis into the protected architecture | analysis-run registry on `main`; remaining runtime bridge | +| #87 | Milestone 2.1 normalized runtime-analysis schema bridge | related analysis-run work | +| #269 | Authenticated Global Ask MCP browser-safe and admission-bounded | Ask stack | +| #271 | Evidence-honest knowledge-cutoff scope on Global Ask | Ask stack | +| #272 | Verify Global Ask KG/ontology/semantic claims with public SearXNG evidence | Ask stack | +| #274 | Persist and explain Event Lineage channel evidence | #387 | +| #277 | TEPP: persist accepted receipts, poll completed results, keep measurement authority distinct | #468, #417 | +| #280 | Full project-lifecycle history and handover intervals | Tracked with issue #284; no active delivery PR confirmed | +| #284 | Authoritative lifecycle ingestion and idempotent reconciliation | No active delivery PR confirmed | +| #289 | Activate the optional lineage LLM channel through a bounded asynchronous rebuild | #434 | +| #336 | Replace pseudo-CalDAV feed with a Naruon-owned calendar projection | #355 | +| #338 | Evidence-bounded email/project lineage contract for Naruon consumption | #355 | +| #341 | Heterogeneous ontology and provenance explorer separate from Event Lineage | #349 | +| #358 | Batch reauthorize persisted post-Ask evidence without N+1 queries | Ask stack | +| #359 | Centralize Global Ask session storage access | Ask stack | +| #361 | Preserve server diagnostics behind generic orchestrator 503 responses | #383 | +| #362 | Roll back rejected Global Ask turn atomically instead of poisoning the session | Ask stack | +| #363 | Continue ontology neighborhoods beyond the bounded source window | Ask / ontology | +| #372 | Reconcile lowercase and repository-case public namespace IRIs | #426 Pages stack; #492 is merged into that branch, not protected `main` | + +## 5. Open product and technical gaps + +| Gap | Current evidence | Acceptance requirement | +| --- | --- | --- | +| Protected release | 54 PRs open; the audited open #426/#490/#496/#507/#515 delivery set has no independent current-head approval; #496 and #507 have failed Strix checks while #426/#490/#515 remain unsettled | Terminal exact-head checks, no unresolved threads, independent exact-head approvals, protected squash-merge SHA | +| Shared frontend gate | Unauthenticated `AdminPanel` + unused OIDC helpers failed `tsc -b` on `main`; #494's four-file optional-extra diff is merged only into current #426 | Settle #426's exact-head checks and independent review, then land #426 without another add/revert cycle | +| Identifying baseline regression | `main` gap file listed real post identifiers; separately, closed #506 and pre-existing public history contain a private runtime source-table identifier, while current `main` and #507 trees are clean | Land this non-identifying rewrite, then coordinate ADR 0001 history remediation with security/privacy owners; do not reproduce the value, force-push, or delete evidence ad hoc | +| Authorized-corpus runtime | Repository tests use synthetic fixtures; private records remain outside git | Authenticated runtime validation returning only aggregate, non-identifying evidence | +| Image understanding | Region, OCR, and description work exists across active heads (#405, #419), but current runtime acceptance has not yet proved table-image structure, complete region coverage, or summary/image readiness together | Orchestrator-backed rendered workflow, original/derived asset provenance, region-before-OCR processing, and honest unsupported states; reconcile ADR 0052's image-bearing summary readiness with ADR 0098 before changing sequencing | +| Semantic source rendering | Paragraph, table, list, formula, and indentation work exists across stacks (#394, #427, #448–#450); #515 adds synthetic backend/frontend parity for deterministic rows/cells, footnote boundaries, and encoded scripts | Land the #427 → #515 stack, then gather authenticated browser evidence that list nesting, continuation alignment, and formula units render without authoring-layout artifacts | +| Event and project semantics | Multi-project mentions, project-bound actions, 5W1H, requester/processor, and semantic relations exist in ADR 0036/0052/0100/0111/0129 and active stacks | Aggregate authenticated evidence must show distinct projects and events, explicit requester/processor and real R&R, normalized relative time, and product/entity relations without promoting attendance or co-occurrence | +| Knowledge Graph readability | The black evidence-node root cause is an undefined-token fallback; the design-token repair and long-label/evidence-table coverage are present on #490, not protected `main` | Deliver the token repair through protected `main`, then verify light/dark contrast, keyboard graph navigation, full labels, and evidence tables in the authenticated rendered surface | +| Source-code lookup UX | Source state/detail codes remain evidence-bearing machine values and current detail presentation is dense | Catalog-backed display labels with raw-code provenance, compact 5W1H/source-detail hierarchy, keyboard access, and no unsupported customer/project binding | +| Calendar / Naruon | Pseudo-CalDAV remains on `main`; #355 carries the projection contract | Naruon-owned projection, issue #336/#338 acceptance, no invented events | +| SKOS organization aliases | Catalog binding and chip caption live on #480 / #482 | One catalog row per corroborated org; companion caption is hint-only until bound | +| Event Lineage evidence | Channel evidence and Allen relations live on #387 / #484 | Persist channel scores, explain them in the popup, never invent a fused score | +| Scientific measurement | #496 preserves an already accepted TEPP receipt across an unavailable recheck, but its exact-head Strix check failed and approvals remain zero; #499 is merged only into a hidden docs stack, while #507 is the clean protected-main restack with its own Strix failure | Repair exact-head Strix findings, then protect delivery of persisted accepted envelopes and fail-closed weighting; calibration/recovery RMSE; no invented theta | +| Planned-facility intent | #505 and its #509 follow-up are merged only into open #490's non-default branch; current #490 is `63f3231d249f20b0f34f7fc56fbd3f28f62f6d0c` with core/OpenCode checks passing, Strix running, Devin failed, and zero approvals | Settle #490's exact-head gate and obtain independent review, then deliver the stack through protected `main` before making a release claim | +| Accessibility and responsive UX | Unit coverage exists for major surfaces; Storybook inventory incomplete | Keyboard, screen-reader, mobile, and authenticated Playwright acceptance on the exact release head | +| Design tokens and repeated objects | Token extraction started; sanitized Figma Event Lineage desktop/mobile frames exist, while other repeated product surfaces remain incomplete | Tokens in CSS + Storybook stories for board, popup, DAG, Ask, calendar, forms, charts; same-viewport Figma/runtime visual comparison before release | +| External integrations | Search, Zotero, calendar, Keyverse, orchestrator, RankWeave, ThreadWeave, TEPP, disksage, wardnet | Provider conformance, failure/reconciliation behavior, and provenance-bearing integration evidence | +| MSA / modular reuse | LineageWeave must run standalone and as a consumer of org packages | Do not reimplement RankWeave/TEPP/orchestrator/ThreadWeave/Keyverse; fix upstream and PR there | +| Release quality | Local focused/full suites have passed on individual PR heads | Repository-wide coverage, docstrings, Storybook, security, browser, and release evidence on one exact head | +| PII | Masking would paralyze the product; ADR 0001 forbids identifying artifacts in git | ABAC + authorized runtime; synthetic fixtures in git; no mask-in-place that drops names the operator must read | +| Database | PostgreSQL, 3NF, snake_case ≥ two words, hot-partition and lock policy | No file DBs; read/write split if lock management fails; whitelist every migration | + +## 6. UI-UX acceptance inventory (must be defined, reviewed, applied, audited) + +Each item needs a Storybook scene, an edge-case story, and an automated check +before a commercial release claim. Figma File ID `1Su3lDRmiZdcUs47t1QwIX`. + +| Dimension | Current | Gap | +| --- | --- | --- | +| Accessibility | Partial labels/roles on board, popup, login | WCAG 2.2 AA on login, board, popup, Ask, calendar, admin; focus order; live regions | +| Touch & Interaction | Click-first popup and lists | 44px targets, swipe/escape to dismiss popup, no hover-only actions | +| Performance | Board caps and hint render limits exist | Interaction-to-next-paint on board search, DAG, Ask; no N+1 (#358) | +| Style Selection | Korean UI standards merged (#347) | Tokenized light/dark; Anti-Slop-UI density; no decorative noise | +| Layout & Responsive | Desktop popup shell | 402px-class phone layout; stacked GNB; readable DAG | +| Typography & Color | Badge tokens extracted | Contrast on badges, links, error/status; no raw hex in components | +| Animation | Minimal | Reduced-motion; no blocking animation on evidence open | +| Forms & Feedback | Login, Ask, tickets, admin brand | Inline validation, next-action copy, unavailable vs failed distinction | +| Navigation Patterns | Board / customers / calendar / Ask / admin | Deep-link post + OIDC return URL (#426); bookmarkable Ask | +| Charts & Data | Period reports, leftover pairs, Rankings, DAG | Honest empty/unavailable; no invented theta; Storybook chart states | + +## 7. Ecosystem leverage order + +Reuse before rebuild. Consume these ContextualWisdomLab packages in this order +of leverage; open connector PRs there when the defect is upstream: + +1. **contextual-orchestrator** — every LLM/VISION/embedding call (Fugu / Conductor / TRINITY routing). Never a raw provider SDK. +2. **Keyverse** — OIDC issuer, JWKS, tenant principals. +3. **RankWeave** — fused scores and rankings; never invent a fused score or theta. +4. **TEPP** — calibrated measurement; persist receipts; no local reimplementation. +5. **fast-mlsirm** — GRM/GPCM/CAT/FIPC recovery tests (#451–#454) must stay true-parameter RMSE. +6. **ThreadWeave** — tree assembly. +7. **Naruon** — calendar and email/project lineage projection (#336, #338, #355). +8. **disksage / wardnet** — storage and network policy as needed. +9. **ContextualWisdomLab/.github** — required review workflows (OpenCode, Strix, Noema) and the LineageWeave hourly caller (#1259). If stacked PRs miss central review or coverage-evidence fails on pnpm 9 (`--trust-lockfile` is pnpm 11.3) or a missing Vitest coverage provider, fix the org workflow (#1258), not a local bypass. + +## 8. Public ontology publication boundary + +- PR #426 publishes fragment-addressable HTML, byte-identical Turtle, + isomorphic JSON-LD and N-Triples, the PROV-O support profile, and a + source-digest manifest from the authoritative ontology. +- Pull requests validate only. Only protected `main` may publish, and the + generated-directory marker, linked-IRI, duplicate-fragment, symlink, and + source-overlap checks fail closed. +- The lowercase knowledge-graph namespace and repository-case support-profile + namespace remain distinct until issue #372 delivers a versioned migration + and compatibility decision; this publication PR rewrites neither identity. +- Until the protected deployment and exact URL checks succeed, the public + ontology endpoint remains unavailable and must not be represented as live. + +## 9. Evidence boundaries + +- Never add a real record, title, name, identifier, screenshot, log, benchmark + artifact, or documentation example to this repository. +- Attendance or co-occurrence is not responsibility, project, customer, or + affiliation evidence. Preserve uncertainty and provenance. +- Missing transport, model capability, accepted envelope, or persistence is + unavailable or failed evidence, never a placeholder result. +- Local green tests, bot statuses, auto-merge, and warning-only checks do not + prove a protected merge. +- Re-fetch base/head SHAs, checks, review threads, approvals, rulesets, and the + merge SHA immediately before any lifecycle claim. +- Do not self-approve. Independent OpenCode / Strix / Noema review is required. +- Do not force-push. Do not treat GitHub Checks duration as a blocker; repair + the failing check instead. +- `COPILOT_GITHUB_TOKEN` is not used. + +## 10. Next acceptance loop + +1. Let #426's remaining exact-head security/review checks settle; partial green + checks are not a terminal protected gate even though its threads are resolved. +2. Obtain two independent exact-head approvals for #426 and land that stack on + protected `main`; auto-merge being armed does not itself satisfy the gate. +3. Repair #496's failed Strix finding and obtain independent exact-head review + while preserving the durable accepted-receipt behavior across unavailable + rechecks. +4. Treat #505 and #509 merge commits as #490-only evidence. Settle Strix and + repair Devin findings on exact #490 head `63f3231d249f20b0f34f7fc56fbd3f28f62f6d0c`, obtain independent review, and + deliver the resulting stack through protected `main` before a release claim. +5. Repair #507's failed Strix finding, then obtain independent exact-head + approval for `b8d9ce429f223d43a8639d0e2b2b0777e9105d2b`; its 12 threads, + Frontend, Full suite, and OpenCode are green, but zero approvals still block + the clean protected-main path. Do not credit #499's hidden-stack merge as + protected delivery. +6. Coordinate the ADR 0001 history incident with security/privacy owners. Keep + current `main` and #507 clean, never reproduce the private identifier, and + do not force-push or delete public-history evidence ad hoc. +7. After ContextualWisdomLab/.github#1259 is on protected `.github` main, the + minute-4 caller owns the GitHub Actions heartbeat. Close superseded baseline + PRs (#368, #440–#450, #455, #463, #479) once #426 is on + `main`; #368 and #479 also carry already-covered login changes. +8. #494 is already merged only into #426 as `1ff0cd13`; settle #426's exact-head + checks and independent approvals so that four-file optional-extra diff can + reach protected `main` without another add/revert oscillation. +9. Merge smallest shared-gate repairs next (#429, #428, #393, #436, #439) + when independently approved. +10. Advance user-visible gaps in leverage order: Event Lineage evidence (#387 / + #274), Naruon calendar (#355 / #336), SKOS aliases (#480 / #482), ontology + explorer (#349 / #341), Ask Agent (#415–#422 / #358–#363). +11. Keep psychometric tests as true-parameter recovery (RMSE), never fixture + tautologies. +12. Run frontend lint/test/build/Storybook, backend tests, and authenticated + browser/accessibility checks on the exact candidate release head. +13. Fix only evidence-backed failures and repeat the protected merge gate. + +## 11. Spec pointers (derive, do not fork) + +- Product/architecture: `ARCHITECTURE.md`, `AGENTS.md`, `CLAUDE.md` +- Research grounding: ADR 0084, `docs/lineage-bi-research-notes.md` +- Demo identity: ADR 0001 +- Figma boundary: ADR 0002 (File ID `1Su3lDRmiZdcUs47t1QwIX`) +- Orchestrator / paper-grounded models: ADR 0015, ADR 0076 (Fugu, TRINITY, Conductor) +- Ontology / PROV-O / SKOS: ADR 0004, ADR 0011, issue #372 +- Analysis runs / TEPP: ADR 0013–0023, issue #79 / #277 +- Calendar / Naruon: issues #336 / #338, PR #355 +- Ask Agent: issues #269–#272, #358–#363 + +Citations in doctoring and ADRs use APA 7th. Do not invent a heuristic where +the papers leave the decision undecided. diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 28c59bd48..9a38d2837 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -8,7 +8,10 @@ buyer-facing control you can click before changing product CSS. | `Evidence/CitationChip` | Click a cited title to open that source post. | `--color-chip-border`, `--radius-chip`, `CitationChip` | | `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` | | `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` | +| `Admin/AdminPanel` | Change the tenant brand name, then verify the saved or failed state before leaving settings. | `--surface`, `--border`, `--space-panel-block`, `AdminPanel` | +| `Lineage/LineageDag` | Open the current branch node; compare empty, grouped/forked, ungrouped, and long-title states before changing graph CSS. | `--surface`, `--border`, `LineageDag` | | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | +| `Reports/LeftoverPairList` | Read residual R, observed Y, expected E, map rank, and distance after IRT main effects, then open the named post. | `--color-chip-border`, `LeftoverPairList` | Repeated web objects must use `frontend/src/styles/tokens.css` and a module under `frontend/src/components/`. Do not add a second Node package manager; diff --git a/frontend/package.json b/frontend/package.json index e2e996bbe..938ac6bdd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.12.6", + "version": "2.12.18", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.css b/frontend/src/App.css index c72aab078..b88ee953a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -350,6 +350,25 @@ white-space: pre-wrap; } +.post-body-text sup, +.post-body-text sub, +.post-body-table sup, +.post-body-table sub { + font-size: 0.75em; + line-height: 0; + font-weight: 600; +} + +.post-body-text sup, +.post-body-table sup { + vertical-align: super; +} + +.post-body-text sub, +.post-body-table sub { + vertical-align: sub; +} + .post-embedded-image { margin: 0; padding: var(--post-image-padding); @@ -706,6 +725,27 @@ gap: 0.5rem; } +.ranking-hit { + flex-direction: column; + align-items: stretch; + justify-content: flex-start; +} + +.ranking-channel-evidence-copy { + margin: 0 0 0.75rem; +} + +.ranking-channel-evidence { + list-style: none; + margin: 0; + padding: 0 0 0 0.5rem; + font-size: 0.85rem; +} + +.ranking-channel-evidence li { + margin: 0.15rem 0; +} + .ticket-title { flex: 1; } diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..9488a014a 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import App from "./App"; import { setLocale } from "./i18n"; +import { OIDC_RETURN_URL_STORAGE_KEY } from "./oidcReturnUrl"; const signinRedirect = vi.fn(); const signoutRedirect = vi.fn(); @@ -28,11 +29,14 @@ beforeEach(() => { afterEach(() => { vi.unstubAllGlobals(); + window.sessionStorage.clear(); + window.localStorage.clear(); }); describe("App, unauthenticated", () => { it("shows a login button that starts the real OIDC redirect", async () => { render(); + expect(screen.queryByRole("heading", { name: /admin settings/i })).toBeNull(); const button = screen.getByRole("button", { name: /log in/i }); await userEvent.click(button); expect(signinRedirect).toHaveBeenCalledTimes(1); @@ -41,6 +45,10 @@ describe("App, unauthenticated", () => { state: expect.objectContaining({ returnUrl: expect.stringMatching(/^\//) }), }), ); + // Persisted as a fallback in case the OIDC state round-trip is dropped + // (see oidcReturnUrl.ts's restoreOidcReturnUrl, consumed in main.tsx). + expect(window.sessionStorage.getItem(OIDC_RETURN_URL_STORAGE_KEY)).toMatch(/^\//); + expect(window.localStorage.getItem(OIDC_RETURN_URL_STORAGE_KEY)).toMatch(/^\//); }); }); @@ -70,6 +78,14 @@ describe("App, authenticated", () => { post_id: string; post_title: string; fused_rank: number; + channel_evidence?: { + signal_code: string; + signal_label: string; + channel_rank: number; + weight: number; + contribution: number; + rank: number; + }[]; }[]; }; chatUnavailable?: boolean; @@ -932,6 +948,9 @@ describe("App, authenticated", () => { criterion_code: "sales_lead_specificity", leftover_distance: 0.12, leftover_residual: 0.4, + observed_response: 2.4, + expected_response: 2.0, + leftover_map_rank: 1, }, { pair_kind: "farthest", @@ -940,6 +959,9 @@ describe("App, authenticated", () => { criterion_code: "general_sentiment_negative", leftover_distance: 1.84, leftover_residual: -1.1, + observed_response: 0.9, + expected_response: 2.0, + leftover_map_rank: 1, }, ], members: [ @@ -2578,11 +2600,47 @@ describe("App, authenticated", () => { post_id: "post-1", post_title: "Public post", fused_rank: 1, + channel_evidence: [ + { + signal_code: "lexical", + signal_label: "Title overlap", + channel_rank: 2, + weight: 0.75, + contribution: 0.75 / 62, + rank: 1, + }, + { + signal_code: "temporal", + signal_label: "Newest first", + channel_rank: 2, + weight: 0.25, + contribution: 0.25 / 62, + rank: 2, + }, + ], }, { post_id: "post-2", post_title: "Pricing renegotiation: revised quote sent", fused_rank: 2, + channel_evidence: [ + { + signal_code: "lexical", + signal_label: "Title overlap", + channel_rank: 1, + weight: 0.75, + contribution: 0.75 / 61, + rank: 1, + }, + { + signal_code: "temporal", + signal_label: "Newest first", + channel_rank: 1, + weight: 0.25, + contribution: 0.25 / 61, + rank: 2, + }, + ], }, ], }, @@ -2595,6 +2653,17 @@ describe("App, authenticated", () => { expect(rankingButton).toHaveTextContent("Public post"); expect(rankingButton).toHaveTextContent("Rankings · rankweave"); expect(rankingButton).toHaveTextContent("rank 1"); + expect( + screen.getByText( + "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.", + ), + ).toBeInTheDocument(); + expect( + screen.getByRole("list", { name: "Ranking evidence for Public post" }), + ).toHaveTextContent("Title overlap rank 2, contribution 0.012097"); + expect( + screen.getByRole("list", { name: "Ranking evidence for Public post" }), + ).toHaveTextContent("Newest first rank 2, contribution 0.004032"); expect(screen.queryByRole("button", { name: /open ranking: private parent/i })).not.toBeInTheDocument(); await userEvent.click(rankingButton); @@ -3357,13 +3426,19 @@ describe("App, authenticated", () => { }); expect(closestPair).toHaveTextContent("Closest leftover: Public post · sales-lead"); expect(closestPair).toHaveTextContent( - "Open this post to read the criterion it sat closest to after main effects.", + "Read leftover map rank 1, observed Y 2.40, and expected E 2.00 after IRT main effects, then open this post.", ); + expect(closestPair).toHaveTextContent("R +0.40"); + expect(closestPair).toHaveTextContent("Y 2.40 · E 2.00"); + expect(closestPair).toHaveTextContent("rank 1"); expect(closestPair).toHaveTextContent("d 0.12"); expect(farthestPair).toHaveTextContent("Farthest leftover: Specification revision requested · negative"); expect(farthestPair).toHaveTextContent( - "Open this post to read the criterion it sat farthest from after main effects.", + "Read leftover map rank 1, observed Y 0.90, and expected E 2.00 after IRT main effects, then open this post.", ); + expect(farthestPair).toHaveTextContent("R −1.10"); + expect(farthestPair).toHaveTextContent("Y 0.90 · E 2.00"); + expect(farthestPair).toHaveTextContent("rank 1"); expect(farthestPair).toHaveTextContent("d 1.84"); const memberButton = screen.getByRole("button", { name: /open report post: public post/i }); expect(closestPair.compareDocumentPosition(memberButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..3053837d5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { AdminPanel } from "./components/AdminPanel"; +import { LeftoverPairList } from "./components/LeftoverPairList"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useAuth } from "react-oidc-context"; @@ -3057,6 +3058,10 @@ function AnalysisRunsPanel({ ); } +function formatRankingContribution(value: number): string { + return value.toFixed(6); +} + function RankingsPanel({ accessToken, onSelectPost, @@ -3075,9 +3080,9 @@ function RankingsPanel({ }, [accessToken]); return ( -
+
-

Rankings

+

{t("Rankings")}

{ranking && ( {ranking.status === "accepted" @@ -3087,30 +3092,53 @@ function RankingsPanel({ )}
{error &&

{error}

} - {ranking === null && !error &&

Loading rankings...

} + {ranking === null && !error &&

{t("Loading rankings...")}

} {ranking && ranking.status === "unavailable" && ( -

Rankings · RankWeave not available

+

{t("Rankings · RankWeave not available")}

)} {ranking && ranking.status === "accepted" && ranking.rankings.length === 0 && ( -

No fused rankings from RankWeave.

+

{t("No fused rankings from RankWeave.")}

)} {ranking && ranking.rankings.length > 0 && ( -
    - {ranking.rankings.map((hit) => ( -
  • - -
  • - ))} -
- )} + <> +

+ {t( + "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.", + )} +

+
    + {ranking.rankings.map((hit) => ( +
  • + + {(hit.channel_evidence ?? []).length > 0 ? ( +
      + {(hit.channel_evidence ?? []).map((item) => ( +
    • + {tf("{label} rank {rank}, contribution {contribution}", { + label: t(item.signal_label), + rank: String(item.channel_rank), + contribution: formatRankingContribution(item.contribution), + })} +
    • + ))} +
    + ) : null} +
  • + ))} +
+ + )}
); } @@ -3390,35 +3418,11 @@ function ReportsPanel({ )} {report.leftover_pairs && report.leftover_pairs.length > 0 && ( -
    - {report.leftover_pairs.map((pair) => { - const kindLabel = - pair.pair_kind === "farthest" ? "Farthest leftover" : "Closest leftover"; - const nextAction = - pair.pair_kind === "farthest" - ? "Open this post to read the criterion it sat farthest from after main effects." - : "Open this post to read the criterion it sat closest to after main effects."; - const criterion = criterionShortLabel(pair.criterion_code); - return ( -
  • - -
  • - ); - })} -
+ onSelectPost(postId)} + /> )} {report.members.length > 0 && (
    @@ -4610,7 +4614,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
    - {destination === "admin" ? : null}