/fix report not working

#18
by rhbt6767 - opened
DEV_PLAN.md CHANGED
@@ -215,7 +215,7 @@ Status legend: ⬜ not started Β· πŸ”„ in progress Β· βœ… done Β· β›” blocked Β·
215
  | 19 | Decide report author display-name source (`users.fullname` vs Go-passed name) | Sofhia | βœ… | Done 2026-06-25. `AnalysisReport.user_name`; `generator` renders `user_name or user_id`; `api/v1/report.py::_resolve_user_name` reads `users.fullname` never-throw (fallback `user_id`). Decided: resolve in Python (unblocked); swap to Go-passed name later if preferred |
216
  | 20 | **Help handoff:** update `handlers/help.py` + `help.md` β€” drop the `problem_validated` tier + `define_problem_statement` action (the skill it points at is gone as of #1) | Sofhia | βœ… | Done 2026-06-25. `help.py`: actions = `ask_analysis_question` (always) + `generate_report` (if ready); renders objective/business_questions (getattr-tolerant). `help.md` v1β†’v2: 3 tiers, no `/problem_statement`, `/generate report`β†’`/report`. Local test_help updated β†’ 11 pass |
217
  | 21 | Rename `analysis_records` β†’ **`report_inputs`** (table, ORM `ReportInputRow`, store `*ReportInputStore`) | Rifqi | βœ… | Done 2026-06-26. `sed` rename across 9 files; Pydantic `AnalysisRecord` kept; columns stay String (pure rename β€” uuid+FK is the #22 Harry schema). Name `report_inputs` (purpose; avoids Langfuse/`analyses_messages` clash). Write scope = one row per slow-path run. Suite **284 passed** |
218
- | 22 | Finalize `report_inputs` schema β†’ hand to Harry for the dedorch migration | Rifqi β†’ Harry | πŸ”„ | **DDL ready** (uuid `id`/`analysis_id` + FKβ†’`analyses(id)`; `user_id`/`plan_id` text; `data` jsonb = serialized `AnalysisRecord`, shape documented). dedorch has empty `analysis_records` β†’ rename. Resolves #16. **Action: send Harry the DDL + `data` shape** |
219
  | 23 | Report markdown formatting: tables, **bold**, *italic*, horizontal separators | Sofhia | βœ… | Done 2026-06-25. Added `---` separators between header + each section in `_render_markdown`. Tables (EDA) / bold (method labels) / italic (meta + citations) already emitted. Relaxed `report_summary.md` to allow inline `**bold**`/`*italic*` for emphasis (kept no-headings/no-bullets so it doesn't duplicate the section structure / Key Findings). Compile + ruff clean |
220
  | 24 | Clarify report input contract: records table (+ `last_report` for edit mode?) | Rifqi/Sofhia ↔ Harry | ⬜ new | Edit-mode input left open at the checkpoint |
221
  | 25 | Migrate Python chat path to Go `analyses_messages` (+ `analyses`) | Rifqi ↔ Harry | βœ… | Done 2026-07-02. Read path already on `analyses_messages` (commit `0066161`). This change makes Python **read-only**: removed the `save_messages` calls from `/api/v2/chat/stream` so **Go is the sole writer** β€” fixes the double-write both Go+Python were producing. `load_history` still reads `analyses_messages`. v1 `/chat/stream` is unwired so left untouched |
@@ -223,11 +223,14 @@ Status legend: ⬜ not started Β· πŸ”„ in progress Β· βœ… done Β· β›” blocked Β·
223
  | 27 | **Images (DEFERRED):** image table (id, analysis_id, msg/report ref, order) + originals in a bucket | β€” | ⏸️ | Maintenance-heavy; parked |
224
  | 28 | **UI research** (FE): new-analysis form, knowledge menu (user vs analysis level), report artifacts + version selector | Team | ⬜ new | No dedicated UI person; interview + old analysis UI removed |
225
  | 29 | **LLM env quad rename `__4o` β†’ `__54m`** + set the four `azureai__*__54m` secrets on the HF Space | Rifqi | πŸ”„ | Code done 2026-07-14 (pr/17): settings quad + 9 call sites + 2 eval runners renamed; deployment is `gpt-5.4-mini`. Suite **381 passed, 2 failed (both pre-existing), 7 skipped** β€” same as baseline; ruff clean on touched files; `import main` 0. **Hard rename β€” no `__4o` fallback**, so HF fails loudly on the first LLM call until the four `__54m` secrets are set. Root cause it fixes: HF silently ran GPT-4o while local ran 5.4-mini β†’ same question, same catalog, planner hallucinated catalog ids on HF only. **Action: set HF secrets, redeploy, re-run the failing question.** Local `.env` still carries the now-dead `__4o` keys β€” safe to delete |
 
 
 
226
 
227
  ## 5. Critical path & sequencing
228
 
229
- - **Critical path:** #22 (send Harry the `report_inputs` schema). HF deploy (#13) for the playground. (#4 βœ…, #21 βœ…; Harry's #3 no longer blocks us β€” Python is getattr-tolerant.)
230
- - **Parallelizable now:** #22 (handoff). (#4 βœ…, #11 βœ… done.)
231
  - **Harry-blocked / coordinated:** #3 (now πŸ”„, blocks #4), #7 (Go endpoint), #18 (Go state ownership), #24 (contract). **#25 = chat-path migration to `analyses_messages` β€” a cutover blocker.**
232
  - **Demo gate (playground, #13):** deploy the remove-`problem_statement` work to HF β€” slow path (#15 βœ…)
233
  and the report path are verified locally, and #16 is resolved (#22 hands Harry the schema). **Keep it
 
215
  | 19 | Decide report author display-name source (`users.fullname` vs Go-passed name) | Sofhia | βœ… | Done 2026-06-25. `AnalysisReport.user_name`; `generator` renders `user_name or user_id`; `api/v1/report.py::_resolve_user_name` reads `users.fullname` never-throw (fallback `user_id`). Decided: resolve in Python (unblocked); swap to Go-passed name later if preferred |
216
  | 20 | **Help handoff:** update `handlers/help.py` + `help.md` β€” drop the `problem_validated` tier + `define_problem_statement` action (the skill it points at is gone as of #1) | Sofhia | βœ… | Done 2026-06-25. `help.py`: actions = `ask_analysis_question` (always) + `generate_report` (if ready); renders objective/business_questions (getattr-tolerant). `help.md` v1β†’v2: 3 tiers, no `/problem_statement`, `/generate report`β†’`/report`. Local test_help updated β†’ 11 pass |
217
  | 21 | Rename `analysis_records` β†’ **`report_inputs`** (table, ORM `ReportInputRow`, store `*ReportInputStore`) | Rifqi | βœ… | Done 2026-06-26. `sed` rename across 9 files; Pydantic `AnalysisRecord` kept; columns stay String (pure rename β€” uuid+FK is the #22 Harry schema). Name `report_inputs` (purpose; avoids Langfuse/`analyses_messages` clash). Write scope = one row per slow-path run. Suite **284 passed** |
218
+ | 22 | Finalize `report_inputs` schema β†’ hand to Harry for the dedorch migration | Rifqi β†’ Harry | βœ… | **DDL ready** (uuid `id`/`analysis_id` + FKβ†’`analyses(id)`; `user_id`/`plan_id` text; `data` jsonb = serialized `AnalysisRecord`, shape documented). dedorch has empty `analysis_records` β†’ rename. Resolves #16. ~~**Action: send Harry the DDL + `data` shape**~~ **RESOLVED 2026-07-22:** Harry's `0001_create_core_schema.sql` now carries `report_inputs` with exactly this shape β€” FK to `analyses(id)` included. Python's `ReportInputRow` diffed against the live Neon table: **column-for-column identical, no drift.** The two remaining un-migrated Python tables (`message_traceability`, `message_charts`) split out as β†’ #32 |
219
  | 23 | Report markdown formatting: tables, **bold**, *italic*, horizontal separators | Sofhia | βœ… | Done 2026-06-25. Added `---` separators between header + each section in `_render_markdown`. Tables (EDA) / bold (method labels) / italic (meta + citations) already emitted. Relaxed `report_summary.md` to allow inline `**bold**`/`*italic*` for emphasis (kept no-headings/no-bullets so it doesn't duplicate the section structure / Key Findings). Compile + ruff clean |
220
  | 24 | Clarify report input contract: records table (+ `last_report` for edit mode?) | Rifqi/Sofhia ↔ Harry | ⬜ new | Edit-mode input left open at the checkpoint |
221
  | 25 | Migrate Python chat path to Go `analyses_messages` (+ `analyses`) | Rifqi ↔ Harry | βœ… | Done 2026-07-02. Read path already on `analyses_messages` (commit `0066161`). This change makes Python **read-only**: removed the `save_messages` calls from `/api/v2/chat/stream` so **Go is the sole writer** β€” fixes the double-write both Go+Python were producing. `load_history` still reads `analyses_messages`. v1 `/chat/stream` is unwired so left untouched |
 
223
  | 27 | **Images (DEFERRED):** image table (id, analysis_id, msg/report ref, order) + originals in a bucket | β€” | ⏸️ | Maintenance-heavy; parked |
224
  | 28 | **UI research** (FE): new-analysis form, knowledge menu (user vs analysis level), report artifacts + version selector | Team | ⬜ new | No dedicated UI person; interview + old analysis UI removed |
225
  | 29 | **LLM env quad rename `__4o` β†’ `__54m`** + set the four `azureai__*__54m` secrets on the HF Space | Rifqi | πŸ”„ | Code done 2026-07-14 (pr/17): settings quad + 9 call sites + 2 eval runners renamed; deployment is `gpt-5.4-mini`. Suite **381 passed, 2 failed (both pre-existing), 7 skipped** β€” same as baseline; ruff clean on touched files; `import main` 0. **Hard rename β€” no `__4o` fallback**, so HF fails loudly on the first LLM call until the four `__54m` secrets are set. Root cause it fixes: HF silently ran GPT-4o while local ran 5.4-mini β†’ same question, same catalog, planner hallucinated catalog ids on HF only. **Action: set HF secrets, redeploy, re-run the failing question.** Local `.env` still carries the now-dead `__4o` keys β€” safe to delete |
226
+ | 30 | **Neon `reports.user_id` NOT NULL** β€” reconcile the ORM + `ReportStore` write | Rifqi | βœ… | Done 2026-07-22 (pr/18, commit `13f74e1`). `ReportStore.save` never wrote `user_id`; on Neon that column is `text NOT NULL` β†’ **every `POST /api/v1/tools/report` 500'd** with `NotNullViolationError`. Fix: `models.py` adds `user_id = Column(Text)` (**nullable** on purpose β€” Python matches the loosest deployment shape, Β§7D getattr-tolerance) + `store.py` writes `report.user_id`, which the endpoint already required (`user_id: str = Query(...)`) and threaded via `generator.generate`. Live-tested by Rifqi β†’ **201**. Suite 381 passed / 2 pre-existing failures / 7 skipped; `import main` 0; ruff unchanged (3 pre-existing E501 in `models.py`, same count on HEAD). **Correction 2026-07-22 (after pulling Go `dd37b38`):** the commit message and the first version of this row said the column was *not* in Go migrations `0001–0004`. **That was wrong** β€” `0001_create_core_schema.sql` has declared `reports.user_id TEXT NOT NULL` since the schema was written. Real root cause β†’ #31 |
227
+ | 31 | **Go migration set is not convergent** β€” fresh vs migrated dedorch DBs get different NOT NULL constraints | Rifqi β†’ Harry | ⬜ new | Root cause of #30, verified in the Go source 2026-07-22. `0001_create_core_schema.sql` creates `reports.user_id TEXT NOT NULL` and `analyses_messages.user_id TEXT NOT NULL`; `0002_cleanup_legacy_schema.sql` (L95, L92) and `0004_replace_chat_with_analysis_scope.sql` (L61, L57) retrofit the same two columns onto pre-existing DBs as **nullable** `ALTER TABLE … ADD COLUMN IF NOT EXISTS user_id TEXT`. Because `CREATE TABLE IF NOT EXISTS` no-ops on an existing table, **the same migration set yields two different schemas** β€” the old dedorch DB got nullable (hiding Python's missing write for months), Neon got NOT NULL. **Ask Harry:** make the retrofit converge (backfill + `ALTER COLUMN … SET NOT NULL`, or add the constraint in a new migration) so every instance matches `0001`. Until then, `information_schema` on the target instance β€” not the migration files β€” is the only reliable schema source (REPO_STATUS Β§13). **Action: Rifqi raises with Harry** |
228
+ | 32 | **`message_traceability` + `message_charts` are in no Go migration** β€” hand the DDL to Harry | Rifqi β†’ Harry | ⬜ new | Found by the 2026-07-22 drift scan. Both tables exist on Neon **only because they were created by hand** (2026-07-06 / 2026-07-13); neither appears in `0001–0006`. Any newly provisioned dedorch instance will be missing them β†’ traceability flush and chart persist fail (both never-throw, so they degrade **silently** β€” no 500 like #30 to make it visible). DDL for `message_charts` is in `SPINE_V2_PLAN.md` Β§4.4. This is the surviving half of #22. **Action: Rifqi sends Harry both DDL blocks** |
229
 
230
  ## 5. Critical path & sequencing
231
 
232
+ - **Critical path:** ~~#22 (send Harry the `report_inputs` schema)~~ **βœ… resolved 2026-07-22** β€” now **#32** (`message_traceability` + `message_charts` DDL to Harry) and **#31** (non-convergent migration set). HF deploy (#13) for the playground. (#4 βœ…, #21 βœ…; Harry's #3 no longer blocks us β€” Python is getattr-tolerant.)
233
+ - **Parallelizable now:** #31 + #32 (both are Harry handoffs). (#4 βœ…, #11 βœ…, #22 βœ… done.)
234
  - **Harry-blocked / coordinated:** #3 (now πŸ”„, blocks #4), #7 (Go endpoint), #18 (Go state ownership), #24 (contract). **#25 = chat-path migration to `analyses_messages` β€” a cutover blocker.**
235
  - **Demo gate (playground, #13):** deploy the remove-`problem_statement` work to HF β€” slow path (#15 βœ…)
236
  and the report path are verified locally, and #16 is resolved (#22 hands Harry the schema). **Keep it
REPO_STATUS.md CHANGED
@@ -2,7 +2,7 @@
2
 
3
  **Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
4
  **Scope:** what the code does **right now** (branch `pr/4`, ticket KM-652). Describes current state only β€” no roadmap or to-dos.
5
- **Snapshot date:** 2026-06-25. **Data-layer reconcile 2026-07-01:** Β§8/Β§12 updated β€” dedorch cutover done, `data_catalog` model reconciled. **Query-path fix 2026-07-02:** Β§8/Β§13 β€” dedorch catalogs ship no FKs β†’ Python infers them (`fk_inference.py`); shared-Fernet-key gotcha documented. **Agent-quality fixes 2026-07-08 (pr/13):** from the scoped live-test review β€” the planner gains an explicit **infeasible** outcome (`TaskList.infeasible_reason` β†’ deterministic EN/ID data-gap reply via `refusals.data_gap_message`; no more force-mapping absent measures like `pa` AS "revenue"), the IR validator rejects bare selects under `group_by` (self-corrects via the planner retry), `analyze_trend` handles integer year/month columns (was collapsing every row into one 1970-01 bucket), planner few-shots add top-N (Example G) + infeasible (Example H), numeric catalog `sample_values` are base64-decoded at read (`catalog/sample_decode.py` β€” stopgap for Go's byte-marshaling; primary fix is Go-side), traceability no longer emits null source rows for failed retrievals, and `check_data` hides `-1` row counts. **Report v2 + analyze_merge planner support 2026-07-09 (pr/13):** Sofia's `analyze_merge` tool (8abf635, KM-703) is now planner-supported (`_validate_data_source` guards `data_right`, two-retrieveβ†’merge few-shot Example I, planner.md "Two measures per entity" rule); the report gains per-business-question answers (`bq_answers` β€” drafted by the SAME single LLM call, index-based record refs, deterministic fallback unchanged), "Attempted, Unresolved" + "Excluded Analyses" sections (failed runs are no longer silently dropped), evidence tables copied from `results_snapshot` (table-kind outputs, ≀3/record ≀10 rows ≀8 cols, `check_*` skipped), normalized caveat dedupe with caps (12/10), and single-language output via `detect_reply_language`; the report surface adds `GET /tools/report/{analysis_id}/records` (curation list), `GET …/readiness` (FE delta guard), and `exclude_record_ids` on POST β€” see API_CONTRACT_BE_PYTHON.md. **Report compaction 2026-07-09 (pr/13):** the rendered markdown drops the "Notes & Limitations", "Attempted, Unresolved", and "How This Was Analyzed" sections (team decision β€” compact report; render blocks commented out in `report/generator.py`, not deleted). The JSON body keeps `caveats`/`open_questions`/`unresolved`/`method_steps` and the curation/records endpoints are unchanged. **Traceability `data_used` layer 2026-07-13 (pr/15):** `GET /api/v1/traceability` gains a resolved, user-facing `data_used[]` block (one per `retrieve_data` call) β€” real source/table/column names, joins, plain-language filters, and result columns split into read-from-data vs `computed` (with `formula`, e.g. `total_revenue` = `SUM(line_total)`, so an alias is never shown as a real column). Ids are kept but **machine-only (FE must not render)**. Also adds `tool_calls[].summary`, and `sources[]` now carry `source_name` + every table touched. Deterministic catalog resolution (new `src/traceability/resolve.py`), no LLM, never-throw; catalog threaded to the scratchpad at the slow-path composition root. Additive/non-breaking; contract + `TRACEABILITY_FE_HANDOFF.md` updated. **Spine v2 W2+W1 2026-07-13 (pr/16):** Β§6/Β§7/Β§8/Β§9/Β§12 β€” `render_chart` tool lands (first of the `render_*` family: deterministic Plotly-JSON `dataeyond.chart.v1` envelope, hand-built, **no plotly dependency**; planner-selected only on an explicit chart ask, EN/ID) + Python-owned `message_charts` store + `GET /api/v1/charts` (FE fetches on `done`, same pattern as traceability; empty list = valid 200); planner gains a named **recipe table** + viz few-shots (Example J tail, Example K viz-infeasible) + validator Check 10 (`render_chart.data` must reference a table-producing task); and the slow path gains the **S1a quality checkpoint** (`slow_path/checkpoint.py`, 0 LLM, never-throw, between runner and assembler: CK1 all-failed β†’ deterministic honest-failure answer with **no** assembler call, CK2 empty retrieve + downstream, CK3 10k-cap truncation, CK4 single trend bucket, CK5 all-null column, CK6 chart-spec sanity; flags render as an "Execution assessment" block in the assembler input and every flag logs `repair_candidate` β€” the S1b evidence base). Design + handoff doc: `SPINE_V2_PLAN.md`. **LLM model + env rename 2026-07-14 (pr/17):** Β§2/Β§3/Β§9/Β§13 β€” the generation LLM is now **Azure GPT-5.4-mini** (deployment `gpt-5.4-mini`), not GPT-4o, and the settings quad is renamed `azureai__*__4o` β†’ **`azureai__*__54m`** across all 9 LLM call sites. **Hard rename, no `__4o` fallback** β€” an environment that still sets only `__4o` resolves to empty strings and fails on the first LLM call, by design: the silent-wrong-model drift is exactly how HF stayed on GPT-4o while local ran 5.4-mini (identical question, identical catalog, divergent planner output β€” HF hallucinated catalog ids, local planned correctly). Deploying requires all four `__54m` vars set in the HF Space secrets. **Cross-repo update 2026-06-29:** Β§2/Β§8/Β§11/Β§12 re-verified against
6
  the **Go source** (`Orchestrator-Agent-Service`), not its docs. The Go service has moved well past its
7
  own (uncommitted, stale) design docs: it now hosts the **dedorch SQL migrations** in-repo and a full
8
  **`/api/v1/analyses` + `/api/v1/skills`** REST surface. Go does **not** call Python yet β€” those skills
@@ -187,7 +187,7 @@ unless `SKIP_INIT_DB=true`.
187
  | `langchain_pg_embedding` | PGVector document chunks | Go ingestion | DocumentRetriever |
188
  | `report_inputs` *(was `analysis_records`)* | jsonb `AnalysisRecord`, one per slow-path run; **Python-owned** | slow path | ReportGenerator, report readiness |
189
  | `analyses` *(dedorch, plural)* | uuid `id`, `user_id`, `analysis_title`, `objective`, `business_questions` jsonb, `status` (active\|inactive), `data_bind`(+`data_bind_version`), `report_id`, `report_collection` β€” **defined by Go migrations**; `problem_statement`/`problem_validated`/`owner_id` already **dropped** there (`0003`/`0004`) | Go `/api/v1/analyses`; Python state store | gate (no-op), Help, report |
190
- | `reports` *(dedorch)* | uuid, `analysis_id`, `user_id`, `title` + markdown `content` + `version` (UNIQUE per analysis) | Go + Python ReportStore | report API |
191
  | `data_sources` *(dedorch, Go-owned)* | per-analysis binding table. **Python no longer reads or writes it** β€” bindings live in Go's `analyses.data_bind`, which Go materializes into the analysis-scope `data_catalog` row; Python scopes off that row. The table exists (Go migration) but Python is fully decoupled β€” do **not** drop it manually | Go migration | β€” (unused by Python) |
192
  | `analyses_messages` *(dedorch)* | the analysis chat room (`role ∈ user\|ai`); replaces deprecated `rooms`/`chat_messages` | Go `/analyses/{id}/messages` | Python chat path **not yet migrated here** (Β§12) |
193
  | `message_traceability` *(Python-owned)* | one jsonb `TraceabilityPayload` per assistant turn (PK `message_id`); flushed before `done` | chat pipeline (KM-691) | `GET /api/v1/traceability` |
@@ -298,7 +298,9 @@ validator's Check 10 forces its `data` to reference a table-producing task.
298
  ### Report β€” `src/agents/report/`
299
  `generator.py` reads records, deterministically assembles structured fields, 1 LLM call for the
300
  executive summary; `store.py` versions under an advisory lock and persists markdown to dedorch
301
- `reports`; `readiness.py` defines the **report floor** (β‰₯1 successful `analyze_*` **or**, since
 
 
302
  2026-07-14, `render_chart` β€” a chart-only session is substantive; the `problem_validated`
303
  precondition was dropped 2026-06-25) shared by the report API and the Help readiness signal so the
304
  two can't disagree. **2026-07-14:** the report embeds charts β€” `_collect_charts` copies
@@ -359,10 +361,16 @@ Python is consumer-only). State **re-verified against the Go source 2026-06-29**
359
  **dropped** `problem_statement`/`problem_validated`/`owner_id`. Migration `0004` renames the legacy
360
  `rooms`/`chat_messages`/`interview_*` tables to `zdeprecated_*`.
361
  - **`report_inputs`** (the slow-path structured output, formerly `analysis_records`) stays
362
- **Python-owned**; its finalized schema goes to Harry so the dedorch migration creates it post-cutover.
363
- Same pattern for **`message_traceability`** (created manually 2026-07-06) and **`message_charts`**
364
- (created manually 2026-07-13, DDL in `SPINE_V2_PLAN.md` Β§4.4; live e2e verified same day β€”
365
- Harry's migration handoff for both is still the open item).
 
 
 
 
 
 
366
  - **Connection-string cutover DONE (2026-07-01).** Python's `postgres_connstring` now points at
367
  **dedorch** and reads the Go-migrated tables directly. Every ORM model Python reads (`analyses`,
368
  `analyses_messages`, `data_catalog`) has been reconciled to its dedorch shape.
@@ -370,8 +378,11 @@ Python is consumer-only). State **re-verified against the Go source 2026-06-29**
370
  DDL (`ALTER TABLE rooms …`, index creation) fails on Go-owned tables
371
  (`InsufficientPrivilegeError: must be owner of table rooms`). Skipping is safe β€” Go migration `0001`
372
  already provides the `vector` extension + the langchain FTS index. Set `SKIP_INIT_DB=false` (.env or
373
- env) only for a local Python-owned DB. `report_inputs` is not in any Go migration yet (#22) β€” create
374
- it in dedorch before enabling the slow path, else report/slow-path writes fail (chat path unaffected).
 
 
 
375
 
376
  **⚠️ Integration gap (verified β€” the big one).** Go's `/api/v1/analyses` and `/api/v1/skills`
377
  (`help` / `report`) are **placeholders that return dummy data** β€” the `SendMessage` / `GenerateReport`
@@ -413,6 +424,32 @@ records-based report; floor: β‰₯1 `analyze_*` success). Wiring Go β†’ Python is
413
  degrades it into an honest "data not available" answer, so it masquerades as a data problem.
414
  Tell-apart: `BlobNotFound` + `location_ref` starting `object_storage://` β‡’ env gap; set
415
  `storage_provider=supabase_s3` + the five `supabase_s3_*` values (match Go's data plane).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  - **Never-throw seams** are pervasive (tool invoker, query service, executors, state/catalog reads,
417
  record persistence, report summary). Failures degrade into soft output rather than raising β€” good
418
  for UX, but they can mask real breakage (e.g. a missing analysis-scope catalog silently falling
 
2
 
3
  **Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
4
  **Scope:** what the code does **right now** (branch `pr/4`, ticket KM-652). Describes current state only β€” no roadmap or to-dos.
5
+ **Snapshot date:** 2026-06-25. **Data-layer reconcile 2026-07-01:** Β§8/Β§12 updated β€” dedorch cutover done, `data_catalog` model reconciled. **Query-path fix 2026-07-02:** Β§8/Β§13 β€” dedorch catalogs ship no FKs β†’ Python infers them (`fk_inference.py`); shared-Fernet-key gotcha documented. **Agent-quality fixes 2026-07-08 (pr/13):** from the scoped live-test review β€” the planner gains an explicit **infeasible** outcome (`TaskList.infeasible_reason` β†’ deterministic EN/ID data-gap reply via `refusals.data_gap_message`; no more force-mapping absent measures like `pa` AS "revenue"), the IR validator rejects bare selects under `group_by` (self-corrects via the planner retry), `analyze_trend` handles integer year/month columns (was collapsing every row into one 1970-01 bucket), planner few-shots add top-N (Example G) + infeasible (Example H), numeric catalog `sample_values` are base64-decoded at read (`catalog/sample_decode.py` β€” stopgap for Go's byte-marshaling; primary fix is Go-side), traceability no longer emits null source rows for failed retrievals, and `check_data` hides `-1` row counts. **Report v2 + analyze_merge planner support 2026-07-09 (pr/13):** Sofia's `analyze_merge` tool (8abf635, KM-703) is now planner-supported (`_validate_data_source` guards `data_right`, two-retrieveβ†’merge few-shot Example I, planner.md "Two measures per entity" rule); the report gains per-business-question answers (`bq_answers` β€” drafted by the SAME single LLM call, index-based record refs, deterministic fallback unchanged), "Attempted, Unresolved" + "Excluded Analyses" sections (failed runs are no longer silently dropped), evidence tables copied from `results_snapshot` (table-kind outputs, ≀3/record ≀10 rows ≀8 cols, `check_*` skipped), normalized caveat dedupe with caps (12/10), and single-language output via `detect_reply_language`; the report surface adds `GET /tools/report/{analysis_id}/records` (curation list), `GET …/readiness` (FE delta guard), and `exclude_record_ids` on POST β€” see API_CONTRACT_BE_PYTHON.md. **Report compaction 2026-07-09 (pr/13):** the rendered markdown drops the "Notes & Limitations", "Attempted, Unresolved", and "How This Was Analyzed" sections (team decision β€” compact report; render blocks commented out in `report/generator.py`, not deleted). The JSON body keeps `caveats`/`open_questions`/`unresolved`/`method_steps` and the curation/records endpoints are unchanged. **Traceability `data_used` layer 2026-07-13 (pr/15):** `GET /api/v1/traceability` gains a resolved, user-facing `data_used[]` block (one per `retrieve_data` call) β€” real source/table/column names, joins, plain-language filters, and result columns split into read-from-data vs `computed` (with `formula`, e.g. `total_revenue` = `SUM(line_total)`, so an alias is never shown as a real column). Ids are kept but **machine-only (FE must not render)**. Also adds `tool_calls[].summary`, and `sources[]` now carry `source_name` + every table touched. Deterministic catalog resolution (new `src/traceability/resolve.py`), no LLM, never-throw; catalog threaded to the scratchpad at the slow-path composition root. Additive/non-breaking; contract + `TRACEABILITY_FE_HANDOFF.md` updated. **Spine v2 W2+W1 2026-07-13 (pr/16):** Β§6/Β§7/Β§8/Β§9/Β§12 β€” `render_chart` tool lands (first of the `render_*` family: deterministic Plotly-JSON `dataeyond.chart.v1` envelope, hand-built, **no plotly dependency**; planner-selected only on an explicit chart ask, EN/ID) + Python-owned `message_charts` store + `GET /api/v1/charts` (FE fetches on `done`, same pattern as traceability; empty list = valid 200); planner gains a named **recipe table** + viz few-shots (Example J tail, Example K viz-infeasible) + validator Check 10 (`render_chart.data` must reference a table-producing task); and the slow path gains the **S1a quality checkpoint** (`slow_path/checkpoint.py`, 0 LLM, never-throw, between runner and assembler: CK1 all-failed β†’ deterministic honest-failure answer with **no** assembler call, CK2 empty retrieve + downstream, CK3 10k-cap truncation, CK4 single trend bucket, CK5 all-null column, CK6 chart-spec sanity; flags render as an "Execution assessment" block in the assembler input and every flag logs `repair_candidate` β€” the S1b evidence base). Design + handoff doc: `SPINE_V2_PLAN.md`. **LLM model + env rename 2026-07-14 (pr/17):** Β§2/Β§3/Β§9/Β§13 β€” the generation LLM is now **Azure GPT-5.4-mini** (deployment `gpt-5.4-mini`), not GPT-4o, and the settings quad is renamed `azureai__*__4o` β†’ **`azureai__*__54m`** across all 9 LLM call sites. **Hard rename, no `__4o` fallback** β€” an environment that still sets only `__4o` resolves to empty strings and fails on the first LLM call, by design: the silent-wrong-model drift is exactly how HF stayed on GPT-4o while local ran 5.4-mini (identical question, identical catalog, divergent planner output β€” HF hallucinated catalog ids, local planned correctly). Deploying requires all four `__54m` vars set in the HF Space secrets. **`reports.user_id` fix 2026-07-22 (pr/18):** Β§8/Β§9/Β§12/Β§13 β€” `ReportStore.save` never wrote `reports.user_id`, and on the new **Neon** dedorch instance that column is `text NOT NULL`, so **every `POST /api/v1/tools/report` 500'd** (`NotNullViolationError`; report persistence deliberately re-raises rather than degrading). The ORM gains `user_id` (declared **nullable** so Python tolerates every deployment shape) and the store writes `report.user_id`, which the endpoint already required as a query param and threaded through the generator. Live-verified against `information_schema` + a green 201. **Root cause (verified against the Go source after pulling `dd37b38`):** the column is *not* new β€” `0001_create_core_schema.sql` has declared it `NOT NULL` all along, but `0002`/`0004` retrofit it onto pre-existing DBs as a **nullable** `ADD COLUMN IF NOT EXISTS`. The old DB therefore accepted the column-less INSERT and the bug stayed latent; a fresh DB enforces it. Generalized as a Β§13 gotcha β€” **Go's migration set is not convergent, so fresh and migrated dedorch DBs differ in NOT NULL constraints** and the migrations alone can't tell you which shape you have. A full ORM-vs-live drift scan the same day found all five Python-written tables clean; the remaining drift is on read-only or unwired paths. **Cross-repo update 2026-06-29:** Β§2/Β§8/Β§11/Β§12 re-verified against
6
  the **Go source** (`Orchestrator-Agent-Service`), not its docs. The Go service has moved well past its
7
  own (uncommitted, stale) design docs: it now hosts the **dedorch SQL migrations** in-repo and a full
8
  **`/api/v1/analyses` + `/api/v1/skills`** REST surface. Go does **not** call Python yet β€” those skills
 
187
  | `langchain_pg_embedding` | PGVector document chunks | Go ingestion | DocumentRetriever |
188
  | `report_inputs` *(was `analysis_records`)* | jsonb `AnalysisRecord`, one per slow-path run; **Python-owned** | slow path | ReportGenerator, report readiness |
189
  | `analyses` *(dedorch, plural)* | uuid `id`, `user_id`, `analysis_title`, `objective`, `business_questions` jsonb, `status` (active\|inactive), `data_bind`(+`data_bind_version`), `report_id`, `report_collection` β€” **defined by Go migrations**; `problem_statement`/`problem_validated`/`owner_id` already **dropped** there (`0003`/`0004`) | Go `/api/v1/analyses`; Python state store | gate (no-op), Help, report |
190
+ | `reports` *(dedorch)* | uuid, `analysis_id`, **`user_id` text NOT NULL**, `title` + markdown `content` + `version` (UNIQUE per analysis). **2026-07-22:** `AnalysisReportRow` gained `user_id` and `ReportStore.save` now writes it β€” before that the INSERT omitted the column and every `POST /tools/report` 500'd on the new Neon instance (Β§13) | Go + Python ReportStore | report API |
191
  | `data_sources` *(dedorch, Go-owned)* | per-analysis binding table. **Python no longer reads or writes it** β€” bindings live in Go's `analyses.data_bind`, which Go materializes into the analysis-scope `data_catalog` row; Python scopes off that row. The table exists (Go migration) but Python is fully decoupled β€” do **not** drop it manually | Go migration | β€” (unused by Python) |
192
  | `analyses_messages` *(dedorch)* | the analysis chat room (`role ∈ user\|ai`); replaces deprecated `rooms`/`chat_messages` | Go `/analyses/{id}/messages` | Python chat path **not yet migrated here** (Β§12) |
193
  | `message_traceability` *(Python-owned)* | one jsonb `TraceabilityPayload` per assistant turn (PK `message_id`); flushed before `done` | chat pipeline (KM-691) | `GET /api/v1/traceability` |
 
298
  ### Report β€” `src/agents/report/`
299
  `generator.py` reads records, deterministically assembles structured fields, 1 LLM call for the
300
  executive summary; `store.py` versions under an advisory lock and persists markdown to dedorch
301
+ `reports` (**2026-07-22:** the row now carries `user_id`, threaded endpoint β†’ generator β†’ store β€”
302
+ `POST /tools/report` takes it as a required query param, so it is never null on the live path);
303
+ `readiness.py` defines the **report floor** (β‰₯1 successful `analyze_*` **or**, since
304
  2026-07-14, `render_chart` β€” a chart-only session is substantive; the `problem_validated`
305
  precondition was dropped 2026-06-25) shared by the report API and the Help readiness signal so the
306
  two can't disagree. **2026-07-14:** the report embeds charts β€” `_collect_charts` copies
 
361
  **dropped** `problem_statement`/`problem_validated`/`owner_id`. Migration `0004` renames the legacy
362
  `rooms`/`chat_messages`/`interview_*` tables to `zdeprecated_*`.
363
  - **`report_inputs`** (the slow-path structured output, formerly `analysis_records`) stays
364
+ **Python-owned** in the sense that Python is its only writer β€” but **βœ… 2026-07-22: Harry's
365
+ migration now creates it.** `0001_create_core_schema.sql` carries the table (uuid `id`, `analysis_id`
366
+ uuid nullable **with the FK to `analyses(id)`**, `user_id`/`plan_id` text NOT NULL, `data` jsonb
367
+ NOT NULL, `created_at` timestamptz), exactly the DDL handed over in #22 β€” which closes #22.
368
+ Python's `ReportInputRow` was diffed against the live Neon table the same day: **column-for-column
369
+ identical**, no drift.
370
+ **`message_traceability`** (created manually 2026-07-06) and **`message_charts`** (created manually
371
+ 2026-07-13, DDL in `SPINE_V2_PLAN.md` Β§4.4) are **still in no Go migration** β€” they exist on Neon
372
+ because they were created by hand, so a future fresh dedorch DB will not have them. Harry's
373
+ migration handoff for those two remains the open item (DEV_PLAN #32).
374
  - **Connection-string cutover DONE (2026-07-01).** Python's `postgres_connstring` now points at
375
  **dedorch** and reads the Go-migrated tables directly. Every ORM model Python reads (`analyses`,
376
  `analyses_messages`, `data_catalog`) has been reconciled to its dedorch shape.
 
378
  DDL (`ALTER TABLE rooms …`, index creation) fails on Go-owned tables
379
  (`InsufficientPrivilegeError: must be owner of table rooms`). Skipping is safe β€” Go migration `0001`
380
  already provides the `vector` extension + the langchain FTS index. Set `SKIP_INIT_DB=false` (.env or
381
+ env) only for a local Python-owned DB. ~~`report_inputs` is not in any Go migration yet (#22) β€” create
382
+ it in dedorch before enabling the slow path, else report/slow-path writes fail (chat path unaffected).~~
383
+ **RESOLVED 2026-07-22:** `report_inputs` ships in Go's `0001` (above), so a fresh dedorch DB has it.
384
+ The same is **not** true of `message_traceability`/`message_charts` β€” those still need the manual DDL
385
+ on any newly provisioned instance (DEV_PLAN #32).
386
 
387
  **⚠️ Integration gap (verified β€” the big one).** Go's `/api/v1/analyses` and `/api/v1/skills`
388
  (`help` / `report`) are **placeholders that return dummy data** β€” the `SendMessage` / `GenerateReport`
 
424
  degrades it into an honest "data not available" answer, so it masquerades as a data problem.
425
  Tell-apart: `BlobNotFound` + `location_ref` starting `object_storage://` β‡’ env gap; set
426
  `storage_provider=supabase_s3` + the five `supabase_s3_*` values (match Go's data plane).
427
+ - **Fresh vs migrated dedorch DBs have different NOT NULL constraints (gotcha, found 2026-07-22 β€”
428
+ verified against the Go source, corrects an earlier note in this file that said the column was
429
+ missing from the migrations).** Go's migration set is not convergent: `0001_create_core_schema.sql`
430
+ declares `reports.user_id TEXT NOT NULL` **and** `analyses_messages.user_id TEXT NOT NULL` in its
431
+ `CREATE TABLE`s, while `0002`/`0004` retrofit the same columns onto pre-existing databases as
432
+ `ALTER TABLE … ADD COLUMN IF NOT EXISTS user_id TEXT` β€” **nullable**. So a DB where the table
433
+ already existed ends up nullable (the `CREATE TABLE IF NOT EXISTS` no-ops), and a DB created fresh
434
+ from `0001` ends up NOT NULL. **Same migrations, two schemas.**
435
+ This is what the 2026-07-22 report outage actually was: `ReportStore.save` had *never* written
436
+ `user_id`, and the old dedorch DB's nullable column silently accepted it. The Neon instance was
437
+ provisioned fresh, so `NOT NULL` bit immediately β€” every `POST /api/v1/tools/report` 500'd with
438
+ `Report persistence failed: … NotNullViolationError` (report persistence is **not** a never-throw
439
+ seam β€” `api/v1/report.py` re-raises as a 500 by design, so a lost report is loud).
440
+ Tell-apart: `NotNullViolationError` naming a column absent from `src/db/postgres/models.py`.
441
+ Fix pattern: add the column to the ORM as **nullable** (Python then matches the loosest deployment
442
+ shape) and always write it β€” never mirror one DB's NOT NULL into Python's model.
443
+ **Reading the Go migrations is not sufficient** β€” the constraint depends on the DB's history, so
444
+ verify the instance you actually run against:
445
+ `select column_name, is_nullable from information_schema.columns where table_name='<t>'`.
446
+ Full ORM-vs-live drift scan run 2026-07-22 against Neon: all five tables Python **writes**
447
+ (`reports`, `report_inputs`, `message_traceability`, `message_charts`, `analyses`) are clean.
448
+ Remaining drift is on read-only or dead paths and is inert β€” `analyses_messages` has live
449
+ `message_id`/`status`/`note` the ORM lacks (Go-only writer, Β§2.3; added by Go `0006` +
450
+ `981b42b`/`5da7283`), `documents` has a live `chunks_count`, and `message_sources` diverges wholly
451
+ but is referenced only by the **unwired** `api/v1/room.py`. `rooms`/`chat_messages` are absent
452
+ (renamed `zdeprecated_*` by `0004`).
453
  - **Never-throw seams** are pervasive (tool invoker, query service, executors, state/catalog reads,
454
  record persistence, report summary). Failures degrade into soft output rather than raising β€” good
455
  for UX, but they can mask real breakage (e.g. a missing analysis-scope catalog silently falling
src/agents/report/store.py CHANGED
@@ -80,6 +80,7 @@ class ReportStore:
80
  AnalysisReportRow(
81
  id=report.report_id,
82
  analysis_id=report.analysis_id,
 
83
  title=_report_title(report),
84
  content=report.rendered_markdown or "",
85
  generated_at=report.generated_at,
 
80
  AnalysisReportRow(
81
  id=report.report_id,
82
  analysis_id=report.analysis_id,
83
+ user_id=report.user_id,
84
  title=_report_title(report),
85
  content=report.rendered_markdown or "",
86
  generated_at=report.generated_at,
src/db/postgres/models.py CHANGED
@@ -196,6 +196,10 @@ class AnalysisReportRow(Base):
196
 
197
  id = Column(UUID(as_uuid=False), primary_key=True) # AnalysisReport.report_id (uuid)
198
  analysis_id = Column(UUID(as_uuid=False), nullable=False, index=True)
 
 
 
 
199
  title = Column(String, nullable=False)
200
  content = Column(Text, nullable=False) # rendered markdown
201
  generated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
 
196
 
197
  id = Column(UUID(as_uuid=False), primary_key=True) # AnalysisReport.report_id (uuid)
198
  analysis_id = Column(UUID(as_uuid=False), nullable=False, index=True)
199
+ # Added by Go 2026-07 (text, like `analyses.user_id`); NOT NULL in some deployments,
200
+ # nullable in others β€” declared nullable here so Python matches the loosest shape,
201
+ # but ReportStore always writes it (a missing value 500s on the strict DBs).
202
+ user_id = Column(Text)
203
  title = Column(String, nullable=False)
204
  content = Column(Text, nullable=False) # rendered markdown
205
  generated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())