docs/MVP1_RELATIONAL_PIPELINE.md and workers/run_banking_relational_e2e.py.
Pipeline stages
Each issue moves through the following stages, from raw text to a validated, immutable report and workbook: The LLM generates all eight severity factors for each issue in one structured batch response. It does not generate issue type, product area, cost estimates, time estimates, or corrective actions for this strict profile — those enriched attributes exist elsewhere in the research surface but are never used for cluster membership here.Data and transfer boundary
Only de-identified fields ever leave the process, and different stages see progressively less:
Owner, customer/account identifiers, status, product area, and raw tags are excluded from every external call. The
OPENAI_API_KEY used for these calls is loaded from the process environment (or an ignored local env file); it is never written to reports, workbooks, logs, or PostgreSQL payloads.
The explicit transfer approval mechanism
A live run requires two things together — a boolean flag is not sufficient on its own:--approve-openai-llmon the command line.--openai-authorization "<exact statement>"— a literal, dataset-specific string the runner compares byte-for-byte against a constant baked into the script. For the banking dataset, that constant is defined directly inworkers/run_banking_relational_e2e.py:
_async_main in that same script asserts both conditions before doing anything else — args.approve_openai_llm must be truthy, and args.openai_authorization must equal BANKING_OPENAI_AUTHORIZATION exactly. Passing the flag without the exact string (or a paraphrase of it) fails fast before any network call, migration, or database write. The HomeCare entrypoint (workers/run_homecare_relational_e2e.py) enforces the same two-part mechanism with its own dataset-specific authorization sentence.
The runner also asserts its own client configuration before running, so a silent drift in batch size or model can’t slip through unnoticed: the embedding provider must resolve to text-embedding-3-small; the severity client must use batch_size=8 and max_concurrency=8; the cluster-label and fishbone clients must both use max_concurrency=8.
Content-addressed reuse in PostgreSQL
Alembic migrations20260711_0002 through 20260714_0007 (migrations/versions/) create a normalized relational runtime purpose-built for this pipeline:
- Pipeline runs, steps, and append-only events
- Leased LLM batches, attempts, and batch items
- One severity result plus exactly eight factor rows per issue
- pgvector metadata and vectors, keyed by exact input hash
- Two-tier clusters, with one membership per issue per tier
- Content-addressed OpenAI cluster-label results and their per-run assignments
- Content-addressed issue-route results, canonical leaf allocations, and per-run assignments
- Content-addressed path-synthesis results, cause groups, exact evidence partitions, and per-run/variant assignments
- Immutable report and artifact publications, with hashes and sizes
packages/causegraph/src/causegraph/storage/mvp1_postgres.py (Mvp1PostgresRepository) is the transactional repository behind all of this; packages/causegraph/src/causegraph/providers/mvp1_llm.py, mvp1_cluster_labels.py, and mvp1_fishbone.py are the batched OpenAI clients for severity, cluster labeling, and issue routing/path synthesis respectively.
Reviewed Excel workbook contract
The pipeline’s output artifact is a reviewed nine-sheet workbook, built byworkers/artifacts/build_mvp1_clustering_workbook.mjs (a Node script that imports the bundled @oai/artifact-tool package — see Research modes for the local Node-path setup this requires):
OverviewTier 1 ClustersTier 2 ClustersIssue AssignmentsFishbone AllocationsFishbone PathsCause & RemediationSeverity MatrixLLM Audit
Dataset entrypoints
Both entrypoints share the same underlying runner logic (workers/run_homecare_relational.py’s RelationalHomeCareRunner, despite the module’s name — it’s the dataset-neutral relational execution engine both scripts drive) and apply Alembic before executing.
- Banking (reproducible from a fresh clone)
- HomeCare (requires a local file)
The banking source defaults to the tracked synthetic snapshot at The same loader also accepts the original workbook via
docs/issue-set-2-records.json — 250 records, no external file required:--source, but a fresh clone does not need it.The 250-record file’s own internal
dataset field is cause_loop_issue_set_2 — the same identifier the interactive /mvp1 product routes use as their dataset_id default (see docs/MVP1_API_CONTRACT.md). The relational banking runner, however, uses its own Postgres content-addressing identifier, DATASET_ID = "banking_issue_set_250", and its own tenant scope (TENANT_ID = "demo"). These are two different identifiers for two different consumers of the same 250 rows — don’t assume one implies the other when cross-referencing the interactive service and this relational pipeline.Output layout
Output defaults to the repository-relativeoutputs/mvp1/ directory. It is untracked by convention — nothing in the codebase creates or commits files there — but it is not covered by .gitignore, so nothing structurally prevents a run’s output from being accidentally git added: