diff --git a/Makefile b/Makefile index 6468b8857..62e1b3198 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: up down logs smoke seed ps +.PHONY: up down logs smoke seed ps load-http # Keep provider credentials outside the repository. Compose interpolation must # read the same home env file as the orchestrator container's env_file. @@ -29,3 +29,10 @@ smoke: seed: @test -n "$${KEYCLOAK_ADMIN_PASSWORD:-}" || { echo "KEYCLOAK_ADMIN_PASSWORD is required" >&2; exit 1; }; \ uv run --locked python scripts/seed_demo_data.py + +# Authenticated Compose measurement with no invented pass/fail threshold. +# The operator must supply a representative concurrency and observation window. +load-http: + @test -n "$${LINEAGEWEAVE_VUS:-}" || { echo "LINEAGEWEAVE_VUS is required" >&2; exit 1; } + @test -n "$${LINEAGEWEAVE_DURATION:-}" || { echo "LINEAGEWEAVE_DURATION is required" >&2; exit 1; } + k6 run --vus "$${LINEAGEWEAVE_VUS}" --duration "$${LINEAGEWEAVE_DURATION}" scripts/k6_http_e2e.js diff --git a/docs/operability/http-concurrency-evidence.md b/docs/operability/http-concurrency-evidence.md new file mode 100644 index 000000000..9e846f7ba --- /dev/null +++ b/docs/operability/http-concurrency-evidence.md @@ -0,0 +1,60 @@ +# Authenticated HTTP concurrency evidence + +LineageWeave provides `scripts/k6_http_e2e.js` to measure the real Compose +HTTP boundary while a synthetic Global Ask job is queued or running. It logs +in through the seeded Keycloak realm, submits one non-identifying question to +`POST /api/ask`, then drives concurrent authenticated requests to posts, +Event Lineage, and the Ask-status projection. + +This implements the measurement side of ADR 0204's resource-release decision: +provider work is asynchronous, so ordinary readers should remain observable +while the worker runs. The harness does not prove why a slow observation is +slow. Correlate a run with backend/PostgreSQL/Valkey/orchestrator telemetry and +`pg_stat_activity` before naming a bottleneck. + +## Run + +Start and seed the synthetic stack, then supply the concurrency and observation +window that match the environment under review: + +```bash +make up +KEYCLOAK_ADMIN_PASSWORD=admin_dev_only make seed +k6 run --vus --duration \ + scripts/k6_http_e2e.js +``` + +Pass `BACKEND_URL`, `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, +`K6_USERNAME`, and `K6_PASSWORD` with k6's `-e NAME=value` option to point the +harness at another authorized synthetic environment. Never run repository +performance evidence against identifying production records. + +## Interpret the output + +k6 reports observed request counts, failure rate, and duration distributions. +The custom metrics separate: + +- `lineageweave_ask_enqueue_duration`: time to persist and acknowledge the job; +- `lineageweave_read_duration{endpoint:posts|lineage}`: ordinary reader paths; +- `lineageweave_ask_poll_duration`: owner-scoped status polling. + +There are deliberately no pass/fail thresholds. A latency or concurrency SLO +requires a named deployment, representative workload, capacity evidence, and +product approval; CI runner capacity is not that evidence. Store the raw k6 +output with the environment's CPU, memory, database pool, worker concurrency, +dataset counts, exact Git SHA, and observation time. Do not promote one laptop +or shared-runner result to a product guarantee. + +Figma and screenshot review do not apply: this is a non-UI HTTP load harness. + +## Current-main verification record + +On 2026-08-25, a worktree based on protected-main commit `48f013a2` passed +`k6 inspect` for this script. A fresh Compose project did not reach an +application-ready state: the build was stopped +after backend dependency synchronization alone had reached 225.5 seconds and +was still incomplete; other observed BuildKit metadata, copy, and image-export +steps ranged up to 292.3 seconds. No containers were running afterward, so no +HTTP latency distribution was produced and no application bottleneck is +claimed. This is local build-environment evidence only. Re-run the command +above on an application-ready stack to obtain the product measurement. diff --git a/docs/product-requirements.md b/docs/product-requirements.md index 8c0719da8..0bf87f9e7 100644 --- a/docs/product-requirements.md +++ b/docs/product-requirements.md @@ -121,6 +121,12 @@ vendor selector, duplicate identity store, or psychometric substitute appears. than destructive mask-in-place behavior. - PostgreSQL objects use normalized multiword names, idempotent replayable migrations, short transactions, and hot-partition-aware access paths. +- Provider-bound work that can outlive an interactive request uses a durable + asynchronous job boundary and does not retain a pooled database transaction + during provider execution. Authenticated concurrent HTTP behavior is + measured end to end against synthetic Compose data; latency and concurrency + become release thresholds only after a named deployment and representative + workload establish an approved capacity/SLO contract. - Public APIs have bounded inputs, stable typed responses, and provenance- preserving failure states. - WCAG 2.2 AA, keyboard/touch parity, responsive layouts, reduced motion, @@ -158,6 +164,7 @@ A release claim requires one exact protected-main head that proves: ## 7. Traceability - Product/data boundary: ADR 0001, ADR 0089. +- Asynchronous delivery and database-pool isolation: ADR 0204. - Knowledge Graph, ontology, and provenance: ADR 0004, ADR 0011, ADR 0065, ADR 0184, ADR 0207. - Semantic units and retrieval: ADR 0047, ADR 0062, ADR 0102. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index dcf693837..9c56016b1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -368,6 +368,7 @@ this file per ยง3.5 of the prior snapshot). | Shared frontend gate | The ADR 0109 login repair is on protected `main`; eight older branches carried the defect and received the same verified repair this loop (#521โ€“#560) | Keep every future branch cut from post-repair bases; re-verify with frontend lint/test/build before push | | 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 | +| Concurrent web responsiveness | ADR 0204 releases pooled transactions during provider work, and the synthetic Compose boundary now has an authenticated k6 E2E harness for Ask enqueue, concurrent reads, and job polling. No environment-specific measurement is a protected-main product guarantee | Run `make load-http` with declared environment concurrency/window; retain raw distributions and resource configuration, then diagnose bottlenecks with backend, PostgreSQL, Valkey, and orchestrator evidence before setting any approved SLO | | 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 | diff --git a/scripts/k6_http_e2e.js b/scripts/k6_http_e2e.js new file mode 100644 index 000000000..d9b79bf25 --- /dev/null +++ b/scripts/k6_http_e2e.js @@ -0,0 +1,72 @@ +/** + * Measure authenticated HTTP responsiveness while one synthetic Ask job runs. + * + * This is an observation harness, not a release gate: it defines no latency, + * error-rate, or throughput threshold. The operator supplies concurrency and + * duration for the environment being measured. + */ + +import http from "k6/http"; +import { check, fail } from "k6"; +import { Trend } from "k6/metrics"; + +const backendUrl = (__ENV.BACKEND_URL || "http://localhost:18420").replace(/\/$/, ""); +const keycloakUrl = (__ENV.KEYCLOAK_URL || "http://localhost:18080").replace(/\/$/, ""); +const realm = __ENV.KEYCLOAK_REALM || "lineageweave-demo"; +const clientId = __ENV.KEYCLOAK_CLIENT_ID || "lineageweave-frontend"; +const username = __ENV.K6_USERNAME || "demo.analyst"; +const password = __ENV.K6_PASSWORD || "lineageweave-demo-only"; + +const askEnqueueDuration = new Trend("lineageweave_ask_enqueue_duration", true); +const readDuration = new Trend("lineageweave_read_duration", true); +const askPollDuration = new Trend("lineageweave_ask_poll_duration", true); + +export function setup() { + const tokenResponse = http.post( + `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`, + { + grant_type: "password", + client_id: clientId, + username, + password, + }, + { tags: { endpoint: "oidc_token" } }, + ); + if (tokenResponse.status !== 200) { + fail(`synthetic OIDC login failed with HTTP ${tokenResponse.status}`); + } + + const token = tokenResponse.json("access_token"); + const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; + const submitted = http.post( + `${backendUrl}/api/ask`, + JSON.stringify({ question: "Summarize the synthetic demo lineage evidence." }), + { headers, tags: { endpoint: "ask_enqueue" } }, + ); + askEnqueueDuration.add(submitted.timings.duration); + if (submitted.status !== 202) { + fail(`synthetic Ask enqueue failed with HTTP ${submitted.status}: ${submitted.body}`); + } + return { token, askJobId: submitted.json("ask_job_id") }; +} + +export default function (data) { + const params = { headers: { Authorization: `Bearer ${data.token}` } }; + const responses = http.batch([ + ["GET", `${backendUrl}/api/posts`, null, { ...params, tags: { endpoint: "posts" } }], + ["GET", `${backendUrl}/api/lineage`, null, { ...params, tags: { endpoint: "lineage" } }], + [ + "GET", + `${backendUrl}/api/ask/jobs/${data.askJobId}`, + null, + { ...params, tags: { endpoint: "ask_poll" } }, + ], + ]); + + readDuration.add(responses[0].timings.duration, { endpoint: "posts" }); + readDuration.add(responses[1].timings.duration, { endpoint: "lineage" }); + askPollDuration.add(responses[2].timings.duration); + check(responses[0], { "posts read succeeds": (response) => response.status === 200 }); + check(responses[1], { "lineage read succeeds": (response) => response.status === 200 }); + check(responses[2], { "Ask poll succeeds": (response) => response.status === 200 }); +}