Task — engineering-spec@1

"KB Ask-this-page Q&A — CUO-grounded answer over current + linked docs with span-level citations and answer-or-decline gate"

draftTASK-KB-007
module kb · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-KB-001, TASK-KB-006, TASK-CUO-101, TASK-MEMORY-108 · blocks none

§1 — Description (BCP-14 normative)

The KB service MUST ship Q&A at services/kb/src/qa/ grounded in current page + 1-hop links, span-cited, decline-on-low-confidence, 4 memory audit kinds.

  1. MUST validate qa_answer_kind against closed enum per DEC-1941.
  1. MUST assemble context at context_assembler.rs::assemble(doc_id, question) per DEC-1940:
  1. MUST generate answer at answer_generator.rs::generate(context, question) per DEC-1940 with TASK-AI-003 prompt:
  1. MUST extract citations at citation_extractor.rs::extract(answer) per DEC-1942:
  1. MUST apply confidence gate per DEC-1943:
  1. MUST enforce rate limit per DEC-1944 — 50/user/day; return 429.
  1. MUST define table at migration 0007: ``sql CREATE TABLE kb_qa_questions ( question_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, doc_id UUID NOT NULL, question_text TEXT NOT NULL, answer_kind TEXT NOT NULL CHECK (answer_kind IN ('confident','partial','decline_no_evidence','decline_low_confidence')), answer_text TEXT, confidence_score NUMERIC(3,2), citations JSONB NOT NULL DEFAULT '[]', asked_by UUID NOT NULL, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX qa_user_day_idx ON kb_qa_questions(tenant_id, asked_by, created_at DESC); ALTER TABLE kb_qa_questions ENABLE ROW LEVEL SECURITY; CREATE POLICY qa_rls ON kb_qa_questions USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON kb_qa_questions FROM cyberos_app; -- Append-only — Q&A history immutable ``
  1. MUST expose endpoint: ``text POST /v1/kb/docs/{id}/ask body: {question} ``
  1. MUST emit 4 memory audit kinds per DEC-1945. PII per TASK-MEMORY-111: question + answer text SHA-256 hashed; citation ids ok.
  1. MUST thread trace_id from ask → assemble → generate → cite → audit.
  1. MUST NOT search open world per DEC-1940 (only doc + 1-hop links).
  1. MUST NOT return answer without ≥1 citation per DEC-1942.
  1. MUST NOT return below confidence threshold per DEC-1943.

§2 — Why this design

Why 1-hop linked (DEC-1940)? Docs cross-reference; questions often need linked context (e.g. "what's the policy?" → linked Decree 145).

Why span citations (DEC-1942)? UI highlights source on hover; users verify claims; eliminates trust-but-can't-verify problem.

Why confidence gate (DEC-1943)? Better to say "I don't know" than confidently mislead; trust over coverage.

Why rate limit (DEC-1944)? AI cost per call non-trivial; per-user cap prevents runaway.


§3 — API contract

POST /v1/kb/docs/{id}/ask

Sample request:

{
  "question": "What's the maximum OT per week?"
}

Sample response (confident):

{
  "answer_kind": "confident",
  "answer_text": "Maximum OT per week is 12 hours per Decree 145 Art. 107.",
  "confidence_score": 0.92,
  "citations": [
    {
      "doc_id": "uuid-decree-145",
      "chunk_id": "uuid-art-107-chunk",
      "char_start": 1200,
      "char_end": 1280,
      "snippet": "...overtime shall not exceed 12 hours per week..."
    }
  ]
}

Sample response (decline):

{
  "answer_kind": "decline_no_evidence",
  "answer_text": "Not enough evidence in this page + linked docs to answer.",
  "confidence_score": 0.3,
  "citations": []
}

§4 — Acceptance criteria

  1. answer_kind enum cardinality 4. 2. Context = current doc + 1-hop linked. 3. No open-world search. 4. Every claim cited with chunk + char range. 5. Confidence ≥0.7 → confident or partial. 6. <0.7 → decline_low_confidence. 7. 0 citations → decline_no_evidence. 8. Rate limit 50/user/day. 9. 4 memory audit kinds emitted. 10. PII scrubbed (question + answer SHA256). 11. RLS denies cross-tenant. 12. Trace_id preserved. 13. Context capped 50k tokens. 14. Append-only via REVOKE. 15. UI span highlight from citations. 16. Decline messages user-friendly. 17. History queryable per user. 18. Failure → decline_low_confidence + sev-2. 19. Linked docs respect TASK-KB-003 visibility. 20. Question length capped 1000 chars.

§5 — Verification

#[tokio::test]
async fn confident_answer_has_citations() {
    let ctx = TestContext::with_doc_about_ot_caps().await;
    let r = ctx.ask(ctx.doc_id, "What's the max OT per week?").await;
    assert_eq!(r.answer_kind, "confident");
    assert!(!r.citations.is_empty());
    for c in &r.citations {
        assert!(c.char_end > c.char_start);
    }
}

#[tokio::test]
async fn decline_when_no_evidence() {
    let ctx = TestContext::with_doc_about_finance().await;
    let r = ctx.ask(ctx.doc_id, "What's the capital of France?").await;
    assert_eq!(r.answer_kind, "decline_no_evidence");
    assert_eq!(r.citations.len(), 0);
}

#[tokio::test]
async fn rate_limit_50_per_day() {
    let ctx = TestContext::with_user().await;
    for _ in 0..50 { ctx.ask(ctx.doc_id, "test").await; }
    let r = ctx.ask(ctx.doc_id, "test").await;
    assert_eq!(r.status_code, 429);
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-KB-006, TASK-CUO-101. Cross-module: TASK-AI-003 (LLM), TASK-MEMORY-108 (link graph for 1-hop), TASK-KB-003 (linked doc visibility), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
AI timeoutretry 1xdecline_low_confidence + sev-2inherent
Context exceeds 50k tokenstruncatesev-3 auditinherent
1-hop links emptyuse doc-only contextinherentinherent
Citation extraction failmark declinesev-2inherent
Rate limit window edgesliding 24hinherentinherent
Cross-tenant askRLS403inherent
Linked doc forbiddenfilterexclude from contextinherent
Question malformedvalidate400rephrase
Citation char range invalidreject answersev-2bug fix
AI quotadownstreamsev-2inherent

§11 — Implementation notes


End of TASK-KB-007 spec.