fix(web): render full document content, keep graph connected under categories
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Two fixes to the new category taxonomy:

- Knowledge Base couldn't show a document/investigation/runbook's own
  markdown body — knowledge_entities.content was never exposed by any
  endpoint (GetEntityKnowledge answers "what knowledge references this
  entity", not "what is this entity's content"). Add GET
  /api/v1/knowledge/content/{id} and render it with the existing
  marked+DOMPurify pipeline in a new Content section.

- The graph hid any edge whose other endpoint wasn't in the active
  category, so nodes with only cross-category neighbors rendered as
  disconnected dots. Queried the real relationship table: ~70% of infra
  edges cross Fleet/Network/Services/Storage lines (compute+network+
  software+storage+physical used to be one "infrastructure" layer).
  EntityGraph now keeps 1-hop neighbors visible but dimmed instead of
  hiding them, so the edges — and what they connect to — stay visible.

- categories.ts: `cognition` domain conflated true knowledge (document/
  investigation/runbook, 58 entities) with operational telemetry
  (execution/check/task/signal/approval/pattern/skill/classification/
  feedback, 300+ entities with their own Operations/Signals/Learning
  pages). Mapping the whole domain to Knowledge pulled in 245 execution
  entities fanning out from ~17 compute nodes via `targets` edges — the
  single biggest source of graph clutter. Knowledge now maps by type
  (document/investigation/runbook only); the rest of cognition is
  excluded from Knowledge Base browsing entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 10:17:13 +02:00
parent 35c54ceef5
commit 61ad785fef
7 changed files with 208 additions and 20 deletions

View File

@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
@@ -107,6 +108,43 @@ func (s *Server) serveRecentKnowledge(w http.ResponseWriter, req *http.Request)
})
}
// serveKnowledgeContent returns the full markdown body for a document/
// investigation/runbook entity, by its own entity id or slug. Nothing else
// exposes knowledge_entities.content — GetEntityKnowledge (below) answers a
// different question ("what knowledge references THIS entity"), and
// SearchKnowledge only returns a short ts_headline snippet. The KB detail
// panel needs the entity's own full content when it IS a knowledge entity.
func (s *Server) serveKnowledgeContent(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
idOrSlug := chi.URLParam(req, "id")
var title, content, source string
var tags []string
var updatedAt string
err := s.pool.QueryRow(ctx, `
SELECT ke.title, ke.content, COALESCE(ke.source,''), ke.tags, ke.updated_at::text
FROM knowledge_entities ke
JOIN entities e ON e.id = ke.entity_id
WHERE e.slug = $1 OR e.id::text = $1`, idOrSlug).
Scan(&title, &content, &source, &tags, &updatedAt)
if err != nil {
writeProblem(w, req, http.StatusNotFound, "no knowledge content for entity", "")
return
}
if tags == nil {
tags = []string{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"title": title,
"content": content,
"source": source,
"tags": tags,
"updated_at": updatedAt,
})
}
func (s *Server) SearchKnowledge(ctx context.Context, request gen.SearchKnowledgeRequestObject) (gen.SearchKnowledgeResponseObject, error) {
q := request.Params.Q
limit := clampLimit(request.Params.Limit)