/** * Incident timeline append helpers (Sprint 19). * * Every helper that writes a domain artifact (observations.jsonl, * actions.jsonl, changelog.jsonl, runbook-execution.jsonl) ALSO appends * a corresponding event to timeline.jsonl in the same mutex tick. This * makes "show me what happened in chronological order" a single file read. * * Append atomicity: fs.open with O_WRONLY|O_APPEND|O_CREAT plus explicit * fh.chmod(0o600) after open — mirrors the audit.ts pattern from Sprint 13. * fs.appendFile is NOT used because it does not reliably honor the mode * argument across all Node versions (see audit.ts header comment). * * POSIX O_APPEND atomicity: single-line records are well under PIPE_BUF * (4096 bytes), so cross-process appends are safe in practice. * * Concurrent appends to the same incident are serialized via a per-incidentId * Promise-chain mutex. Unrelated incidents proceed in parallel. * * setIncidentStatus uses atomic temp-file + rename to avoid a torn write * if the process crashes mid-update. * * Sprint 19 — src/incident/timeline.ts */ import type { BoberConfig } from "../config/schema.js"; import { type IncidentId, type IncidentMetadata, type IncidentStatus, type IncidentSummary, type TimelineEvent, type ObservationEntry, type ActionEntry, type ChangeEntry, type RunbookExecutionEntry } from "./types.js"; import type { VerifyResult } from "./resolution-verify.js"; /** * Derive a short kebab-case slug from a symptom string. * * Rules: * 1. Lowercase the input. * 2. Split on whitespace; take the first 3 non-empty tokens. * 3. For each token, strip all characters outside [a-z0-9] (unicode is stripped). * 4. Join surviving tokens with '-'. * 5. Strip leading/trailing hyphens. * 6. Truncate to 30 characters (hard limit). * 7. If the result is empty (empty input / all-punctuation / unicode-only), return 'untitled'. * * Exported for unit testing. */ export declare function deriveSlug(symptom: string): string; /** * Create a new incident artifact directory with all required files. * * Layout created: * ``` * .bober/incidents// * incident.json — metadata (JSON) * timeline.jsonl — chronological master event log (JSONL, empty) * observations.jsonl — verified facts (JSONL, empty) * actions.jsonl — actions taken/proposed (JSONL, empty) * changelog.jsonl — deploys/config changes (JSONL, empty) * runbook-execution.jsonl — runbook step results (JSONL, empty) * hypotheses.md — current hypotheses (Markdown, empty) * diagnoses/ — diagnosis JSON files from bober-diagnoser * ``` * * @param symptom Human-readable description of the incident trigger. * @param projectRoot Absolute path to the project root (caller resolves this). * @returns The new incident ID, e.g. 'inc-20260524-500-errors-on'. */ export declare function createIncident(symptom: string, projectRoot: string): Promise; /** * Append a TimelineEvent to timeline.jsonl. * * This is the only append helper that writes ONLY to timeline.jsonl. * All other append helpers call this implicitly via the double-write pattern. */ export declare function appendTimeline(projectRoot: string, incidentId: IncidentId, event: TimelineEvent): Promise; /** * Append an ObservationEntry to observations.jsonl AND emit a timeline event. * * Both writes happen inside the same mutex tick so they are ordered * deterministically relative to other concurrent appends on this incident. */ export declare function appendObservation(projectRoot: string, incidentId: IncidentId, entry: ObservationEntry): Promise; /** * Append an ActionEntry to actions.jsonl AND emit a timeline event. */ export declare function appendAction(projectRoot: string, incidentId: IncidentId, entry: ActionEntry): Promise; /** * Append a ChangeEntry to changelog.jsonl AND emit a timeline event. * * Throws a ZodError if `entry.inverse` is missing — the field is REQUIRED * at the schema level so Sprint 21 rollback awareness can always find an * inverse for every executed change. * * The zod validation runs BEFORE the mutex is entered so no file is * touched if validation fails. */ export declare function appendChange(projectRoot: string, incidentId: IncidentId, entry: ChangeEntry): Promise; /** * Append a RunbookExecutionEntry to runbook-execution.jsonl AND emit a * timeline event. */ export declare function appendRunbookExecution(projectRoot: string, incidentId: IncidentId, entry: RunbookExecutionEntry): Promise; /** Options for the resolution gate (Sprint 22) + postmortem trigger (Sprint 23) + telemetry (Sprint 28). */ export interface SetStatusOpts { /** REQUIRED when status='resolved' (unless overrideToken given). Must have verified=true. */ verifyResult?: VerifyResult; /** REQUIRED when status='resolved' AND no verifyResult. Format: 'SKIP_METRIC_VERIFY: '. Empty reason rejects. */ overrideToken?: string; /** When true (and status === 'resolved'), trigger async postmortem generation after the * status write. Only the explicit literal false disables auto-gen. Default: true. * Sprint 23. */ autoPostmortem?: boolean; /** Test seam: if provided, the function calls back with the Promise that resolves when * postmortem synthesis completes. Production callers leave this undefined and rely on * fire-and-forget behavior. Sprint 23. */ onPostmortemPromise?: (p: Promise) => void; /** Sprint 28 — optional config for telemetry. When provided and telemetry.enabled=true, * an 'incident-resolved' event is emitted (fire-and-forget) after the status write. * Callers that don't have config available leave this undefined; no telemetry fires. */ config?: BoberConfig; /** Sprint 28 — absolute project root for telemetry file path resolution. * Required when config is provided for telemetry emit. */ telemetryProjectRoot?: string; } /** * Update the status field in incident.json atomically. * * If status is 'resolved', sets resolvedAt to now (ISO-8601) automatically. * Any additional fields in `extras` are merged in. * * Sprint 22 resolution gate: when transitioning to 'resolved', one of the * following MUST be provided via opts: * 1. opts.verifyResult with verified=true — metric verification passed. * 2. opts.overrideToken matching 'SKIP_METRIC_VERIFY: ' with a * non-empty, non-whitespace reason — operator override with audit trail. * Any other call to setIncidentStatus(id, 'resolved') will THROW. * * Uses temp-file + POSIX rename for crash safety. */ export declare function setIncidentStatus(projectRoot: string, incidentId: IncidentId, status: IncidentStatus, extras?: Partial>, opts?: SetStatusOpts): Promise; /** * List all incidents in .bober/incidents/, sorted by createdAt descending. * * Gracefully handles: * - Missing .bober/incidents/ directory → returns []. * - Malformed incident.json → logged via logger.warn, skipped. * * ENOENT on readdir is caught and returns []. Any other readdir error is * re-thrown (do NOT silence unexpected failures). */ export declare function listIncidents(projectRoot: string): Promise; //# sourceMappingURL=timeline.d.ts.map