Commit History
-
[NOTICKET] feat(knowledge-domain): the declared half — domain TBox + review (V4-9, V4-10)
Rifqi Hafizuddin +1: Claude Opus 5The half that could not be derived: what a domain IS, and what it deliberately is NOT. `measures`, `policies` and `units` fall out of entries; identity and boundary do not, and that is what the review loop exists for. **`boundary` IS DERIVED, and that is the design decision worth keeping.** v3's real lesson was not the rename - it was changing the question so the answer is checkable, `purpose` (composed, unverifiable) becoming `purpose_verbatim` (located, span-checked). A boundary sentence is inherently unquotable, being a statement about ABSENCE, so asking a model to write one reproduces exactly the unfalsifiable claim v3 removed. Instead it is the complement of the covered subdomains over a closed 11-member enum. On the BUMA corpus: covers production, equipment, planning, hauling, loading -> "no approved knowledge about maintenance, drilling_blasting, safety, quality, cost, geology". Deterministic, checkable, and it turns an honest refusal into a fact about the corpus rat…601937c -
[NOTICKET] fix(knowledge): entry-id collisions and orphaned run rows
Rifqi Hafizuddin +1: Claude Opus 5Both found the expensive way: the authorised extraction run spent ~90 LLM calls and persisted NOTHING. BUG 1 - a UNIQUE violation the store handed to the database itself. `(run_id, kind, entity_id)` is UNIQUE and ids are content-derived, so two entries CAN collide inside one run: `formula_id` falls back name -> latex -> chunk_id, and the real extraction produced two formulas sharing a name, hence one id. The insert was rejected and the whole batch was lost. `save_entries` now dedupes on (kind, entity_id) before writing. Dropping one silently would be worse than the crash, so the collision is logged at warning, counted on the run row, and exposed as `last_collisions` - the id scheme failed to distinguish two things and someone must be able to see that. A collision ACROSS kinds is not a collision and is not dropped. BUG 2 - the run row lied about an expensive failure. Only `extract_all` was inside the try block, so a failure in persistence left the row `running` with n_calls…25aafc0 -
[NOTICKET] feat(knowledge): DocumentBrief split + cross-document measure identity
Rifqi Hafizuddin +1: Claude Opus 5Two changes that belong together, both made BEFORE the authorised extraction run so the fresh rows land in the right shape and nothing needs re-keying after. V4-4 - THE DOCUMENT BRIEF SPLIT. The per-document object is now `DocumentBrief`, `kind='document'`, prefix `b_`, and `domain` is reserved for the scope-level semantic model composed in src/knowledge_domain/. Prefix history, and it is not churn: b_ (BriefContext) -> d_ (DomainContext, 2026-09-01) -> b_ again (DocumentBrief, 2026-09-08). The middle move renamed the object without changing what it held - a per-document card. v4 builds the real domain context and gives d_ to it, so this one goes back to the name that describes it. Safe only because nothing is approved against it: an id prefix change after an expert has ruled is a migration that loses their approvals. WHAT A DOMAIN IS THE UNIT OF (Rifqi's question, answered in the plan section 3.0). Per SCOPE - the company - composed across all its documents and runs. …b5de3b4 -
[NOTICKET] fix(knowledge): the diff baseline never reached the ingest path
Rifqi Hafizuddin +1: Claude Opus 5`run_document` called `extract_and_persist` WITHOUT `active_glossary`, so the glossary diff ran against an empty list and every entry came back `diff_status="new"`. Only the re-extract path passed the approved set. The consequence is the thing the review queue exists to make tractable: a company approves terms from document 1, ingests document 2, and document 2's terms all read as `new` even where they duplicate or contradict what an expert already ruled on. Cross-document dedup silently did not fire on the path people will actually use most. Invisible in the end-to-end run because there were no approvals yet - all 88 entries showed `new`, which was correct then and would be wrong now. Fixed at the single point that needs it rather than at the call site: `extract_and_persist` now fetches `approved_glossary(scope_id)` itself when the argument is omitted, so no caller can forget it again, and the redundant fetch in `_run_reextract` is gone. An explicit `[]` still means "diff against n…65f84bf
-
[NOTICKET] feat(knowledge): read + review routes, closing the loop (N10b, N12b)
Rifqi Hafizuddin +1: Claude Opus 5Five routes, taking the surface from "ingest and poll" to the full cycle: extract, read, review, and feed approvals back into the next extraction. GET /api/v1/knowledge/documents stored artifacts POST /api/v1/knowledge/documents/{id}/extract re-extract (no re-parse) GET /api/v1/knowledge/entries candidates + decisions GET /api/v1/knowledge/queue the review queue POST /api/v1/knowledge/entries/{entity_id}/reviews record a decision THE LOOP NOW CLOSES. `approved_glossary()` reads the latest decision per entity and hands the approved set to the next extraction as `active_glossary`, which is what the diff classifies new candidates against - so an expert working the queue makes the following run better rather than merely annotating this one. An `edited` decision contributes the expert's corrected payload, not the model's original claim. Passed explicitly rather than read from wherever the las…f118878 -
[NOTICKET] feat(knowledge): create the tables and wire the pipeline end to end
Rifqi Hafizuddin +1: Claude Opus 5Stages C, D and E of the persistence plan. The knowledge pipeline now runs from a documents row to persisted rows. TABLES CREATED (stage C). Five tables + 10 indexes applied to dedorch, exactly as docs/knowledge/knowledge_tables.sql, which is committed so what ran is reproducible rather than retyped from prose. Idempotent and additive only: no DROP, no ALTER, nothing touching an existing table. Verified by introspection - column counts 18/20/15/11/15 match the contract, all FKs and CHECKs present. ⚠️ Go still owns the migration (CLAUDE.md 2.2). Python executes no DDL: this was a one-time operator run following the 2026-07-13 message_charts precedent. That precedent's MISTAKE must not repeat - the schema was run by hand and never handed over, so a fresh environment built from Go's migrations comes up without it and both write paths fail silently. Run by hand, then hand over; never by hand only. Harry ask is in the contract section 5 Q5. Two CHECK constraints were correc…c2164ae -
[NOTICKET] feat(knowledge-ingest): job lifecycle + ingestion routes (N10)
Rifqi Hafizuddin +1: Claude Opus 5First slice of the ingestion endpoint: batch admission, document selection, tenant scope, job lifecycle and the three job routes. The pipeline itself is NOT wired yet - see the blockers below - so every document currently fails with a legible reason rather than silently doing nothing. POST /api/v1/knowledge/ingest -> 202 {job_id} GET /api/v1/knowledge/jobs/{job_id} -> state + per-document results GET /api/v1/knowledge/jobs -> recent jobs for a user Async by necessity: parse (~80s) plus extract (~170s) for nine pages is far past any HTTP timeout. Polling, not SSE (decided 2026-09-07) - a job row survives a refresh and a dropped connection, a stream does not, and a 4-minute stream holds a worker for its duration. 404 on an unknown job_id follows GET /traceability rather than the tri-state 200 of GET /charts: a polled id was handed to the caller, so its absence is genuinely exceptional, unlike a chartless turn. New package src/knowledge_ingest/ - orchestra…b14034b