From 3fbce01ecc6b252053afc29846d26add8cfe835b Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Thu, 27 Aug 2026 12:59:55 +0300 Subject: [PATCH 01/14] docs(reins): knowledge workspaces spec and retrospective (CLEAN-48) Current-state audit of the knowledge module and the specification built on it. The audit's central finding: retrieval isolation does not exist. The workspace namespace is attached to writes only, the retrieval service binds it at process start and none is configured, so every base answers from one shared pool and the graph describes the whole installation. The product owner settled the open scope question on 2026-08-27: Ranch is personal, and an agent given one base must neither read nor inspect another. That reversed two earlier decisions -- the isolation boundary is the knowledge base rather than a container above it, and the transition needs a one-time re-index because the shared pool cannot be split after the fact. The re-index costs the operator nothing: every source type is rebuildable from Ranch's own storage. 36 requirements, 14 success criteria, 6 prioritised stories. Ready for planning, which owes an arrangement for running one retrieval process per isolated base. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SErhdDayRqyC2hRhmFvLnf --- .specify/feature.json | 2 +- .../checklists/requirements.md | 62 ++ .../retrospective.md | 296 ++++++++++ .../007-knowledge-workspaces-research/spec.md | 558 ++++++++++++++++++ 4 files changed, 917 insertions(+), 1 deletion(-) create mode 100644 specs/007-knowledge-workspaces-research/checklists/requirements.md create mode 100644 specs/007-knowledge-workspaces-research/retrospective.md create mode 100644 specs/007-knowledge-workspaces-research/spec.md diff --git a/.specify/feature.json b/.specify/feature.json index 126b2cb..588ae7d 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/006-agent-workspace-tabs" + "feature_directory": "specs/007-knowledge-workspaces-research" } diff --git a/specs/007-knowledge-workspaces-research/checklists/requirements.md b/specs/007-knowledge-workspaces-research/checklists/requirements.md new file mode 100644 index 0000000..2db58c4 --- /dev/null +++ b/specs/007-knowledge-workspaces-research/checklists/requirements.md @@ -0,0 +1,62 @@ +# Specification Quality Checklist: Knowledge workspaces + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-08-26 +**Feature**: [spec.md](../spec.md) · [retrospective.md](../retrospective.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Iteration 1 (2026-08-26): all items passed except the clarification markers — + three scope decisions the specification could not make on its own. +- Iteration 2 (2026-08-26): the three were answered and recorded in the + **Decisions** section, and a clarity-without-weight requirement line was added + from the follow-up ask. +- Iteration 3 (2026-08-27): the product owner answered the one question the code + could not — Ranch is personal, and an agent given one base must not reach into + or inspect another. That **reversed D2 and D3** and retired the workspace + entity from D1: the isolation boundary is the knowledge base, the transition is + a one-time automatic re-index, and grouping is handled by navigation (D4). The + Decisions section records each reversal against what it replaces. Overview, + US1, US2, edge cases, the isolation and organisation requirements, key + entities, success criteria and assumptions were rewritten accordingly; the + clarity, interface and status requirements were unaffected. **All items pass.** +- Deliberate exception to "no implementation details": the Decisions section and + the assumptions carry deployment-cost rationale — a running process per + isolated base, and what that reserves — because the reversals cannot be + justified or re-litigated without it. They state cost, not mechanism. +- Technical evidence stays in `retrospective.md`, not in `spec.md`, so the + specification stays readable by non-technical stakeholders while the audit + stays verifiable. +- The verification item carried into planning is no longer the default-namespace + compatibility check; it retired with D3's reversal, because the transition + writes fresh per-base areas rather than adopting the existing pool. What + planning owes instead is the arrangement that pays for one retrieval process + per isolated base: right-sized instances, start-on-demand, a pool, or a + reported ceiling. +- 36 functional requirements, 14 success criteria, 6 prioritised user stories + (2×P1, 3×P2, 1×P3). Ready for `/speckit-plan`. diff --git a/specs/007-knowledge-workspaces-research/retrospective.md b/specs/007-knowledge-workspaces-research/retrospective.md new file mode 100644 index 0000000..76793e6 --- /dev/null +++ b/specs/007-knowledge-workspaces-research/retrospective.md @@ -0,0 +1,296 @@ +# Knowledge (`reins`) — current-state retrospective + +**Date**: 2026-08-26 · **Tracker**: [CLEAN-48](https://dreamvention.atlassian.net/browse/CLEAN-48) + +This is the evidence document behind `spec.md`. It answers one question the spec +only states as an outcome: **where does the data on the Query and Graph tabs +actually come from?** Everything below was read out of the repository at +`955516f`; file references are `path:line`. + +--- + +## 1. What exists today + +| Surface | Where | What it does | +|---|---|---| +| Sidebar entry "Knowledges" | `admin/slices/reins/plugins/menu.ts` | Main group, sort 30 | +| List | `pages/knowledges/index.vue` → `components/knowledge/list/Provider.vue` | Setup wizard until the service is ready, then one flat table of every base | +| Create | `pages/knowledges/create.vue` | Name + description only | +| Base shell | `pages/knowledges/[id].vue` | Header, index status, **Index** button, 4 tabs | +| General | `[id]/edit.vue` | Name + description | +| Sources | `[id]/sources.vue` | Add file / url / text, add-from-sitemap, add-from-zip, list, delete | +| Graph | `[id]/graph.vue` | Entity picker, max depth, max nodes, Sigma canvas, legend | +| Query | `[id]/query.vue` | Question, mode, top-K, answer + references | +| Agent binding | `admin/slices/agent/agent/components/agent/item/Form.vue:210-235` | Checkbox column of every base | +| Template binding | `admin/slices/agent/template/components/template/item/Form.vue:147` | Same | +| Agent read-only view | `agent/knowledge/Tab.vue` | Lists the bases resolved for that agent | +| Agent runtime | `api/src/slices/reins/knowledge/knowledge.tool.ts` | MCP tool `query_knowledge` | + +There is **no `app/` (end-user console) surface at all** — knowledge is +admin-only, as the original design deliberately scoped it. + +`README.md:97` still describes the slice as "Access control / API keys". It is +the knowledge slice. Documentation drift. + +There are **no tests** anywhere under `api/src/slices/reins/` or +`admin/slices/reins/`. + +--- + +## 2. Where the data comes from — the ingest side + +All three source types funnel into a single external retrieval service +(LightRAG), one document at a time, only when an operator presses **Index**: + +- **file** — uploaded to S3 under `knowledges/{knowledgeId}/…` + (`source/data/source.gateway.ts:44`), downloaded again at index time and + forwarded as a multipart upload. +- **url** — *not* fetched at add time. At index time `ranch-api` fetches the + page itself with a browser user-agent and reduces the HTML to text with a + regex stripper (`lightrag/data/lightragHttp.client.ts`, `stripHtmlToText`), + then posts that text. +- **text** — posted verbatim. + +Bulk entry points: multi-file upload (cap 250 files, +`source/source.controller.ts:38`), zip archive (cap 1 GiB, processed in the +background), and `sitemap.xml` import, which creates one `url` source per +discovered page. + +Indexing is incremental in one direction only: `runIndex` skips any source that +already has a document id (`knowledge/domain/knowledge.service.ts`, `if +(source.indexed) continue`). **A source is never re-fetched or re-indexed.** A +URL source is frozen at whatever the page said the first time it was indexed. + +--- + +## 3. Where the data comes from — the retrieval side (the actual finding) + +Ranch computes a per-base namespace, `workspaceOf(id) = +knowledge_` (`lightrag/data/workspace.ts`), and the +original design recorded the intent plainly: + +> "Each `Knowledge` row maps to one LightRAG workspace (namespace-level +> isolation inside a single LightRAG instance)." +> — `docs/superpowers/specs/2026-04-23-reins-lightrag-integration-design.md:75` + +That isolation does not exist in the running system. Two independent reasons: + +**(a) Ranch only attaches the namespace to writes.** + +| Call | Namespace sent? | Evidence | +|---|---|---| +| ingest text | yes | `lightragHttp.client.ts` — `workspace` in the JSON body | +| ingest file | yes | `form.append('workspace', …)` | +| **query** | **no** | `query()` builds `{query, mode, top_k, include_references}` — `input.workspace` is accepted by the interface and then dropped | +| **graph** | **no** | `getGraph()` sends only `label`, `max_depth`, `max_nodes` | +| **graph labels** | **no** | `getGraphLabels()` sends nothing | +| **delete documents** | **no** | scoped by document id instead, which happens to work | + +`KnowledgeGateway.getGraphLabels()` / `.getGraph()` do not even take a knowledge +id (`knowledge/data/knowledge.gateway.ts`), and the HTTP routes for them sit +*above* `/:id` in the controller — `GET /knowledges/graph` is a **global** +endpoint, not a per-base one (`knowledge.controller.ts:100-118`). + +**(b) The retrieval service does not accept a per-request namespace anyway.** + +LightRAG's `workspace` is an *instance-level* setting, fixed at process start +(`WORKSPACE` env var or `--workspace` CLI flag) and documented as **immutable +after initialization**. Isolating two corpora means running two configured +instances — the project's own "multi-site deployment" guidance. There is no +per-request workspace field and no per-document filter on query +(`QueryParam` has modes and token budgets, no document/id scoping). + +And the deployed instance sets no workspace at all: neither +`k8s/platform/lightrag/deployment.yaml` nor `api/docker-compose.yml` defines +`WORKSPACE`. Everything lands in the default namespace. + +### What that means in the product + +1. Every knowledge base writes into **one shared corpus**. +2. **Query on base A answers from A, B, C and everything else ever indexed.** + That is the direct cause of "не понятно, откуда большинство данных берётся". +3. The **Graph tab and its Entity list show the whole instance**, not the base + you opened. The picker is not "entities of this base" — it is "entities of + everything". +4. The `query_knowledge` tool's headline feature — "omit `knowledge_id` to + search all your bound bases" — fans out N calls that all hit the same corpus + and return near-identical answers at N× the LLM cost + (`knowledge.tool.ts`, multi-base branch). +5. Deletion is the one thing that *is* scoped, because it works by document id. +6. Nothing enforces that a base an agent is bound to is the base that answers. + +--- + +## 4. Settings nobody can interpret + +| Setting | Where | Reality | +|---|---|---| +| `entityTypes` | DB column, DTOs, domain types, admin mapper | **Dead.** Never sent anywhere. Grep shows reads and writes only within the persistence and API-typing layers. The design intended it to constrain entity extraction; nothing consumes it. | +| `relationshipTypes` | same | **Dead**, same story. | +| `workspace` column | `Knowledge.workspace @unique` | Written as `'pending'` then patched to `workspaceOf(id)` on create; **never read at runtime** — the 2026-05-01 refactor replaced it with the pure function and kept the column for its unique constraint. | +| Graph **Max depth** / **Max nodes** | Graph tab | Forwarded, but against a global corpus their meaning is arbitrary — they bound a traversal over everyone's data. | +| Query **Mode** (hybrid/local/global/naive) | Query tab | Forwarded, unexplained in the UI. The `mix` mode — the one the upstream project recommends when a reranker is on — is not offered. | +| Query **Top K** | Query tab | Forwarded, unexplained. | +| Setup step "Restart LightRAG" | Setup wizard | Asks the operator to copy `make dev` or a `kubectl rollout restart` into a terminal to apply credentials chosen in the UI. | + +Note that the **General tab shows only name and description** — the two array +settings are no longer even editable (`components/knowledge/item/Form.vue`), yet +they remain in the schema, the DTOs and the generated SDK. What the user +perceives as "непонятные настройки" is the residue: knobs that exist in the +contract, are described in the docs, and do nothing. + +--- + +## 5. Status that is not true + +- `Source.indexed` is derived as `lightragDocId !== null` + (`source/data/source.mapper.ts`). The id it stores is a **track id returned at + submission**; the service processes the document asynchronously afterwards. + So the badge means *submitted*, not *searchable*. +- `Knowledge.indexStatus = 'ready'` is set when at least one source got a track + id back (`knowledge.service.ts`, `runIndex`). +- Consequence: a base can read **ready / Indexed** while nothing is retrievable + yet, and there is no per-source progress, no per-source error — every failure + in a run is flattened into one truncated `indexError` string ("5 source(s) + failed: … ; ..."). +- There is a `track_status` endpoint the client already calls for deletion + (`resolveDocIdsByTrackId`) — the pipeline state exists, it is just never + surfaced. + +--- + +## 6. UI defects, root-caused + +**Tabs never highlight the active one** — `pages/knowledges/[id].vue:100-112`. +The link carries `class="border-b-2 border-transparent … text-muted-foreground"` +and `active-class="border-primary text-foreground"`. Both pairs set the same CSS +properties (border-color, color) at the same specificity, so the winner is +stylesheet order, not attribute order — the base utilities win and the active +state is invisible. Neighbouring navs (`setting/nav/Menu.vue`, +`llm/nav/Menu.vue`) have the same conflict but survive it because their active +class also sets `bg-muted`, which nothing else claims. Secondary: there is no +`[id]/index.vue`, so opening `/knowledges/:id` directly renders the shell with +an empty body and no tab selected at all. + +**Entity picker freezes the page** — +`components/knowledge/graph/Provider.vue`. `loadLabels()` pulls every graph +label in one call and the template renders one `SelectItem` per label with no +search, no windowing, no paging. Two multipliers: the labels are global (§3), so +the list grows with the whole instance rather than with your base; and the +select mounts the full option list on open. On a real corpus that is thousands +of DOM nodes in one synchronous mount. + +**Other rough edges** + +- List: client-side substring search only, no pagination, no sort, no grouping + (`list/Provider.vue`). +- Binding UI: a `max-h-64` scroll box of checkboxes over *every* base, in both + the agent and the template form. +- `Agent.knowledgeIds` is a bare `String[]` with no foreign key + (`agent/agent/agent.prisma`) — deleting a base silently leaves dangling ids on + agents and templates. +- The Query tab's mode selector is a raw `