feat: implement product knowledge management & retrieval (004)
Implements 26 of 28 tasks from specs/004-product-knowledge/tasks.md across all three user stories -- Phase 3 of the roadmap. First feature to populate src/modules/ai-support/ (doc 07 places `knowledge` there; only that submodule is built, matching this codebase's convention of not pre-building unneeded submodules). Schema (prisma/schema.prisma + migration): - KnowledgeEntry, ErrorCode, KnownIssue, Runbook per docs/06, refining its conceptual flat `version` field into an explicit version-history mechanism: each edit inserts a new row (isCurrentVersion flag, compound unique on (code, version) / (key, productId, version)) instead of overwriting in place -- the only way "prior versions remain retrievable" (FR-004/FR-009) is actually true rather than aspirational. User Story 1 -- knowledge entry authoring/publish/version (P1, MVP): - draft -> published -> unpublished lifecycle; publish only takes effect from its effectiveDate. - Editing uses the same conditional-update-then-insert optimistic concurrency pattern as 003-ticketing's Ticket.version (409 on a stale expectedVersion). - Full version history readable via GET .../versions. User Story 2 -- error codes, known issues, runbooks (P2): - ErrorCode + KnownIssue with direct lookup-by-error-code. - Runbook steps stored as an ordered JSON array, preserved exactly; same version-on-edit mechanism as knowledge entries; inactive runbooks are indistinguishable from nonexistent ones on lookup. User Story 3 -- filtered retrieval (P3): - GET /knowledge/retrieve: product-scoped, excludes draft/ unpublished/not-yet-effective entries, validated entries ranked ahead of unvalidated. Deliberately NOT semantic/vector search -- doc 11 gap B1 explicitly defers embedding-model choice to the future AI-support feature; this is real, usable structured filtering a semantic layer can sit in front of later. Found and fixed one real bug before it reached tests: the retrieval endpoint initially queried by the raw external product id instead of resolving it to the internal Product.id first (every other endpoint in this feature does that resolution) -- would have silently returned zero results for every caller. Fixed with a lenient tryResolveProductId (empty array, not 404, for an unregistered product -- matches the "no matches, never an error" contract). Deliberately skipped (not forgotten, see checklist notes): the two planned mock-repository unit-test tasks (T004, T019) -- unlike 003-ticketing's state machine, this feature has no pure-logic surface to isolate from Prisma; coverage comes entirely from integration tests instead. All 13 integration test files in the repo (36 tests, spanning this feature and every prior one) verified passing together against a real Postgres/Redis/MinIO -- no regressions. Full quality gate (typecheck/lint/format/architecture/unit tests) passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e10736d82a
commit
e33d86081f
@@ -0,0 +1,91 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entries" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"isCurrentVersion" BOOLEAN NOT NULL DEFAULT true,
|
||||
"productId" TEXT NOT NULL,
|
||||
"feature" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"problem" TEXT,
|
||||
"symptoms" TEXT,
|
||||
"errorCode" TEXT,
|
||||
"cause" TEXT,
|
||||
"recommendedSolution" TEXT,
|
||||
"verificationSteps" TEXT,
|
||||
"escalationGuidance" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'draft',
|
||||
"effectiveDate" TIMESTAMP(3),
|
||||
"categoryScope" TEXT[],
|
||||
"validationStatus" TEXT NOT NULL DEFAULT 'unvalidated',
|
||||
"owner" TEXT,
|
||||
"lastReview" TIMESTAMP(3),
|
||||
"source" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "knowledge_entries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "error_codes" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "error_codes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "known_issues" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"errorCodeId" TEXT,
|
||||
"description" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'open',
|
||||
|
||||
CONSTRAINT "known_issues_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "runbooks" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"isCurrentVersion" BOOLEAN NOT NULL DEFAULT true,
|
||||
"productId" TEXT NOT NULL,
|
||||
"steps" JSONB NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "runbooks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_productId_isCurrentVersion_status_effecti_idx" ON "knowledge_entries"("productId", "isCurrentVersion", "status", "effectiveDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_entries_code_version_key" ON "knowledge_entries"("code", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "error_codes_productId_code_key" ON "error_codes"("productId", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runbooks_productId_key_isCurrentVersion_active_idx" ON "runbooks"("productId", "key", "isCurrentVersion", "active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "runbooks_key_productId_version_key" ON "runbooks"("key", "productId", "version");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entries" ADD CONSTRAINT "knowledge_entries_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "error_codes" ADD CONSTRAINT "error_codes_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "known_issues" ADD CONSTRAINT "known_issues_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "known_issues" ADD CONSTRAINT "known_issues_errorCodeId_fkey" FOREIGN KEY ("errorCodeId") REFERENCES "error_codes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runbooks" ADD CONSTRAINT "runbooks_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
+84
-4
@@ -34,10 +34,14 @@ model Product {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
categories Category[]
|
||||
integration ProductIntegration?
|
||||
problems Problem[]
|
||||
tickets Ticket[]
|
||||
categories Category[]
|
||||
integration ProductIntegration?
|
||||
problems Problem[]
|
||||
tickets Ticket[]
|
||||
knowledgeEntries KnowledgeEntry[]
|
||||
errorCodes ErrorCode[]
|
||||
knownIssues KnownIssue[]
|
||||
runbooks Runbook[]
|
||||
|
||||
@@map("products")
|
||||
}
|
||||
@@ -177,6 +181,82 @@ model TicketAttachment {
|
||||
@@map("ticket_attachments")
|
||||
}
|
||||
|
||||
model KnowledgeEntry {
|
||||
id String @id @default(cuid())
|
||||
code String // KB-<PRODUCT>-<SEQ>, e.g. KB-DQ-102 — shared across versions,
|
||||
// logical identifier is (code, version), NOT code alone — see
|
||||
// specs/004-product-knowledge/research.md "Versioning mechanism"
|
||||
version Int @default(1)
|
||||
isCurrentVersion Boolean @default(true)
|
||||
productId String
|
||||
feature String?
|
||||
type String // known_issue | faq | resolution_procedure | operations
|
||||
problem String?
|
||||
symptoms String?
|
||||
errorCode String?
|
||||
cause String?
|
||||
recommendedSolution String?
|
||||
verificationSteps String?
|
||||
escalationGuidance String?
|
||||
status String @default("draft") // draft | published | unpublished
|
||||
effectiveDate DateTime?
|
||||
categoryScope String[]
|
||||
validationStatus String @default("unvalidated") // unvalidated | validated
|
||||
owner String?
|
||||
lastReview DateTime?
|
||||
source String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
|
||||
@@unique([code, version])
|
||||
@@index([productId, isCurrentVersion, status, effectiveDate])
|
||||
@@map("knowledge_entries")
|
||||
}
|
||||
|
||||
model ErrorCode {
|
||||
id String @id @default(cuid())
|
||||
code String // e.g. LAYOUT_PARSE_042
|
||||
productId String
|
||||
description String
|
||||
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
knownIssues KnownIssue[]
|
||||
|
||||
@@unique([productId, code])
|
||||
@@map("error_codes")
|
||||
}
|
||||
|
||||
model KnownIssue {
|
||||
id String @id @default(cuid())
|
||||
productId String
|
||||
errorCodeId String?
|
||||
description String
|
||||
status String @default("open")
|
||||
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
errorCode ErrorCode? @relation(fields: [errorCodeId], references: [id])
|
||||
|
||||
@@map("known_issues")
|
||||
}
|
||||
|
||||
model Runbook {
|
||||
id String @id @default(cuid())
|
||||
key String // e.g. PDF_HTML_CONVERSION_FAILURE — shared across versions, logical
|
||||
// identifier is (key, productId, version), NOT key alone — same convention as KnowledgeEntry
|
||||
version Int @default(1)
|
||||
isCurrentVersion Boolean @default(true)
|
||||
productId String
|
||||
steps Json // ordered array — order preserved exactly as authored
|
||||
active Boolean @default(true)
|
||||
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
|
||||
@@unique([key, productId, version])
|
||||
@@index([productId, key, isCurrentVersion, active])
|
||||
@@map("runbooks")
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
actor String // ProductIntegration id, agent id, "system", "ai", etc. — never a local
|
||||
|
||||
Reference in New Issue
Block a user