Rifqi Hafizuddin
Claude Opus 5
[NOTICKET] docs: sync REPO_STATUS / DEV_PLAN / contract for the 8 Sep list (K24)
be227a8 |
Download DEV_PLAN.md from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 86.8 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/refs%2Fpr%2F21/DEV_PLAN.md
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog@refs/pr/21/DEV_PLAN.md
-
curl -L -o DEV_PLAN.md https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/refs%2Fpr%2F21/DEV_PLAN.md
86.8 kB
| # Data Eyond β Current Development Plan (post 2026-06-24 β 2026-06-30 checkpoints) | |
| <!-- doc-status --> | |
| > β **LIVE β CANONICAL: what is in flight.** Sprint plan, task table and locked decisions (Β§1/Β§1.5 are locked; Β§6 lists what is still open). Built state: [REPO_STATUS.md](REPO_STATUS.md) Β· all other docs: [docs/](docs/). | |
| **Purpose:** context file for Claude Code sessions working on the current sprint. | |
| **Branch:** `pr/5` Β· **Snapshot:** 2026-06-30. | |
| **Companion:** [REPO_STATUS.md](REPO_STATUS.md) describes the repo's *current built state*; this file | |
| describes the *in-flight plan* that changes it. The **active sprint is pr/5** ([Β§0](#0-current-sprint--pr5-observability--endpoint-restructure)); sections Β§1βΒ§6 are the prior | |
| 2026-06-24/25 pivot (now largely β ), kept for context. | |
| --- | |
| ## 0. Current sprint β pr/5: Observability + Endpoint Restructure | |
| From the **2026-06-30 checkpoint**. Direction: **Python β generation/AI-only**; Go owns the analysis | |
| lifecycle + data plane. Endpoint contract sent to Harry on 2026-06-30: | |
| [API_ENDPOINTS_RESTRUCTURE.md](docs/archive/API_ENDPOINTS_RESTRUCTURE.md) (chatβv2, tools regroup, observability β | |
| observability marked tentative). REPO_STATUS carries a matching `pr/5` direction banner. | |
| Mentor's task order: **unwire β regroup endpoints β add tools (retrieve-data + observability)**; share | |
| the endpoint contract *before* coding the tools. Status legend: β¬ not started Β· π in progress Β· β done Β· | |
| β blocked Β· π verify Β· βΈοΈ deferred. | |
| | Phase | Task | Owner | Status | Notes | | |
| |---|---|---|---|---| | |
| | **P0 β contract** | Draft + send endpoint contract to Harry (chat v2 Β· tools group Β· observability) | Rifqi + Sofhia | β | `API_ENDPOINTS_RESTRUCTURE.md` sent 2026-06-30 (before-noon deadline met). Observability section flagged tentative. | | |
| | **1 β unwire** | Unwire `users`(login)/`document`/`room`/`db_client`/`data_catalog`/`analysis` from `main` + Swagger | Sofhia | β | **KM-686**, commit `0b2d678`. Commented, not deleted; `chat`/`report`/`tools` kept mounted. Resolves the analysis-CRUD scope Q β whole `analysis` router unwired (Go owns it). | | |
| | **2 β v2 + regroup** | Create `src/api/v2/` and move the chat pilot there | Rifqi | β | New `src/api/v2/__init__.py` + `src/api/v2/chat.py` (`POST /api/v2/chat/stream`), mounted in `main.py`. Only chat in v2; v1 `/chat/stream` kept mounted until FE moves over. Routes import-verified. | | |
| | **2 β v2 + regroup** | Chat: `room_id` β **`analysis_id`** (request field + handler + history) | Rifqi | β | v2 `ChatRequest{user_id, analysis_id, message}`; reuses warm `ChatHandler` + v1 cache/history helpers; `done` returns `{message_id}` (always minted Python-side, server-authoritative β open-Q #1 resolved in pr/6). Persistence kept transitionally β still ties to #25 (`analyses_messages`); ruff-clean. | | |
| | **2 β v2 + regroup** | Move report under tools β `/api/v1/tools/report` (+ version routes) | Rifqi | β | report router re-prefixed `/api/v1` β `/api/v1/tools` (all 3 routes move together), tag β `Tools`; old `/api/v1/report` gone. Same functionality, new home. Import-verified. | | |
| | **2 β v2 + regroup** | Move help under tools β `POST /api/v1/tools/help` (dedicated endpoint) | Sofhia | β | New `src/api/v1/help.py` (SSE: `sources:[]`β`chunk`β`done{message_id}`) + additive `ChatHandler.stream_help()` (reuses HelpAgent+state+readiness, no router). Generative-only (no persist). **Router `help` intent KEPT** β both paths live by design. message_id always minted Python-side, server-authoritative (open-Q #1 resolved, pr/6). Import-verified. | | |
| | **2 β v2 + regroup** | Tools list β `/api/v1/tools/list` | Sofhia | β | Renamed route `GET /api/v1/tools` β `GET /api/v1/tools/list` ([tools.py:133](src/api/v1/tools.py:133)). | | |
| | **2 β v2 + regroup** | FE: slash menu = `/help` only; report = right-side button | Mentor (FE) | β¬ | Coordination note, not Python work. | | |
| | **3 β tools + obs** | Finish `help` so it actually **calls** (not just lists) + test | Sofhia | β¬ | Mentor: help currently only lists tools. Core #2 after chat. | | |
| | **3 β tools + obs** | Traceability **scratchpad** accumulating in the chat agent | Rifqi + Sofhia | β | **KM-691.** `TraceabilityScratchpad` + `TraceabilityToolInvoker` (`src/traceability/`) capture planning / tool I/O / sources during the run; flushed one row before every `done` (all 8 sites; error turns = no row). Renamed observabilityβtraceability (vs. Langfuse). | | |
| | **3 β tools + obs** | Audit `report_inputs` β covers planning + tool I/O + source? add cols / new store | Rifqi | β | **KM-691.** Chose a dedicated store: `message_traceability` = 1 JSONB row per message (Python-owned, like `report_inputs`; DDL run manually against dedorch, handed to Harry). Langfuse kept for engineering. | | |
| | **3 β tools + obs** | Build `GET /api/v1/traceability` (one merged response) | Rifqi | β | **KM-691.** `src/api/v1/traceability.py` β store.get β payload/404. Intent-based source rules (greeting/help/refusals = none; retrieve = required); full planning only on slow path. Contract Β§7 updated. | | |
| | **3 β tools + obs** | Keep stream **text-only**; traceability is a separate parallel call | Rifqi | β | **KM-691.** No trace data in the SSE stream; the FE fetches `/traceability` on `done`. | | |
| | **3 β tools + obs** | Resolve `message_id` correlation (stream β traceability) with Harry | Rifqi β Harry | β | **RESOLVED (pr/6):** Python is the **sole minter** β `message_id` dropped from the `/api/v2/chat/stream` + `/api/v1/tools/help` request bodies; always minted server-side (server-authoritative, FE-security) and returned on `done`. Any caller-sent `message_id` is ignored. Contract open-Q #1 closed. **Updated 2026-07-09 (pr/13):** id format changed from `msg_<hex>` to a canonical UUID string (`str(uuid.uuid4())`, both mint sites) to mirror Go's `analyses_messages.id` shape β the value is still independently Python-minted (not the real row id), only format-compatible for a future swap. | | |
| | **4 β biz questions** | Get Go folder; confirm `business_questions` in create-analysis (max 5); sync Python | Harry/Mentor β Rifqi | β¬ | Go currently missing the field ("lagi difixing"). Python already models objective + business_questions. | | |
| | **deferred** | Report formats: PPT (preferred) / PDF / infographic on download | β | βΈοΈ | MD is fine for the FE preview stage now. | | |
| | **deferred** | Charts (PlotlyβJSON) + images tables | β | βΈοΈ | Carried from Β§4 #26/#27. | | |
| **Next up:** Phase 2 Python work is **done** (chatβv2 `analysis_id`; `help`/`report`/`list` regrouped | |
| under `/api/v1/tools/`). The `message_id` correlation contract is now settled in **pr/6** (Python sole | |
| minter, stream-only). The **Phase 3 traceability build** β scratchpad + `GET /api/v1/traceability` | |
| (contract Β§7) β is **done (KM-691)**. Next is **Phase 4** (business questions, Go-blocked). | |
| --- | |
| ## 0.5. pr/13 sprint β agent-quality fixes (2026-07-08 live-test review) | |
| Findings from the scoped live sessions (mining analysis, 2026-07-07/08 traces): the planner | |
| force-mapped absent measures (`pa` aliased as "revenue"), top-N ranked raw rows (duplicate models), | |
| `analyze_trend` collapsed integer months into a single 1970-01 bucket, an invalid grouped IR reached | |
| Postgres, failed retrievals wrote all-null traceability sources, and numeric catalog samples arrive | |
| base64-mangled from Go. Fix tasks (same status legend as Β§0): | |
| | # | Task | Owner | Status | Note | | |
| |---|---|---|---|---| | |
| | Q1 | IR validator: reject bare selects under `group_by` (planner retry self-corrects) | Rifqi | β | `query/ir/validator.py` | | |
| | Q2 | Planner **infeasible** path: `TaskList.infeasible_reason` + deterministic EN/ID data-gap reply | Rifqi | β | schemas/validator/coordinator/refusals + planner.md "When the catalog cannot answer"; refusal wording β Rifqi to review | | |
| | Q3 | `analyze_trend`: integer year/month handling (epoch-parse bug) | Rifqi | β | `temporal.py` + 5 local tests | | |
| | Q4 | Planner few-shots: top-N (Example G) + infeasible (Example H) + entity-vs-row ranking rule | Rifqi | β | live-tested 2026-07-08: backlog top-3 correct via single-IR group+sum; "best PA performance" correct in-process (avg-per-model, assumption recorded). Stale-server trace was a false alarm | | |
| | Q5 | Catalog numeric `sample_values` base64-decode stopgap (`catalog/sample_decode.py`) | Rifqi | β | self-disabling; **primary fix = Go marshaling β DDL-free handoff to Harry** | | |
| | Q6 | Traceability null-source suppression + `check_data` `-1` row-count hiding | Rifqi | β | `scratchpad.py` / `data_access.py` | | |
| | Q7 | `analyze_merge` two-table combine tool (unblocks "worst A + biggest B" questions) | tool owner | β | tool shipped by Sofia (8abf635, KM-703); planner slice done 2026-07-09: `_validate_data_source` guards `data_right`, two-retrieveβmerge few-shot (Example I), planner.md "Two measures per entity" bullet | | |
| | Q8 | Report v2: business-question answer section, unresolved/excluded sections, evidence tables from `results_snapshot`, caveat dedupe, single language | Rifqi/Sofhia | β | done 2026-07-09: still exactly ONE LLM call (extended to also draft `bq_answers`, index-based record refs, deterministic fallback = v1 behavior); evidence tables from table-kind outputs (β€3/record, β€10 rows, β€8 cols, `check_*` skipped); reply language via `detect_reply_language` on objective+BQs; verified in-process against live analysis 935a091e | | |
| | Q9 | Record-curation endpoint (`GET β¦/records` + `exclude_record_ids`) + readiness GET for the FE delta guard | Rifqi β FE | β | done 2026-07-09: `GET /tools/report/{analysis_id}/records` + `/readiness` (registered before `/{version}` β int-coercion route-order trap), `exclude_record_ids` on POST; contract updated same change; FE wiring pending (Rifqi β FE) | | |
| | Q10 | Traceability `data_used` layer β resolve IR ids β real names for the FE (users couldn't map `c_β¦`/`t_β¦` ids back to their data; aggregate aliases like `total_revenue` looked like real columns) | Rifqi β FE | β | done 2026-07-13 (pr/15): new `src/traceability/resolve.py` builds `data_used[]` (real source/table/column names; joins; plain-language filters; `columns_read` vs `output_columns` with `computed`+`formula`), `tool_calls[].summary`, `sources[]` gains `source_name`+all tables; **ids kept but machine-only (FE must not render)**; deterministic no-LLM, never-throw; catalog threaded to the scratchpad at the composition root. Contract + `TRACEABILITY_FE_HANDOFF.md` updated; FE wiring pending (Rifqi β FE). Additive/non-breaking | | |
| | Q11 | IR wart: `OrderByClause.column_id` may hold a SELECT **alias** (a computed output), not a catalog column_id | Rifqi | π | surfaced by Q10 β the resolver tolerates it (`kind: "computed"` fallback), but the IR field name is misleading. Consider an explicit `by_alias` field or renaming. Low priority; no functional bug | | |
| ## 0.6. pr/16 sprint β Spine v2: W2 charts + W1 checkpoint (SPINE_V2_PLAN, approved 2026-07-13) | |
| Scope approved 2026-07-13 (Rifqi + Sofia): build W2 (`render_chart` + chart store + `GET /charts` | |
| + planner viz slice) and W1 (S1a quality checkpoint). **W3 (activate deferred `analyze_*`) deferred | |
| at approval β do not start until Rifqi re-opens.** W4 (S1b repair) stays gated on INV-6 sign-off + | |
| S1a telemetry. Design + handoff source: `SPINE_V2_PLAN.md`. | |
| | # | Task | Owner | Status | Note | | |
| |---|---|---|---|---| | |
| | V1 | `render_chart` tool slice: `visualization.py` (Plotly-JSON `dataeyond.chart.v1` envelope, no plotly dep) + `ToolOutput.kind` `"chart"` + registry + invoker | Rifqi (Sofia signed off on the tool-layer edit) | β | done 2026-07-13; deterministic spec builder (bar/line/pie/scatter, fixed style preset), traceability scratchpad summarizes chart outputs compactly (point_count, not the raw arrays) | | |
| | V2 | Chart store + API: `MessageChartRow` (`message_charts`) + `src/charts/store.py` (never-throw save) + write site in `_run_slow_path` + `GET /api/v1/charts` + contract Β§charts | Rifqi | β | done 2026-07-13; empty list = valid 200; `done` event unchanged (no `chart_count` β open, Harry); FE fetches unconditionally on `done` | | |
| | V3 | Planner viz slice: recipe table + "Charts only on explicit ask" rule (planner.md), Example J (viz tail) + Example K (viz-infeasible), validator Check 10 (`render_chart.data` must be table-kind), assembler chart one-liner guard | Rifqi | β | done 2026-07-13; behavioral matrix verified in-process (real LLM): explicit ask EN/ID β chart tail; plain question β no chart; absent dimension β infeasible (Example K + prompt guard added after the first smoke force-mapped `status` AS "region") | | |
| | V4 | W1 S1a quality checkpoint: `slow_path/checkpoint.py` (CK1βCK6) + `RunAssessment` schemas + coordinator call site + "Execution assessment" block in the assembler input + `refusals.run_failure_message` | Rifqi | β | done 2026-07-13; 13 local tests (one per CK rule + never-throw + prompt + coordinator); CK1 all-failed β deterministic honest failure, **no assembler call**; clean run renders nothing (zero behavior change); every flag logs `repair_candidate` (S1b evidence) | | |
| | V5 | `message_charts` DDL: run manually against dedorch (block for the live e2e chart test), then hand the schema to Harry for the dedorch migration | Rifqi β Harry | β | Rifqi ran the DDL 2026-07-13; **live e2e ALL PASS** same day (real v2 endpoint: viz turn β chart row keyed by `done` message_id β `GET /charts` valid v1 envelope; chartless β 200 empty; injected `render_chart` failure β answer streams as a table, no row). **Remaining: send Harry the migration handoff** (schema + contract Β§charts) | | |
| | V6 | Restore the `eval.chat_sim` harness β `eval/chat_sim/*.py` is missing from disk AND git (only `__pycache__` remains); Β§7B prompt gate can't run | Rifqi | β | restored by Rifqi 2026-07-13 (accidental delete). β οΈ its hard-coded `DEFAULT_USER_ID`/`TITANIC_SOURCE_ID` are stale (that user has no catalog; the Titanic blobs are gone) β update the constants before the next full run | | |
| | V7 | Local `.env` lagged Go's Supabase-S3 data plane: `storage_provider=azure_blob` + empty `supabase_s3_*` made EVERY local tabular retrieve fail `BlobNotFound` | Rifqi | β | found during the e2e (masked as "data not available" by the honest-degrade path); Rifqi set the six values 2026-07-13. Gotcha documented in REPO_STATUS Β§13 | | |
| | V8 | Lead review of `GET /charts`: lookup by `message_id` alone + tri-state response marker (`status: success \| empty \| not_found` + `message`) instead of a bare list | Rifqi (lead ask) | β | done 2026-07-14; `not_found` vs `empty` decided against the turn's traceability row (PK lookup); always HTTP 200; contract Β§charts updated. β οΈ additive DDL for the new lookup: `CREATE INDEX IF NOT EXISTS idx_message_charts_message ON message_charts (message_id);` (run manually + include in Harry's migration). Traceability GET still takes both params β aligning it is open | | |
| | V9 | Report chart embedding: `AnalysisReport.charts` (verbatim envelopes per record) + `## EDA` section with ` ```plotly ` fences (content = the **full v1 envelope**, pretty-printed β the FE hook's verified shape); `has_successful_analysis` extended so a successful `render_chart` counts (chart-only sessions satisfy the report floor) | Rifqi | β | done 2026-07-14; first cut emitted bare `{data, layout}` β FE test showed the hook parses the full envelope, fixed same day; live reports v3 (wrong fence) β **v4 (correct)** for analysis `7be50846β¦` (3 charts embedded); suite **381 passed, 7 skipped** (+5 chart-embed tests; same 2 pre-existing failures) | | |
| Full-suite evidence for this sprint: **376 passed, 7 skipped** (+13 new checkpoint tests; the 2 | |
| failures β `test_chat_handler::test_structured_flow_runs_slow_path`, | |
| `test_reader::test_structured_read_falls_back_to_user_scope_when_no_analysis_row` β reproduce at | |
| HEAD before this diff, i.e. pre-existing). Ruff clean on all touched paths; `import main` OK. | |
| ## 1. The direction change (locked decisions from 2026-06-24) | |
| 1. **"Problem statement" is replaced by two user-entered fields: `objective` + `business_questions`.** | |
| User fills them at onboarding; **both mandatory to submit; NO agent validation.** | |
| 2. The **gate (`problem_validated`) and the `problem_statement` skill/intent are removed** (comment out, don't delete). | |
| 3. **Report is records-based** (reads persisted `AnalysisRecord`s) β **decided and pushed** (KM-674). | |
| It is formal markdown: title, date, "generated by {user}", objective, business questions, | |
| findings, insights. **NOT gated** on whether business questions were answered. | |
| 4. **`owner_id` β `user_id`** everywhere (Harry mirrors in dedorch/Go). | |
| 5. **State writes go through a request to Go**, not direct Python DB writes. | |
| 6. **FE-callable surface = 4 endpoints:** `call_agent` (chat/stream), `list_skills` (`GET /tools`), | |
| **skill: help**, **skill: report**. `problem_statement` removed; `check_data` not FE-facing from | |
| Python (Go provides it); analysis CRUD not needed from FE (comment, don't delete). | |
| 7. Deliverables for Harry: (a) API endpoint doc (MD); (b) full Python project doc (MD β PDF/Word BRD). | |
| 8. Integration tested via Swagger `/docs` on the HF Python build (simulating FE manually). Target ~Wed. | |
| ## 1.5. 2026-06-25 checkpoint deltas | |
| Confirms the 2026-06-24 direction and adds these concrete changes (folded into Β§4 as tasks 21β28): | |
| 1. **Rename `analysis_records` β `report_inputs`** (DONE #21) β names the table by purpose (the rows | |
| report generation reads); avoids clashing with Go's `analyses_messages` and with Langfuse | |
| observability. **Stays Python-owned**; finalized schema handed to Harry so his dedorch migration | |
| creates it post-`SKIP_INIT_DB` (#22, resolves #16). Write scope = **one row per slow-path analysis | |
| run** (decided β not per-agent-call telemetry; that stays Langfuse). | |
| 2. **`analyses` table (Go) β `status`, `data_bind` + `data_bind_version`, `report_collection`** (id+version). | |
| **Verified 2026-06-25: these + `user_id` are ALREADY present in dedorch `analyses`.** Plus Harry drops | |
| the duplicate/wrong singular `analysis` table. (β #3) | |
| 3. **`analyses_messages` (Go) = the analysis chat room** (user Q + agent A) β replaces the now-**deprecated** | |
| `chat_messages`/`rooms`; Python's chat read/write must migrate here before cutover. (β #25) | |
| 4. **Reports: Go owns ALL writes.** Report stays a **skill** (no router intent): FE β Go β Python; | |
| Python only returns content. Input = the records table (now `agent_observability`); edit-mode may | |
| also need the last report. (β #7/#18/#24) | |
| 5. **Markdown minimum now:** tables, **bold**, *italic*, horizontal separators β optimize that before | |
| anything fancier. (β #23) | |
| 6. **Deferred:** charts (prefer **PlotlyβJSON** in a future `chart` table over matplotlib PNGs) and | |
| images (image table keyed by analysis/message/report + originals in a bucket). (β #26/#27) | |
| 7. **Near-term:** the remove-`problem_statement` work isn't on HF yet β **PR + deploy + test in the | |
| playground** (#13). Harry stabilizes Go ~Fri; FE manual testing ~Mon. **Keep it playground-able.** | |
| 8. **UI research** (no dedicated UI person): new-analysis form (title/objective/business_questions), | |
| knowledge menu (user-level vs analysis-level binding), report artifacts panel + version selector; | |
| interview + old analysis UI removed. (β #28) | |
| ## 2. What is already done (KM-674, pushed on `pr/4`) | |
| Report layer adapted to the new goal shape: | |
| - `report/schemas.py::ProblemStatement` β `objective: str` + `business_questions: list[str]` | |
| (old `target_value`/`scope`/`metric_direction`/`target_metric` dropped). Class name kept for now | |
| (rename to `ReportGoal` once the upstream AnalysisState rename lands). | |
| - `report/generator.py` renders **Objective** + numbered **Business Questions** + a | |
| **"generated by {user}"** line. | |
| - `api/v1/report.py::_problem_statement_from` is **tolerant**: prefers new `objective` / | |
| `business_questions` from state, falls back to legacy `problem_statement` β works before AND after | |
| Harry's migration. | |
| - `config/prompts/report_summary.md` updated to objective + business questions. | |
| - Report stays **records-based**; the floor gate (`problem_validated`) was deliberately left for task #2. | |
| **This tolerant-migration pattern (getattr fallback) is the model for tasks #2 and #4.** | |
| ## 3. Assessment β gaps & contradictions to resolve before building | |
| These came out of reviewing the plan against the actual code. They are folded into the task table (Β§4) as tasks 15β19. | |
| - **G1 (β task 15). Records-based reports need the slow path ON.** `AnalysisRecord`s persist only in | |
| `chat_handler._run_slow_path`, which runs only when `ENABLE_SLOW_PATH=true`. Default is off β no | |
| records β `POST /report` 409s. The Swagger demo can't show a non-empty report unless slow path is | |
| flipped on and a `structured_flow` question is run first. `BusinessContext` is still a stub but the | |
| slow path runs fine on it. | |
| - **G2 (β task 16). `analysis_records` ownership is now required and collides with `SKIP_INIT_DB`.** | |
| It's created today by Python `create_all` (`db/postgres/init_db.py`), is in no dedorch/Go migration, | |
| and after the dedorch cutover (`SKIP_INIT_DB=true`) Python stops running `create_all` β the table | |
| won't exist β reports break. Decide: dedorch migration (Harry) OR a Python carve-out that creates | |
| just this one table even under `SKIP_INIT_DB`. *(Resolved 2026-06-25 β see Β§1.5.1 / #16 / #22.)* | |
| - **G3 (β task 17). `chat_history` in the report contract is vestigial.** Records-based generation | |
| reads records by `analysis_id`; it never uses chat history. Drop `chat_history` from the report | |
| skill contract, or mark it reserved/unused. | |
| - **G4 (β task 4 note). Make #2/#4 tolerant of both state shapes.** If Harry drops | |
| `problem_validated`/`owner_id` from dedorch before Python stops reading them, Python's gate + | |
| state_store break. Use the same `getattr` tolerance KM-674 used. The `owner_id`β`user_id` rename | |
| also touches `api/v1/analysis.py` (`_serialize_state`, `list_analyses`, `get_analysis`), not just | |
| the model + state_store. | |
| - **G5 (β task 18). "State writes via Go" is bigger than `report_id`.** Python still writes state in | |
| `/analysis/create` (state + room + bindings, plus the data-first gate and soon the mandatory-field | |
| check) and in `state_store.ensure` per turn. If creation moves to Go (consistent with commenting | |
| analysis CRUD), then Go owns ALL state writes + both creation gates, and Python's `ensure` must | |
| become a **read-only get** (Go must guarantee the row exists before any chat turn). | |
| - **G6 (smaller).** | |
| - Removing `problem_statement` (task 1) means neutering it in 4 places: the `Intent` literal | |
| (`agents/orchestration.py`), the router prompt (`config/prompts/intent_router.md`), the handler, | |
| and the gate's redirect *target*. Do it with task 2. | |
| - "generated by {user}" currently prints the raw `user_id`; a formal report wants a name β source | |
| from `users.fullname` or have Go pass a display name (task 19). | |
| - The meeting's outline (background / EDA / insights) isn't fully in the renderer; map those | |
| sections onto the record fields deliberately (task 5 follow-up). | |
| - The full project doc (task 11) should reuse [REPO_STATUS.md](REPO_STATUS.md), not restart. | |
| ## 4. Task table | |
| Status legend: β¬ not started Β· π in progress Β· β done Β· β blocked Β· π verify Β· βΈοΈ deferred. | |
| | # | Task | Owner | Status | Note | | |
| |---|---|---|---|---| | |
| | 1 | Comment out `problem_statement` skill **+ `Intent` literal + router prompt + gate redirect target**; remove `/problem-statement` from `list_tools` | Rifqi | β | Done 2026-06-25 (one commit w/ #2). Unwired in `orchestration.py`, `intent_router.md`, `chat_handler.py`, `tools.py`; `problem_statement.py` kept intact | | |
| | 2 | Drop `problem_validated`: gate neutered; `is_report_ready`/`report_floor` β **β₯1 completed analysis** only, no-LLM | Rifqi | β | Done 2026-06-25. `gate.py` no-op, gate call site commented in `chat_handler.py`, `report_floor` drops the goal check. Tests updated (`test_gate`/`test_chat_handler`/`test_readiness`). Suite: **284 passed, 7 skipped**; ruff clean | | |
| | 3 | dedorch `analyses` migration: drop `problem_statement`/`problem_validated`, add `objective` + `business_questions` | Harry | π | **Verified dedorch 2026-06-25:** `analyses` (plural) ALREADY has `user_id` + `status` + `data_bind` + `data_bind_version` + `report_collection` β those parts done. **Remaining:** drop `problem_statement`/`problem_validated` + add `objective`/`business_questions`. Singular `analysis` = deprecated duplicate to drop | | |
| | 4 | Update Python `analyses` model + `state_store` + `analysis.py` to match dedorch; `owner_id`β`user_id` | Rifqi/Sofhia | β | Done 2026-06-26. `owner_id`β`user_id` + added `status`/`data_bind`/`data_bind_version`/`report_collection` (DB-only, not in the `AnalysisState` pydantic) across `models.py`/`gate.py`/`state_store.py`/`analysis.py` + 3 local tests; also `report_inputs` `id`/`analysis_id` β `uuid`. Kept `problem_statement`/`problem_validated`; `objective`/`business_questions` wait on Harry's #3. Suite **284 passed** | | |
| | 5 | Report generator β `objective`+`business_questions`, "generated by {user}", formal outline | Sofhia | β | Goal-shape (KM-674) + author name (#19) + outline (KM-680): Objective β Business Questions β Executive Summary β Key Findings β EDA β Notes & Limitations β How This Was Analyzed | | |
| | 6 | Report skill input contract: `analysis_id` + `user_id` (no `chat_history`) | Sofhia/Rifqi | β | No-op: `POST /report` already takes only analysis_id + user_id (records-based). Documented in API_ENDPOINTS.md Β§5. *(Edit-mode input revisited in #24.)* | | |
| | 7 | `report_id` state update via request to Go, not direct DB | Sofhia + Harry | β¬ | Needs Go endpoint. **Checkpoint:** Go owns ALL `reports` writes; Python stops any direct insert/update and only returns content; report stays a **skill** (no intent). See #18 | | |
| | 8 | Expose/confirm 4 FE endpoints; comment `check_data` + analysis CRUD | Sofhia | β | KM-678: `list_tools` trimmed to `/help` + `/report` (analytics/check/retrieve commented in the **menu**). `help` confirmed as a `call_agent` intent β no own endpoint. Analysis CRUD endpoint left **registered**: "comment the rest" was about the FE slash menu, not killing HTTP routes Go needs | | |
| | 9 | Verify `analysis_id` in `call_agent` contract | Sofhia | β | Verified: no separate field β carried as `room_id` (`analysis_id == room_id`), per REPO_STATUS Β§4/Β§11. Action for Go: send the id as `room_id` | | |
| | 10 | API endpoint doc (MD), 4 endpoints, for Go integration | Rifqi + Sofhia | β | Done 2026-06-25 β `API_ENDPOINTS.md` (repo root). 4 FE surfaces with request/response **examples** (chat SSE transcript, report 201/409 JSON, version list), schemas, Β§9 full 32-route inventory + task-8 reading | | |
| | 11 | Full Python project doc (MD β PDF/Word BRD) | Rifqi | β | Done 2026-06-26 β `PROJECT_BRD.md` (repo root): purpose/context, FR-1..9 capabilities, lifecycle, architecture, data model, API (β API_ENDPOINTS), NFRs, integrations, open items. Reuses REPO_STATUS/API_ENDPOINTS; convert to PDF/Word for distribution | | |
| | 12 | Reconcile/open the `list_tools` PR cleanly (stacked commits) | Rifqi | β | N/A β we develop directly on the single active branch `pr/4` (KM-652 + KM-678 already stacked there); no separate PR to reconcile | | |
| | 13 | Deploy HF Python build (remove-`problem_statement` work) β test 4 endpoints via Swagger / playground | Sofhia + Harry | π | **Unblocked (#15 β ).** Remove-PS work is on `pr/4` but **not on HF `main` yet** β PR + deploy, then manual test. Harry stabilizes Go ~Fri; FE testing ~Mon | | |
| | 14 | `analysis_records` home | Rifqi + Sofhia + lead | β | **Resolved 2026-06-25:** stays Python-owned, **renamed** (β #21); schema handed to Harry so the dedorch migration creates it post-cutover (β #22). Not moved to Go | | |
| | 15 | Flip `ENABLE_SLOW_PATH=true` + verify an `AnalysisRecord` persists from a `structured_flow` question | Rifqi | β | Verified locally 2026-06-25 (in-process). structured_flow on Titanic.csv β 3-task plan `check_dataβretrieve_dataβanalyze_aggregate` (all success) β AnalysisRecord persisted (substantive) β `report_floor` pass β report generates (201). HF env-flip + Swagger run folds into #13 | | |
| | 16 | Decide `analysis_records` creation under `SKIP_INIT_DB` | Rifqi + Harry | β | **Resolved 2026-06-25:** Python defines it; **Harry's dedorch migration creates it** on env-move (Python still creates locally meanwhile) β exists post-cutover. Execution = #22 | | |
| | 17 | Reconcile report contract with records-based: remove/flag `chat_history` | Sofhia/Rifqi | β | Nothing to remove β `chat_history` was never in the report contract/code (only in help.md). Confirmed via grep; API_ENDPOINTS.md Β§5 documents the clean contract | | |
| | 18 | Confirm Go owns ALL analysis-state writes + both creation gates; make Python `state_store.ensure` read-only | Rifqi + Harry | β¬ | **Confirmed by 2026-06-25 checkpoint** (Python read-only; Go owns writes + new tables). Execution pending Go endpoints | | |
| | 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 | | |
| | 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 | | |
| | 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** | | |
| | 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 | | |
| | 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 | | |
| | 24 | Clarify report input contract: records table (+ `last_report` for edit mode?) | Rifqi/Sofhia β Harry | β¬ new | Edit-mode input left open at the checkpoint | | |
| | 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 | | |
| | 26 | **Charts (DEFERRED):** store Plotly JSON in a future `chart` table (not matplotlib PNG) | β | π | **Landed 2026-07-13 (pr/16, Β§0.6 V1βV3):** `render_chart` + `message_charts` + `GET /api/v1/charts`, Plotly JSON as decided. π pending the dedorch DDL run (V5) + live e2e | | |
| | 27 | **Images (DEFERRED):** image table (id, analysis_id, msg/report ref, order) + originals in a bucket | β | βΈοΈ | Maintenance-heavy; parked | | |
| | 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 | | |
| | 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 | | |
| | 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 | | |
| | 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** | | |
| | 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** | | |
| ## 0.7. pr/19 β code review remediation (2026-07-23) | |
| From the end-to-end review in `CODE_REVIEW_2026-07-23.md` (findings are cited as **F-n** there) | |
| plus the live report bug on analysis `966224d4β¦`. Same status legend as Β§0. | |
| | # | Task | Owner | Status | Note | | |
| |---|---|---|---|---| | |
| | 33 | **Report body vs floor split** β `has_reportable_result` for the body, `has_successful_analysis` stays the floor | Rifqi | β | Shipped 2026-07-23. Root cause: planner R2/R2b make `analyze_*` optional, so a correct analyze-free run was classed non-substantive, dropped from the body, and its business question rendered **"Unanswered"**. Live-verified by Rifqi on `966224d4β¦` | | |
| | 34 | **Report floor extension** β a successful `retrieve_data` **that returned rows** clears the floor | Rifqi | β | Same root cause; fixes a hard **409** for a session where every question is R2/R2b-shaped. Guardrail-adjacent, authorised 2026-07-23. NOT the "Floor Fixer" failure mode: the floor still asks "did we produce a real result" β empty retrievals, `check_*`-only and fully-failed runs all still fail it | | |
| | 35 | **`GET β¦/records` `substantive` flag** repointed to the body predicate + contract Β§records updated | Rifqi | β | The curation list was contradicting the artifact it curates. Behavioral, non-breaking: no field added/removed/retyped | | |
| | 36 | **CK5b** β quality checkpoint covers analyze-free plans | Rifqi | β | CK5 only inspected `analyze_*` tasks, so an all-null aggregate column on an R2/R2b plan reached the answer unflagged. `check_*` excluded (uncounted tables legitimately carry nulls) | | |
| | 37 | **F-2 service-secret gate** β `X-Dataeyond-Service-Secret`, router-level dependency | Rifqi | βΈοΈ | Shipped inert 2026-07-23. **UNWIRED 2026-07-27 (lead decision).** Verified in `E2E-Frontend-Data-Eyond/src/services/agenticApi.ts` that the **browser SPA is the only caller** of Python and we don't own it β it sends only `Content-Type`, and cannot be changed by us to send the header. So the gate could never be armed without a 401 outage: a wired-but-unarmable gate is a footgun (any operator setting `dataeyond__service__secret` breaks prod). Guard dependency removed from all six router mounts in `main.py`; `service_auth.py` kept in-tree but parked (comment-out-don't-delete); the `dataeyond_service_secret` setting commented. A prepared FE proxy (Node `server.js` holding the secret and injecting the header server-side) is the way to arm it later without exposing the secret to the browser β but that needs FE-repo access we don't have. **Net: the live surface is unauthenticated by design; real auth = #43. F-1 tenant predicates (#38) stay** as defensive-in-depth. | | |
| | 38 | **F-1 tenant scoping** β `user_id` predicate on the six analysis-keyed reads | Rifqi | β | `CatalogStore.get_by_analysis` filtered on `analysis_id` alone where Go filters on both; the catalog payload carries the owner's `user_id`, so `DbExecutor`'s ownership check compared the victim's id against itself and passed β cross-tenant **query execution against a customer DB**. Defence-in-depth only until #37 is armed (`user_id` is caller-supplied, and `GET /traceability` leaks it) | | |
| | 39 | **Stale tests resolved** β the two long-standing suite failures | Rifqi | β | Both encoded the pre-2026-07-13 user-scope fallback that `reader.py` deliberately removed. Not product bugs. Suite is now **394 passed / 0 failed / 7 skipped** β fully green for the first time | | |
| | 40 | ~~**F-3** β scope `GET /charts` + `GET /traceability` by `user_id`~~ | Rifqi | β | **RESOLVED 2026-07-23 β DECLINED by Rifqi. Do not re-open without his sign-off.** Both endpoints keep their existing lookup keys: `/traceability` by `(analysis_id, message_id)`, `/charts` by `message_id` alone (the 2026-07-13 lead decision). F-3 proposed adding a `user_id` parameter to both; an optional-param version was implemented on 2026-07-23 and then **reverted in full** (endpoints, charts-store predicates, and the contract notes) once the decision was restated β no FE change is required and none should be requested. **Accepted consequence:** both endpoints remain unauthenticated capability URLs over real customer data (`charts[].spec.plotly.data` is actual table values; the traceability payload carries 5-row previews, the executed SQL, and the owner's `user_id`). **The service-secret gate (#37) is therefore the only control protecting them** β which raises #37 from important to load-bearing. The stores' optional `user_id` parameters are kept as dead capability for a future Go-forwarded identity (#43); `PostgresChartStore` was returned to its message_id-only form. | | |
| | 41 | **F-12 / F-13** β bound the planner catalog render and the tabular blob read | Rifqi | β | Shipped 2026-07-23. **F-13:** neither storage backend could report an object size, so `object_size()` was added to both (S3 `head_object`β`ContentLength`, Azure `get_blob_properties().size`, each returning None rather than raising) and `TabularExecutor` now refuses >500 MB **before** downloading, with a post-download byte check as the fallback when the probe is unavailable. **F-12:** `render()` gained TWO ceilings β `_MAX_TABLES=150` and `_MAX_CATALOG_CHARS=250_000` β because a table-count cap alone leaves wide tables unbounded (20 tables Γ 300 cols is as fatal as 400 Γ 30). Measured after: 400Γ30 went from ~241k tokens to ~63k; 100Γ30 (~45k tokens) still renders **in full**, so no realistic catalog is touched. Truncation emits an explicit "N more tables not shown" line so the planner knows it saw a subset, and logs β that log is the signal to retune. 12 new tests | | |
| | 42 | **F-20 observability** β `degraded_seam=<name>` on every never-throw / silent-drop path | Rifqi | π | The 2026-07-23 report bug was invisible by construction: the record was dropped with zero logging. **Partially shipped 2026-07-23** β the 10 seams where silent degradation is user-visible now emit a stable `degraded_seam` field (+ `repr(e)` instead of `str(e)`, so an empty-`str()` Fernet error is no longer a blank log): `input_guard_fail_open`, `analysis_catalog_read`, `report_floor_record_read`, `traceability_persist`, `traceability_flush`, `chart_persist` (Γ2), `report_input_persist` (Γ2), `analysis_state_ensure`. **Remaining:** the other ~76 `except Exception` sites, most of which are in unwired routers (`db_client`, `data_catalog`, `users`) or non-live paths β deliberately not swept, since a blanket edit across unwired code is exactly the drive-by Β§7A forbids. Control flow unchanged throughout (Β§5.4) | | |
| | 43 | **Go identity contract** β what does Go forward, and when? | Rifqi β Harry | β¬ | **Now the sole path to caller auth** after #37 was unwired 2026-07-27. Python cannot authenticate the caller alone; either Go forwards a verified per-user token, or the FE (once we can change it) forwards the Go bearer token it already holds (`orchestrationApi.ts` shows the FE has one). Until then the live surface is unauthenticated and the #38 predicates are defensive only. Options for arming interim protection when FE access returns: the `server.js` BFF proxy (secret server-side) or route agentic calls through Go | | |
| | 44 | **F-17 / F-18 / F-25 compiler-parity batch** | Rifqi | β new | Shipped 2026-07-23. Three execution-verified review findings that had **no task row** β the tracker jumped from #39 to #40 and lost them. **F-17 (High):** the bare-select check was gated on `if ir.group_by`, so a mixed select with `group_by=[]` passed; Postgres then failed loudly but the pandas path silently DROPPED the column while `output_columns` still advertised it β a real-looking table with a fabricated all-null column. Fixed in `validator.py` (fires whenever any agg is present β no false positives possible) + a presence backstop in `TabularExecutor`. **F-18:** `astype(str)` ran before `na=False`, so `NULL LIKE '%an%'` matched the literal `"nan"`/`"None"`. **F-25:** `SqlCompiler` raised on an empty `in`/`not_in` where pandas and `_column_values`' own docstring implement the empty-set semantics β a two-step plan whose first step legitimately returned zero rows hard-failed instead of answering. 16 new parity tests; one stale test updated (it pinned the old F-25 raise) | | |
| | 45 | **F-9 PII in persisted artifacts** β mask the traceability preview + report evidence tables | Rifqi | β | Shipped 2026-07-24. `pii_flag` was an **ingestion-time control only**: it nulls `sample_values` into the planner prompt, but nothing stops the planner SELECTing a flagged column β and "list our top 20 customers" legitimately selects `customer_name`/`email`. Real values then reached two PERSISTED sinks: `message_traceability.data` (served by an unauthenticated GET, F-3 declined) and report evidence tables frozen permanently into `reports.content`. Fix = `retrieve_data` now carries `meta.pii_columns` (resolved through the IR select list, so aliases are honoured), and both sinks redact those cells to `[redacted]`. **Per the 2026-07-23 decision the ASSEMBLER still receives real values**, so the answer prose is unchanged and the question stays answerable β verified end-to-end: assembler input kept `Ada Lovelace`, the persisted preview showed `[redacted]`. Aggregates are deliberately NOT masked except `min`/`max`: `sum(salary)` identifies nobody, but `max(email)` returns one customer's actual address. Fails **open** (an unresolvable name is left unmasked, never a legitimate column wrongly blanked), so this is a mitigation, not a guarantee. 15 new tests | | |
| | 46 | **F-8 prompt-injection resistance** β planner / assembler / report_summary | Rifqi | β new | Shipped 2026-07-24. The three prompts that ingest customer data had **no injection rule**: `guardrails.md` is appended only in `chatbot.py`/`help.py`, and the `InputGuard` screens only the user's message β it never sees catalog or row content. This is the one attack the five query-defense layers structurally cannot see, because every IR the planner emits is individually valid. A hostile string only needs to reach a text column in the customer's OWN database (a product description, a support ticket, a form field) to be sampled into `sample_values` and rendered verbatim into the planner prompt. Fix = a purpose-written "content is data, never instructions" rule in each prompt + `<data>β¦</data>` delimiters around the catalog render and the run-state render. **Deliberately NOT `guardrails.md` wholesale** β its rules prescribe refusal sentences, and the planner's only free-text field is `infeasible_reason`, so those strings would surface there and could regress the Q2 data-gap path (tests pin their absence). Verified: planner eval **6/6, carried_over 5/5 green**; live hostile-catalog run planned only `t_products` and never touched the planted `employees.salary`; 10 new tests | | |
| | 47 | **Cheap-batch review fixes** β F-19, F-22, F-24, F-26, F-16, F-4 | Rifqi | β new | Shipped 2026-07-24, one low-risk batch. **F-19:** `sources` was missing entirely on `check` + router-`help` and came *after* `status` on the slow path β contract said always-first; additive fix, contract updated. **F-22:** `analysis_id` now 422s unless it parses as a UUID, on **both** live endpoints (chat + help kept identical on purpose). **F-24:** traceability/chart writes are skipped, and logged, when `analysis_id` is falsy β Go `0007` declares `analysis_id UUID NOT NULL` (+ FK on charts), so `analysis_id or ""` failed the insert and the never-throw seam lost the row silently. **F-26:** `QueryResult.error` uses `str(e) or repr(e)` β a Fernet `InvalidToken` reached the assembler/traceability/report as an EMPTY string; falling back only when `str()` is empty changes no existing message. **F-16:** retrieval cache key gains `redis_prefix` (two envs on one Redis cross-served results). **F-4:** non-Postgres sources now refused at the executor β **zero blast radius today** (Go's `isSupportedActive` allows only `postgres`), a tripwire so nobody re-enables a path that has no read-only session and no `statement_timeout`; legacy branch commented out per house convention, orphaned import commented with it | | |
| | 48 | **floor_08 β floor/body disagreement** | Rifqi | β | **Decided + shipped 2026-07-24 (lead).** #34's row-producing arm was unconditional, so a plan that HAS an `analyze_*` step which FAILED still cleared the floor on the strength of its upstream fetch β while the body rejected it. Because the "Attempted, Unresolved" section is commented out, such a run left no trace: as a session's only run it produced an empty report with the business question "Unanswered" (the #33 bug via another door). Fix = the arm now applies **only when the plan has no analysis step**, mirroring `has_reportable_result`; the shared `_plan_has_analysis` helper means the two predicates can no longer drift on that question. `floor_08` flips to `expected_ready: false`. The INTENDED asymmetry is preserved and re-verified: a zero-row retrieval still fails the floor but passes the body (floor_03). Readiness eval **17/17** | | |
| | 49 | **F-5 timeout does not stop the customer's query** | Rifqi | β new | Shipped 2026-07-24. `asyncio.wait_for` cancels the awaiting **coroutine**; the `to_thread` worker is not cancellable and runs to completion, holding a thread and a connection on the **customer's** database after we already answered "timed out". Two fixes. **(a) Dedicated bounded pool** β DB work moves to its own `ThreadPoolExecutor(50, 'dbexec')` via `run_in_executor`; abandoned workers previously accumulated in the shared default pool (`min(32, cpu_count+4)`) alongside every other `to_thread` caller, notably the tabular Parquet loader, so a few slow customer queries could stall unrelated work process-wide. **(b) Session hardening is no longer best-effort** β and a **latent bug** was found while reading it: both SETs shared one `try`, so a `statement_timeout` failure **skipped `default_transaction_read_only` entirely** and the connection served queries in a WRITABLE session behind a `logger.warning`. Now independent: read-only **fails the connection** if it cannot be set (a writable session against a customer DB is not something to degrade into); `statement_timeout` logs at **error** with `degraded_seam` but does not refuse service, since it is their I/O at risk rather than our correctness. β οΈ **Blast radius to watch:** if any deployment currently fails that SET silently, its sources now fail loudly instead. Believed impossible (Neon accepts it as a SET β the existing comment documents this), but it is the one judgement call here. 7 new tests | | |
| **Reading `eval/readiness/results/` (note for future sessions).** Four files are dated | |
| 2026-07-23. `β¦_150632.json` scores **4/15 (26.7%)** β that is **not** a product | |
| regression. It is the run that exposed the eval harness itself being broken by | |
| `fd4865b` (`_FakeRecord` had no `results_snapshot` for #34, `_FakeStore` took no | |
| `user_id` for #38; both errors were swallowed by `report_floor`'s fail-closed seam, so | |
| every case reported "not ready"). `β¦_150859` (15/15) is post-harness-fix, `β¦_150948` | |
| and `β¦_152615` (17/17) add the two cases that exercise #34. **The current baseline is | |
| `β¦_152615.json` β 17/17.** The intermediate files are kept as the audit trail for the | |
| drift; per Β§7F no result file is ever deleted or overwritten. | |
| **Not re-raised:** F-4 (non-Postgres read-only/timeout gap) was **downgraded to latent** β | |
| Go's `database_clients.Service.Create` enforces `isSupportedActive`, and only `postgres` is | |
| `active`, so no such source can be registered today. | |
| **Decided 2026-07-27 (lead) β F-2 gate unwired, not armed.** The service-secret gate (#37) was | |
| removed from the router mounts because the sole caller is a browser SPA we don't own and can't | |
| change to send the header; arming it would 401 the whole app. The live surface is **unauthenticated | |
| by design** until #43 (Go-forwarded identity). Not a gap to re-raise β it is a recorded posture. | |
| CORS was left at `["*"]` on purpose (tightening it needs the FE origin as config, which we chose | |
| not to set for now). | |
| ## 0.8. Knowledge pipeline β test integration (2026-08-19 β **building, 2026-08-21**) | |
| > ## Updated 2026-09-11 β the open-queue pass (K17βK22) | |
| > | |
| > Six items off `knowledge_pipeline_runs/SESSION_HANDOFF.md` Β§4, **zero API spend**: every number | |
| > below comes from replaying the six persisted paid runs or re-running the free stages on the three | |
| > v2 artifacts. Detail in REPO_STATUS Β§14; per-item rows K17βK22 in the table below. | |
| > | |
| > **The headline is Step 2**, the item everyone agreed on and nobody had done: the review queue put | |
| > 24 unresolved key parameters in front of Komatsu's `Engine` (108 mentions, rank 25). It is rank 1 | |
| > now, with nothing dropped. | |
| > | |
| > **Two findings worth reading as findings, not as changes:** | |
| > 1. **The spend ceiling was gating on a partial number.** `estimate_cost` summed only the glossary | |
| > branch under a field named `estimated_prompt_tokens`. Real undercount 8.8%β18.4%; Open Pit is at | |
| > **78%** of the ceiling, not 71%. | |
| > 2. **Escalation has never worked.** 48 calls across three documents and two configurations, | |
| > **0 definitions** recovered. Retired by constant, loop left in place. | |
| > | |
| > **What is deliberately NOT switched on:** genre routing. The classifier is right on all three | |
| > documents, but the label sets are unscoreable without a second gold set and branch-skipping rests | |
| > on a single document, so `ROUTE_LABELS` / `ROUTE_BRANCHES` are both off and the verdict is recorded | |
| > rather than acted on. **Flipping either is Rifqi's call and one line.** | |
| > | |
| > **Still open and unchanged:** the paid Step 0 band (needs authorised repeated runs), Phase 4's | |
| > vision call, Step 8, Step 7, R4, and E5 β Sofhia still has not seen the `src/knowledge_parsing/` | |
| > diff. | |
| > ## Updated 2026-09-11 β v2: clustering fix + parsing 0.4.0 (asset pointers) | |
| > | |
| > Two changes, measured on three documents (BUMA standard Β· Komatsu shop manual Β· Open Pit textbook | |
| > Ch.1) at **zero API spend** for the free stages β the parse cache fingerprint is unchanged, so | |
| > re-parses reuse the cached Mistral responses. Full write-up: | |
| > `knowledge_pipeline_runs/CHANGELOG_v2.md`. | |
| > | |
| > **The fuzzy clustering pass is RETIRED (K13).** `token_set_ratio` returns exactly 100 when one token | |
| > set is a subset of the other, so a head noun always absorbed its own compounds and the threshold of | |
| > 92 was **inert**. 131 of its 140 merges were subset matches (all wrong); 6 were plurals, now folded | |
| > deterministically; 3 were distinct metrics wrongly merged. Token-subset merges **15/18/97 β 0**, and | |
| > post-cluster gold recall **0.8293 β 0.8537** β the gold term `EWH`, which `Working Hours` had | |
| > absorbed, is recovered. `_fuzzy_match` is parked, not deleted. | |
| > | |
| > **Parsing `schema_version` 0.4.0 (K14).** A figure is no longer its own chunk. It was, with an EMPTY | |
| > `text`, which made it unreachable by construction β **13 figures across three documents, each | |
| > described by a vision model we pay for, and ZERO reaching any prompt.** Now the `` | |
| > reference is folded into the prose chunk at its reading-order position and the figure travels as an | |
| > `Asset` (content-addressed, persisted bytes, verbatim caption, unwrapped description). Image chunks | |
| > **2/3/8 β 0/0/0**; recall unmoved (E1 and E1c both identical v1βv2). Tables deliberately stay their | |
| > own chunk. `adapter.py` now carries `assets`, `referenced_by` and **`table_html`**, which parsing had | |
| > emitted for months while the adapter dropped it. | |
| > | |
| > **New metric E1c (post-cluster recall)**, and **E2's compression kill line REMOVED** β over-merging | |
| > *raises* compression, so the metric was rewarding the defect it should have exposed. | |
| > | |
| > **Two settings changes measured and REJECTED:** `WINDOW_WORDS` 256 (truncates 46 of 99 Open Pit | |
| > windows β the "flat recall" justifying it was measured on BUMA, the one document whose truncation | |
| > count does not vary with window size) and `SPAN_SCORE_THRESHOLD` 0.15 (+50% clusters against a +20% | |
| > bar). `EVIDENCE_K` raised 3 β 5 on Rifqi's call; measured cost +1.8% to +15% evidence tokens, and on | |
| > the one gold-set document top-5 recovers the same 16/18 as top-3. | |
| > | |
| > **Export for the team:** `knowledge_pipeline_runs/06_export_knowledge_parsing_v0.4.0/`, verified to | |
| > import and validate a fixture with no repo on `sys.path`. β οΈ `src/knowledge_parsing/` is Sofhia's | |
| > half β she needs the diff before it goes out. | |
| > | |
| > **Paid comparison landed 2026-09-11 (K15).** Definitions 7β10 / 0β0 / 25β49; queue junk rows down | |
| > 37β65%; Komatsu still 0, as predicted. Caveats: the `source_wording` gain is largely tautological, | |
| > and **no definition cited a figure caption on any document** β assets are reachable but unused, which | |
| > points at the caption-to-filter path (built, flag off) as the next free experiment. Open Pit now sits | |
| > at **71% of the 2M spend ceiling** for one chapter. | |
| > **Updated 2026-08-21.** The banner here said *"Nothing has landed in this repo"* β that is no | |
| > longer true. **Both halves are now in-tree** (`src/knowledge_parsing/` + `src/knowledge_extraction/` | |
| > + `eval/knowledge/`, ~5,360 lines across commits `024c30aβ¦a7bdeae`), and three dependencies were | |
| > added. ~~Still true, and the part not to misread: **no HTTP surface, no database table, and nothing | |
| > mounted in `main.py`** β both halves are offline CLIs.~~ **Superseded 2026-09-07: there is now an | |
| > HTTP surface (8 routes) and five tables.** See the 2026-09-07 delta below and REPO_STATUS Β§14. | |
| > **Updated 2026-09-07 β what this pipeline is for, and what Data Eyond actually needs.** Recorded | |
| > because it changes the definition of done. The review loop, the admin ingestion role and the | |
| > curated-artifact lifecycle belong to a **separate B2B knowledge product**, where a company admin | |
| > configures the company's knowledge and an expert reviews candidates before the company's normal | |
| > users consume them. **This repo is where that concept is being tested cheaply, on real documents.** | |
| > **Data Eyond's own version is one added step:** a user clicks a button and knowledge is extracted | |
| > from **all of that user's documents in one batch** β no admin role, no review queue, no curation UI. | |
| > Full framing: [knowledge_pipeline_context.md](docs/knowledge/knowledge_pipeline_context.md) Β§1.5/Β§1.6. | |
| > | |
| > Consequences already folded into the docs: | |
| > - **Multi-document semantics settled** β entries **append, never merge**; `entity_id` stays | |
| > document-scoped; cross-document overlap is *classified* (`new`/`duplicate`/`conflicting`) and | |
| > surfaced, never silently collapsed; `DomainContext` stays per document. A canonical company-level | |
| > concept layer is **deferred to Product B** β it needs an expert to rule on conflicts, and Data | |
| > Eyond's flow has none. | |
| > - **Persistence contract revised to v2** (five tables): Q1 closed β `documents.id` is `text` PK, | |
| > verified by read-only introspection of live dedorch; **`knowledge_jobs` added** (batch envelope, | |
| > and a parse failure has nowhere else to live); and **`scope_id` β company scope β added to every | |
| > table** (Rifqi, 2026-09-07): knowledge belongs to the company, not the uploader, and a per-user key | |
| > means two employees' documents never diff against each other. dedorch has **no tenant entity** | |
| > (15 tables, none of them one) and `users.company` is nullable free text, so the value is provisional | |
| > β normalised `users.company`, else `user:<user_id>` β and **`scope_id` is deliberately excluded from | |
| > the `entity_id` derivation**, which is what keeps re-pointing it a value migration rather than a | |
| > re-key that would destroy expert approvals. Raised to Harry as **Q6**: we want a real `companies` | |
| > table. | |
| > - **N7 closed.** "MinerU cannot install in the Space" predates the Mistral backend; Mistral OCR is | |
| > a hosted API. **Corrected 2026-09-07 β "void" was too strong:** the `python-docx`/`pypdf` conflict | |
| > was making `uv lock` fail for the *whole project*, silently blocking every dependency change. Closed | |
| > by **dropping MinerU** from the extra (`26ba1ab`), not by lifting the pin. **N7b done** in the same | |
| > change: torch pinned to the Linux CPU wheel, 36 `nvidia-*` packages and `triton` gone from the lock. | |
| > GLiNER *is* the term filter, so torch cannot become an optional extra if extraction runs in the Space. | |
| > - **D4 re-opened.** "Admin/service-auth" was right for Product B; Data Eyond's caller is the browser | |
| > SPA, which is the #37 problem. Rate limit + pre-flight spend ceiling + idempotency bound the damage | |
| > but do not identify the caller β Rifqi's call, see the surface plan Β§6. | |
| > - **Q5 for Harry:** `message_traceability` and `message_charts` exist in dedorch (80 and 6 rows, | |
| > `idx_message_charts_message` present) but were created by hand and are **not in Go's migration | |
| > set** β a fresh environment comes up without them and both write paths fail silently. | |
| Context and intent: [knowledge_pipeline_context.md](docs/knowledge/knowledge_pipeline_context.md). Task breakdown, | |
| owners and verified statuses: [KNOWLEDGE_PIPELINE_TODO.md](docs/knowledge/KNOWLEDGE_PIPELINE_TODO.md). | |
| **Shape.** A six-stage pipeline (parse β free CPU/regex filter β one LLM call per *term cluster* β | |
| deterministic span validation β diff vs. the active glossary β frequency-sorted expert review) that | |
| turns client documents into reviewable knowledge entries, ultimately served over MCP. It splits into | |
| two independently runnable halves β **parsing (Sofhia, Mistral OCR; MinerU dropped 2026-09-07)** and **knowledge extraction (Rifqi)** β meeting only at a **persisted, versioned | |
| parsed-document artifact**. Extraction never | |
| takes a file path; that constraint is the whole point of the split. | |
| **Where it started (audited 2026-08-19).** A working **prototype** (`kex`, ~2,763 LOC, 21 passing | |
| tests, 8-command CLI, four experiments against real Azure calls) exists outside version control at | |
| `Documents/Dataeyond_catalog/knowledge-pipeline/`. It is **not the product and was not ported** β | |
| it answered the design questions; extraction v2 was rebuilt here. What carried forward is the | |
| *evidence* (gold set, experiment results) and the *tuned constants*, not the code. | |
| Measured on the BUMA standard (9 pages): GLiNER Indonesian recall **0.854** (kill line 0.70, PASS β | |
| this closes the context doc's headline open risk), clustering compression **2.56Γ** (kill 2.0, PASS), | |
| nano schema-fill precision **0.75** (kill 0.80, **FAIL**), span-validation rejection **1.00** with | |
| 0.00 false rejections (kill 0.90, PASS). Cost **$0.0069** for the document. | |
| **Where it stands (2026-08-21).** Both halves run end to end as offline CLIs, on the same document: | |
| | | | | |
| |---|---| | |
| | `python -m src.knowledge_parsing.run --input <folder>` | PDF/DOCX β versioned `ParsedDocument` | | |
| | `python -m src.knowledge_extraction.cli <artifact.json> --extract` | artifact β entries + review queue | | |
| The seam holds from both sides: `ParsedDocument` is the only shape crossing it, extraction takes no | |
| file path, and local seam tests import `knowledge_parsing.contracts` directly so a rename on either | |
| side fails loudly. **Rebuild verified against the frozen baseline: term-filter recall 0.8537, | |
| identical to the prototype** (`eval/knowledge/results/v2_term_filter_2026-08-19_144318.json`). | |
| A live 3-call pilot on `gpt-5.4-nano` returned correct Indonesian definitions with 0 span | |
| rejections and 7,168 of 8,029 prompt tokens served from cache. | |
| **Not done, and not to be read as done:** no persistence (stage output is JSON on disk β D2 still | |
| owes Harry a DDL block) and no HTTP surface. ~~no full-document extraction run, and **E3 has never | |
| been scored on v2**~~ β **RESOLVED 2026-08-27 (K9):** the full-document run is routine now and E3 | |
| scores **1.000** on a committed fixture; see the K9 row for how to read that number. | |
| | # | Task | Owner | Status | Note | | |
| |---|---|---|---|---| | |
| | P1 | Freeze the `kex` prototype (read-only snapshot) | Rifqi | β | **2026-08-19: kept as-is, unversioned β Rifqi's call.** Risk accepted, and much reduced once P2/P3 lifted the two things that could not be recreated | | |
| | P2 | Harvest the prototype's tuned constants | Rifqi | β | **Done** β `KNOWLEDGE_PIPELINE_CALIBRATION.md`, 10 sections: every value with the reason it has that value, plus Β§7 negative findings and Β§9/Β§10 the deviations made while rebuilding | | |
| | P3 | Migrate the evidence into `eval/knowledge/` | Rifqi | β | **Done.** Gold set (41 terms + 15 rules), ported scorer, frozen baseline. Migration proved faithful: the ported scorer re-derives the prototype numbers exactly | | |
| | S1 | Agree the parsed-artifact schema | Sofhia + Rifqi | β | **Settled 2026-08-21.** `src/knowledge_parsing/contracts.py` (`schema_version 0.2.0`; **0.3.0** as of the 2026-08-27 fixture) is the shape of record and the extraction adapter consumes it directly. Envelope, verbatim heading, 0-based `page_idx`, prose `text` with `latex`/`table_html` beside it | | |
| | S5 | Keep the seam off the file path | Sofhia + Rifqi | β | **Satisfied both sides.** `ParsedDocument` carries no handle to the parser, and `knowledge_extraction.adapter` accepts only the artifact. Locked by local seam tests importing the parsing contract directly | | |
| | X* | Build extraction v2 in this repo | Rifqi | β | **Done 2026-08-21** β `src/knowledge_extraction/`: adapter Β· filters (span/cue/legend) Β· cluster Β· rank Β· extract (4 branches) Β· validate Β· diff Β· queue Β· service facade Β· CLI. Recall matches the frozen baseline exactly | | |
| | D2 | Artifact + candidate-entry persistence | Rifqi β Harry | β (Python) / β¬ (Go) | **Built and live 2026-09-07.** Five tables created in dedorch (`docs/knowledge/knowledge_tables.sql`), ORM models, `knowledge_store.py`, `PostgresJobStore`. Verified end to end: 88 entry rows persisted from the BUMA standard. **β οΈ Harry must still fold the SQL into Go's migration set** β the tables exist only because they were run by hand, the same gap that left `message_traceability`/`message_charts` out | | |
| | D4 | Admin ingest surface | Rifqi | β | **Built 2026-09-07.** 8 routes under `/api/v1/knowledge` (contract Β§Knowledge): ingest + poll, re-extract, entries, queue, reviews. Async batch (202 + poll, not SSE), company-scoped, rate-limited, spend-gated, single-flight. **Auth posture recorded, not solved:** unauthenticated like the rest of the surface, and `POST β¦/reviews` accepts content that reaches a later prompt β the strongest case yet for #43 | | |
| | R1 | Fix the literal-source-wording violation | Rifqi | β | **Fixed and live-verified 2026-08-21.** `source_wording` is read from the document's own heading rather than asked of the model (asked directly, it quoted the normalised page-1 form); the span haystack includes headings; the queue flags the mismatch. PA now surfaces as *"source wording differs from the expanded name"* | | |
| | D3 | Provision a mini deployment | Rifqi | βΈοΈ | **Deferred 2026-08-19: staying on nano.** The `__54n` quad is wired in `src/config/settings.py`. Accepted knowingly β nano measured 0.75 against a 0.80 line, and `rule`/`summary` run on the tier whose failure mode is least detectable | | |
| | D6 | Expert review of the gold set | Rifqi β Mas Beta | βΈοΈ | **Deferred: provisional gold is good enough for now.** Standing caveat on every score, E1 and E3 both move when it is reviewed | | |
| | D5 | v2 module name | Rifqi | β | **`src/knowledge_extraction/`**, beside Sofhia's `src/knowledge_parsing/`. Neither collides with the existing `src/knowledge/` OCRβpgvector service, which is untouched | | |
| | D1 | New dependencies | Rifqi | β | **Added 2026-08-21.** `rapidfuzz`, `gliner`, `torch>=2.6` as main deps; **`mineru==3.4.4` as an optional extra** (`.[knowledge-parsing]`) so the deployed Space never builds it. **Updated 2026-09-07 (`26ba1ab`): `mineru` dropped** β it made `uv lock` fail project-wide; `torch` pinned to the Linux CPU wheel. The Β§6 gliner/torch question is now RESOLVED | | |
| | K9 | Full-document extraction run + score E3 on v2 | Rifqi | β | **Done β latest run 2026-08-27** (`eval/knowledge/results/v2_full_2026-08-27_133757.json`, 90 calls, 170 s). E1 **0.8293** / E2 **2.556Γ** / E3 **1.000** β all three above their kill lines; rule recall 0.4286 (6/14, tracked not gated). **The prototype's failing experiment now passes**, but read E3 as *precision on a scoreable base of 6*: 57 of 63 entries abstain, so recall against the 18 gold terms with criteria is 0.33. 0 fields rejected by the span check | | |
| | K10 | Fixture artifact committed | Rifqi | β | **Done 2026-08-27** β `eval/knowledge/fixtures/parsed_STD_2026_006_MNO.json` (BUMA standard, MinerU 3.4.4 `hybrid`, `schema_version 0.3.0`, 31 chunks). The eval and the extraction CLI both run with MinerU absent. The Open Pit textbook fixture is still open (S7 in the checklist) | | |
| | K11 | Output schema v2 (Harry's contract review) | Rifqi | π | **Landed 2026-08-26, unevaluated.** Deterministic entity ids, glossary references formulas instead of restating them, rules link to formulas and terms, 16 fields cut (50 β 42). Contract: [docs/knowledge/KNOWLEDGE_OUTPUT_CONTRACT.md](docs/knowledge/KNOWLEDGE_OUTPUT_CONTRACT.md); detail in the checklist as X24. Fixed two live defects β neither `cluster_id` (a frequency ordinal) nor `rule_id` (model-written) survived a re-run, which breaks an expert's approvals, not just links. Suite 497 passed / 7 skipped; **no eval run** β the prompt edits join X20's in the unevaluated queue | | |
| | K12 | Output schema v3 β `DomainContext` | Rifqi | β | **Done 2026-09-02** (`7f47c61` Β· `6f5d619` Β· `e2865ac` Β· `172d8dc`; checklist **X26**). `BriefContext` β **`DomainContext`**, renamed while nothing is persisted β afterwards an id-prefix change is a migration that loses an expert's approvals. **The branch is no longer the pipeline's one unverifiable output:** it was made guardable by changing the question rather than the model β the prompt asks it to *locate* the document's own statement of purpose (`purpose` β `purpose_verbatim`) instead of composing a summary, so `GUARDED_FIELDS["summary"]` goes `()` β `("title", "purpose_verbatim")`. Adds derived `outline`, `subdomains` and entry counts; `key_parameters` gains a corroborating `term_id`, and an unresolved one earns its own review-queue row. **Contract change for Harry:** queue rows now carry `row_kind`, and a `key_parameter` row has no `term_id`, definition or mention count. Measured on the BUMA fixture (nano, 12 calls): 0 fields rejected, fabricated purpose refused, key-parameter linking 0/5 β 4/5 after a bug the smoke caught. Suite 541 passed / 7 skipped | | |
| | K13 | **Clustering: retire the fuzzy pass** | Rifqi | β | **Done 2026-09-10.** `token_set_ratio` is blind to the subset case (returns 100), so the threshold never applied and head nouns ate their compounds. Measured across three documents: 131 of 140 merges were subset matches. Now `exact (plural-folded) β abbreviation β expansion` only; `depluralise()` handles the 6 legitimate plural merges. Subset merges **15/18/97 β 0**; E1c_strict **0.8293 β 0.8537** (`EWH` recovered). New metric **E1c** added to `score.py`/`run_eval.py`; **E2's kill line removed** (it rises when the defect fires). Suite 676 passed / 7 skipped | | |
| | K14 | **Parsing 0.4.0 β asset pointers** | Rifqi (Sofhia's half) | β | **Done 2026-09-10.** Figures fold into the prose chunk as `` at reading-order position; `Asset` carries a content-addressed id, persisted bytes, a **verbatim** caption and a **model-written** description that never enters `text`. Image chunks **2/3/8 β 0/0/0**, 0 dangling refs, 0 description leaks, captions 17/17 preserved, **recall unchanged**. Tables keep their own chunk + two-way pointers. Seam: `adapter.py` carries `assets`/`referenced_by`/**`table_html`**; `SUPPORTED_SCHEMA` β (0,4), so parsing and extraction ship together. Found and fixed a Mistral bug: `image_id` mislabelled on one block, collapsing two figures onto one hash. β οΈ **Sofhia needs the diff before the team export goes out** | | |
| | K15 | Paid v1-vs-v2 comparison | Rifqi | β | **Done 2026-09-11**, 3 runs, ~9Β’. Definitions **7β10 / 0β0 / 25β49** β Open Pit nearly doubled; junk review-queue rows β37% (Komatsu) / β65% (Open Pit); cache rate +7% on all three; 0 retries. **Komatsu stays at 0 definitions**, as predicted: a troubleshooting manual states symptoms, not definitions, and no plumbing change reaches that. Two caveats recorded rather than buried: `source_wording`'s headline +1608% is mostly **tautological** (informative 12β101, not 13β222), and **`definitions_citing_a_caption` = 0 on every document** β the assets are reachable but nothing consumed them, most likely because captions do not feed the term filter (flag off by decision). β οΈ Cost: Open Pit is now **71% of the 2M pre-flight ceiling** (was 56%) for one chapter. Three variables moved at once, so no single number is attributable. Write-up: `knowledge_pipeline_runs/V1_VS_V2_PAID_COMPARISON.md` | | |
| | K16 | Caption hypothesis for the 0 caption-citations | Rifqi | β | **Tested 2026-09-11, HYPOTHESIS FALSIFIED.** Both candidate explanations for `definitions_citing_a_caption = 0` are false: captions ALREADY reach the term filter (they are part of `Chunk.text`; 2/2 BUMA and 3/8 Open Pit captions already produce mentions), and figure-bearing chunks are ALREADY selected (a captioned chunk is in the top-5 for 22/83 BUMA and 64/427 Open Pit clusters). Built the missing ranker signal `asset_caption_names_term` to test it properly: at weight 2.5 it changed the top-5 of **1 cluster out of 603** and moved none from no-figure to has-figure. **The real reason: not one caption in the corpus is a definition** β they are figure titles ("Figure 1.3. Relative ability to influence costs"). A model declining to quote one is CORRECT. Signal ships at **weight 0.0** with the measurement beside it. β οΈ Also corrects a false claim in the earlier docs: the caption-to-filter flag was described as "built, off by decision" β it was never built. Redirects the remaining figure value to Phase 4 (vision call on the bytes), which is now the only untested path | | |
| | K17 | **Step 2 β review queue ordering + key-parameter heads** | Rifqi | β | **Done 2026-09-11.** Unresolved key parameters held their own sort tier, so every one outranked every routine term; the branch emits them in bulk on the documents it understands least, so the promotion fired hardest where least deserved. Now spliced at most **1 per 5** term rows. Replayed on the three persisted v2 runs: Komatsu `Engine` (108 mentions) **rank 25 β 1**, Open Pit `Resource` **8 β 1**, first junk row **1 β 6**, **row counts identical** (nothing dropped), BUMA unchanged (0 unresolved). `term` now carries a head (first `.` `:` or newline, only past 60 chars β "No. of units" survives) with the full text in new **`surface_full`**. Contract: OUTPUT_CONTRACT Β§9 | | |
| | K18 | **Step 0 β measurement hardening** | Rifqi | π | **Harness done 2026-09-11, the paid band is NOT run.** `eval/knowledge/invariants.py`: asserts on **fields and invariants, never exact text** β span locatable, entity ids present and unique, a rejected field still null (rejected-never-repaired, checkable only from outside), page in range, queue ranks contiguous, no leaked sort keys. `--runs N` repeats **only the paid stage** (free stages are deterministic) and reports min/median/max with the **median** as the value, plus whether all runs minted identical entity ids β the K11 defect class, unwatched since K11. `EXPECTED_MODEL_SNAPSHOT` turns X25's recording into an **assertion** (exit 6). New exit codes: 5 invariant, 6 snapshot. **Needs Rifqi's go-ahead to produce a real band** | | |
| | K19 | **Step 6 β spend ceiling in cluster units** | Rifqi | β | **Done 2026-09-11.** Found first: `estimate_cost` summed **only the glossary branch** under a field named `estimated_prompt_tokens`, so the ceiling read a partial number as a total β undercount **BUMA 18.4% Β· Komatsu 8.8% Β· Open Pit 15.2%**, moving Open Pit 67.7% β **78.0%** of the 2M ceiling. Now all four branches (glossary exact, the rest approximated from prefix + chunk text, both labelled). Second ceiling **`MAX_CLUSTERS = 750`** β exact, free, and what drives spend; at ~3,200 tokens/cluster the token ceiling still binds first for these documents, so this guards the *other* shape (many cheap clusters, 494 calls β 15 min). Refusals name the ceiling and the declined call count; `IngestJob.estimated_cost` (declared since the job model, **never populated**) now carries the per-document breakdown; a wholly-refused batch settles **`blocked`**, which the contract documented and the code could not produce | | |
| | K20 | **Step 5 β genre classification** | Rifqi | π | **Classifier done and correct on all three documents; routing deliberately OFF.** `src/knowledge_extraction/genre.py` β `standard` Β· `diagnostic` Β· `conceptual` Β· `unknown` from four free signals (heading coverage Β· tabular ratio Β· abbrev pairs Β· rule density), which separate in *different directions* β measured BUMA 46%/29%/5/0.57, Komatsu 1%/74%/0/0.01, Open Pit 86%/7%/0/1.48. **`ROUTE_LABELS` and `ROUTE_BRANCHES` are both `False`**: the genre label sets are unscoreable without a second gold set (more mentions β better recall) and branch-skipping rests on one document. Today it is observability β `well_served=False` warns *before* spend that a shop manual yields almost nothing, and heading coverage raises a per-document warning (**P5**, partly: the warning exists, a parsing-side fallback does not). **Flipping either lever is Rifqi's call** | | |
| | K21 | **Finding 6 β escalation retired** | Rifqi | β | **Done 2026-09-11, with the number Finding 6 asked for.** Across all six persisted paid runs (v1 K=3 and v2 K=5 Γ three documents): **48 escalation calls, 0 definitions recovered** β `extraction_status == "escalated"` appears zero times in every file. Why it cannot work as built: escalation hands the model the *next* k evidence chunks, ranked **below** ones it already declined, and 16 of 18 gold definitions sit at rank β€3. `MAX_ESCALATION_ROUNDS = 0`; loop and call site untouched. Reviving it means changing the **question** (different prompt or tier), not the evidence | | |
| | K22 | **The over-cap window (pre-existing)** | Rifqi | β | **Done 2026-09-11.** `WINDOW_WORDS` caps words; GLiNER truncates on **subwords** and warns rather than failing. Measured with GLiNER's own tokeniser: **1 window of 368** exceeds the 384-token cap β BUMA `STD_2026_006_MNO::0024`, 130 words / 738 chars β **409 tokens** β losing its tail at *every* window size including 130, exactly as the 2026-09-10 sweep suspected and left. Windows are now checked against the real tokeniser and split only when they do not fit (a character budget would have to assume the tightest ratio seen, 1.57, and chop up the 367 that fit). Halves overlap by `SPAN_TOKEN_CAP` words so no term straddles a cut. Fixture: 3 splits, mentions **186 β 208**, clusters 74 β 80 (**+8% spend**), **E1 0.8293 and E1c unchanged** | | |
| | K23 | Step 8 + Step 7 design proposals | Rifqi | π‘ | **Written 2026-09-11, nothing built, both need sign-off.** [TABLE_BRANCH_PROPOSAL.md](docs/knowledge/TABLE_BRANCH_PROPOSAL.md) β deterministic (row header, column header, cell mark) β assertion, one call per **table** (59 on Komatsu, fewer than the 93 the glossary branch spends to abstain 93 times), provenance at the cell; five decisions listed, **T4 (what `table_html` shapes the parser emits) is free and comes first**. [TERM_HIERARCHY_PROPOSAL.md](docs/knowledge/TERM_HIERARCHY_PROPOSAL.md) β parent/child **with** cross-document identity because SciCo's point is they are one problem; the dominating constraint is that **hierarchy must not enter `entity_id`**, or every correction invalidates expert approvals. Sequenced behind **R4** (still not asked) and Step 0 | | |
| | K24 | **The 2026-09-08 discussion list β structure, incremental ingest, review** | Rifqi | β | **Done 2026-09-14 (pr/21, 7 commits).** *Struktur data 7/8:* `label` carries a readable `TYPE_SUBJECT` key (`keys.py`) β prefixes from the `kind` vocabulary, rules borrow the term they govern plus a digest since a rule has no canonical surface, and the key is a **label, never an identity** (`entity_id` stays the hash approvals hang on). `content`/`provenance`/`doc_ids` promoted out of `payload`; `get_by_key()` + `glossary_index()` make lookup O(1) with no model call. **m7 parked** β the `kind` CHECK would reject `'summary'`. *Ingest bertahap 7/7:* same `doc_id` + new `content_hash` = a new VERSION (reuse check moved to any-version, or idempotency would become a paid no-op); an approved term skips the paid call; staleness compares **only the ruled-on fields** (`mutation.py`) because comparing the payload re-opens everything on every ingest and comparing document `content_hash` flips untouched entries; `impacted_by_document()`; withdrawal marks and keeps rather than deletes. *Sistem review 4/4:* `review_mode` at ingest, `decided_by` + `authorised_by` so an LLM-mode approval can say whose decision it was, and `review_diff()` naming which fields moved. **7 columns added to dedorch** (Β§6/Β§7 of `knowledge_tables.sql`, run by hand **after** the handoff went to Harry, verified read-only against live β Harry still needs both in the migration set). Suite **816 passed / 7 skipped** (was 742), ruff clean on touched paths, `import main` 0. **Inert until the review queue has rows:** reuse and staleness both key on approved entries, and `knowledge_reviews` is at 0 | | |
| **Not decided, do not assume:** whether the admin ingest surface is HTTP at all (the offline CLI is | |
| the honest milestone and both halves are there now); and GLiNER + LangExtract ensembling stays βΈοΈ | |
| flagged as scope creep. **`kex` was not ported** β it answered the design questions; v2 was built | |
| here against this repo's patterns. | |
| ## 5. Critical path & sequencing | |
| - **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.) | |
| - **Parallelizable now:** #31 + #32 (both are Harry handoffs). (#4 β , #11 β , #22 β done.) | |
| - **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.** | |
| - **Demo gate (playground, #13):** deploy the remove-`problem_statement` work to HF β slow path (#15 β ) | |
| and the report path are verified locally, and #16 is resolved (#22 hands Harry the schema). **Keep it | |
| playground-able.** | |
| ## 6. Decisions still open (need the team / Harry / lead) | |
| - ~~`analysis_records`: dedorch-owned vs Python-owned (#16/#14).~~ RESOLVED: Python-owned + renamed **`report_inputs`** (#21 done); Harry's migration creates it (#22). | |
| - ~~Whether `help` is its own endpoint or via `call_agent` (#8).~~ RESOLVED: `help` is a `call_agent` intent (no own endpoint). | |
| - ~~Author display-name source for the report (#19).~~ RESOLVED: Python resolves `users.fullname` (fallback `user_id`); swap to a Go-passed name later if preferred. | |
| - ~~Keep vs drop `chat_history` in the report contract (#17).~~ RESOLVED: never in the contract; report is records-based (analysis_id + user_id only). | |
| - Confirm Go takes over analysis creation + both creation gates (data-first + mandatory fields) (#18). | |
| - **Report input for edit mode** β does Python need the last report content? (#24) | |
| - ~~`report_inputs` write scope β every agent call vs slow-path-only? (#21)~~ RESOLVED: one row per slow-path run (telemetry stays Langfuse). | |
| - **Python history source** β confirm Go's `analysis_message` (#25). | |
| - **`done.chart_count`** β additive field on the SSE `done` event so the FE can skip `GET /charts` | |
| on chartless turns? (Harry; SPINE_V2_PLAN Β§4.5.) Until decided the FE fetches unconditionally. | |
| - **W3 re-open timing** (deferred `analyze_*` activation) β Rifqi (deferred at the 2026-07-13 approval). | |
| - **INV-6 relaxation for S1b targeted repair** β team, only after S1a `repair_candidate` telemetry | |
| shows a meaningful hit-rate (SPINE_V2_PLAN Β§6). | |
| - ~~**Knowledge pipeline β v2 module name** (Β§0.8 D5).~~ **RESOLVED 2026-08-21:** | |
| `src/knowledge_extraction/` and `src/knowledge_parsing/`; the existing `src/knowledge/` | |
| OCRβpgvector service is untouched. | |
| - ~~**Knowledge pipeline β should `gliner`/`torch` be an optional extra too?** (Β§0.8 D1)~~ | |
| **RESOLVED 2026-09-07 (`26ba1ab`): no β they cannot be, and the size problem was solved another | |
| way.** GLiNER *is* the term filter (`use_span_filter=False` yields zero mentions), so extraction in | |
| the Space requires torch **installed**; a lazily-imported extra that is absent is a 500, not a | |
| graceful skip. What was actually costly was not torch but its CUDA tail: the default PyPI Linux | |
| wheel drags the whole runtime as `nvidia-*` dependencies. Pinning the `+cpu` build from PyTorch's | |
| own index removed **36 `nvidia-*` packages and `triton`** from `uv.lock` with zero behavioural | |
| change. `mineru` β the comparison this question was built on β was **dropped** in the same change, | |
| because it made `uv lock` fail project-wide. | |
| - **Knowledge pipeline β admin ingest surface** (Β§0.8 D4): whether it is HTTP at all, and if so its | |
| auth posture. A write surface triggered by an admin is a different risk class from the current | |
| unauthenticated read-only chat surface (Β§0.7) β raise it, do not inherit the posture by default. | |