/** * SOP Service * * Manages Standard Operating Procedures (SOPs) including loading, * indexing, matching, and creating custom SOPs. * * @module services/sop/sop.service */ import { SOP, SOPRole, SOPCategory, SOPIndex, SOPIndexEntry, SOPMatchParams } from '../../types/sop.types.js'; /** * Interface for the SOP Service */ export interface ISOPService { /** * Initialize the service (create directories, build index) */ initialize(): Promise; /** * Rebuild the SOP index from filesystem */ rebuildIndex(): Promise; /** * Get an SOP by ID * * @param id - SOP identifier * @returns SOP or null if not found */ getSOP(id: string): Promise; /** * Get all SOPs for a specific role * * @param role - Role to filter by * @returns Array of SOPs */ getSOPsByRole(role: SOPRole | 'all'): Promise; /** * Get all SOPs in a category * * @param category - Category to filter by * @returns Array of SOPs */ getSOPsByCategory(category: SOPCategory): Promise; /** * Find SOPs relevant to the given context * * @param params - Match parameters * @returns Array of matching SOPs */ findRelevantSOPs(params: SOPMatchParams): Promise; /** * Generate SOP context string for prompts * * @param params - Match parameters * @returns Formatted SOP context string */ generateSOPContext(params: SOPMatchParams): Promise; /** * Create a custom SOP * * @param sop - SOP data (without id, timestamps, version) * @returns Created SOP ID */ createCustomSOP(sop: Omit): Promise; /** * Update an existing SOP * * @param id - SOP identifier * @param updates - Partial SOP updates */ updateSOP(id: string, updates: Partial): Promise; /** * Delete a custom SOP * * @param id - SOP identifier */ deleteSOP(id: string): Promise; /** * Get the current SOP index * * @returns SOP index */ getIndex(): Promise; /** * Search SOPs by query string * * @param query - Search query * @returns Matching index entries */ searchSOPs(query: string): Promise; /** * Get the reason for the last graceful fallback, if any. * * Used by the API layer to surface degraded-mode telemetry to callers * (e.g. when the index file is missing and an empty in-memory fallback * was substituted). Returns null after any operation that successfully * read the index from disk. * * @returns Fallback reason string, or null if last operation served * from real data on disk. */ getLastFallbackReason(): string | null; } /** * Service for managing Standard Operating Procedures * * @example * ```typescript * const service = SOPService.getInstance(); * await service.initialize(); * * const sops = await service.findRelevantSOPs({ * role: 'developer', * taskContext: 'implementing a new API endpoint', * }); * ``` */ export declare class SOPService implements ISOPService { private static instance; private readonly logger; private readonly basePath; private readonly systemPath; private readonly customPath; private readonly indexPath; private index; private sopCache; private initialized; private lastFallbackReason; /** * Sentinel reason emitted when the canonical index is missing/unparseable * and we synthesise an empty in-memory index so callers can degrade * cleanly instead of receiving a 500. * * Surfaced via `getLastFallbackReason()` and propagated by the API * controller to clients in the response envelope. */ static readonly FALLBACK_REASON_INDEX_MISSING = "index-missing-graceful-fallback"; /** * Private constructor for singleton pattern * * @param basePath - Optional custom base path for SOPs */ private constructor(); /** * Gets the singleton instance * * @returns The SOPService instance */ static getInstance(): SOPService; /** * Clears the singleton instance (for testing) */ static clearInstance(): void; /** * Create an instance with a custom base path (for testing) * * @param basePath - Custom base path * @returns SOPService instance */ static createWithPath(basePath: string): SOPService; /** * Get the Crewly home directory * * @returns Home directory path */ private getCrewlyHome; /** * Initialize the service */ initialize(): Promise; /** * Rebuild the SOP index from filesystem. * * Resilience contract (F8 — get-sops graceful fallback): * - Walks `systemPath` and `customPath` to collect entries (each walker * silently no-ops when its directory is absent). * - Populates `this.index` with the collected entries (possibly empty) * BEFORE attempting any disk write, so callers reading the index * in-memory always see a valid object even if persistence fails. * - Persists by ensuring `basePath` exists (`mkdir -p`) and then * writing the index file. Persistence failure is logged-warn and * swallowed — the in-memory index remains canonical for this process. * * Should not throw under normal failure modes (missing dir, ENOSPC, * permission to write index). Pathological errors in helpers are * logged inside them; this method returns successfully with an * in-memory index regardless. */ rebuildIndex(): Promise; /** * Ensure the SOP base directory exists before writing the index. * * Idempotent. Created with `recursive: true` so any missing parent * (e.g. a fresh `~/.crewly` install where `sops/` was never created) * is materialised in one call. */ private ensureBasePathExists; /** * Index a directory recursively * * @param dirPath - Directory path to index * @param entries - Array to add entries to * @param isSystem - Whether these are system SOPs */ private indexDirectory; /** * Parse an SOP file from disk * * @param filePath - Path to the SOP file * @returns Parsed SOP or null on error */ private parseSOPFile; /** * Parse SOP content (frontmatter + body) * * @param content - Raw file content * @returns Parsed SOP or null on error */ private parseSOPContent; /** * Convert parsed SOP to full SOP object * * @param parsed - Parsed SOP * @returns Full SOP object */ private parsedToSOP; /** * Get an SOP by ID * * @param id - SOP identifier * @returns SOP or null if not found */ getSOP(id: string): Promise; /** * Get all SOPs for a specific role * * @param role - Role to filter by * @returns Array of SOPs */ getSOPsByRole(role: SOPRole | 'all'): Promise; /** * Get all SOPs in a category * * @param category - Category to filter by * @returns Array of SOPs */ getSOPsByCategory(category: SOPCategory): Promise; /** * Find SOPs relevant to the given context * * @param params - Match parameters * @returns Array of matching SOPs */ findRelevantSOPs(params: SOPMatchParams): Promise; /** * Check if an SOP matches a role * * @param entry - SOP index entry * @param role - Role to check * @returns True if matches */ private matchesRole; /** * Score how relevant an SOP is to the given context * * @param entry - SOP index entry * @param context - Context string * @param taskType - Optional task type * @returns Relevance score (0-1) */ scoreRelevance(entry: SOPIndexEntry, context: string, taskType?: string): number; /** * Generate SOP context string for prompts * * @param params - Match parameters * @returns Formatted SOP context string */ generateSOPContext(params: SOPMatchParams): Promise; /** * Read a team's installed/custom SOP library from * `~/.crewly/teams//sops/` (recursively). Each `.md` file becomes a * SOP entry with its frontmatter `title`/`category` (falling back to the * filename / parent folder). Read-only; tolerant of a missing directory. * * @param teamId - The team whose SOP library to read. * @returns Team SOP entries (possibly empty). */ private readTeamSOPs; /** * Create a custom SOP * * @param sopData - SOP data (without id, timestamps, version) * @returns Created SOP ID */ createCustomSOP(sopData: Omit): Promise; /** * Update an existing SOP * * @param id - SOP identifier * @param updates - Partial SOP updates */ updateSOP(id: string, updates: Partial): Promise; /** * Delete a custom SOP * * @param id - SOP identifier */ deleteSOP(id: string): Promise; /** * Get the current SOP index * * @returns SOP index */ getIndex(): Promise; /** * Search SOPs by query string * * @param query - Search query * @returns Matching index entries */ searchSOPs(query: string): Promise; /** * Ensure the index exists (load from disk or rebuild from filesystem). * * Graceful-fallback contract (F8): * - Happy path: read `index.json`, JSON.parse it, set `this.index`. * `lastFallbackReason` is cleared. * - Index missing/unparseable: invoke `rebuildIndex()`. If rebuild * succeeded with N>=0 entries, mark fallback reason iff the index * was rebuilt due to missing-on-disk (so the controller can surface * `index-missing-graceful-fallback` to clients). * - Catastrophic rebuild failure (extremely unlikely — rebuild itself * swallows persistence errors): synthesise an empty in-memory index * and stamp `lastFallbackReason`. The endpoint returns 200 with an * empty payload rather than 500. */ private ensureIndex; /** * Get the reason for the last graceful fallback, if any. * * Returns the `FALLBACK_REASON_INDEX_MISSING` sentinel when the most * recent index load fell back to a regenerated/synthesised in-memory * index. Returns null when the cached index was loaded directly from * a healthy `index.json` on disk. * * @returns Fallback reason string, or null. */ getLastFallbackReason(): string | null; /** * Save the index to disk */ private saveIndex; /** * Convert an SOP to Markdown format * * @param sop - SOP to convert * @returns Markdown string */ private sopToMarkdown; /** * Clear the SOP cache */ clearCache(): void; } //# sourceMappingURL=sop.service.d.ts.map