# Knowledge Module - Developer Guide

Comprehensive developer documentation for the Knowledge Module implementation, architecture, and contribution guidelines.

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [Database Schema](#database-schema)
3. [Service Layer](#service-layer)
4. [Caching Strategy](#caching-strategy)
5. [Wiki-Link System](#wiki-link-system)
6. [Testing Guide](#testing-guide)
7. [Contributing](#contributing)

---

## Architecture Overview

The Knowledge Module follows a **layered architecture** pattern:

```
┌─────────────────────────────────────────┐
│          Controllers (REST)             │
│  knowledge | search | tags | import     │
└────────────────┬────────────────────────┘
                 │
┌────────────────▼────────────────────────┐
│           Service Layer                 │
│  KnowledgeService | SearchService       │
│  LinkSyncService  | GraphService        │
│  TagsService      | ImportExportService │
│  StatsService     | CacheService        │
└────────────────┬────────────────────────┘
                 │
┌────────────────▼────────────────────────┐
│        Data Access (Prisma ORM)         │
│  KnowledgeEntry | KnowledgeLink         │
│  KnowledgeTag   | KnowledgeEntryVersion │
│  KnowledgeEmbedding                     │
└────────────────┬────────────────────────┘
                 │
┌────────────────▼────────────────────────┐
│       PostgreSQL 17 + pgvector          │
└─────────────────────────────────────────┘
```

### Module Structure

```
apps/api/src/knowledge/
├── controllers/
│   ├── knowledge.controller.ts       # Entry CRUD endpoints
│   ├── search.controller.ts          # Search endpoints
│   ├── tags.controller.ts            # Tag management
│   ├── import-export.controller.ts   # Import/export
│   └── stats.controller.ts           # Statistics
├── services/
│   ├── cache.service.ts              # Valkey caching
│   ├── graph.service.ts              # Graph traversal
│   ├── import-export.service.ts      # File import/export
│   ├── link-resolution.service.ts    # Link resolution
│   ├── link-sync.service.ts          # Link synchronization
│   ├── search.service.ts             # Full-text search
│   └── stats.service.ts              # Statistics aggregation
├── entities/
│   ├── knowledge-entry.entity.ts     # Entry DTOs
│   ├── knowledge-entry-version.entity.ts
│   ├── graph.entity.ts               # Graph DTOs
│   └── stats.entity.ts               # Stats DTOs
├── dto/
│   ├── create-entry.dto.ts
│   ├── update-entry.dto.ts
│   ├── entry-query.dto.ts
│   ├── search-query.dto.ts
│   └── ...
├── utils/
│   ├── wiki-link-parser.ts           # Wiki-link parsing
│   └── markdown.ts                   # Markdown rendering
├── knowledge.service.ts              # Core entry service
├── tags.service.ts                   # Tag service
└── knowledge.module.ts               # NestJS module
```

### Key Responsibilities

**Controllers**

- HTTP request/response handling
- Input validation (DTOs)
- Permission enforcement (guards)
- Error handling

**Services**

- Business logic
- Data transformation
- Transaction management
- Cache invalidation

**Repositories (Prisma)**

- Database queries
- Relation loading
- Type-safe data access

---

## Database Schema

### Core Models

#### KnowledgeEntry

Main entity for knowledge base entries.

```prisma
model KnowledgeEntry {
  id          String    @id @default(uuid()) @db.Uuid
  workspaceId String    @map("workspace_id") @db.Uuid
  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  // Identity
  slug  String  // URL-friendly identifier
  title String  // Display name

  // Content
  content     String  @db.Text       // Raw markdown
  contentHtml String? @map("content_html") @db.Text  // Rendered HTML
  summary     String?                 // Optional brief description

  // Status
  status     EntryStatus @default(DRAFT)      // DRAFT | PUBLISHED | ARCHIVED
  visibility Visibility  @default(PRIVATE)    // PRIVATE | WORKSPACE | PUBLIC

  // Audit
  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
  updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
  createdBy String   @map("created_by") @db.Uuid
  updatedBy String   @map("updated_by") @db.Uuid

  // Relations
  tags          KnowledgeEntryTag[]
  outgoingLinks KnowledgeLink[]         @relation("SourceEntry")
  incomingLinks KnowledgeLink[]         @relation("TargetEntry")
  versions      KnowledgeEntryVersion[]
  embedding     KnowledgeEmbedding?

  @@unique([workspaceId, slug])
  @@index([workspaceId, status])
  @@index([workspaceId, updatedAt])
  @@map("knowledge_entries")
}
```

**Indexes:**

- `workspaceId, slug` (unique constraint)
- `workspaceId, status` (filtering)
- `workspaceId, updatedAt` (recent entries)

#### KnowledgeLink

Represents wiki-links between entries.

```prisma
model KnowledgeLink {
  id String @id @default(uuid()) @db.Uuid

  sourceId String         @map("source_id") @db.Uuid
  source   KnowledgeEntry @relation("SourceEntry", fields: [sourceId], references: [id], onDelete: Cascade)

  targetId String         @map("target_id") @db.Uuid
  target   KnowledgeEntry @relation("TargetEntry", fields: [targetId], references: [id], onDelete: Cascade)

  // Link metadata
  linkText String  @map("link_text")  // Original link text from markdown
  context  String?                     // Surrounding text (future feature)

  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz

  @@unique([sourceId, targetId])
  @@index([sourceId])
  @@index([targetId])
  @@map("knowledge_links")
}
```

**Unique constraint:**

- `sourceId, targetId` (prevents duplicate links)

**Indexes:**

- `sourceId` (outgoing links lookup)
- `targetId` (backlinks lookup)

#### KnowledgeEntryVersion

Version history for entries.

```prisma
model KnowledgeEntryVersion {
  id      String         @id @default(uuid()) @db.Uuid
  entryId String         @map("entry_id") @db.Uuid
  entry   KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)

  version Int     // Version number (1, 2, 3, ...)
  title   String
  content String  @db.Text
  summary String?

  createdAt  DateTime @default(now()) @map("created_at") @db.Timestamptz
  createdBy  String   @map("created_by") @db.Uuid
  author     User     @relation("EntryVersionAuthor", fields: [createdBy], references: [id])
  changeNote String?  @map("change_note")  // Optional change description

  @@unique([entryId, version])
  @@index([entryId, version])
  @@map("knowledge_entry_versions")
}
```

**Versioning strategy:**

- Auto-incrementing version numbers
- Immutable history (no updates or deletes)
- Snapshot of title, content, summary at time of save

#### KnowledgeTag

Tags for categorization.

```prisma
model KnowledgeTag {
  id          String    @id @default(uuid()) @db.Uuid
  workspaceId String    @map("workspace_id") @db.Uuid
  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  name        String   // Display name
  slug        String   // URL-friendly identifier
  color       String?  // Hex color (e.g., "#3b82f6")
  description String?

  entries KnowledgeEntryTag[]

  @@unique([workspaceId, slug])
  @@index([workspaceId])
  @@map("knowledge_tags")
}
```

#### KnowledgeEntryTag

Many-to-many junction table for entries and tags.

```prisma
model KnowledgeEntryTag {
  entryId String         @map("entry_id") @db.Uuid
  entry   KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)

  tagId String       @map("tag_id") @db.Uuid
  tag   KnowledgeTag @relation(fields: [tagId], references: [id], onDelete: Cascade)

  @@id([entryId, tagId])
  @@index([entryId])
  @@index([tagId])
  @@map("knowledge_entry_tags")
}
```

#### KnowledgeEmbedding

Semantic search embeddings (future feature).

```prisma
model KnowledgeEmbedding {
  id      String         @id @default(uuid()) @db.Uuid
  entryId String         @unique @map("entry_id") @db.Uuid
  entry   KnowledgeEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)

  embedding Unsupported("vector(1536)")  // pgvector type
  model     String                       // Model used (e.g., "text-embedding-ada-002")

  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
  updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz

  @@index([entryId])
  @@map("knowledge_embeddings")
}
```

---

## Service Layer

### KnowledgeService

Core service for entry management.

**Key methods:**

```typescript
class KnowledgeService {
  // CRUD operations
  async findAll(workspaceId, query): Promise<PaginatedEntries>;
  async findOne(workspaceId, slug): Promise<KnowledgeEntryWithTags>;
  async create(workspaceId, userId, dto): Promise<KnowledgeEntryWithTags>;
  async update(workspaceId, slug, userId, dto): Promise<KnowledgeEntryWithTags>;
  async remove(workspaceId, slug, userId): Promise<void>;

  // Version management
  async findVersions(workspaceId, slug, page, limit): Promise<PaginatedVersions>;
  async findVersion(workspaceId, slug, version): Promise<KnowledgeEntryVersion>;
  async restoreVersion(
    workspaceId,
    slug,
    version,
    userId,
    changeNote
  ): Promise<KnowledgeEntryWithTags>;
}
```

**Create flow:**

```typescript
async create(workspaceId, userId, dto) {
  // 1. Generate slug from title
  const slug = this.generateUniqueSlug(dto.title, workspaceId);

  // 2. Render markdown to HTML
  const contentHtml = renderMarkdown(dto.content);

  // 3. Create entry in transaction
  const entry = await this.prisma.$transaction(async (tx) => {
    // Create entry
    const newEntry = await tx.knowledgeEntry.create({
      data: {
        workspaceId,
        slug,
        title: dto.title,
        content: dto.content,
        contentHtml,
        summary: dto.summary,
        status: dto.status || 'DRAFT',
        visibility: dto.visibility || 'PRIVATE',
        createdBy: userId,
        updatedBy: userId,
      },
    });

    // Create initial version (v1)
    await tx.knowledgeEntryVersion.create({
      data: {
        entryId: newEntry.id,
        version: 1,
        title: newEntry.title,
        content: newEntry.content,
        summary: newEntry.summary,
        createdBy: userId,
        changeNote: dto.changeNote || 'Initial version',
      },
    });

    // Handle tags (create or link)
    if (dto.tags && dto.tags.length > 0) {
      await this.linkTags(tx, newEntry.id, workspaceId, dto.tags);
    }

    return newEntry;
  });

  // 4. Parse and sync wiki-links (outside transaction)
  await this.linkSync.syncLinks(entry.id, dto.content);

  // 5. Invalidate caches
  await this.cache.invalidateSearchCaches(workspaceId);

  // 6. Return with tags
  return this.findOne(workspaceId, slug);
}
```

**Update flow:**

```typescript
async update(workspaceId, slug, userId, dto) {
  // 1. Find existing entry
  const existing = await this.findOne(workspaceId, slug);

  // 2. Get next version number
  const latestVersion = await this.getLatestVersion(existing.id);
  const nextVersion = latestVersion.version + 1;

  // 3. Render new HTML if content changed
  const contentHtml = dto.content
    ? renderMarkdown(dto.content)
    : existing.contentHtml;

  // 4. Update in transaction
  const updated = await this.prisma.$transaction(async (tx) => {
    // Update entry
    const updatedEntry = await tx.knowledgeEntry.update({
      where: { id: existing.id },
      data: {
        title: dto.title ?? existing.title,
        content: dto.content ?? existing.content,
        contentHtml,
        summary: dto.summary ?? existing.summary,
        status: dto.status ?? existing.status,
        visibility: dto.visibility ?? existing.visibility,
        updatedBy: userId,
      },
    });

    // Create version snapshot
    await tx.knowledgeEntryVersion.create({
      data: {
        entryId: updatedEntry.id,
        version: nextVersion,
        title: updatedEntry.title,
        content: updatedEntry.content,
        summary: updatedEntry.summary,
        createdBy: userId,
        changeNote: dto.changeNote || `Update to version ${nextVersion}`,
      },
    });

    // Update tags if provided
    if (dto.tags !== undefined) {
      await this.replaceTags(tx, updatedEntry.id, workspaceId, dto.tags);
    }

    return updatedEntry;
  });

  // 5. Re-sync links if content changed
  if (dto.content) {
    await this.linkSync.syncLinks(updated.id, dto.content);
  }

  // 6. Invalidate caches
  await this.cache.invalidateEntry(workspaceId, slug);
  await this.cache.invalidateSearchCaches(workspaceId);
  await this.cache.invalidateGraphCachesForEntry(updated.id);

  // 7. Return updated entry
  return this.findOne(workspaceId, slug);
}
```

### LinkSyncService

Manages wiki-link parsing and synchronization.

**Key methods:**

```typescript
class LinkSyncService {
  async syncLinks(entryId: string, content: string): Promise<void>;
  async getBacklinks(entryId: string): Promise<BacklinkWithSource[]>;
}
```

**Link sync flow:**

```typescript
async syncLinks(entryId, content) {
  // 1. Parse wiki-links from content
  const parsedLinks = parseWikiLinks(content);

  // 2. Get source entry details
  const entry = await this.prisma.knowledgeEntry.findUnique({
    where: { id: entryId },
    select: { workspaceId: true },
  });

  // 3. Delete existing links from this entry
  await this.prisma.knowledgeLink.deleteMany({
    where: { sourceId: entryId },
  });

  // 4. For each parsed link:
  for (const link of parsedLinks) {
    // Try to resolve target entry
    const target = await this.linkResolver.resolve(
      link.target,
      entry.workspaceId
    );

    if (target) {
      // Create resolved link
      await this.prisma.knowledgeLink.create({
        data: {
          sourceId: entryId,
          targetId: target.id,
          linkText: link.displayText,
        },
      });
    }
    // Note: Unresolved links are simply not created
    // They may resolve later when target entry is created
  }

  // 5. Invalidate graph caches
  await this.cache.invalidateGraphCachesForEntry(entryId);
}
```

### LinkResolutionService

Resolves wiki-link targets to actual entries.

**Resolution strategy:**

```typescript
async resolve(target: string, workspaceId: string) {
  // Strategy 1: Match by exact slug
  let entry = await this.prisma.knowledgeEntry.findUnique({
    where: {
      workspaceId_slug: { workspaceId, slug: target },
    },
  });

  if (entry) return entry;

  // Strategy 2: Generate slug from target and try again
  const slugified = slugify(target, { lower: true, strict: true });
  entry = await this.prisma.knowledgeEntry.findUnique({
    where: {
      workspaceId_slug: { workspaceId, slug: slugified },
    },
  });

  if (entry) return entry;

  // Strategy 3: Match by title (case-insensitive)
  entry = await this.prisma.knowledgeEntry.findFirst({
    where: {
      workspaceId,
      title: {
        equals: target,
        mode: 'insensitive',
      },
    },
  });

  return entry || null;
}
```

**Resolution examples:**

| Link Target   | Resolution                                   |
| ------------- | -------------------------------------------- |
| `react-hooks` | Exact slug match                             |
| `React Hooks` | Slugify to `react-hooks`, then match         |
| `REACT HOOKS` | Case-insensitive title match → `React Hooks` |

### SearchService

Full-text search and filtering.

**Key methods:**

```typescript
class SearchService {
  async search(query, workspaceId, options): Promise<PaginatedSearchResults>;
  async searchByTags(tags, workspaceId, options): Promise<PaginatedEntries>;
  async recentEntries(workspaceId, limit, status?): Promise<KnowledgeEntryWithTags[]>;
}
```

**Search implementation:**

```typescript
async search(query, workspaceId, options) {
  // Check cache first
  const cached = await this.cache.getSearch(workspaceId, query, options);
  if (cached) return cached;

  // Build where clause
  const where: Prisma.KnowledgeEntryWhereInput = {
    workspaceId,
    OR: [
      { title: { contains: query, mode: 'insensitive' } },
      { content: { contains: query, mode: 'insensitive' } },
    ],
  };

  if (options.status) {
    where.status = options.status;
  }

  // Execute search with pagination
  const [entries, total] = await Promise.all([
    this.prisma.knowledgeEntry.findMany({
      where,
      include: { tags: { include: { tag: true } } },
      orderBy: { updatedAt: 'desc' },
      skip: (options.page - 1) * options.limit,
      take: options.limit,
    }),
    this.prisma.knowledgeEntry.count({ where }),
  ]);

  const result = {
    data: entries,
    pagination: {
      page: options.page,
      limit: options.limit,
      total,
      totalPages: Math.ceil(total / options.limit),
    },
  };

  // Cache the result
  await this.cache.setSearch(workspaceId, query, options, result);

  return result;
}
```

### GraphService

Knowledge graph traversal.

**Key methods:**

```typescript
class GraphService {
  async getEntryGraph(
    workspaceId: string,
    entryId: string,
    maxDepth: number = 1
  ): Promise<EntryGraphResponse>;
}
```

**BFS graph traversal:**

```typescript
async getEntryGraph(workspaceId, entryId, maxDepth) {
  // Check cache
  const cached = await this.cache.getGraph(workspaceId, entryId, maxDepth);
  if (cached) return cached;

  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];
  const visited = new Set<string>();
  const queue: Array<[string, number]> = [[entryId, 0]];

  visited.add(entryId);

  while (queue.length > 0) {
    const [currentId, depth] = queue.shift()!;

    // Fetch entry with links
    const entry = await this.prisma.knowledgeEntry.findUnique({
      where: { id: currentId },
      include: {
        tags: { include: { tag: true } },
        outgoingLinks: { include: { target: true } },
        incomingLinks: { include: { source: true } },
      },
    });

    if (!entry) continue;

    // Add node
    nodes.push({
      id: entry.id,
      slug: entry.slug,
      title: entry.title,
      summary: entry.summary,
      tags: entry.tags.map(et => ({
        id: et.tag.id,
        name: et.tag.name,
        slug: et.tag.slug,
        color: et.tag.color,
      })),
      depth,
    });

    // Continue BFS if not at max depth
    if (depth < maxDepth) {
      // Process outgoing links
      for (const link of entry.outgoingLinks) {
        edges.push({
          id: link.id,
          sourceId: link.sourceId,
          targetId: link.targetId,
          linkText: link.linkText,
        });

        if (!visited.has(link.targetId)) {
          visited.add(link.targetId);
          queue.push([link.targetId, depth + 1]);
        }
      }

      // Process incoming links
      for (const link of entry.incomingLinks) {
        const edgeExists = edges.some(
          e => e.sourceId === link.sourceId && e.targetId === link.targetId
        );
        if (!edgeExists) {
          edges.push({
            id: link.id,
            sourceId: link.sourceId,
            targetId: link.targetId,
            linkText: link.linkText,
          });
        }

        if (!visited.has(link.sourceId)) {
          visited.add(link.sourceId);
          queue.push([link.sourceId, depth + 1]);
        }
      }
    }
  }

  const result = {
    centerNode: nodes.find(n => n.id === entryId)!,
    nodes,
    edges,
    stats: {
      totalNodes: nodes.length,
      totalEdges: edges.length,
      maxDepth,
    },
  };

  // Cache result
  await this.cache.setGraph(workspaceId, entryId, maxDepth, result);

  return result;
}
```

---

## Caching Strategy

The Knowledge Module uses **Valkey** (Redis-compatible) for high-performance caching.

### Cache Keys

```
knowledge:entry:{workspaceId}:{slug}
knowledge:search:{workspaceId}:{query}:{options_hash}
knowledge:search-tags:{workspaceId}:{tags}:{options_hash}
knowledge:graph:{workspaceId}:{entryId}:{depth}
```

### Cache Configuration

```typescript
class KnowledgeCacheService {
  private readonly ENTRY_PREFIX = "knowledge:entry:";
  private readonly SEARCH_PREFIX = "knowledge:search:";
  private readonly SEARCH_TAGS_PREFIX = "knowledge:search-tags:";
  private readonly GRAPH_PREFIX = "knowledge:graph:";

  private readonly DEFAULT_TTL = 300; // 5 minutes
  private readonly isEnabled: boolean;

  constructor() {
    this.isEnabled = process.env.KNOWLEDGE_CACHE_ENABLED !== "false";
    this.DEFAULT_TTL = parseInt(process.env.KNOWLEDGE_CACHE_TTL || "300");
  }
}
```

### Invalidation Strategy

**Entry changes:**

- **Create**: Invalidate search caches
- **Update**: Invalidate entry cache, search caches, graph caches
- **Delete**: Invalidate entry cache, search caches, graph caches

**Link changes:**

- Invalidate graph caches for source and target entries

**Tag changes:**

- Invalidate tag-based search caches

```typescript
async invalidateEntry(workspaceId: string, slug: string) {
  const key = `${this.ENTRY_PREFIX}${workspaceId}:${slug}`;
  await this.valkey.del(key);
  this.stats.deletes++;
}

async invalidateSearchCaches(workspaceId: string) {
  const pattern = `${this.SEARCH_PREFIX}${workspaceId}:*`;
  const keys = await this.valkey.keys(pattern);
  if (keys.length > 0) {
    await this.valkey.del(...keys);
    this.stats.deletes += keys.length;
  }
}

async invalidateGraphCachesForEntry(entryId: string) {
  // Graph caches include entryId in the key
  const pattern = `${this.GRAPH_PREFIX}*:${entryId}:*`;
  const keys = await this.valkey.keys(pattern);
  if (keys.length > 0) {
    await this.valkey.del(...keys);
    this.stats.deletes += keys.length;
  }
}
```

### Performance Metrics

Track cache effectiveness:

```typescript
interface CacheStats {
  hits: number;
  misses: number;
  sets: number;
  deletes: number;
  hitRate: number;
}

getStats(): CacheStats {
  const total = this.stats.hits + this.stats.misses;
  return {
    ...this.stats,
    hitRate: total > 0 ? this.stats.hits / total : 0,
  };
}
```

---

## Wiki-Link System

### Parsing Algorithm

The wiki-link parser handles complex edge cases:

**Supported syntax:**

```markdown
[[Page Name]] → Link to "Page Name"
[[page-slug]] → Link by slug
[[Page Name|Display Text]] → Custom display text
```

**Edge cases handled:**

- Nested brackets: `[[Link with [brackets] inside]]`
- Code blocks: `` `[[not a link]]` ``
- Fenced code: ` ```[[not a link]]``` `
- Escaped brackets: `\[[not a link]]`
- Triple brackets: `[[[not a link]]]`

**Parsing flow:**

1. **Find excluded regions** (code blocks, inline code)
2. **Scan for `[[` patterns**
3. **Find matching `]]`**
4. **Validate link target**
5. **Parse pipe separator for display text**
6. **Return array of WikiLink objects**

```typescript
interface WikiLink {
  raw: string; // "[[Page Name]]"
  target: string; // "Page Name" or "page-slug"
  displayText: string; // "Page Name" or custom text
  start: number; // Position in content
  end: number; // Position in content
}
```

### Link Resolution

**Three-step resolution:**

```
1. Exact slug match: "react-hooks" → entry with slug "react-hooks"
2. Slugified match: "React Hooks" → slugify → "react-hooks" → match
3. Title match: Case-insensitive title search
```

**Unresolved links:**

- Not stored in database
- Will auto-resolve when target entry is created
- Future: UI indication for broken links

### Link Synchronization

**On entry create/update:**

```
1. Parse wiki-links from content
2. Delete existing links from this entry
3. For each parsed link:
   a. Try to resolve target
   b. If resolved, create KnowledgeLink record
   c. If unresolved, skip (may resolve later)
4. Invalidate graph caches
```

**Why delete-and-recreate?**

- Simpler than diffing changes
- Ensures consistency with current content
- Links are cheap to recreate

---

## Testing Guide

### Test Structure

```
apps/api/src/knowledge/
├── knowledge.service.spec.ts
├── tags.service.spec.ts
├── search.controller.spec.ts
├── tags.controller.spec.ts
├── services/
│   ├── cache.service.spec.ts
│   ├── graph.service.spec.ts
│   ├── link-resolution.service.spec.ts
│   ├── link-sync.service.spec.ts
│   └── search.service.spec.ts
└── utils/
    ├── markdown.spec.ts
    └── wiki-link-parser.spec.ts
```

### Running Tests

```bash
# All knowledge module tests
pnpm test knowledge

# Specific test file
pnpm test knowledge.service.spec.ts

# Watch mode
pnpm test:watch knowledge

# Coverage
pnpm test:coverage
```

### Test Coverage Requirements

- **Minimum:** 85% overall coverage
- **Critical paths:** 100% coverage required
  - Entry CRUD operations
  - Version management
  - Link resolution
  - Wiki-link parsing

### Writing Tests

**Service tests** (unit):

```typescript
describe("KnowledgeService", () => {
  let service: KnowledgeService;
  let prisma: PrismaService;
  let linkSync: LinkSyncService;
  let cache: KnowledgeCacheService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        KnowledgeService,
        {
          provide: PrismaService,
          useValue: mockDeep<PrismaService>(),
        },
        {
          provide: LinkSyncService,
          useValue: mockDeep<LinkSyncService>(),
        },
        {
          provide: KnowledgeCacheService,
          useValue: mockDeep<KnowledgeCacheService>(),
        },
      ],
    }).compile();

    service = module.get(KnowledgeService);
    prisma = module.get(PrismaService);
    linkSync = module.get(LinkSyncService);
    cache = module.get(KnowledgeCacheService);
  });

  describe("create", () => {
    it("should create entry with unique slug", async () => {
      const dto = {
        title: "Test Entry",
        content: "# Test",
      };

      prisma.knowledgeEntry.create.mockResolvedValue({
        id: "entry-id",
        slug: "test-entry",
        ...dto,
      });

      const result = await service.create("workspace-id", "user-id", dto);

      expect(result.slug).toBe("test-entry");
      expect(linkSync.syncLinks).toHaveBeenCalledWith("entry-id", dto.content);
      expect(cache.invalidateSearchCaches).toHaveBeenCalled();
    });
  });
});
```

**Controller tests** (integration):

```typescript
describe("KnowledgeController (e2e)", () => {
  let app: INestApplication;
  let prisma: PrismaService;

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = module.createNestApplication();
    await app.init();

    prisma = module.get(PrismaService);
  });

  afterAll(async () => {
    await prisma.$disconnect();
    await app.close();
  });

  describe("POST /knowledge/entries", () => {
    it("should create entry and return 201", () => {
      return request(app.getHttpServer())
        .post("/knowledge/entries")
        .set("Authorization", `Bearer ${authToken}`)
        .set("x-workspace-id", workspaceId)
        .send({
          title: "Test Entry",
          content: "# Test Content",
          status: "DRAFT",
        })
        .expect(201)
        .expect((res) => {
          expect(res.body.slug).toBe("test-entry");
          expect(res.body.status).toBe("DRAFT");
        });
    });
  });
});
```

**Utility tests:**

````typescript
describe("parseWikiLinks", () => {
  it("should parse simple wiki link", () => {
    const content = "See [[My Page]] for details.";
    const links = parseWikiLinks(content);

    expect(links).toHaveLength(1);
    expect(links[0]).toMatchObject({
      target: "My Page",
      displayText: "My Page",
      raw: "[[My Page]]",
    });
  });

  it("should parse link with custom display text", () => {
    const content = "See [[page-slug|custom text]] here.";
    const links = parseWikiLinks(content);

    expect(links[0]).toMatchObject({
      target: "page-slug",
      displayText: "custom text",
    });
  });

  it("should ignore links in code blocks", () => {
    const content = "```\n[[Not A Link]]\n```";
    const links = parseWikiLinks(content);

    expect(links).toHaveLength(0);
  });
});
````

### Test Data Setup

Create reusable fixtures:

```typescript
// test/fixtures/knowledge.fixtures.ts
export const createMockEntry = (overrides = {}) => ({
  id: "entry-uuid",
  workspaceId: "workspace-uuid",
  slug: "test-entry",
  title: "Test Entry",
  content: "# Test",
  contentHtml: "<h1>Test</h1>",
  summary: null,
  status: "DRAFT",
  visibility: "PRIVATE",
  createdAt: new Date(),
  updatedAt: new Date(),
  createdBy: "user-uuid",
  updatedBy: "user-uuid",
  ...overrides,
});

export const createMockVersion = (entryId: string, version: number) => ({
  id: `version-${version}-uuid`,
  entryId,
  version,
  title: "Test Entry",
  content: `# Version ${version}`,
  summary: null,
  changeNote: `Update ${version}`,
  createdAt: new Date(),
  createdBy: "user-uuid",
});
```

---

## Contributing

### Development Workflow

1. **Create feature branch**

   ```bash
   git checkout -b feature/your-feature develop
   ```

2. **Write tests first** (TDD approach)

   ```bash
   pnpm test:watch knowledge
   ```

3. **Implement feature**
   - Follow TypeScript strict mode
   - Use existing patterns
   - Add JSDoc comments

4. **Run tests and linting**

   ```bash
   pnpm test knowledge
   pnpm lint
   pnpm format
   ```

5. **Commit with conventional format**

   ```bash
   git commit -m "feat(knowledge): add semantic search endpoint"
   ```

6. **Create pull request to `develop`**

### Code Style

**TypeScript:**

- Strict mode enabled
- No `any` types
- Explicit return types
- Interface over type when possible

**NestJS conventions:**

- Services are `@Injectable()`
- Controllers use `@Controller()`, `@Get()`, etc.
- DTOs with class-validator decorators
- Dependency injection via constructor

**Naming:**

- `camelCase` for variables and functions
- `PascalCase` for classes and interfaces
- `UPPER_SNAKE_CASE` for constants
- `kebab-case` for file names

### Adding New Features

**New endpoint:**

1. Create DTO in `dto/`
2. Add controller method with proper guards
3. Implement service method
4. Write tests (unit + integration)
5. Update API documentation

**New service:**

1. Create service class in `services/`
2. Add `@Injectable()` decorator
3. Register in `knowledge.module.ts`
4. Write comprehensive tests
5. Document public API with JSDoc

**Database changes:**

1. Update `schema.prisma`
2. Create migration: `pnpm prisma migrate dev --name your_migration_name`
3. Update entity interfaces in `entities/`
4. Update services to use new schema
5. Write migration tests

### Performance Considerations

**Always consider:**

- Database query efficiency (use indexes)
- N+1 query problems (use `include` wisely)
- Cache invalidation strategy
- Transaction boundaries
- Large content handling

**Optimization checklist:**

- [ ] Proper indexes on database columns
- [ ] Caching for expensive operations
- [ ] Pagination for list endpoints
- [ ] Lazy loading for relations
- [ ] Bulk operations where possible

### Security Checklist

- [ ] Input validation (DTOs)
- [ ] Permission guards on endpoints
- [ ] Workspace isolation (never cross workspaces)
- [ ] SQL injection prevention (Prisma handles this)
- [ ] No sensitive data in logs
- [ ] Rate limiting (future)

---

## Additional Resources

- **[User Guide](KNOWLEDGE_USER_GUIDE.md)** — End-user documentation
- **[API Documentation](KNOWLEDGE_API.md)** — Complete API reference
- **[Main README](README.md)** — Project overview
- **[NestJS Docs](https://docs.nestjs.com/)** — Framework documentation
- **[Prisma Docs](https://www.prisma.io/docs)** — ORM documentation

---

**Happy coding! 🚀**
