feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b20c94ea1a
commit
bd3401df1c
33 changed files with 7132 additions and 554 deletions
|
|
@ -246,6 +246,206 @@ export const AnalyzerJob = z.object({
|
|||
});
|
||||
export type AnalyzerJob = z.infer<typeof AnalyzerJob>;
|
||||
|
||||
// =============================================================================
|
||||
// Phase 2 — Stage 6 fingerprint schema (cross-ticket aggregation).
|
||||
// =============================================================================
|
||||
|
||||
export const RootCauseClass = z.enum([
|
||||
'configuration_drift',
|
||||
'user_error',
|
||||
'vendor_issue',
|
||||
'hardware_failure',
|
||||
'documentation_gap',
|
||||
'process_gap',
|
||||
'unknown',
|
||||
'other',
|
||||
]);
|
||||
export type RootCauseClass = z.infer<typeof RootCauseClass>;
|
||||
|
||||
export const ResolutionPath = z.enum([
|
||||
'resolved_by_wulf',
|
||||
'resolved_by_vendor',
|
||||
'resolved_by_client',
|
||||
'unresolved',
|
||||
'self_resolved_before_wulf_action',
|
||||
]);
|
||||
export type ResolutionPath = z.infer<typeof ResolutionPath>;
|
||||
|
||||
export const FingerprintConfidence = z.enum(['low', 'medium', 'high']);
|
||||
export type FingerprintConfidence = z.infer<typeof FingerprintConfidence>;
|
||||
|
||||
export const AggregateFingerprint = z.object({
|
||||
category: z.string(),
|
||||
subcategories: z.array(z.string()),
|
||||
ticket_type_inferred: z.string(),
|
||||
root_cause_class: RootCauseClass,
|
||||
|
||||
client_name: z.string(),
|
||||
vendors_involved: z.array(z.string()),
|
||||
applications_involved: z.array(z.string()),
|
||||
device_classes: z.array(z.string()),
|
||||
|
||||
wulf_actions_taken: z.array(z.string()),
|
||||
vendor_cases_opened: z.number().int().nonnegative(),
|
||||
resolution_path: ResolutionPath,
|
||||
|
||||
documentation_gaps_observed: z
|
||||
.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
confidence: FingerprintConfidence,
|
||||
})
|
||||
)
|
||||
.max(5),
|
||||
process_gaps_observed: z
|
||||
.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
severity: Severity,
|
||||
})
|
||||
)
|
||||
.max(5),
|
||||
|
||||
similar_to_signals: z.array(z.string()),
|
||||
tags: z.array(z.string()),
|
||||
|
||||
generated_by_model: z.string(),
|
||||
generated_at: z.string().datetime({ offset: true }),
|
||||
});
|
||||
export type AggregateFingerprint = z.infer<typeof AggregateFingerprint>;
|
||||
|
||||
// =============================================================================
|
||||
// Phase 2 — Stage execution row (per-stage I/O persistence).
|
||||
// =============================================================================
|
||||
|
||||
export const StageName = z.enum([
|
||||
'preprocess',
|
||||
'triage',
|
||||
'itglue',
|
||||
'analyze',
|
||||
'deep_review',
|
||||
'fingerprint',
|
||||
]);
|
||||
export type StageName = z.infer<typeof StageName>;
|
||||
|
||||
/**
|
||||
* In-memory shape used by the pipeline to record per-stage I/O. The pipeline
|
||||
* pushes one of these into the worker's array via the onStageRecord callback,
|
||||
* and the worker bulk-inserts them after the analyzer_analyses row exists
|
||||
* (success OR failure).
|
||||
*/
|
||||
export interface StageExecutionRecord {
|
||||
stage: StageName;
|
||||
stage_order: number;
|
||||
model_id: string | null;
|
||||
input_payload: unknown;
|
||||
output_payload: unknown;
|
||||
input_tokens: number | null;
|
||||
output_tokens: number | null;
|
||||
latency_ms: number | null;
|
||||
started_at: Date;
|
||||
completed_at: Date;
|
||||
error_message: string | null;
|
||||
}
|
||||
|
||||
export const StageExecution = z.object({
|
||||
id: z.string().uuid(),
|
||||
analysisId: z.string().uuid(),
|
||||
stage: StageName,
|
||||
stageOrder: z.number().int().positive(),
|
||||
modelId: z.string().nullable(),
|
||||
inputPayload: z.unknown(),
|
||||
outputPayload: z.unknown(),
|
||||
inputTokens: z.number().int().nullable(),
|
||||
outputTokens: z.number().int().nullable(),
|
||||
latencyMs: z.number().int().nullable(),
|
||||
startedAt: z.string().datetime({ offset: true }),
|
||||
completedAt: z.string().datetime({ offset: true }),
|
||||
errorMessage: z.string().nullable(),
|
||||
});
|
||||
export type StageExecution = z.infer<typeof StageExecution>;
|
||||
|
||||
// =============================================================================
|
||||
// Phase 2.6 — Aggregate report (reduce step) schemas.
|
||||
// =============================================================================
|
||||
|
||||
export const ITGlueCheck = z.enum([
|
||||
'no_doc_exists',
|
||||
'doc_exists_but_unused',
|
||||
'unable_to_verify',
|
||||
]);
|
||||
export type ITGlueCheck = z.infer<typeof ITGlueCheck>;
|
||||
|
||||
export const RecommendedActionType = z.enum([
|
||||
'documentation',
|
||||
'process',
|
||||
'training',
|
||||
'tooling',
|
||||
]);
|
||||
export type RecommendedActionType = z.infer<typeof RecommendedActionType>;
|
||||
|
||||
export const AggregateReduceResponse = z.object({
|
||||
documentation_gaps: z.array(
|
||||
z.object({
|
||||
gap: z.string(),
|
||||
frequency: z.number().int().nonnegative(),
|
||||
example_ticket_numbers: z.array(z.string()),
|
||||
evidence: z.string(),
|
||||
itglue_check: ITGlueCheck,
|
||||
})
|
||||
),
|
||||
process_gaps: z.array(
|
||||
z.object({
|
||||
gap: z.string(),
|
||||
frequency: z.number().int().nonnegative(),
|
||||
severity: Severity,
|
||||
example_ticket_numbers: z.array(z.string()),
|
||||
evidence: z.string(),
|
||||
})
|
||||
),
|
||||
client_patterns: z.array(
|
||||
z.object({
|
||||
client: z.string(),
|
||||
pattern: z.string(),
|
||||
frequency: z.number().int().nonnegative(),
|
||||
example_ticket_numbers: z.array(z.string()),
|
||||
})
|
||||
),
|
||||
recurrence_clusters: z.array(
|
||||
z.object({
|
||||
theme: z.string(),
|
||||
ticket_numbers: z.array(z.string()),
|
||||
summary: z.string(),
|
||||
})
|
||||
),
|
||||
systemic_observations: z.array(
|
||||
z.object({
|
||||
observation: z.string(),
|
||||
evidence: z.string(),
|
||||
severity: Severity,
|
||||
})
|
||||
),
|
||||
recommended_actions: z.array(
|
||||
z.object({
|
||||
action: z.string(),
|
||||
rationale: z.string(),
|
||||
priority: Severity,
|
||||
type: RecommendedActionType,
|
||||
})
|
||||
),
|
||||
executive_summary: z.string(),
|
||||
narrative_summary: z.string(),
|
||||
});
|
||||
export type AggregateReduceResponse = z.infer<typeof AggregateReduceResponse>;
|
||||
|
||||
export const AggregateReportStatus = z.enum([
|
||||
'pending',
|
||||
'running',
|
||||
'complete',
|
||||
'failed',
|
||||
]);
|
||||
export type AggregateReportStatus = z.infer<typeof AggregateReportStatus>;
|
||||
|
||||
// =============================================================================
|
||||
// API request bodies.
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue