Task — engineering-spec@1

"KB runbook category — applicability tags (provider / region / severity) for OBS triage with TASK-OBS-007 incident routing"

draftTASK-KB-008
module kb · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-KB-001, TASK-OBS-007 · blocks none

§1 — Description (BCP-14 normative)

The KB service MUST ship runbook tagging at services/kb/src/runbook/ with 3-dim tags (provider/region/severity), OBS triage match, 2 memory audit kinds.

  1. MUST validate enums per DEC-1951/1952/1953.
  1. MUST define table extension at migration 0008: ```sql ALTER TABLE kb_documents ADD COLUMN is_runbook BOOLEAN NOT NULL DEFAULT false; ALTER TABLE kb_documents ADD COLUMN runbook_providers TEXT[]; ALTER TABLE kb_documents ADD COLUMN runbook_regions TEXT[]; ALTER TABLE kb_documents ADD COLUMN runbook_severities TEXT[];

ALTER TABLE kb_documents ADD CONSTRAINT runbook_providers_valid CHECK (runbook_providers IS NULL OR runbook_providers <@ ARRAY['aws','gcp','azure','vercel','supabase','custom']::TEXT[]); ALTER TABLE kb_documents ADD CONSTRAINT runbook_regions_valid CHECK (runbook_regions IS NULL OR runbook_regions <@ ARRAY['global','sg-1','eu-1','us-1','vn-1']::TEXT[]); ALTER TABLE kb_documents ADD CONSTRAINT runbook_severities_valid CHECK (runbook_severities IS NULL OR runbook_severities <@ ARRAY['sev1_critical','sev2_high','sev3_medium','sev4_low']::TEXT[]);

CREATE INDEX runbook_provider_idx ON kb_documents USING GIN (runbook_providers) WHERE is_runbook = true; CREATE INDEX runbook_region_idx ON kb_documents USING GIN (runbook_regions) WHERE is_runbook = true; CREATE INDEX runbook_severity_idx ON kb_documents USING GIN (runbook_severities) WHERE is_runbook = true;

GRANT UPDATE (is_runbook, runbook_providers, runbook_regions, runbook_severities) ON kb_documents TO cyberos_app; ```

  1. MUST match for incident at tag_matcher.rs::match(incident_provider, incident_region, incident_severity) per DEC-1954:
  1. MUST expose endpoints: ``text PUT /v1/kb/docs/{id}/runbook-tags body: {providers, regions, severities} GET /v1/kb/runbooks/match?provider=aws&region=sg-1&severity=sev1_critical ``
  1. MUST emit 2 memory audit kinds per DEC-1955. PII per TASK-MEMORY-111: tag enums + counts ok.
  1. MUST thread trace_id from OBS-007 incident → matcher → audit.
  1. MUST NOT accept invalid enum values per DEC-1951/1952/1953 (CHECK constraints).

§2 — Why this design

Why 3 dims (DEC-1950)? Triage filtering needs all three — same-severity AWS runbook irrelevant if incident is GCP.

Why arrays (DEC-1950)? A runbook may apply to multiple providers (e.g. "S3 / GCS bucket misconfigured") — array models that naturally.

Why specificity ranking (DEC-1954)? Multiple matches likely — global runbook + region-specific; show specific first.


§3 — API contract

Sample runbook tag set:

PUT /v1/kb/docs/{id}/runbook-tags
{
  "providers": ["aws"],
  "regions": ["sg-1"],
  "severities": ["sev1_critical", "sev2_high"]
}

Sample match query:

GET /v1/kb/runbooks/match?provider=aws&region=sg-1&severity=sev1_critical

Response:
{
  "matches": [
    {"doc_id": "uuid", "title": "AWS SG-1 critical outage runbook", "specificity_score": 3},
    {"doc_id": "uuid", "title": "Global AWS outage runbook", "specificity_score": 1}
  ]
}

§4 — Acceptance criteria

  1. provider enum cardinality 6. 2. region enum cardinality 5. 3. severity enum cardinality 4. 4. CHECK constraints enforce enum values. 5. Multi-tag match (provider AND region AND severity). 6. Global tag matches any incident region. 7. Custom provider catches non-cloud-vendor incidents. 8. GIN indexes on each tag array. 9. Specificity ranking. 10. 2 memory audit kinds emitted. 11. PII: tag enums (public) ok. 12. RLS denies cross-tenant. 13. Trace_id preserved. 14. CTO-only tag write. 15. TASK-OBS-007 integration tested. 16. Non-runbook docs not indexed (WHERE is_runbook). 17. Empty tag arrays = no match (not all match). 18. Append-only via REVOKE except 4 tag cols. 19. Multiple incident tags supported. 20. Severity escalation (higher sev runbooks shown for sev1).

§5 — Verification

#[tokio::test]
async fn multi_tag_match_returns_specific_first() {
    let ctx = TestContext::with_runbooks_aws_sg_and_global().await;
    let r = ctx.match_runbooks("aws", "sg-1", "sev1_critical").await;
    assert!(r.matches[0].title.contains("SG-1"));
}

#[tokio::test]
async fn global_tag_catches_any_region() {
    let ctx = TestContext::with_global_runbook().await;
    let r = ctx.match_runbooks("aws", "us-1", "sev1_critical").await;
    assert!(!r.matches.is_empty());
}

#[tokio::test]
async fn invalid_enum_rejected() {
    let r = ctx.set_runbook_tags(ctx.doc_id, vec!["invalid_provider"], vec!["sg-1"], vec!["sev1"]).await;
    assert!(r.is_err());
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-KB-001, TASK-OBS-007. Cross-module: TASK-MEMORY-111 (audit).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Invalid enum tagCHECK400use valid
Empty tag arraysinherentno matchinherent
Incident region missinguse 'global'inherentinherent
Multiple matches same specificitytiedinherentinherent
GIN index slowEXPLAINtuneinherent
Tag updated mid-incidentsnapshot at match timeinherentinherent
Cross-tenant runbook leakRLS0 rowsinherent
Non-runbook doc taggedis_runbook=true required400set flag
Severity hierarchy unclearorder: sev1>sev2>sev3>sev4inherentinherent
Custom provider catch-allinherentinherentinherent

§11 — Implementation notes


End of TASK-KB-008 spec.