Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
60 changes: 60 additions & 0 deletions docs/operability/http-concurrency-evidence.md
Original file line number Diff line number Diff line change
@@ -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 <measured-concurrency> --duration <observation-window> \
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.
7 changes: 7 additions & 0 deletions docs/product-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
72 changes: 72 additions & 0 deletions scripts/k6_http_e2e.js
Original file line number Diff line number Diff line change
@@ -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") };
Comment thread
seonghobae marked this conversation as resolved.
}

export default function (data) {
const params = { headers: { Authorization: `Bearer ${data.token}` } };
Comment thread
seonghobae marked this conversation as resolved.
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 });
}
Loading