From 3fd4904e8d285f718a08c1fa19146ca5f326a311 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 17:46:31 -0700 Subject: [PATCH 1/8] feat(core): add feedback outcome storage --- .../feedback/session-outcomes.test.ts | 116 +++++ .../storage/sqlite/migrations.test.ts | 65 ++- packages/core/src/di/container.ts | 2 + .../core/src/storage/repositories/index.ts | 7 + .../src/storage/repositories/interfaces.ts | 28 + .../sqlite/session-outcome.repository.ts | 493 ++++++++++++++++++ .../core/src/storage/sqlite/migrations.ts | 59 +++ packages/core/src/types.ts | 136 +++++ 8 files changed, 900 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/__tests__/feedback/session-outcomes.test.ts create mode 100644 packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts diff --git a/packages/core/src/__tests__/feedback/session-outcomes.test.ts b/packages/core/src/__tests__/feedback/session-outcomes.test.ts new file mode 100644 index 00000000..38b5d5ee --- /dev/null +++ b/packages/core/src/__tests__/feedback/session-outcomes.test.ts @@ -0,0 +1,116 @@ +/** + * Tests for structured session outcome storage. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import Database from 'better-sqlite3'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { SqliteSessionOutcomeRepository } from '../../storage/repositories/sqlite/session-outcome.repository.js'; +import { runMigrations } from '../../storage/sqlite/migrations.js'; + +describe('SqliteSessionOutcomeRepository', () => { + let db: Database.Database; + let repo: SqliteSessionOutcomeRepository; + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nw-session-outcomes-test-')); + db = new Database(path.join(tempDir, 'test.db')); + db.pragma('journal_mode = WAL'); + runMigrations(db); + repo = new SqliteSessionOutcomeRepository(db); + }); + + afterEach(() => { + db.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should record structured session outcome', () => { + const outcome = repo.insertOutcome({ + projectPath: '/tmp/project', + jobType: 'executor', + providerKey: 'codex', + prdFile: 'docs/prds/example.md', + prNumber: 97, + branchName: 'night-watch/nw-97', + startedAt: 1_700_000_000, + finishedAt: 1_700_000_090, + durationSeconds: 90, + outcome: 'failure', + exitCode: 1, + attempt: 2, + retryCount: 1, + failureCategory: 'tests', + failureSignature: 'vitest failed in session-outcomes.test.ts', + metadata: { + command: 'yarn workspace @night-watch/core test', + failures: ['expected true to be false'], + }, + }); + + expect(outcome.id).toBeGreaterThan(0); + expect(outcome.projectPath).toBe('/tmp/project'); + expect(outcome.jobType).toBe('executor'); + expect(outcome.providerKey).toBe('codex'); + expect(outcome.prdFile).toBe('docs/prds/example.md'); + expect(outcome.prNumber).toBe(97); + expect(outcome.durationSeconds).toBe(90); + expect(outcome.outcome).toBe('failure'); + expect(outcome.attempt).toBe(2); + expect(outcome.retryCount).toBe(1); + expect(outcome.metadata).toEqual({ + command: 'yarn workspace @night-watch/core test', + failures: ['expected true to be false'], + }); + + const queried = repo.queryOutcomes({ projectPath: '/tmp/project', jobType: 'executor' }); + expect(queried).toHaveLength(1); + expect(queried[0]).toEqual(outcome); + + const summary = repo.querySummary({ projectPath: '/tmp/project' }); + expect(summary.totalCount).toBe(1); + expect(summary.failureCount).toBe(1); + expect(summary.byFailureCategory).toEqual({ tests: 1 }); + expect(summary.averageDurationSeconds).toBe(90); + }); + + it('should redact secrets in metadata', () => { + const outcome = repo.insertOutcome({ + projectPath: '/tmp/project', + jobType: 'reviewer', + providerKey: 'claude', + startedAt: 1_700_000_000, + finishedAt: 1_700_000_030, + outcome: 'failure', + metadata: { + apiKey: 'sk-1234567890abcdefghijklmnopqrstuvwxyz', + nested: { + authorization: 'Bearer secret-token-value-12345', + log: 'request failed with token=ghp_abcdefghijklmnopqrstuvwxyz1234567890ABCD', + }, + safe: 'keep this value', + }, + }); + + expect(outcome.metadata).toEqual({ + apiKey: '[REDACTED_SECRET]', + nested: { + authorization: '[REDACTED_SECRET]', + log: 'request failed with token=[REDACTED_SECRET]', + }, + safe: 'keep this value', + }); + + const raw = db + .prepare('SELECT metadata_json FROM session_outcomes WHERE id = ?') + .get(outcome.id) as { metadata_json: string }; + expect(raw.metadata_json).not.toContain('sk-1234567890abcdefghijklmnopqrstuvwxyz'); + expect(raw.metadata_json).not.toContain('secret-token-value-12345'); + expect(raw.metadata_json).not.toContain('ghp_abcdefghijklmnopqrstuvwxyz1234567890ABCD'); + }); +}); diff --git a/packages/core/src/__tests__/storage/sqlite/migrations.test.ts b/packages/core/src/__tests__/storage/sqlite/migrations.test.ts index 24592aa3..c8fcfdeb 100644 --- a/packages/core/src/__tests__/storage/sqlite/migrations.test.ts +++ b/packages/core/src/__tests__/storage/sqlite/migrations.test.ts @@ -13,14 +13,17 @@ import { runMigrations } from '../../../storage/sqlite/migrations.js'; const EXPECTED_TABLES = [ 'agent_personas', 'execution_history', + 'feedback_patterns', 'job_queue', 'job_runs', 'kanban_comments', 'kanban_issues', 'prd_states', 'projects', + 'prompt_augmentations', 'roadmap_states', 'schema_meta', + 'session_outcomes', ]; let tmpDir: string; @@ -97,9 +100,12 @@ describe('runMigrations', () => { it('creates job_runs table with correct columns', () => { runMigrations(db); - const columns = db - .prepare(`PRAGMA table_info(job_runs)`) - .all() as Array<{ name: string; type: string; notnull: number; dflt_value: string | null }>; + const columns = db.prepare(`PRAGMA table_info(job_runs)`).all() as Array<{ + name: string; + type: string; + notnull: number; + dflt_value: string | null; + }>; const colNames = columns.map((c) => c.name); expect(colNames).toEqual( @@ -130,9 +136,7 @@ describe('runMigrations', () => { it('creates job_queue table without pressure columns', () => { runMigrations(db); - const columns = db - .prepare(`PRAGMA table_info(job_queue)`) - .all() as Array<{ name: string }>; + const columns = db.prepare(`PRAGMA table_info(job_queue)`).all() as Array<{ name: string }>; const colNames = columns.map((c) => c.name); @@ -153,4 +157,53 @@ describe('runMigrations', () => { expect(indexes.map((i) => i.name)).toContain('idx_job_runs_lookup'); }); + + it('creates feedback-loop tables and lookup indexes', () => { + runMigrations(db); + + const outcomeColumns = db.prepare(`PRAGMA table_info(session_outcomes)`).all() as Array<{ + name: string; + dflt_value: string | null; + }>; + const outcomeIndexes = db + .prepare(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='session_outcomes'`) + .all() as Array<{ name: string }>; + const patternIndexes = db + .prepare(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='feedback_patterns'`) + .all() as Array<{ name: string }>; + const augmentationIndexes = db + .prepare( + `SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='prompt_augmentations'`, + ) + .all() as Array<{ name: string }>; + + expect(outcomeColumns.map((c) => c.name)).toEqual( + expect.arrayContaining([ + 'id', + 'project_path', + 'job_type', + 'provider_key', + 'prd_file', + 'pr_number', + 'branch_name', + 'started_at', + 'finished_at', + 'duration_seconds', + 'outcome', + 'exit_code', + 'attempt', + 'retry_count', + 'review_score', + 'ci_status', + 'failure_category', + 'failure_signature', + 'metadata_json', + ]), + ); + expect(outcomeColumns.find((c) => c.name === 'attempt')?.dflt_value).toBe('1'); + expect(outcomeColumns.find((c) => c.name === 'retry_count')?.dflt_value).toBe('0'); + expect(outcomeIndexes.map((i) => i.name)).toContain('idx_session_outcomes_lookup'); + expect(patternIndexes.map((i) => i.name)).toContain('idx_feedback_patterns_lookup'); + expect(augmentationIndexes.map((i) => i.name)).toContain('idx_prompt_augmentations_active'); + }); }); diff --git a/packages/core/src/di/container.ts b/packages/core/src/di/container.ts index 19d6a020..3ce9db33 100644 --- a/packages/core/src/di/container.ts +++ b/packages/core/src/di/container.ts @@ -16,6 +16,7 @@ import { SqliteKanbanIssueRepository } from '@/storage/repositories/sqlite/kanba import { SqlitePrdStateRepository } from '@/storage/repositories/sqlite/prd-state.repository.js'; import { SqliteProjectRegistryRepository } from '@/storage/repositories/sqlite/project-registry.repository.js'; import { SqliteRoadmapStateRepository } from '@/storage/repositories/sqlite/roadmap-state.repository.js'; +import { SqliteSessionOutcomeRepository } from '@/storage/repositories/sqlite/session-outcome.repository.js'; import { createDbForDir } from '@/storage/sqlite/client.js'; import { runMigrations } from '@/storage/sqlite/migrations.js'; @@ -53,6 +54,7 @@ export function initContainer(projectDir: string): void { container.registerSingleton(SqlitePrdStateRepository); container.registerSingleton(SqliteProjectRegistryRepository); container.registerSingleton(SqliteRoadmapStateRepository); + container.registerSingleton(SqliteSessionOutcomeRepository); } /** diff --git a/packages/core/src/storage/repositories/index.ts b/packages/core/src/storage/repositories/index.ts index bf1d6220..241c99b8 100644 --- a/packages/core/src/storage/repositories/index.ts +++ b/packages/core/src/storage/repositories/index.ts @@ -16,17 +16,20 @@ import { IPrdStateRepository, IProjectRegistryRepository, IRoadmapStateRepository, + ISessionOutcomeRepository, } from './interfaces.js'; import { SqliteProjectRegistryRepository } from './sqlite/project-registry.repository.js'; import { SqliteExecutionHistoryRepository } from './sqlite/execution-history.repository.js'; import { SqlitePrdStateRepository } from './sqlite/prd-state.repository.js'; import { SqliteRoadmapStateRepository } from './sqlite/roadmap-state.repository.js'; +import { SqliteSessionOutcomeRepository } from './sqlite/session-outcome.repository.js'; export interface IRepositories { projectRegistry: IProjectRegistryRepository; executionHistory: IExecutionHistoryRepository; prdState: IPrdStateRepository; roadmapState: IRoadmapStateRepository; + sessionOutcomes: ISessionOutcomeRepository; } let _initialized = false; @@ -46,6 +49,7 @@ export function getRepositories(): IRepositories { executionHistory: container.resolve(SqliteExecutionHistoryRepository), prdState: container.resolve(SqlitePrdStateRepository), roadmapState: container.resolve(SqliteRoadmapStateRepository), + sessionOutcomes: container.resolve(SqliteSessionOutcomeRepository), }; } @@ -62,6 +66,7 @@ export function getRepositories(): IRepositories { executionHistory: new SqliteExecutionHistoryRepository(db), prdState: new SqlitePrdStateRepository(db), roadmapState: new SqliteRoadmapStateRepository(db), + sessionOutcomes: new SqliteSessionOutcomeRepository(db), }; } @@ -72,3 +77,5 @@ export function getRepositories(): IRepositories { export function resetRepositories(): void { _initialized = false; } + +export { SqliteSessionOutcomeRepository } from './sqlite/session-outcome.repository.js'; diff --git a/packages/core/src/storage/repositories/interfaces.ts b/packages/core/src/storage/repositories/interfaces.ts index 60474353..a494fa23 100644 --- a/packages/core/src/storage/repositories/interfaces.ts +++ b/packages/core/src/storage/repositories/interfaces.ts @@ -4,6 +4,19 @@ */ import { BoardColumnName } from '@/board/types.js'; +import type { + IFeedbackPattern, + IFeedbackPatternUpsertInput, + IPromptAugmentation, + IPromptAugmentationInsertInput, + ISessionOutcome, + ISessionOutcomeInsertInput, + ISessionOutcomeQueryInput, + ISessionOutcomeSummary, + ISessionOutcomeSummaryInput, + JobType, + PromptAugmentationStatus, +} from '@/types.js'; import { IRegistryEntry } from '@/utils/registry.js'; import { IExecutionRecord } from '@/utils/execution-history.js'; import { IPrdStateEntry } from '@/utils/prd-states.js'; @@ -65,3 +78,18 @@ export interface IKanbanIssueRepository { close(number: number): void; addComment(number: number, body: string): void; } + +export interface ISessionOutcomeRepository { + insertOutcome(input: ISessionOutcomeInsertInput): ISessionOutcome; + queryOutcomes(input: ISessionOutcomeQueryInput): ISessionOutcome[]; + querySummary(input: ISessionOutcomeSummaryInput): ISessionOutcomeSummary; + upsertPattern(input: IFeedbackPatternUpsertInput): IFeedbackPattern; + createAugmentation(input: IPromptAugmentationInsertInput): IPromptAugmentation; + listActiveAugmentations( + projectPath: string, + jobType: JobType, + now?: number, + ): IPromptAugmentation[]; + updateAugmentationStatus(id: number, status: PromptAugmentationStatus): void; + incrementAugmentationCounts(id: number, success?: boolean): void; +} diff --git a/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts b/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts new file mode 100644 index 00000000..fd1fdb2d --- /dev/null +++ b/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts @@ -0,0 +1,493 @@ +/** + * SQLite implementation of ISessionOutcomeRepository. + * Persists structured feedback-loop outcomes, patterns, and prompt augmentations. + */ + +import Database from 'better-sqlite3'; +import { inject, injectable } from 'tsyringe'; + +import type { + FeedbackPatternStatus, + IFeedbackPattern, + IFeedbackPatternUpsertInput, + IPromptAugmentation, + IPromptAugmentationInsertInput, + ISessionOutcome, + ISessionOutcomeInsertInput, + ISessionOutcomeQueryInput, + ISessionOutcomeSummary, + ISessionOutcomeSummaryInput, + JobType, + PromptAugmentationStatus, + SessionOutcomeStatus, +} from '@/types.js'; + +import { ISessionOutcomeRepository } from '../interfaces.js'; + +interface ISessionOutcomeRow { + id: number; + project_path: string; + job_type: string; + provider_key: string; + prd_file: string | null; + pr_number: number | null; + branch_name: string | null; + started_at: number; + finished_at: number; + duration_seconds: number | null; + outcome: string; + exit_code: number | null; + attempt: number; + retry_count: number; + review_score: number | null; + ci_status: string | null; + failure_category: string | null; + failure_signature: string | null; + metadata_json: string; +} + +interface IFeedbackPatternRow { + id: number; + project_path: string; + pattern_key: string; + job_type: string; + category: string; + title: string; + description: string; + sample_count: number; + confidence: number; + first_seen_at: number; + last_seen_at: number; + status: string; + metadata_json: string; +} + +interface IPromptAugmentationRow { + id: number; + project_path: string; + pattern_id: number | null; + job_type: string; + prompt_text: string; + status: string; + created_at: number; + updated_at: number; + expires_at: number | null; + applied_count: number; + success_count: number; +} + +interface ISummaryCountRow { + key: string | null; + count: number; +} + +const SECRET_PLACEHOLDER = '[REDACTED_SECRET]'; +const SECRET_KEY_PATTERN = + /(?:api[_-]?key|authorization|client[_-]?secret|cookie|password|private[_-]?key|secret|token)/i; +const SECRET_TEXT_PATTERNS: Array<[RegExp, string]> = [ + [ + /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, + SECRET_PLACEHOLDER, + ], + [/\bsk-ant-[\w-]{20,}\b/g, SECRET_PLACEHOLDER], + [/\bsk-[\w-]{20,}\b/g, SECRET_PLACEHOLDER], + [/\bgh[opsru]_\w{30,}\b/g, SECRET_PLACEHOLDER], + [/\bxox[baprs]-[\w-]{20,}\b/g, SECRET_PLACEHOLDER], + [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, SECRET_PLACEHOLDER], + [/\b(Bearer|Basic)\s+[\w.~+/=-]{12,}/gi, `$1 ${SECRET_PLACEHOLDER}`], +]; + +function redactText(value: string): string { + return SECRET_TEXT_PATTERNS.reduce( + (current, [pattern, replacement]) => current.replace(pattern, replacement), + value, + ); +} + +function redactOptionalText(value: string | null | undefined): string | null { + return value == null ? null : redactText(value); +} + +function redactMetadataValue( + value: unknown, + key: string | undefined, + seen: WeakSet, +): unknown { + if (key && SECRET_KEY_PATTERN.test(key)) { + return SECRET_PLACEHOLDER; + } + + if (typeof value === 'string') { + return redactText(value); + } + + if (value === null || typeof value !== 'object') { + return value; + } + + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + + if (Array.isArray(value)) { + const redactedArray = value.map((item) => redactMetadataValue(item, undefined, seen)); + seen.delete(value); + return redactedArray; + } + + const redacted: Record = {}; + for (const [entryKey, entryValue] of Object.entries(value)) { + redacted[entryKey] = redactMetadataValue(entryValue, entryKey, seen); + } + seen.delete(value); + return redacted; +} + +function redactMetadata(metadata: Record | undefined): Record { + const value = redactMetadataValue(metadata ?? {}, undefined, new WeakSet()); + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return value as Record; + } + return {}; +} + +function parseMetadata(metadataJson: string): Record { + try { + const parsed: unknown = JSON.parse(metadataJson); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { + return {}; + } + return {}; +} + +function rowToOutcome(row: ISessionOutcomeRow): ISessionOutcome { + return { + id: row.id, + projectPath: row.project_path, + jobType: row.job_type as JobType, + providerKey: row.provider_key, + prdFile: row.prd_file, + prNumber: row.pr_number, + branchName: row.branch_name, + startedAt: row.started_at, + finishedAt: row.finished_at, + durationSeconds: row.duration_seconds, + outcome: row.outcome as SessionOutcomeStatus, + exitCode: row.exit_code, + attempt: row.attempt, + retryCount: row.retry_count, + reviewScore: row.review_score, + ciStatus: row.ci_status, + failureCategory: row.failure_category, + failureSignature: row.failure_signature, + metadata: parseMetadata(row.metadata_json), + }; +} + +function rowToPattern(row: IFeedbackPatternRow): IFeedbackPattern { + return { + id: row.id, + projectPath: row.project_path, + patternKey: row.pattern_key, + jobType: row.job_type as JobType, + category: row.category, + title: row.title, + description: row.description, + sampleCount: row.sample_count, + confidence: row.confidence, + firstSeenAt: row.first_seen_at, + lastSeenAt: row.last_seen_at, + status: row.status as FeedbackPatternStatus, + metadata: parseMetadata(row.metadata_json), + }; +} + +function rowToAugmentation(row: IPromptAugmentationRow): IPromptAugmentation { + return { + id: row.id, + projectPath: row.project_path, + patternId: row.pattern_id, + jobType: row.job_type as JobType, + promptText: row.prompt_text, + status: row.status as PromptAugmentationStatus, + createdAt: row.created_at, + updatedAt: row.updated_at, + expiresAt: row.expires_at, + appliedCount: row.applied_count, + successCount: row.success_count, + }; +} + +function buildOutcomeWhere(input: ISessionOutcomeSummaryInput | ISessionOutcomeQueryInput): { + params: Array; + where: string; +} { + const clauses = ['project_path = ?']; + const params: Array = [input.projectPath]; + + if (input.jobType) { + clauses.push('job_type = ?'); + params.push(input.jobType); + } + if ('outcome' in input && input.outcome) { + clauses.push('outcome = ?'); + params.push(input.outcome); + } + if (input.fromFinishedAt != null) { + clauses.push('finished_at >= ?'); + params.push(input.fromFinishedAt); + } + if (input.toFinishedAt != null) { + clauses.push('finished_at <= ?'); + params.push(input.toFinishedAt); + } + + return { params, where: clauses.join(' AND ') }; +} + +@injectable() +export class SqliteSessionOutcomeRepository implements ISessionOutcomeRepository { + private readonly db: Database.Database; + + constructor(@inject('Database') db: Database.Database) { + this.db = db; + } + + insertOutcome(input: ISessionOutcomeInsertInput): ISessionOutcome { + const metadataJson = JSON.stringify(redactMetadata(input.metadata)); + const result = this.db + .prepare( + `INSERT INTO session_outcomes + (project_path, job_type, provider_key, prd_file, pr_number, branch_name, + started_at, finished_at, duration_seconds, outcome, exit_code, attempt, + retry_count, review_score, ci_status, failure_category, failure_signature, + metadata_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .run( + input.projectPath, + input.jobType, + input.providerKey, + input.prdFile ?? null, + input.prNumber ?? null, + redactOptionalText(input.branchName), + input.startedAt, + input.finishedAt, + input.durationSeconds ?? null, + input.outcome, + input.exitCode ?? null, + input.attempt ?? 1, + input.retryCount ?? 0, + input.reviewScore ?? null, + redactOptionalText(input.ciStatus), + redactOptionalText(input.failureCategory), + redactOptionalText(input.failureSignature), + metadataJson, + ); + + return this.getOutcomeById(Number(result.lastInsertRowid))!; + } + + queryOutcomes(input: ISessionOutcomeQueryInput): ISessionOutcome[] { + const { params, where } = buildOutcomeWhere(input); + const limit = Math.min(Math.max(input.limit ?? 100, 1), 500); + const rows = this.db + .prepare( + `SELECT * + FROM session_outcomes + WHERE ${where} + ORDER BY finished_at DESC, id DESC + LIMIT ?`, + ) + .all(...params, limit) as ISessionOutcomeRow[]; + + return rows.map(rowToOutcome); + } + + querySummary(input: ISessionOutcomeSummaryInput): ISessionOutcomeSummary { + const { params, where } = buildOutcomeWhere(input); + const outcomeRows = this.db + .prepare( + `SELECT outcome as key, COUNT(*) as count + FROM session_outcomes + WHERE ${where} + GROUP BY outcome`, + ) + .all(...params) as ISummaryCountRow[]; + + const categoryRows = this.db + .prepare( + `SELECT failure_category as key, COUNT(*) as count + FROM session_outcomes + WHERE ${where} AND failure_category IS NOT NULL + GROUP BY failure_category`, + ) + .all(...params) as ISummaryCountRow[]; + + const averageRow = this.db + .prepare( + `SELECT AVG(duration_seconds) as average_duration + FROM session_outcomes + WHERE ${where} AND duration_seconds IS NOT NULL`, + ) + .get(...params) as { average_duration: number | null } | undefined; + + const byOutcome = Object.fromEntries( + outcomeRows.map((row) => [row.key ?? 'unknown', row.count]), + ) as Record; + const byFailureCategory = Object.fromEntries( + categoryRows.map((row) => [row.key ?? 'unknown', row.count]), + ) as Record; + + return { + totalCount: outcomeRows.reduce((total, row) => total + row.count, 0), + successCount: byOutcome.success ?? 0, + failureCount: byOutcome.failure ?? 0, + timeoutCount: byOutcome.timeout ?? 0, + rateLimitedCount: byOutcome.rate_limited ?? 0, + skippedCount: byOutcome.skipped ?? 0, + averageDurationSeconds: averageRow?.average_duration ?? null, + byOutcome, + byFailureCategory, + }; + } + + upsertPattern(input: IFeedbackPatternUpsertInput): IFeedbackPattern { + const now = Date.now(); + const existing = this.getPattern(input.projectPath, input.patternKey, input.jobType); + const firstSeenAt = existing?.firstSeenAt ?? input.firstSeenAt ?? now; + const lastSeenAt = input.lastSeenAt ?? now; + const sampleCount = input.sampleCount ?? (existing ? existing.sampleCount + 1 : 1); + const confidence = input.confidence ?? existing?.confidence ?? 0; + const status = input.status ?? existing?.status ?? 'observing'; + const metadataJson = JSON.stringify(redactMetadata(input.metadata ?? existing?.metadata)); + + this.db + .prepare( + `INSERT INTO feedback_patterns + (project_path, pattern_key, job_type, category, title, description, sample_count, + confidence, first_seen_at, last_seen_at, status, metadata_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(project_path, pattern_key, job_type) + DO UPDATE SET category = excluded.category, + title = excluded.title, + description = excluded.description, + sample_count = excluded.sample_count, + confidence = excluded.confidence, + last_seen_at = excluded.last_seen_at, + status = excluded.status, + metadata_json = excluded.metadata_json`, + ) + .run( + input.projectPath, + input.patternKey, + input.jobType, + redactText(input.category), + redactText(input.title), + redactText(input.description), + sampleCount, + confidence, + firstSeenAt, + lastSeenAt, + status, + metadataJson, + ); + + return this.getPattern(input.projectPath, input.patternKey, input.jobType)!; + } + + createAugmentation(input: IPromptAugmentationInsertInput): IPromptAugmentation { + const now = Date.now(); + const createdAt = input.createdAt ?? now; + const updatedAt = input.updatedAt ?? createdAt; + const result = this.db + .prepare( + `INSERT INTO prompt_augmentations + (project_path, pattern_id, job_type, prompt_text, status, created_at, updated_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .run( + input.projectPath, + input.patternId ?? null, + input.jobType, + redactText(input.promptText), + input.status ?? 'active', + createdAt, + updatedAt, + input.expiresAt ?? null, + ); + + return this.getAugmentationById(Number(result.lastInsertRowid))!; + } + + listActiveAugmentations( + projectPath: string, + jobType: JobType, + now = Date.now(), + ): IPromptAugmentation[] { + const rows = this.db + .prepare( + `SELECT * + FROM prompt_augmentations + WHERE project_path = ? + AND job_type = ? + AND status = 'active' + AND (expires_at IS NULL OR expires_at > ?) + ORDER BY created_at ASC, id ASC`, + ) + .all(projectPath, jobType, now) as IPromptAugmentationRow[]; + + return rows.map(rowToAugmentation); + } + + updateAugmentationStatus(id: number, status: PromptAugmentationStatus): void { + this.db + .prepare('UPDATE prompt_augmentations SET status = ?, updated_at = ? WHERE id = ?') + .run(status, Date.now(), id); + } + + incrementAugmentationCounts(id: number, success = false): void { + this.db + .prepare( + `UPDATE prompt_augmentations + SET applied_count = applied_count + 1, + success_count = success_count + ?, + updated_at = ? + WHERE id = ?`, + ) + .run(success ? 1 : 0, Date.now(), id); + } + + private getOutcomeById(id: number): ISessionOutcome | null { + const row = this.db.prepare('SELECT * FROM session_outcomes WHERE id = ?').get(id) as + | ISessionOutcomeRow + | undefined; + return row ? rowToOutcome(row) : null; + } + + private getPattern( + projectPath: string, + patternKey: string, + jobType: JobType, + ): IFeedbackPattern | null { + const row = this.db + .prepare( + `SELECT * + FROM feedback_patterns + WHERE project_path = ? AND pattern_key = ? AND job_type = ?`, + ) + .get(projectPath, patternKey, jobType) as IFeedbackPatternRow | undefined; + return row ? rowToPattern(row) : null; + } + + private getAugmentationById(id: number): IPromptAugmentation | null { + const row = this.db.prepare('SELECT * FROM prompt_augmentations WHERE id = ?').get(id) as + | IPromptAugmentationRow + | undefined; + return row ? rowToAugmentation(row) : null; + } +} diff --git a/packages/core/src/storage/sqlite/migrations.ts b/packages/core/src/storage/sqlite/migrations.ts index e61ce954..500f943d 100644 --- a/packages/core/src/storage/sqlite/migrations.ts +++ b/packages/core/src/storage/sqlite/migrations.ts @@ -123,6 +123,65 @@ export function runMigrations(db: Database.Database): void { ); CREATE INDEX IF NOT EXISTS idx_job_runs_lookup ON job_runs(project_path, started_at DESC, job_type, provider_key); + + CREATE TABLE IF NOT EXISTS session_outcomes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_path TEXT NOT NULL, + job_type TEXT NOT NULL, + provider_key TEXT NOT NULL, + prd_file TEXT, + pr_number INTEGER, + branch_name TEXT, + started_at INTEGER NOT NULL, + finished_at INTEGER NOT NULL, + duration_seconds INTEGER, + outcome TEXT NOT NULL, + exit_code INTEGER, + attempt INTEGER NOT NULL DEFAULT 1, + retry_count INTEGER NOT NULL DEFAULT 0, + review_score INTEGER, + ci_status TEXT, + failure_category TEXT, + failure_signature TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}' + ); + CREATE INDEX IF NOT EXISTS idx_session_outcomes_lookup + ON session_outcomes(project_path, finished_at DESC, job_type, outcome); + + CREATE TABLE IF NOT EXISTS feedback_patterns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_path TEXT NOT NULL, + pattern_key TEXT NOT NULL, + job_type TEXT NOT NULL, + category TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + sample_count INTEGER NOT NULL DEFAULT 0, + confidence REAL NOT NULL DEFAULT 0, + first_seen_at INTEGER NOT NULL, + last_seen_at INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'observing', + metadata_json TEXT NOT NULL DEFAULT '{}', + UNIQUE(project_path, pattern_key, job_type) + ); + CREATE INDEX IF NOT EXISTS idx_feedback_patterns_lookup + ON feedback_patterns(project_path, job_type, status, confidence DESC); + + CREATE TABLE IF NOT EXISTS prompt_augmentations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_path TEXT NOT NULL, + pattern_id INTEGER REFERENCES feedback_patterns(id), + job_type TEXT NOT NULL, + prompt_text TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + expires_at INTEGER, + applied_count INTEGER NOT NULL DEFAULT 0, + success_count INTEGER NOT NULL DEFAULT 0 + ); + CREATE INDEX IF NOT EXISTS idx_prompt_augmentations_active + ON prompt_augmentations(project_path, job_type, status, expires_at); `); // Phase 2 cleanup: drop slack_discussions table (multi-agent deliberation removed) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 526a831a..b39f9c88 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -538,6 +538,142 @@ export type JobRunStatus = | 'rate_limited' | 'skipped'; +export type SessionOutcomeStatus = 'success' | 'failure' | 'timeout' | 'rate_limited' | 'skipped'; + +export type FeedbackPatternStatus = 'observing' | 'active' | 'dismissed' | 'resolved'; + +export type PromptAugmentationStatus = 'active' | 'paused' | 'expired' | 'archived'; + +/** + * Structured outcome for a completed Night Watch job session. + */ +export interface ISessionOutcome { + id: number; + projectPath: string; + jobType: JobType; + providerKey: string; + prdFile: string | null; + prNumber: number | null; + branchName: string | null; + startedAt: number; + finishedAt: number; + durationSeconds: number | null; + outcome: SessionOutcomeStatus; + exitCode: number | null; + attempt: number; + retryCount: number; + reviewScore: number | null; + ciStatus: string | null; + failureCategory: string | null; + failureSignature: string | null; + metadata: Record; +} + +export interface ISessionOutcomeInsertInput { + projectPath: string; + jobType: JobType; + providerKey: string; + prdFile?: string | null; + prNumber?: number | null; + branchName?: string | null; + startedAt: number; + finishedAt: number; + durationSeconds?: number | null; + outcome: SessionOutcomeStatus; + exitCode?: number | null; + attempt?: number; + retryCount?: number; + reviewScore?: number | null; + ciStatus?: string | null; + failureCategory?: string | null; + failureSignature?: string | null; + metadata?: Record; +} + +export interface ISessionOutcomeQueryInput { + projectPath: string; + jobType?: JobType; + outcome?: SessionOutcomeStatus; + fromFinishedAt?: number; + toFinishedAt?: number; + limit?: number; +} + +export interface ISessionOutcomeSummaryInput { + projectPath: string; + jobType?: JobType; + fromFinishedAt?: number; + toFinishedAt?: number; +} + +export interface ISessionOutcomeSummary { + totalCount: number; + successCount: number; + failureCount: number; + timeoutCount: number; + rateLimitedCount: number; + skippedCount: number; + averageDurationSeconds: number | null; + byOutcome: Record; + byFailureCategory: Record; +} + +export interface IFeedbackPattern { + id: number; + projectPath: string; + patternKey: string; + jobType: JobType; + category: string; + title: string; + description: string; + sampleCount: number; + confidence: number; + firstSeenAt: number; + lastSeenAt: number; + status: FeedbackPatternStatus; + metadata: Record; +} + +export interface IFeedbackPatternUpsertInput { + projectPath: string; + patternKey: string; + jobType: JobType; + category: string; + title: string; + description: string; + sampleCount?: number; + confidence?: number; + firstSeenAt?: number; + lastSeenAt?: number; + status?: FeedbackPatternStatus; + metadata?: Record; +} + +export interface IPromptAugmentation { + id: number; + projectPath: string; + patternId: number | null; + jobType: JobType; + promptText: string; + status: PromptAugmentationStatus; + createdAt: number; + updatedAt: number; + expiresAt: number | null; + appliedCount: number; + successCount: number; +} + +export interface IPromptAugmentationInsertInput { + projectPath: string; + patternId?: number | null; + jobType: JobType; + promptText: string; + status?: PromptAugmentationStatus; + createdAt?: number; + updatedAt?: number; + expiresAt?: number | null; +} + /** * A record of a single job execution stored in the job_runs table */ From 4184938028262f4b32198cfd741d926a8ff73951 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 17:59:27 -0700 Subject: [PATCH 2/8] feat(feedback): record outcomes and expose API --- .../cli/src/__tests__/commands/run.test.ts | 59 +++ packages/cli/src/commands/merge.ts | 35 +- packages/cli/src/commands/qa.ts | 26 + packages/cli/src/commands/resolve.ts | 28 ++ packages/cli/src/commands/review.ts | 27 + packages/cli/src/commands/run.ts | 73 +++ .../__tests__/feedback/outcome-parser.test.ts | 52 ++ packages/core/src/feedback/outcome-parser.ts | 464 ++++++++++++++++++ packages/core/src/index.ts | 1 + .../src/storage/repositories/interfaces.ts | 10 +- .../sqlite/session-outcome.repository.ts | 83 +++- packages/core/src/types.ts | 16 + .../src/__tests__/server/feedback.test.ts | 169 +++++++ packages/server/src/index.ts | 3 + packages/server/src/routes/feedback.routes.ts | 306 ++++++++++++ web/api.ts | 108 ++++ 16 files changed, 1452 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/__tests__/feedback/outcome-parser.test.ts create mode 100644 packages/core/src/feedback/outcome-parser.ts create mode 100644 packages/server/src/__tests__/server/feedback.test.ts create mode 100644 packages/server/src/routes/feedback.routes.ts diff --git a/packages/cli/src/__tests__/commands/run.test.ts b/packages/cli/src/__tests__/commands/run.test.ts index 138172a2..69a8f29c 100644 --- a/packages/cli/src/__tests__/commands/run.test.ts +++ b/packages/cli/src/__tests__/commands/run.test.ts @@ -31,11 +31,17 @@ import { scanPrdDirectory, getRateLimitFallbackTelegramWebhooks, isRateLimitFallbackTriggered, + recordRunSessionOutcome, resolveRunNotificationEvent, shouldAttemptCrossProjectFallback, } from '@/cli/commands/run.js'; import { applyScheduleOffset, buildCronPathPrefix } from '@/cli/commands/install.js'; import { INightWatchConfig } from '@night-watch/core/types.js'; +import { closeDb } from '@night-watch/core/storage/sqlite/client.js'; +import { + getRepositories, + resetRepositories, +} from '@night-watch/core/storage/repositories/index.js'; import { sendNotifications } from '@night-watch/core/utils/notify.js'; // Helper to create a valid config without budget fields @@ -62,6 +68,7 @@ function createTestConfig(overrides: Partial = {}): INightWat describe('run command', () => { let tempDir: string; let originalEnv: NodeJS.ProcessEnv; + let originalNightWatchHome: string | undefined; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'night-watch-test-')); @@ -69,6 +76,7 @@ describe('run command', () => { // Save original environment originalEnv = { ...process.env }; + originalNightWatchHome = process.env.NIGHT_WATCH_HOME; // Clear NW_* environment variables for (const key of Object.keys(process.env)) { @@ -81,8 +89,16 @@ describe('run command', () => { }); afterEach(() => { + closeDb(); + resetRepositories(); fs.rmSync(tempDir, { recursive: true, force: true }); + if (originalNightWatchHome === undefined) { + delete process.env.NIGHT_WATCH_HOME; + } else { + process.env.NIGHT_WATCH_HOME = originalNightWatchHome; + } + // Restore original environment for (const key of Object.keys(process.env)) { if (key.startsWith('NW_')) { @@ -480,6 +496,49 @@ describe('run command', () => { }); }); + describe('outcome recording', () => { + it('should record executor outcome after script exits', () => { + process.env.NIGHT_WATCH_HOME = path.join(tempDir, '.night-watch-home'); + closeDb(); + resetRepositories(); + + const config = createTestConfig(); + const startedAt = 1_700_000_000_000; + const finishedAt = 1_700_000_003_000; + + recordRunSessionOutcome({ + projectDir: tempDir, + config, + envVars: { + NW_PROVIDER_KEY: 'claude-native', + NW_PROVIDER_CMD: 'claude', + NW_PROVIDER_LABEL: 'Claude', + }, + startedAt, + finishedAt, + exitCode: 1, + stderr: "packages/core/src/index.ts:1:1 - error TS2305: Module has no exported member 'x'.", + scriptResult: { + status: 'failure', + data: { prd: '97-feedback.md', branch: 'night-watch/nw-97' }, + }, + }); + + const outcomes = getRepositories().sessionOutcomes.queryOutcomes({ + projectPath: tempDir, + jobType: 'executor', + }); + + expect(outcomes).toHaveLength(1); + expect(outcomes[0].providerKey).toBe('claude-native'); + expect(outcomes[0].durationSeconds).toBe(3); + expect(outcomes[0].outcome).toBe('failure'); + expect(outcomes[0].failureCategory).toBe('typescript'); + expect(outcomes[0].prdFile).toBe('97-feedback.md'); + expect(outcomes[0].branchName).toBe('night-watch/nw-97'); + }); + }); + describe('applyScheduleOffset', () => { it('should replace minute field with offset', () => { expect(applyScheduleOffset('0 0-21 * * *', 15)).toBe('15 0-21 * * *'); diff --git a/packages/cli/src/commands/merge.ts b/packages/cli/src/commands/merge.ts index 10a34586..af1f9c13 100644 --- a/packages/cli/src/commands/merge.ts +++ b/packages/cli/src/commands/merge.ts @@ -5,10 +5,12 @@ import { Command } from 'commander'; import { INightWatchConfig, + buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, + getRepositories, getScriptPath, header, info, @@ -49,9 +51,7 @@ export function buildEnvVars( env.NW_MERGER_MERGE_METHOD = config.merger.mergeMethod; env.NW_MERGER_MIN_REVIEW_SCORE = String(config.merger.minReviewScore); env.NW_MERGER_BRANCH_PATTERNS = ( - config.merger.branchPatterns.length > 0 - ? config.merger.branchPatterns - : config.branchPatterns + config.merger.branchPatterns.length > 0 ? config.merger.branchPatterns : config.branchPatterns ).join(','); env.NW_MERGER_REBASE_BEFORE_MERGE = config.merger.rebaseBeforeMerge ? '1' : '0'; env.NW_MERGER_MAX_PRS_PER_RUN = String(config.merger.maxPrsPerRun); @@ -186,12 +186,14 @@ export function mergeCommand(program: Command): void { spinner.start(); try { + const startedAt = Date.now(); await maybeApplyCronSchedulingDelay(config, 'merger', projectDir); const { exitCode, stdout, stderr } = await executeScriptWithOutput( scriptPath, [projectDir], envVars, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); if (exitCode === 0) { @@ -212,6 +214,33 @@ export function mergeCommand(program: Command): void { const notificationEvent = resolveMergeNotificationEvent(exitCode, mergedCount, failedCount); + if (!options.dryRun) { + try { + getRepositories().sessionOutcomes.insertOutcome( + buildSessionOutcomeInput({ + projectPath: projectDir, + jobType: 'merger', + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'merger'), + startedAt, + finishedAt, + exitCode, + stdout, + stderr, + scriptResult, + minReviewScore: config.merger.minReviewScore, + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + mergedCount, + failedCount, + }, + }), + ); + } catch { + // Outcome persistence must not change command exit behavior. + } + } + if (notificationEvent) { await sendNotifications(config, { event: notificationEvent, diff --git a/packages/cli/src/commands/qa.ts b/packages/cli/src/commands/qa.ts index baf14ee1..360e6529 100644 --- a/packages/cli/src/commands/qa.ts +++ b/packages/cli/src/commands/qa.ts @@ -7,12 +7,14 @@ import { CLAUDE_MODEL_IDS, INightWatchConfig, PROVIDER_COMMANDS, + buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, fetchPrDetailsByNumber, fetchQaScreenshotUrlsForPr, + getRepositories, getScriptPath, header, info, @@ -218,12 +220,14 @@ export function qaCommand(program: Command): void { spinner.start(); try { + const startedAt = Date.now(); await maybeApplyCronSchedulingDelay(config, 'qa', projectDir); const { exitCode, stdout, stderr } = await executeScriptWithOutput( scriptPath, [projectDir], envVars, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); if (exitCode === 0) { @@ -242,6 +246,28 @@ export function qaCommand(program: Command): void { // Send notifications (fire-and-forget, failures do not affect exit code) if (!options.dryRun) { + try { + getRepositories().sessionOutcomes.insertOutcome( + buildSessionOutcomeInput({ + projectPath: projectDir, + jobType: 'qa', + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'qa'), + startedAt, + finishedAt, + exitCode, + stdout, + stderr, + scriptResult, + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + }), + ); + } catch { + // Outcome persistence must not change command exit behavior. + } + const skipNotification = !shouldSendQaNotification(scriptResult?.status); if (skipNotification) { diff --git a/packages/cli/src/commands/resolve.ts b/packages/cli/src/commands/resolve.ts index ad7c0bf2..efae4d7e 100644 --- a/packages/cli/src/commands/resolve.ts +++ b/packages/cli/src/commands/resolve.ts @@ -5,10 +5,12 @@ import { Command } from 'commander'; import { INightWatchConfig, + buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, + getRepositories, getScriptPath, header, info, @@ -210,12 +212,14 @@ export function resolveCommand(program: Command): void { spinner.start(); try { + const startedAt = Date.now(); await maybeApplyCronSchedulingDelay(config, 'pr-resolver', projectDir); const { exitCode, stdout, stderr } = await executeScriptWithOutput( scriptPath, [projectDir], envVars, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); if (exitCode === 0) { @@ -234,6 +238,30 @@ export function resolveCommand(program: Command): void { const notificationEvent = exitCode === 0 ? ('pr_resolver_completed' as const) : ('pr_resolver_failed' as const); + if (!options.dryRun) { + try { + getRepositories().sessionOutcomes.insertOutcome( + buildSessionOutcomeInput({ + projectPath: projectDir, + jobType: 'pr-resolver', + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'pr-resolver'), + startedAt, + finishedAt, + exitCode, + stdout, + stderr, + scriptResult, + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + }), + ); + } catch { + // Outcome persistence must not change command exit behavior. + } + } + await sendNotifications(config, { event: notificationEvent, projectName: path.basename(projectDir), diff --git a/packages/cli/src/commands/review.ts b/packages/cli/src/commands/review.ts index 0c095424..74313c37 100644 --- a/packages/cli/src/commands/review.ts +++ b/packages/cli/src/commands/review.ts @@ -7,12 +7,14 @@ import { CLAUDE_MODEL_IDS, INightWatchConfig, PROVIDER_COMMANDS, + buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, fetchPrDetailsByNumber, fetchReviewedPrDetails, + getRepositories, getScriptPath, header, info, @@ -431,12 +433,14 @@ export function reviewCommand(program: Command): void { spinner.start(); try { + const startedAt = Date.now(); await maybeApplyCronSchedulingDelay(config, 'reviewer', projectDir); const { exitCode, stdout, stderr } = await executeScriptWithOutput( scriptPath, [projectDir], envVars, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); if (exitCode === 0) { @@ -453,6 +457,29 @@ export function reviewCommand(program: Command): void { // Send notifications (fire-and-forget, failures do not affect exit code) if (!options.dryRun) { + try { + getRepositories().sessionOutcomes.insertOutcome( + buildSessionOutcomeInput({ + projectPath: projectDir, + jobType: 'reviewer', + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'reviewer'), + startedAt, + finishedAt, + exitCode, + stdout, + stderr, + scriptResult, + minReviewScore: config.minReviewScore, + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + }), + ); + } catch { + // Outcome persistence must not change command exit behavior. + } + const shouldNotifyCompletion = shouldSendReviewCompletionNotification( exitCode, scriptResult?.status, diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index eb3dae26..916b4728 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -10,6 +10,7 @@ import { IWebhookConfig, NotificationEvent, PROVIDER_COMMANDS, + buildSessionOutcomeInput, createBoardProvider, createSpinner, createTable, @@ -17,6 +18,7 @@ import { executeScriptWithOutput, fetchPrDetails, fetchPrDetailsForBranch, + getRepositories, getScriptPath, header, info, @@ -44,6 +46,19 @@ export interface IRunOptions { crossProjectFallback?: boolean; } +export interface IRunOutcomeRecordInput { + projectDir: string; + config: INightWatchConfig; + envVars: Record; + startedAt: number; + finishedAt: number; + exitCode: number; + stdout?: string; + stderr?: string; + scriptResult?: ReturnType; + metadata?: Record; +} + /** * Map executor exit/result state to a notification event. * Returns null when the run completed with no actionable work (skip/no-op). @@ -206,14 +221,33 @@ async function runCrossProjectFallback( envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = '1'; try { + const startedAt = Date.now(); const { exitCode, stdout, stderr } = await executeScriptWithOutput( scriptPath, [candidate.path], envVars, { cwd: candidate.path }, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); + try { + recordRunSessionOutcome({ + projectDir: candidate.path, + config: candidateConfig, + envVars, + startedAt, + finishedAt, + exitCode, + stdout, + stderr, + scriptResult, + metadata: { crossProjectFallback: true }, + }); + } catch { + // Outcome persistence must not change fallback execution behavior. + } + if (!options.dryRun) { await sendRunCompletionNotifications( candidateConfig, @@ -276,6 +310,27 @@ export function isRateLimitFallbackTriggered(resultData?: Record return resultData?.rate_limit_fallback === '1'; } +export function recordRunSessionOutcome(input: IRunOutcomeRecordInput): void { + const outcome = buildSessionOutcomeInput({ + projectPath: input.projectDir, + jobType: 'executor', + providerKey: input.envVars.NW_PROVIDER_KEY ?? resolveJobProvider(input.config, 'executor'), + startedAt: input.startedAt, + finishedAt: input.finishedAt, + exitCode: input.exitCode, + stdout: input.stdout, + stderr: input.stderr, + scriptResult: input.scriptResult, + metadata: { + providerCommand: input.envVars.NW_PROVIDER_CMD, + providerLabel: input.envVars.NW_PROVIDER_LABEL, + ...(input.metadata ?? {}), + }, + }); + + getRepositories().sessionOutcomes.insertOutcome(outcome); +} + /** * Build environment variables map from config and CLI options */ @@ -620,6 +675,7 @@ export function runCommand(program: Command): void { spinner.start(); try { + const startedAt = Date.now(); await maybeApplyCronSchedulingDelay(config, 'executor', projectDir); const { exitCode, stdout, stderr } = await executeScriptWithOutput( scriptPath, @@ -627,6 +683,7 @@ export function runCommand(program: Command): void { envVars, { cwd: projectDir }, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); if (exitCode === 0) { @@ -645,6 +702,22 @@ export function runCommand(program: Command): void { // Send completion notifications (fire-and-forget, failures do not affect exit code) if (!options.dryRun) { + try { + recordRunSessionOutcome({ + projectDir, + config, + envVars, + startedAt, + finishedAt, + exitCode, + stdout, + stderr, + scriptResult, + }); + } catch { + // Outcome persistence must not change command exit behavior. + } + await sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult); } diff --git a/packages/core/src/__tests__/feedback/outcome-parser.test.ts b/packages/core/src/__tests__/feedback/outcome-parser.test.ts new file mode 100644 index 00000000..70ae97af --- /dev/null +++ b/packages/core/src/__tests__/feedback/outcome-parser.test.ts @@ -0,0 +1,52 @@ +/** + * Tests for structured outcome parsing. + */ + +import { describe, expect, it } from 'vitest'; + +import { buildSessionOutcomeInput, classifyFailure } from '../../feedback/outcome-parser.js'; +import { parseScriptResult } from '../../utils/script-result.js'; + +describe('outcome parser', () => { + it('should classify TypeScript errors', () => { + const stderr = ` +packages/core/src/feedback/outcome-parser.ts:42:7 - error TS2322: Type 'string' is not assignable to type 'number'. +`; + + const result = classifyFailure({ + projectPath: '/tmp/night-watch', + stderr, + }); + + expect(result.category).toBe('typescript'); + expect(result.failureSignature).toContain('typescript|packages/core/src'); + expect(result.failureSignature).toContain('ts2322'); + }); + + it('should classify ESLint errors', () => { + const stdout = ` +/tmp/night-watch/packages/cli/src/commands/run.ts + 12:8 error 'unused' is assigned a value but never used @typescript-eslint/no-unused-vars + +✖ 1 problem (1 error, 0 warnings) +`; + + const result = buildSessionOutcomeInput({ + projectPath: '/tmp/night-watch', + jobType: 'executor', + providerKey: 'codex', + startedAt: 1_700_000_000_000, + finishedAt: 1_700_000_001_500, + exitCode: 1, + stdout, + scriptResult: parseScriptResult('NIGHT_WATCH_RESULT:failure|prd=97.md|branch=nw-97'), + }); + + expect(result.outcome).toBe('failure'); + expect(result.failureCategory).toBe('eslint'); + expect(result.failureSignature).toContain('eslint|packages/cli/src'); + expect(result.durationSeconds).toBe(2); + expect(result.prdFile).toBe('97.md'); + expect(result.branchName).toBe('nw-97'); + }); +}); diff --git a/packages/core/src/feedback/outcome-parser.ts b/packages/core/src/feedback/outcome-parser.ts new file mode 100644 index 00000000..57c064d8 --- /dev/null +++ b/packages/core/src/feedback/outcome-parser.ts @@ -0,0 +1,464 @@ +/** + * Tolerant parsing for Night Watch job outcomes. + */ + +import type { ISessionOutcomeInsertInput, JobType, SessionOutcomeStatus } from '@/types.js'; +import type { IScriptResult } from '@/utils/script-result.js'; + +export const FAILURE_CATEGORIES = [ + 'typescript', + 'eslint', + 'test', + 'ci', + 'review-score', + 'rate-limit', + 'timeout', + 'conflict', + 'unknown', +] as const; + +export type FailureCategory = (typeof FAILURE_CATEGORIES)[number]; + +export interface IOutcomeParserInput { + projectPath: string; + jobType: JobType; + providerKey: string; + startedAt: number; + finishedAt: number; + exitCode: number; + stdout?: string; + stderr?: string; + scriptResult?: IScriptResult | null; + minReviewScore?: number; + metadata?: Record; +} + +export interface IFailureClassification { + category: FailureCategory; + failureSignature: string; + fileArea: string | null; + firstErrorLine: string | null; +} + +export interface IFailureClassificationInput { + projectPath: string; + stdout?: string; + stderr?: string; + scriptResult?: IScriptResult | null; + minReviewScore?: number; + exitCode?: number; +} + +interface IClassifierRule { + category: FailureCategory; + pattern: RegExp; +} + +const SECRET_PLACEHOLDER = '[REDACTED_SECRET]'; +const MAX_SIGNATURE_LENGTH = 240; +const MAX_ERROR_LINE_LENGTH = 300; +const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'); +const FILE_PATH_PATTERN = /\.(?:[cm]?[jt]sx?|json|md|css|scss|ya?ml)$/i; +const TOKEN_SPLIT_PATTERN = /[\s('"`]+/; + +const SECRET_TEXT_PATTERNS: Array<[RegExp, string]> = [ + [ + /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, + SECRET_PLACEHOLDER, + ], + [/\bsk-ant-[\w-]{20,}\b/g, SECRET_PLACEHOLDER], + [/\bsk-[\w-]{20,}\b/g, SECRET_PLACEHOLDER], + [/\bgh[opsru]_\w{30,}\b/g, SECRET_PLACEHOLDER], + [/\bxox[baprs]-[\w-]{20,}\b/g, SECRET_PLACEHOLDER], + [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, SECRET_PLACEHOLDER], + [/\b(Bearer|Basic)\s+[\w.~+/=-]{12,}/gi, `$1 ${SECRET_PLACEHOLDER}`], + [/\b(token|api[_-]?key|password|secret)=["']?[\w.~+/=-]{12,}/gi, `$1=${SECRET_PLACEHOLDER}`], +]; + +const CLASSIFIER_RULES: IClassifierRule[] = [ + { + category: 'timeout', + pattern: + /\b(timed?\s*out|timeout|etimedout|operation was aborted|exit code 124|signal sigterm)\b/i, + }, + { + category: 'rate-limit', + pattern: + /\b(429|rate[- ]?limit(?:ed)?|too many requests|quota exceeded|resource_exhausted|overloaded)\b/i, + }, + { + category: 'conflict', + pattern: + /\b(merge conflict|conflict \(|conflict:|unmerged files|needs merge|automatic merge failed|both modified:)\b/i, + }, + { + category: 'typescript', + pattern: /\b(TS\d{4}|typescript error|tsc\b.*(?:failed|error)|error TS\d{4})\b/i, + }, + { + category: 'eslint', + pattern: + /\b(eslint|@typescript-eslint|no-unused-vars|no-explicit-any|react-hooks\/rules-of-hooks)\b/i, + }, + { + category: 'test', + pattern: /\b(vitest|jest|playwright|cypress|mocha|assertionerror)\b/i, + }, + { + category: 'test', + pattern: /\b(test files?|tests?)\b.*\bfailed\b/i, + }, + { + category: 'test', + pattern: /\b(expect\(|locator\(|FAIL\s+\S+\.(?:test|spec)\.)/i, + }, + { + category: 'review-score', + pattern: + /\b(review score|final_score|score)\b.*\b(below|minimum|min|required|threshold|failed|miss)\b/i, + }, + { + category: 'ci', + pattern: + /\b(ci|github actions|workflow|status check|required check|check run|failing checks?)\b.*\b(fail|error|cancel|timed out|action_required)\b/i, + }, +]; + +function stripAnsi(value: string): string { + return value.replace(ANSI_PATTERN, ''); +} + +export function redactOutcomeText(value: string): string { + return SECRET_TEXT_PATTERNS.reduce( + (current, [pattern, replacement]) => current.replace(pattern, replacement), + value, + ); +} + +function trimLine(value: string, maxLength: number): string { + return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`; +} + +function normalizeLine(value: string, projectPath: string): string { + let normalized = stripAnsi(redactOutcomeText(value)).trim(); + if (projectPath) { + normalized = normalized.replaceAll(projectPath, ''); + } + + return trimLine( + normalized + .replace(/:\d+:\d+/g, '::') + .replace(/:\d+\b/g, ':') + .replace(/\b0x[0-9a-f]+\b/gi, '') + .replace(/\b\d{4,}\b/g, '') + .replace(/\s+/g, ' ') + .toLowerCase(), + MAX_ERROR_LINE_LENGTH, + ); +} + +function getOutputLines(stdout: string | undefined, stderr: string | undefined): string[] { + return `${stdout ?? ''}\n${stderr ?? ''}` + .split(/\r?\n/) + .map((line) => stripAnsi(redactOutcomeText(line)).trim()) + .filter((line) => line.length > 0 && !line.startsWith('NIGHT_WATCH_RESULT:')); +} + +function extractFilePath(line: string, projectPath: string): string | null { + const normalizedLine = line.replaceAll('\\', '/'); + const normalizedProjectPath = projectPath.replaceAll('\\', '/'); + + if (normalizedProjectPath) { + const projectPrefix = `${normalizedProjectPath}/`; + const projectIndex = normalizedLine.indexOf(projectPrefix); + if (projectIndex >= 0) { + const relativeLine = normalizedLine.slice(projectIndex + projectPrefix.length); + const relativePath = extractFilePathToken(relativeLine.split(TOKEN_SPLIT_PATTERN)); + if (relativePath) { + return relativePath; + } + } + } + + return extractFilePathToken(normalizedLine.split(TOKEN_SPLIT_PATTERN)); +} + +function extractFilePathToken(tokens: string[]): string | null { + for (const token of tokens) { + const withoutLocation = cleanFilePathToken(token); + if (FILE_PATH_PATTERN.test(withoutLocation)) { + return withoutLocation; + } + } + return null; +} + +function cleanFilePathToken(token: string): string { + let candidate = token; + const locationIndex = findLocationIndex(candidate); + if (locationIndex >= 0) { + candidate = candidate.slice(0, locationIndex); + } + + while (candidate.startsWith('(') || candidate.startsWith('[') || candidate.startsWith('{')) { + candidate = candidate.slice(1); + } + while (candidate.endsWith(')') || candidate.endsWith(',') || candidate.endsWith(';')) { + candidate = candidate.slice(0, -1); + } + return candidate.startsWith('./') ? candidate.slice(2) : candidate; +} + +function findLocationIndex(value: string): number { + for (let index = 0; index < value.length - 1; index += 1) { + if (value[index] === ':' && value[index + 1] >= '0' && value[index + 1] <= '9') { + return index; + } + } + return -1; +} + +function filePathToArea(filePath: string | null): string | null { + if (!filePath) { + return null; + } + + const segments = filePath.split('/').filter(Boolean); + if (segments.length <= 1) { + return '.'; + } + return segments.slice(0, Math.min(segments.length - 1, 3)).join('/'); +} + +function findFirstMatchingLine(lines: string[], category: FailureCategory): string | null { + const rule = CLASSIFIER_RULES.find((entry) => entry.category === category); + if (rule) { + const matched = lines.find((line) => rule.pattern.test(line)); + if (matched) { + return matched; + } + } + + return ( + lines.find((line) => + /\b(error|failed|failure|fatal|exception|conflict|timeout)\b/i.test(line), + ) ?? + lines[0] ?? + null + ); +} + +function classifyCategory( + lines: string[], + scriptResult: IScriptResult | null | undefined, + minReviewScore: number | undefined, + exitCode: number | undefined, +): FailureCategory { + const status = scriptResult?.status ?? ''; + const data = scriptResult?.data ?? {}; + const combined = [...lines, status, data.reason ?? '', data.detail ?? ''].join('\n'); + + if (exitCode === 124 || status === 'timeout') { + return 'timeout'; + } + if (status === 'rate_limited' || data.rate_limit_fallback === '1') { + return 'rate-limit'; + } + + const reviewScore = parseOptionalNumber(data.final_score ?? data.review_score); + if ( + reviewScore != null && + minReviewScore != null && + Number.isFinite(minReviewScore) && + reviewScore < minReviewScore + ) { + return 'review-score'; + } + + for (const rule of CLASSIFIER_RULES) { + if (rule.pattern.test(combined)) { + return rule.category; + } + } + + return 'unknown'; +} + +export function classifyFailure(input: IFailureClassificationInput): IFailureClassification { + const lines = getOutputLines(input.stdout, input.stderr); + const category = classifyCategory( + lines, + input.scriptResult, + input.minReviewScore, + input.exitCode, + ); + const firstErrorLine = findFirstMatchingLine(lines, category); + const filePath = + (firstErrorLine ? extractFilePath(firstErrorLine, input.projectPath) : null) ?? + lines.map((line) => extractFilePath(line, input.projectPath)).find((value) => value != null) ?? + null; + const fileArea = filePathToArea(filePath); + const normalizedLine = firstErrorLine + ? normalizeLine(firstErrorLine, input.projectPath) + : 'no-error-line'; + const failureSignature = trimLine( + `${category}|${fileArea ?? 'unknown-area'}|${normalizedLine}`, + MAX_SIGNATURE_LENGTH, + ); + + return { + category, + failureSignature, + fileArea, + firstErrorLine: firstErrorLine ? trimLine(firstErrorLine, MAX_ERROR_LINE_LENGTH) : null, + }; +} + +function parseOptionalNumber(value: string | undefined): number | null { + if (!value) { + return null; + } + const normalized = value.trim().replace(/^#/, ''); + const parsed = parseInt(normalized, 10); + return Number.isNaN(parsed) ? null : parsed; +} + +function parseFirstPrNumber(scriptResult: IScriptResult | null | undefined): number | null { + const data = scriptResult?.data ?? {}; + const direct = + parseOptionalNumber(data.pr_number) ?? + parseOptionalNumber(data.prNumber) ?? + parseOptionalNumber(data.pr) ?? + parseOptionalNumber(data.failed_pr); + if (direct != null) { + return direct; + } + + const urlMatch = data.pr_url?.match(/\/pull\/(\d+)/); + if (urlMatch?.[1]) { + return parseOptionalNumber(urlMatch[1]); + } + + const prsRaw = data.prs ?? data.auto_merged; + if (!prsRaw) { + return null; + } + const firstToken = prsRaw.split(',').find((token) => parseOptionalNumber(token) != null); + return parseOptionalNumber(firstToken); +} + +function parseAttemptCount( + scriptResult: IScriptResult | null | undefined, + lines: string[], +): number { + const fromData = + parseOptionalNumber(scriptResult?.data.attempt) ?? + parseOptionalNumber(scriptResult?.data.attempts); + if (fromData != null && fromData > 0) { + return fromData; + } + + let maxAttempt = 1; + for (const line of lines) { + const match = /\bATTEMPT:\s*(\d+)\//i.exec(line) ?? /\bStarting attempt\s+(\d+)\//i.exec(line); + if (match?.[1]) { + maxAttempt = Math.max(maxAttempt, parseInt(match[1], 10)); + } + } + return maxAttempt; +} + +function parseRetryCount(scriptResult: IScriptResult | null | undefined, attempt: number): number { + const retryCount = parseOptionalNumber(scriptResult?.data.retry_count); + if (retryCount != null && retryCount >= 0) { + return retryCount; + } + return Math.max(0, attempt - 1); +} + +function markerIndicatesFailure(scriptResult: IScriptResult | null | undefined): boolean { + const data = scriptResult?.data ?? {}; + const positiveFailureCount = + (parseOptionalNumber(data.failed) ?? 0) > 0 || + (parseOptionalNumber(data.prs_failed) ?? 0) > 0 || + (parseOptionalNumber(data.failed_count) ?? 0) > 0; + if (positiveFailureCount) { + return true; + } + + return [data.failed_pr, data.auto_merge_failed, data.failed_automation] + .filter((value): value is string => typeof value === 'string') + .some((value) => value.trim().length > 0 && value.trim().toLowerCase() !== 'none'); +} + +function determineOutcome( + exitCode: number, + scriptResult: IScriptResult | null | undefined, + category: FailureCategory, +): SessionOutcomeStatus { + const status = scriptResult?.status ?? ''; + if (status === 'queued' || status.startsWith('skip_')) { + return 'skipped'; + } + if (exitCode === 124 || status === 'timeout' || category === 'timeout') { + return 'timeout'; + } + if (status === 'rate_limited' || (exitCode !== 0 && category === 'rate-limit')) { + return 'rate_limited'; + } + if ( + status.startsWith('failure') || + category === 'review-score' || + markerIndicatesFailure(scriptResult) + ) { + return 'failure'; + } + return exitCode === 0 ? 'success' : 'failure'; +} + +function redactMetadata(metadata: Record): Record { + return JSON.parse(redactOutcomeText(JSON.stringify(metadata))) as Record; +} + +export function buildSessionOutcomeInput(input: IOutcomeParserInput): ISessionOutcomeInsertInput { + const lines = getOutputLines(input.stdout, input.stderr); + const classification = classifyFailure(input); + const outcome = determineOutcome(input.exitCode, input.scriptResult, classification.category); + const attempt = parseAttemptCount(input.scriptResult, lines); + const retryCount = parseRetryCount(input.scriptResult, attempt); + const reviewScore = parseOptionalNumber( + input.scriptResult?.data.final_score ?? input.scriptResult?.data.review_score, + ); + const failureCategory = + outcome === 'failure' || outcome === 'timeout' || outcome === 'rate_limited' + ? classification.category + : null; + + return { + projectPath: input.projectPath, + jobType: input.jobType, + providerKey: input.providerKey || 'unknown', + prdFile: input.scriptResult?.data.prd ?? input.scriptResult?.data.prd_file ?? null, + prNumber: parseFirstPrNumber(input.scriptResult), + branchName: input.scriptResult?.data.branch ?? null, + startedAt: input.startedAt, + finishedAt: input.finishedAt, + durationSeconds: Math.max(0, Math.round((input.finishedAt - input.startedAt) / 1000)), + outcome, + exitCode: input.exitCode, + attempt, + retryCount, + reviewScore, + ciStatus: failureCategory === 'ci' ? 'fail' : (input.scriptResult?.data.ci_status ?? null), + failureCategory, + failureSignature: failureCategory ? classification.failureSignature : null, + metadata: redactMetadata({ + ...(input.metadata ?? {}), + scriptStatus: input.scriptResult?.status ?? null, + scriptData: input.scriptResult?.data ?? {}, + minReviewScore: input.minReviewScore ?? null, + firstErrorLine: classification.firstErrorLine, + fileArea: classification.fileArea, + }), + }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8f46e11c..9c48ce76 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -46,6 +46,7 @@ export * from './utils/job-queue.js'; export * from './utils/summary.js'; export * from './analytics/index.js'; export * from './audit/index.js'; +export * from './feedback/outcome-parser.js'; export * from './templates/prd-template.js'; export * from './templates/slicer-prompt.js'; // Note: shared/types are re-exported selectively through types.ts to avoid duplicates. diff --git a/packages/core/src/storage/repositories/interfaces.ts b/packages/core/src/storage/repositories/interfaces.ts index a494fa23..990c5964 100644 --- a/packages/core/src/storage/repositories/interfaces.ts +++ b/packages/core/src/storage/repositories/interfaces.ts @@ -6,9 +6,11 @@ import { BoardColumnName } from '@/board/types.js'; import type { IFeedbackPattern, + IFeedbackPatternQueryInput, IFeedbackPatternUpsertInput, IPromptAugmentation, IPromptAugmentationInsertInput, + IPromptAugmentationQueryInput, ISessionOutcome, ISessionOutcomeInsertInput, ISessionOutcomeQueryInput, @@ -84,12 +86,18 @@ export interface ISessionOutcomeRepository { queryOutcomes(input: ISessionOutcomeQueryInput): ISessionOutcome[]; querySummary(input: ISessionOutcomeSummaryInput): ISessionOutcomeSummary; upsertPattern(input: IFeedbackPatternUpsertInput): IFeedbackPattern; + listPatterns(input: IFeedbackPatternQueryInput): IFeedbackPattern[]; createAugmentation(input: IPromptAugmentationInsertInput): IPromptAugmentation; + listAugmentations(input: IPromptAugmentationQueryInput): IPromptAugmentation[]; listActiveAugmentations( projectPath: string, jobType: JobType, now?: number, ): IPromptAugmentation[]; - updateAugmentationStatus(id: number, status: PromptAugmentationStatus): void; + updateAugmentationStatus( + id: number, + status: PromptAugmentationStatus, + projectPath?: string, + ): IPromptAugmentation | null; incrementAugmentationCounts(id: number, success?: boolean): void; } diff --git a/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts b/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts index fd1fdb2d..fdc9a991 100644 --- a/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts +++ b/packages/core/src/storage/repositories/sqlite/session-outcome.repository.ts @@ -9,9 +9,11 @@ import { inject, injectable } from 'tsyringe'; import type { FeedbackPatternStatus, IFeedbackPattern, + IFeedbackPatternQueryInput, IFeedbackPatternUpsertInput, IPromptAugmentation, IPromptAugmentationInsertInput, + IPromptAugmentationQueryInput, ISessionOutcome, ISessionOutcomeInsertInput, ISessionOutcomeQueryInput, @@ -400,6 +402,33 @@ export class SqliteSessionOutcomeRepository implements ISessionOutcomeRepository return this.getPattern(input.projectPath, input.patternKey, input.jobType)!; } + listPatterns(input: IFeedbackPatternQueryInput): IFeedbackPattern[] { + const clauses = ['project_path = ?']; + const params: Array = [input.projectPath]; + + if (input.jobType) { + clauses.push('job_type = ?'); + params.push(input.jobType); + } + if (input.status) { + clauses.push('status = ?'); + params.push(input.status); + } + + const limit = Math.min(Math.max(input.limit ?? 25, 1), 100); + const rows = this.db + .prepare( + `SELECT * + FROM feedback_patterns + WHERE ${clauses.join(' AND ')} + ORDER BY sample_count DESC, confidence DESC, last_seen_at DESC, id DESC + LIMIT ?`, + ) + .all(...params, limit) as IFeedbackPatternRow[]; + + return rows.map(rowToPattern); + } + createAugmentation(input: IPromptAugmentationInsertInput): IPromptAugmentation { const now = Date.now(); const createdAt = input.createdAt ?? now; @@ -424,6 +453,37 @@ export class SqliteSessionOutcomeRepository implements ISessionOutcomeRepository return this.getAugmentationById(Number(result.lastInsertRowid))!; } + listAugmentations(input: IPromptAugmentationQueryInput): IPromptAugmentation[] { + const clauses = ['project_path = ?']; + const params: Array = [input.projectPath]; + + if (input.jobType) { + clauses.push('job_type = ?'); + params.push(input.jobType); + } + if (input.status) { + clauses.push('status = ?'); + params.push(input.status); + } + if (!input.includeExpired) { + clauses.push('(expires_at IS NULL OR expires_at > ?)'); + params.push(input.now ?? Date.now()); + } + + const limit = Math.min(Math.max(input.limit ?? 100, 1), 250); + const rows = this.db + .prepare( + `SELECT * + FROM prompt_augmentations + WHERE ${clauses.join(' AND ')} + ORDER BY created_at ASC, id ASC + LIMIT ?`, + ) + .all(...params, limit) as IPromptAugmentationRow[]; + + return rows.map(rowToAugmentation); + } + listActiveAugmentations( projectPath: string, jobType: JobType, @@ -444,10 +504,25 @@ export class SqliteSessionOutcomeRepository implements ISessionOutcomeRepository return rows.map(rowToAugmentation); } - updateAugmentationStatus(id: number, status: PromptAugmentationStatus): void { - this.db - .prepare('UPDATE prompt_augmentations SET status = ?, updated_at = ? WHERE id = ?') - .run(status, Date.now(), id); + updateAugmentationStatus( + id: number, + status: PromptAugmentationStatus, + projectPath?: string, + ): IPromptAugmentation | null { + const result = + projectPath === undefined + ? this.db + .prepare('UPDATE prompt_augmentations SET status = ?, updated_at = ? WHERE id = ?') + .run(status, Date.now(), id) + : this.db + .prepare( + `UPDATE prompt_augmentations + SET status = ?, updated_at = ? + WHERE id = ? AND project_path = ?`, + ) + .run(status, Date.now(), id, projectPath); + + return result.changes > 0 ? this.getAugmentationById(id) : null; } incrementAugmentationCounts(id: number, success = false): void { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b39f9c88..40c9ea7b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -618,6 +618,13 @@ export interface ISessionOutcomeSummary { byFailureCategory: Record; } +export interface IFeedbackPatternQueryInput { + projectPath: string; + jobType?: JobType; + status?: FeedbackPatternStatus; + limit?: number; +} + export interface IFeedbackPattern { id: number; projectPath: string; @@ -674,6 +681,15 @@ export interface IPromptAugmentationInsertInput { expiresAt?: number | null; } +export interface IPromptAugmentationQueryInput { + projectPath: string; + jobType?: JobType; + status?: PromptAugmentationStatus; + includeExpired?: boolean; + now?: number; + limit?: number; +} + /** * A record of a single job execution stored in the job_runs table */ diff --git a/packages/server/src/__tests__/server/feedback.test.ts b/packages/server/src/__tests__/server/feedback.test.ts new file mode 100644 index 00000000..a1c70aeb --- /dev/null +++ b/packages/server/src/__tests__/server/feedback.test.ts @@ -0,0 +1,169 @@ +/** + * Tests for feedback dashboard API routes. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import request from 'supertest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { closeDb, getRepositories, resetRepositories } from '@night-watch/core'; +import { createApp } from '../../index.js'; + +vi.mock('child_process', () => ({ + exec: vi.fn( + ( + _cmd: string, + _opts: unknown, + cb?: (err: Error | null, result: { stdout: string; stderr: string }) => void, + ) => { + const callback = typeof _opts === 'function' ? (_opts as typeof cb) : cb; + callback?.(null, { stdout: '', stderr: '' }); + }, + ), + execFile: vi.fn(), + execSync: vi.fn(() => ''), + spawn: vi.fn(), +})); + +vi.mock('@night-watch/core/board/factory.js', () => ({ + createBoardProvider: vi.fn(() => ({ + closeIssue: vi.fn(), + commentOnIssue: vi.fn(), + createIssue: vi.fn(), + getAllIssues: vi.fn(), + getBoard: vi.fn(), + getColumns: vi.fn(), + getIssue: vi.fn(), + getIssuesByColumn: vi.fn(), + moveIssue: vi.fn(), + setupBoard: vi.fn(), + })), +})); + +vi.mock('@night-watch/core/utils/crontab.js', () => ({ + generateMarker: vi.fn((name: string) => `# night-watch-cli: ${name}`), + getEntries: vi.fn(() => []), + getProjectEntries: vi.fn(() => []), +})); + +function writeMinimalConfig(dir: string): void { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'test-project' })); + fs.writeFileSync( + path.join(dir, 'night-watch.config.json'), + JSON.stringify({ + defaultBranch: 'main', + projectName: 'test-project', + provider: 'claude', + reviewerEnabled: true, + }), + ); + fs.mkdirSync(path.join(dir, 'docs', 'PRDs', 'night-watch', 'done'), { recursive: true }); +} + +describe('feedback API routes', () => { + let app: ReturnType; + let tempDir: string; + + beforeEach(() => { + vi.resetAllMocks(); + closeDb(); + resetRepositories(); + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nw-feedback-routes-test-')); + process.env.NIGHT_WATCH_HOME = tempDir; + writeMinimalConfig(tempDir); + app = createApp(tempDir); + }); + + afterEach(() => { + closeDb(); + resetRepositories(); + delete process.env.NIGHT_WATCH_HOME; + fs.rmSync(tempDir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('should return feedback summary', async () => { + const repo = getRepositories().sessionOutcomes; + const now = Date.now(); + + repo.insertOutcome({ + projectPath: tempDir, + jobType: 'executor', + providerKey: 'codex', + startedAt: now - 30_000, + finishedAt: now - 10_000, + durationSeconds: 20, + outcome: 'success', + }); + repo.insertOutcome({ + projectPath: tempDir, + jobType: 'reviewer', + providerKey: 'claude', + startedAt: now - 70_000, + finishedAt: now - 40_000, + durationSeconds: 30, + outcome: 'failure', + failureCategory: 'tests', + failureSignature: 'vitest failed', + }); + repo.insertOutcome({ + projectPath: tempDir, + jobType: 'executor', + providerKey: 'codex', + startedAt: now - 10 * 24 * 60 * 60 * 1000, + finishedAt: now - 10 * 24 * 60 * 60 * 1000 + 20_000, + durationSeconds: 20, + outcome: 'failure', + failureCategory: 'lint', + failureSignature: 'eslint failed', + }); + repo.createAugmentation({ + projectPath: tempDir, + jobType: 'reviewer', + promptText: 'Check for repeated test failures before editing.', + status: 'active', + }); + + const response = await request(app).get('/api/feedback/summary'); + + expect(response.status).toBe(200); + expect(response.body.projectPath).toBe(tempDir); + expect(response.body.windows.last7Days.totalCount).toBe(2); + expect(response.body.windows.last7Days.successCount).toBe(1); + expect(response.body.windows.last7Days.failureCount).toBe(1); + expect(response.body.windows.last7Days.successRate).toBe(0.5); + expect(response.body.windows.last7Days.byJobType.executor.totalCount).toBe(1); + expect(response.body.windows.last7Days.byProvider.codex.successCount).toBe(1); + expect(response.body.windows.last30Days.totalCount).toBe(3); + expect(response.body.activeAugmentations).toHaveLength(1); + }); + + it('should disable augmentation', async () => { + const repo = getRepositories().sessionOutcomes; + const augmentation = repo.createAugmentation({ + projectPath: tempDir, + jobType: 'executor', + promptText: 'Prefer the known fix for flaky tests.', + status: 'active', + }); + + const response = await request(app) + .patch(`/api/feedback/augmentations/${augmentation.id}`) + .send({ enabled: false }); + + expect(response.status).toBe(200); + expect(response.body.augmentation.id).toBe(augmentation.id); + expect(response.body.augmentation.status).toBe('paused'); + + const summary = await request(app).get('/api/feedback/summary'); + expect(summary.status).toBe(200); + expect(summary.body.windows.last7Days.totalCount).toBe(0); + expect(summary.body.windows.last7Days.successRate).toBeNull(); + expect(summary.body.windows.last7Days.byJobType).toEqual({}); + expect(summary.body.windows.last7Days.byProvider).toEqual({}); + expect(summary.body.activeAugmentations).toHaveLength(0); + }); +}); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index e5e4d4f2..ad4c4633 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -36,6 +36,7 @@ import { createProjectConfigRoutes, } from './routes/config.routes.js'; import { createDoctorRoutes, createProjectDoctorRoutes } from './routes/doctor.routes.js'; +import { createFeedbackRoutes, createProjectFeedbackRoutes } from './routes/feedback.routes.js'; import { createLogRoutes, createProjectLogRoutes } from './routes/log.routes.js'; import { createPrdRoutes, createProjectPrdRoutes } from './routes/prd.routes.js'; import { createProjectRoadmapRoutes, createRoadmapRoutes } from './routes/roadmap.routes.js'; @@ -130,6 +131,7 @@ export function createApp(projectDir: string): Express { app.use('/api/logs', createLogRoutes({ projectDir })); app.use('/api/doctor', createDoctorRoutes({ projectDir, getConfig: () => config })); app.use('/api/queue', createQueueRoutes({ getConfig: () => config })); + app.use('/api/feedback', createFeedbackRoutes({ projectDir })); app.use('/api/global-notifications', createGlobalNotificationsRoutes()); app.get('/api/prs', async (_req: Request, res: Response): Promise => { @@ -181,6 +183,7 @@ function createProjectRouter() { router.use(createProjectBoardRoutes()); router.use(createProjectActionRoutes({ projectSseClients })); router.use(createProjectRoadmapRoutes()); + router.use(createProjectFeedbackRoutes()); router.get('/prs', async (req: Request, res: Response): Promise => { try { diff --git a/packages/server/src/routes/feedback.routes.ts b/packages/server/src/routes/feedback.routes.ts new file mode 100644 index 00000000..609db736 --- /dev/null +++ b/packages/server/src/routes/feedback.routes.ts @@ -0,0 +1,306 @@ +/** + * Feedback routes: /api/feedback/* + */ + +import { Request, Response, Router } from 'express'; + +import { + IFeedbackPattern, + IPromptAugmentation, + ISessionOutcome, + ISessionOutcomeSummary, + JobType, + PromptAugmentationStatus, + SessionOutcomeStatus, + getRepositories, + getValidJobTypes, +} from '@night-watch/core'; + +const DAY_MS = 24 * 60 * 60 * 1000; +const WINDOW_DAYS = [7, 30] as const; +const VALID_AUGMENTATION_STATUSES: PromptAugmentationStatus[] = [ + 'active', + 'paused', + 'expired', + 'archived', +]; + +interface IFeedbackRoutesContext { + getProjectDir: (req: Request) => string; + pathPrefix: string; +} + +interface IFeedbackBreakdownSummary { + totalCount: number; + successCount: number; + failureCount: number; + timeoutCount: number; + rateLimitedCount: number; + skippedCount: number; + successRate: number | null; +} + +interface IFeedbackWindowSummary extends ISessionOutcomeSummary { + days: number; + fromFinishedAt: number; + toFinishedAt: number; + successRate: number | null; + byJobType: Record; + byProvider: Record; +} + +interface IFeedbackSummaryResponse { + projectPath: string; + windows: { + last7Days: IFeedbackWindowSummary; + last30Days: IFeedbackWindowSummary; + }; + activeAugmentations: IPromptAugmentation[]; +} + +interface ITopFailurePattern { + key: string; + jobType: JobType; + providerKey: string; + category: string | null; + signature: string | null; + sampleCount: number; + lastSeenAt: number; +} + +interface IFeedbackPatternsResponse { + projectPath: string; + patterns: IFeedbackPattern[]; + topFailurePatterns: ITopFailurePattern[]; +} + +interface IAugmentationPatchBody { + action?: 'enable' | 'disable' | 'expire'; + enabled?: boolean; + status?: PromptAugmentationStatus; +} + +function emptyBreakdown(): IFeedbackBreakdownSummary { + return { + totalCount: 0, + successCount: 0, + failureCount: 0, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: null, + }; +} + +function applyOutcome(summary: IFeedbackBreakdownSummary, outcome: SessionOutcomeStatus): void { + summary.totalCount += 1; + if (outcome === 'success') summary.successCount += 1; + if (outcome === 'failure') summary.failureCount += 1; + if (outcome === 'timeout') summary.timeoutCount += 1; + if (outcome === 'rate_limited') summary.rateLimitedCount += 1; + if (outcome === 'skipped') summary.skippedCount += 1; +} + +function finalizeBreakdown(summary: IFeedbackBreakdownSummary): IFeedbackBreakdownSummary { + return { + ...summary, + successRate: summary.totalCount > 0 ? summary.successCount / summary.totalCount : null, + }; +} + +function summarizeOutcomesBy( + outcomes: ISessionOutcome[], + getKey: (outcome: ISessionOutcome) => string, +): Record { + const grouped: Record = {}; + for (const outcome of outcomes) { + const key = getKey(outcome); + grouped[key] ??= emptyBreakdown(); + applyOutcome(grouped[key], outcome.outcome); + } + + return Object.fromEntries( + Object.entries(grouped).map(([key, summary]) => [key, finalizeBreakdown(summary)]), + ); +} + +function buildWindowSummary(projectPath: string, days: number): IFeedbackWindowSummary { + const repo = getRepositories().sessionOutcomes; + const toFinishedAt = Date.now(); + const fromFinishedAt = toFinishedAt - days * DAY_MS; + const base = repo.querySummary({ projectPath, fromFinishedAt, toFinishedAt }); + const outcomes = repo.queryOutcomes({ projectPath, fromFinishedAt, toFinishedAt, limit: 500 }); + + const byJobType = Object.fromEntries( + getValidJobTypes() + .map((jobType) => { + const summary = repo.querySummary({ projectPath, jobType, fromFinishedAt, toFinishedAt }); + return [ + jobType, + finalizeBreakdown({ + totalCount: summary.totalCount, + successCount: summary.successCount, + failureCount: summary.failureCount, + timeoutCount: summary.timeoutCount, + rateLimitedCount: summary.rateLimitedCount, + skippedCount: summary.skippedCount, + }), + ] as const; + }) + .filter(([, summary]) => summary.totalCount > 0), + ); + + return { + ...base, + days, + fromFinishedAt, + toFinishedAt, + successRate: base.totalCount > 0 ? base.successCount / base.totalCount : null, + byJobType, + byProvider: summarizeOutcomesBy(outcomes, (outcome) => outcome.providerKey), + }; +} + +function getActiveAugmentations(projectPath: string): IPromptAugmentation[] { + return getRepositories().sessionOutcomes.listAugmentations({ + projectPath, + status: 'active', + includeExpired: false, + limit: 250, + }); +} + +function buildFailurePatterns(projectPath: string): ITopFailurePattern[] { + const outcomes = getRepositories().sessionOutcomes.queryOutcomes({ + projectPath, + outcome: 'failure', + limit: 500, + }); + const patterns = new Map(); + + for (const outcome of outcomes) { + const key = [ + outcome.jobType, + outcome.providerKey, + outcome.failureCategory ?? 'uncategorized', + outcome.failureSignature ?? 'unknown', + ].join(':'); + const current = patterns.get(key); + if (current) { + current.sampleCount += 1; + current.lastSeenAt = Math.max(current.lastSeenAt, outcome.finishedAt); + continue; + } + + patterns.set(key, { + key, + jobType: outcome.jobType, + providerKey: outcome.providerKey, + category: outcome.failureCategory, + signature: outcome.failureSignature, + sampleCount: 1, + lastSeenAt: outcome.finishedAt, + }); + } + + return [...patterns.values()] + .sort((a, b) => b.sampleCount - a.sampleCount || b.lastSeenAt - a.lastSeenAt) + .slice(0, 10); +} + +function resolveAugmentationStatus(body: IAugmentationPatchBody): PromptAugmentationStatus | null { + if (body.action === 'enable') return 'active'; + if (body.action === 'disable') return 'paused'; + if (body.action === 'expire') return 'expired'; + if (body.enabled === true) return 'active'; + if (body.enabled === false) return 'paused'; + if (body.status && VALID_AUGMENTATION_STATUSES.includes(body.status)) return body.status; + return null; +} + +function createFeedbackRouteHandlers(ctx: IFeedbackRoutesContext): Router { + const router = Router({ mergeParams: true }); + const p = ctx.pathPrefix; + + router.get(`/${p}summary`, (req: Request, res: Response): void => { + try { + const projectPath = ctx.getProjectDir(req); + const response: IFeedbackSummaryResponse = { + projectPath, + windows: { + last7Days: buildWindowSummary(projectPath, WINDOW_DAYS[0]), + last30Days: buildWindowSummary(projectPath, WINDOW_DAYS[1]), + }, + activeAugmentations: getActiveAugmentations(projectPath), + }; + res.json(response); + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); + } + }); + + router.get(`/${p}patterns`, (req: Request, res: Response): void => { + try { + const projectPath = ctx.getProjectDir(req); + const response: IFeedbackPatternsResponse = { + projectPath, + patterns: getRepositories().sessionOutcomes.listPatterns({ projectPath, limit: 25 }), + topFailurePatterns: buildFailurePatterns(projectPath), + }; + res.json(response); + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); + } + }); + + router.patch(`/${p}augmentations/:id`, (req: Request, res: Response): void => { + try { + const id = parseInt(req.params.id as string, 10); + if (!Number.isInteger(id) || id <= 0) { + res.status(400).json({ error: 'Invalid augmentation id' }); + return; + } + + const status = resolveAugmentationStatus(req.body as IAugmentationPatchBody); + if (!status) { + res.status(400).json({ error: 'Expected action, enabled, or status update' }); + return; + } + + const projectPath = ctx.getProjectDir(req); + const augmentation = getRepositories().sessionOutcomes.updateAugmentationStatus( + id, + status, + projectPath, + ); + if (!augmentation) { + res.status(404).json({ error: 'Augmentation not found' }); + return; + } + + res.json({ augmentation }); + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); + } + }); + + return router; +} + +export interface IFeedbackRoutesDeps { + projectDir: string; +} + +export function createFeedbackRoutes(deps: IFeedbackRoutesDeps): Router { + return createFeedbackRouteHandlers({ + getProjectDir: () => deps.projectDir, + pathPrefix: '', + }); +} + +export function createProjectFeedbackRoutes(): Router { + return createFeedbackRouteHandlers({ + getProjectDir: (req) => req.projectDir!, + pathPrefix: 'feedback/', + }); +} diff --git a/web/api.ts b/web/api.ts index 0376dd9e..6a134e61 100644 --- a/web/api.ts +++ b/web/api.ts @@ -255,12 +255,120 @@ export interface ActionResult { error?: string; } +// ==================== Feedback Dashboard ==================== + +export type PromptAugmentationStatus = 'active' | 'paused' | 'expired' | 'archived'; + +export interface IFeedbackBreakdownSummary { + totalCount: number; + successCount: number; + failureCount: number; + timeoutCount: number; + rateLimitedCount: number; + skippedCount: number; + successRate: number | null; +} + +export interface IFeedbackWindowSummary extends IFeedbackBreakdownSummary { + days: number; + fromFinishedAt: number; + toFinishedAt: number; + averageDurationSeconds: number | null; + byOutcome: Record; + byFailureCategory: Record; + byJobType: Record; + byProvider: Record; +} + +export interface IPromptAugmentation { + id: number; + projectPath: string; + patternId: number | null; + jobType: JobType; + promptText: string; + status: PromptAugmentationStatus; + createdAt: number; + updatedAt: number; + expiresAt: number | null; + appliedCount: number; + successCount: number; +} + +export interface IFeedbackSummary { + projectPath: string; + windows: { + last7Days: IFeedbackWindowSummary; + last30Days: IFeedbackWindowSummary; + }; + activeAugmentations: IPromptAugmentation[]; +} + +export interface IFeedbackPattern { + id: number; + projectPath: string; + patternKey: string; + jobType: JobType; + category: string; + title: string; + description: string; + sampleCount: number; + confidence: number; + firstSeenAt: number; + lastSeenAt: number; + status: 'observing' | 'active' | 'dismissed' | 'resolved'; + metadata: Record; +} + +export interface ITopFailurePattern { + key: string; + jobType: JobType; + providerKey: string; + category: string | null; + signature: string | null; + sampleCount: number; + lastSeenAt: number; +} + +export interface IFeedbackPatterns { + projectPath: string; + patterns: IFeedbackPattern[]; + topFailurePatterns: ITopFailurePattern[]; +} + +export interface IAugmentationUpdate { + action?: 'enable' | 'disable' | 'expire'; + enabled?: boolean; + status?: PromptAugmentationStatus; +} + +export interface IAugmentationUpdateResult { + augmentation: IPromptAugmentation; +} + // ==================== API Functions ==================== export function fetchStatus(): Promise { return apiFetch(apiPath('/api/status')); } +export function fetchFeedbackSummary(): Promise { + return apiFetch(apiPath('/api/feedback/summary')); +} + +export function fetchFeedbackPatterns(): Promise { + return apiFetch(apiPath('/api/feedback/patterns')); +} + +export function updateFeedbackAugmentation( + id: number, + update: IAugmentationUpdate, +): Promise { + return apiFetch(apiPath(`/api/feedback/augmentations/${id}`), { + method: 'PATCH', + body: JSON.stringify(update), + }); +} + export function fetchPrs(): Promise { return apiFetch(apiPath('/api/prs')); } From 9c643cfd1bd1acc3c477e04a004f626f5c5f08e4 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 18:12:57 -0700 Subject: [PATCH 3/8] feat(feedback): add pattern-driven prompt guidance --- packages/cli/src/commands/review.ts | 56 ++- packages/cli/src/commands/run.ts | 39 +- .../feedback/pattern-analyzer.test.ts | 87 ++++ .../feedback/prompt-augmenter.test.ts | 60 +++ .../core/src/feedback/pattern-analyzer.ts | 399 ++++++++++++++++++ .../core/src/feedback/prompt-augmenter.ts | 132 ++++++ packages/core/src/index.ts | 2 + scripts/night-watch-cron.sh | 5 + scripts/night-watch-pr-reviewer-cron.sh | 4 + web/components/feedback/PatternList.tsx | 197 +++++++++ .../feedback/PerformanceDashboard.tsx | 256 +++++++++++ .../feedback/__tests__/PatternList.test.tsx | 39 ++ .../__tests__/PerformanceDashboard.test.tsx | 134 ++++++ web/pages/Dashboard.tsx | 15 +- web/pages/settings/JobsTab.tsx | 57 +++ web/vitest.config.ts | 7 +- 16 files changed, 1465 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/__tests__/feedback/pattern-analyzer.test.ts create mode 100644 packages/core/src/__tests__/feedback/prompt-augmenter.test.ts create mode 100644 packages/core/src/feedback/pattern-analyzer.ts create mode 100644 packages/core/src/feedback/prompt-augmenter.ts create mode 100644 web/components/feedback/PatternList.tsx create mode 100644 web/components/feedback/PerformanceDashboard.tsx create mode 100644 web/components/feedback/__tests__/PatternList.test.tsx create mode 100644 web/components/feedback/__tests__/PerformanceDashboard.test.tsx diff --git a/packages/cli/src/commands/review.ts b/packages/cli/src/commands/review.ts index 74313c37..fcaaf768 100644 --- a/packages/cli/src/commands/review.ts +++ b/packages/cli/src/commands/review.ts @@ -7,6 +7,8 @@ import { CLAUDE_MODEL_IDS, INightWatchConfig, PROVIDER_COMMANDS, + analyzeFeedbackOutcome, + buildProjectFeedbackPromptBlock, buildSessionOutcomeInput, createSpinner, createTable, @@ -18,6 +20,7 @@ import { getScriptPath, header, info, + isFeedbackPromptEnabled, loadConfig, parseScriptResult, resolveJobProvider, @@ -29,7 +32,7 @@ import { formatProviderDisplay, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; -import type { IPrDetails } from '@night-watch/core'; +import type { IPrDetails, JobType } from '@night-watch/core'; import { execFileSync } from 'child_process'; import * as path from 'path'; @@ -132,6 +135,32 @@ export function buildReviewNotificationTargets( })); } +export function applyProjectFeedbackPromptEnv( + envVars: Record, + projectDir: string, + jobType: JobType, + markApplied = true, +): void { + delete envVars.NW_PROJECT_FEEDBACK_PROMPT; + if (!isFeedbackPromptEnabled()) { + return; + } + + try { + const { promptBlock } = buildProjectFeedbackPromptBlock( + getRepositories().sessionOutcomes, + projectDir, + jobType, + { markApplied }, + ); + if (promptBlock.length > 0) { + envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock; + } + } catch { + // Feedback prompt context must never block the primary reviewer path. + } +} + /** * Parse retry attempts from script result data. * Returns the number of attempts (defaults to 1 if not present or invalid). @@ -344,6 +373,7 @@ export function reviewCommand(program: Command): void { // Build environment variables const envVars = buildEnvVars(config, options); + applyProjectFeedbackPromptEnv(envVars, projectDir, 'reviewer', !options.dryRun); // Get the script path const scriptPath = getScriptPath('night-watch-pr-reviewer-cron.sh'); @@ -458,24 +488,28 @@ export function reviewCommand(program: Command): void { // Send notifications (fire-and-forget, failures do not affect exit code) if (!options.dryRun) { try { - getRepositories().sessionOutcomes.insertOutcome( + const repository = getRepositories().sessionOutcomes; + const storedOutcome = repository.insertOutcome( buildSessionOutcomeInput({ - projectPath: projectDir, - jobType: 'reviewer', - providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'reviewer'), - startedAt, - finishedAt, exitCode, - stdout, - stderr, - scriptResult, - minReviewScore: config.minReviewScore, + finishedAt, + jobType: 'reviewer', metadata: { providerCommand: envVars.NW_PROVIDER_CMD, providerLabel: envVars.NW_PROVIDER_LABEL, }, + minReviewScore: config.minReviewScore, + projectPath: projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'reviewer'), + scriptResult, + startedAt, + stderr, + stdout, }), ); + if (isFeedbackPromptEnabled()) { + analyzeFeedbackOutcome(repository, storedOutcome); + } } catch { // Outcome persistence must not change command exit behavior. } diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 916b4728..4b861158 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -10,6 +10,8 @@ import { IWebhookConfig, NotificationEvent, PROVIDER_COMMANDS, + analyzeFeedbackOutcome, + buildProjectFeedbackPromptBlock, buildSessionOutcomeInput, createBoardProvider, createSpinner, @@ -22,6 +24,7 @@ import { getScriptPath, header, info, + isFeedbackPromptEnabled, loadConfig, parseScriptResult, resolveJobProvider, @@ -32,7 +35,7 @@ import { warn, } from '@night-watch/core'; import { buildBaseEnvVars, maybeApplyCronSchedulingDelay } from './shared/env-builder.js'; -import type { IPrDetails } from '@night-watch/core'; +import type { IPrDetails, JobType } from '@night-watch/core'; import * as fs from 'fs'; import * as path from 'path'; @@ -218,6 +221,7 @@ async function runCrossProjectFallback( let candidateConfig = loadConfig(candidate.path); candidateConfig = applyCliOverrides(candidateConfig, options); const envVars = buildEnvVars(candidateConfig, options); + applyProjectFeedbackPromptEnv(envVars, candidate.path, 'executor'); envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = '1'; try { @@ -328,7 +332,37 @@ export function recordRunSessionOutcome(input: IRunOutcomeRecordInput): void { }, }); - getRepositories().sessionOutcomes.insertOutcome(outcome); + const repository = getRepositories().sessionOutcomes; + const storedOutcome = repository.insertOutcome(outcome); + if (isFeedbackPromptEnabled()) { + analyzeFeedbackOutcome(repository, storedOutcome); + } +} + +export function applyProjectFeedbackPromptEnv( + envVars: Record, + projectDir: string, + jobType: JobType, + markApplied = true, +): void { + delete envVars.NW_PROJECT_FEEDBACK_PROMPT; + if (!isFeedbackPromptEnabled()) { + return; + } + + try { + const { promptBlock } = buildProjectFeedbackPromptBlock( + getRepositories().sessionOutcomes, + projectDir, + jobType, + { markApplied }, + ); + if (promptBlock.length > 0) { + envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock; + } + } catch { + // Feedback prompt context must never block the primary executor path. + } } /** @@ -562,6 +596,7 @@ export function runCommand(program: Command): void { // Build environment variables const envVars = buildEnvVars(config, options); + applyProjectFeedbackPromptEnv(envVars, projectDir, 'executor', !options.dryRun); // Get the script path const scriptPath = getScriptPath('night-watch-cron.sh'); diff --git a/packages/core/src/__tests__/feedback/pattern-analyzer.test.ts b/packages/core/src/__tests__/feedback/pattern-analyzer.test.ts new file mode 100644 index 00000000..600ff456 --- /dev/null +++ b/packages/core/src/__tests__/feedback/pattern-analyzer.test.ts @@ -0,0 +1,87 @@ +/** + * Tests for feedback pattern detection and activation. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import Database from 'better-sqlite3'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { analyzeFeedbackOutcome } from '../../feedback/pattern-analyzer.js'; +import { SqliteSessionOutcomeRepository } from '../../storage/repositories/sqlite/session-outcome.repository.js'; +import { runMigrations } from '../../storage/sqlite/migrations.js'; + +describe('feedback pattern analyzer', () => { + let db: Database.Database; + let repo: SqliteSessionOutcomeRepository; + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nw-pattern-analyzer-test-')); + db = new Database(path.join(tempDir, 'test.db')); + db.pragma('journal_mode = WAL'); + runMigrations(db); + repo = new SqliteSessionOutcomeRepository(db); + }); + + afterEach(() => { + db.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should activate pattern after repeated failures', () => { + const firstOutcome = repo.insertOutcome({ + failureCategory: 'test', + failureSignature: 'test|packages/core/src|expected true to be false', + finishedAt: 1_700_000_010, + jobType: 'executor', + metadata: { + fileArea: 'packages/core/src', + firstErrorLine: 'expected true to be false', + }, + outcome: 'failure', + projectPath: '/tmp/project', + providerKey: 'codex', + startedAt: 1_700_000_000, + }); + + const firstResult = analyzeFeedbackOutcome(repo, firstOutcome, { + now: 1_700_000_010, + }); + expect(firstResult.pattern?.status).toBe('observing'); + expect(repo.listActiveAugmentations('/tmp/project', 'executor', 1_700_000_010)).toHaveLength(0); + + const secondOutcome = repo.insertOutcome({ + failureCategory: 'test', + failureSignature: 'test|packages/core/src|expected true to be false', + finishedAt: 1_700_000_030, + jobType: 'executor', + metadata: { + fileArea: 'packages/core/src', + firstErrorLine: 'expected true to be false', + }, + outcome: 'failure', + projectPath: '/tmp/project', + providerKey: 'codex', + startedAt: 1_700_000_020, + }); + + const secondResult = analyzeFeedbackOutcome(repo, secondOutcome, { + now: 1_700_000_030, + }); + const activeAugmentations = repo.listActiveAugmentations( + '/tmp/project', + 'executor', + 1_700_000_030, + ); + + expect(secondResult.pattern?.sampleCount).toBe(2); + expect(secondResult.pattern?.status).toBe('active'); + expect(secondResult.pattern?.confidence).toBeGreaterThanOrEqual(0.75); + expect(activeAugmentations).toHaveLength(1); + expect(activeAugmentations[0].promptText).toContain('Provenance: pattern #'); + expect(activeAugmentations[0].promptText).toContain('samples=2'); + }); +}); diff --git a/packages/core/src/__tests__/feedback/prompt-augmenter.test.ts b/packages/core/src/__tests__/feedback/prompt-augmenter.test.ts new file mode 100644 index 00000000..bfe1334f --- /dev/null +++ b/packages/core/src/__tests__/feedback/prompt-augmenter.test.ts @@ -0,0 +1,60 @@ +/** + * Tests for project feedback prompt augmentation rendering. + */ + +import { describe, expect, it } from 'vitest'; + +import { + renderProjectFeedbackBlock, + selectPromptAugmentations, +} from '../../feedback/prompt-augmenter.js'; +import type { IPromptAugmentation } from '../../types.js'; + +function makeAugmentation( + id: number, + promptText: string, + status: IPromptAugmentation['status'] = 'active', +): IPromptAugmentation { + return { + appliedCount: 0, + createdAt: id, + expiresAt: null, + id, + jobType: 'executor', + patternId: id, + projectPath: '/tmp/project', + promptText, + status, + successCount: 0, + updatedAt: id, + }; +} + +describe('prompt augmenter', () => { + it('should cap active prompt snippets', () => { + const augmentations = [ + makeAugmentation(1, 'first repeated failure note'), + makeAugmentation(2, 'second repeated failure note'), + makeAugmentation(3, 'third repeated failure note'), + makeAugmentation(4, 'fourth repeated failure note'), + ]; + + const selected = selectPromptAugmentations(augmentations); + const block = renderProjectFeedbackBlock(augmentations); + + expect(selected.map((augmentation) => augmentation.id)).toEqual([1, 2, 3]); + expect(block).toContain('## Project Feedback'); + expect(block).toContain('first repeated failure note'); + expect(block).toContain('third repeated failure note'); + expect(block).not.toContain('fourth repeated failure note'); + }); + + it('should render prompt block only when augmentations are active', () => { + expect(renderProjectFeedbackBlock([])).toBe(''); + expect(renderProjectFeedbackBlock([makeAugmentation(1, 'paused note', 'paused')])).toBe(''); + + const block = renderProjectFeedbackBlock([makeAugmentation(1, 'active note')]); + expect(block).toContain('## Project Feedback'); + expect(block).toContain('active note'); + }); +}); diff --git a/packages/core/src/feedback/pattern-analyzer.ts b/packages/core/src/feedback/pattern-analyzer.ts new file mode 100644 index 00000000..15ebd729 --- /dev/null +++ b/packages/core/src/feedback/pattern-analyzer.ts @@ -0,0 +1,399 @@ +/** + * Deterministic feedback pattern detection from stored session outcomes. + */ + +import type { ISessionOutcomeRepository } from '@/storage/repositories/interfaces.js'; +import type { IFeedbackPattern, IPromptAugmentation, ISessionOutcome, JobType } from '@/types.js'; + +const DEFAULT_CONFIDENCE_THRESHOLD = 0.75; +const DEFAULT_AUGMENTATION_TTL_MS = 14 * 24 * 60 * 60 * 1000; +const DEFAULT_MAX_ACTIVE_AUGMENTATIONS = 3; +const DEFAULT_SUCCESS_STREAK_TO_EXPIRE = 3; +const RECENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; +const STALE_WINDOW_MS = 14 * 24 * 60 * 60 * 1000; +const MAX_PATTERN_TEXT_LENGTH = 180; +const MAX_SIGNATURE_PROMPT_LENGTH = 90; + +export interface IFeedbackPatternAnalysisOptions { + augmentationTtlMs?: number; + confidenceThreshold?: number; + maxActiveAugmentations?: number; + now?: number; + successStreakToExpire?: number; +} + +export interface IFeedbackPatternAnalysisResult { + augmentation: IPromptAugmentation | null; + expiredAugmentationIds: number[]; + pattern: IFeedbackPattern | null; +} + +interface IFailureStreakStats { + failureStreak: number; + signatureStreak: number; +} + +function isFailureOutcome(outcome: ISessionOutcome): boolean { + return ( + outcome.outcome === 'failure' || + outcome.outcome === 'timeout' || + outcome.outcome === 'rate_limited' + ); +} + +function truncateText(value: string, maxLength = MAX_PATTERN_TEXT_LENGTH): string { + const normalized = value.replace(/\s+/g, ' ').trim(); + if (normalized.length <= maxLength) { + return normalized; + } + return `${normalized.slice(0, maxLength - 3).trimEnd()}...`; +} + +function getStringMetadata(metadata: Record, key: string): string | null { + const value = metadata[key]; + return typeof value === 'string' && value.trim().length > 0 ? value : null; +} + +function getFileArea(outcome: ISessionOutcome): string { + return getStringMetadata(outcome.metadata, 'fileArea') ?? 'unknown area'; +} + +function countRecentStreaks( + repository: ISessionOutcomeRepository, + outcome: ISessionOutcome, +): IFailureStreakStats { + const recentOutcomes = repository.queryOutcomes({ + projectPath: outcome.projectPath, + jobType: outcome.jobType, + limit: 25, + }); + + let failureStreak = 0; + let signatureStreak = 0; + + for (const recentOutcome of recentOutcomes) { + if (!isFailureOutcome(recentOutcome)) { + break; + } + failureStreak += 1; + + if (recentOutcome.failureSignature === outcome.failureSignature) { + signatureStreak += 1; + } else { + break; + } + } + + return { failureStreak, signatureStreak }; +} + +function countSuccessStreak( + repository: ISessionOutcomeRepository, + projectPath: string, + jobType: JobType, +): number { + const recentOutcomes = repository.queryOutcomes({ projectPath, jobType, limit: 25 }); + let successStreak = 0; + for (const outcome of recentOutcomes) { + if (outcome.outcome !== 'success') { + break; + } + successStreak += 1; + } + return successStreak; +} + +function calculateRecencyScore(now: number, lastSeenAt: number): number { + const ageMs = Math.max(0, now - lastSeenAt); + if (ageMs <= RECENT_WINDOW_MS) { + return 1; + } + if (ageMs <= STALE_WINDOW_MS) { + return 0.5; + } + return 0.15; +} + +function calculateConfidence( + sampleCount: number, + lastSeenAt: number, + failureStreak: number, + signatureStreak: number, + now: number, +): number { + const sampleScore = Math.min(1, sampleCount / 2); + const streakScore = Math.min(1, Math.max(failureStreak, signatureStreak) / 2); + const recencyScore = calculateRecencyScore(now, lastSeenAt); + const confidence = sampleScore * 0.45 + streakScore * 0.35 + recencyScore * 0.2; + return Math.round(Math.min(1, confidence) * 100) / 100; +} + +function buildPatternTitle(outcome: ISessionOutcome): string { + const category = outcome.failureCategory ?? 'unknown'; + return truncateText(`Repeated ${category} failure in ${getFileArea(outcome)}`, 120); +} + +function buildPatternDescription(outcome: ISessionOutcome, sampleCount: number): string { + return truncateText( + `Failure signature has appeared ${sampleCount} times for ${outcome.jobType} sessions.`, + ); +} + +function buildAugmentationPrompt(pattern: IFeedbackPattern, outcome: ISessionOutcome): string { + const category = outcome.failureCategory ?? pattern.category; + const fileArea = getFileArea(outcome); + const signature = truncateText( + outcome.failureSignature ?? pattern.patternKey, + MAX_SIGNATURE_PROMPT_LENGTH, + ); + const confidencePercent = Math.round(pattern.confidence * 100); + + const actionByCategory: Record = { + ci: 'Check failing CI details before broad edits and prioritize the repeated failure area.', + conflict: 'Check merge conflicts before editing and resolve the repeated conflict area first.', + eslint: 'Run lint early and fix the repeated ESLint issue before final verification.', + 'rate-limit': + 'Avoid unnecessary provider calls and continue with local evidence when rate limits appear.', + 'review-score': + 'Read prior low-score review feedback before declaring the PR ready and address repeated concerns.', + test: 'Run the targeted test area early and fix the repeated failure before final verification.', + timeout: 'Keep the work scoped and verify incrementally because prior sessions timed out.', + typescript: + 'Run typecheck early and fix the repeated TypeScript issue before final verification.', + unknown: 'Investigate the repeated failure signature before making broad changes.', + }; + + return truncateText( + `${actionByCategory[category] ?? actionByCategory.unknown} Area: ${fileArea}. Provenance: pattern #${pattern.id}, samples=${pattern.sampleCount}, confidence=${confidencePercent}%, signature="${signature}".`, + 320, + ); +} + +function expireStaleAugmentations( + repository: ISessionOutcomeRepository, + projectPath: string, + jobType: JobType, + now: number, +): number[] { + const expired: number[] = []; + const activeAugmentations = repository.listAugmentations({ + includeExpired: true, + jobType, + projectPath, + status: 'active', + }); + + for (const augmentation of activeAugmentations) { + if (augmentation.expiresAt != null && augmentation.expiresAt <= now) { + repository.updateAugmentationStatus(augmentation.id, 'expired', projectPath); + expired.push(augmentation.id); + } + } + + return expired; +} + +function expireAugmentationsAfterSuccessStreak( + repository: ISessionOutcomeRepository, + projectPath: string, + jobType: JobType, + successStreakToExpire: number, + now: number, +): number[] { + if (successStreakToExpire <= 0) { + return []; + } + + const successStreak = countSuccessStreak(repository, projectPath, jobType); + if (successStreak < successStreakToExpire) { + return []; + } + + const expired: number[] = []; + const activeAugmentations = repository.listActiveAugmentations(projectPath, jobType, now); + for (const augmentation of activeAugmentations) { + repository.updateAugmentationStatus(augmentation.id, 'expired', projectPath); + expired.push(augmentation.id); + } + return expired; +} + +function enforceAugmentationCap( + repository: ISessionOutcomeRepository, + projectPath: string, + jobType: JobType, + maxActiveAugmentations: number, + now: number, +): number[] { + if (maxActiveAugmentations < 1) { + return repository.listActiveAugmentations(projectPath, jobType, now).map((augmentation) => { + repository.updateAugmentationStatus(augmentation.id, 'expired', projectPath); + return augmentation.id; + }); + } + + const activeAugmentations = repository.listActiveAugmentations(projectPath, jobType, now); + if (activeAugmentations.length <= maxActiveAugmentations) { + return []; + } + + const activePatterns = repository.listPatterns({ + jobType, + projectPath, + status: 'active', + limit: 100, + }); + const confidenceByPatternId = new Map( + activePatterns.map((pattern) => [pattern.id, pattern.confidence]), + ); + const keepIds = new Set( + activeAugmentations + .slice() + .sort((left, right) => { + const leftConfidence = + left.patternId == null ? 0 : (confidenceByPatternId.get(left.patternId) ?? 0); + const rightConfidence = + right.patternId == null ? 0 : (confidenceByPatternId.get(right.patternId) ?? 0); + if (leftConfidence !== rightConfidence) { + return rightConfidence - leftConfidence; + } + if (left.createdAt !== right.createdAt) { + return right.createdAt - left.createdAt; + } + return right.id - left.id; + }) + .slice(0, maxActiveAugmentations) + .map((augmentation) => augmentation.id), + ); + + const expired: number[] = []; + for (const augmentation of activeAugmentations) { + if (!keepIds.has(augmentation.id)) { + repository.updateAugmentationStatus(augmentation.id, 'expired', projectPath); + expired.push(augmentation.id); + } + } + return expired; +} + +function findActiveAugmentationForPattern( + repository: ISessionOutcomeRepository, + projectPath: string, + jobType: JobType, + patternId: number, + now: number, +): IPromptAugmentation | null { + return ( + repository + .listActiveAugmentations(projectPath, jobType, now) + .find((augmentation) => augmentation.patternId === patternId) ?? null + ); +} + +export function analyzeFeedbackOutcome( + repository: ISessionOutcomeRepository, + outcome: ISessionOutcome, + options: IFeedbackPatternAnalysisOptions = {}, +): IFeedbackPatternAnalysisResult { + const now = options.now ?? outcome.finishedAt ?? Date.now(); + const expiredAugmentationIds = expireStaleAugmentations( + repository, + outcome.projectPath, + outcome.jobType, + now, + ); + + if (!isFailureOutcome(outcome) || !outcome.failureSignature || !outcome.failureCategory) { + expiredAugmentationIds.push( + ...expireAugmentationsAfterSuccessStreak( + repository, + outcome.projectPath, + outcome.jobType, + options.successStreakToExpire ?? DEFAULT_SUCCESS_STREAK_TO_EXPIRE, + now, + ), + ); + return { augmentation: null, expiredAugmentationIds, pattern: null }; + } + + const existingPattern = + repository + .listPatterns({ + jobType: outcome.jobType, + projectPath: outcome.projectPath, + limit: 100, + }) + .find((pattern) => pattern.patternKey === outcome.failureSignature) ?? null; + const sampleCount = (existingPattern?.sampleCount ?? 0) + 1; + const streakStats = countRecentStreaks(repository, outcome); + const confidence = calculateConfidence( + sampleCount, + outcome.finishedAt, + streakStats.failureStreak, + streakStats.signatureStreak, + now, + ); + const status = + confidence >= (options.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD) + ? 'active' + : (existingPattern?.status ?? 'observing'); + + const pattern = repository.upsertPattern({ + category: outcome.failureCategory, + confidence, + description: buildPatternDescription(outcome, sampleCount), + jobType: outcome.jobType, + lastSeenAt: outcome.finishedAt, + metadata: { + confidenceInputs: { + failureStreak: streakStats.failureStreak, + recencyScore: calculateRecencyScore(now, outcome.finishedAt), + sampleCount, + signatureStreak: streakStats.signatureStreak, + }, + failureSignature: outcome.failureSignature, + fileArea: getFileArea(outcome), + firstErrorLine: getStringMetadata(outcome.metadata, 'firstErrorLine'), + lastOutcomeId: outcome.id, + }, + patternKey: outcome.failureSignature, + projectPath: outcome.projectPath, + sampleCount, + status, + title: buildPatternTitle(outcome), + }); + + let augmentation: IPromptAugmentation | null = null; + if (pattern.status === 'active') { + augmentation = findActiveAugmentationForPattern( + repository, + outcome.projectPath, + outcome.jobType, + pattern.id, + now, + ); + + if (!augmentation) { + augmentation = repository.createAugmentation({ + expiresAt: now + (options.augmentationTtlMs ?? DEFAULT_AUGMENTATION_TTL_MS), + jobType: outcome.jobType, + patternId: pattern.id, + projectPath: outcome.projectPath, + promptText: buildAugmentationPrompt(pattern, outcome), + status: 'active', + }); + } + } + + expiredAugmentationIds.push( + ...enforceAugmentationCap( + repository, + outcome.projectPath, + outcome.jobType, + options.maxActiveAugmentations ?? DEFAULT_MAX_ACTIVE_AUGMENTATIONS, + now, + ), + ); + + return { augmentation, expiredAugmentationIds, pattern }; +} diff --git a/packages/core/src/feedback/prompt-augmenter.ts b/packages/core/src/feedback/prompt-augmenter.ts new file mode 100644 index 00000000..bed16c3e --- /dev/null +++ b/packages/core/src/feedback/prompt-augmenter.ts @@ -0,0 +1,132 @@ +/** + * Prompt augmentation helpers for active project feedback snippets. + */ + +import type { ISessionOutcomeRepository } from '@/storage/repositories/interfaces.js'; +import type { IPromptAugmentation, JobType } from '@/types.js'; + +const DEFAULT_MAX_ACTIVE_AUGMENTATIONS = 3; +const MAX_SNIPPET_LENGTH = 260; +const DISABLED_VALUES = new Set(['0', 'false', 'no', 'off', 'disabled']); + +export interface IPromptAugmenterOptions { + feedbackEnabled?: boolean; + markApplied?: boolean; + maxActiveAugmentations?: number; + now?: number; +} + +export interface IProjectFeedbackPromptResult { + augmentationIds: number[]; + promptBlock: string; +} + +function isActiveAt(augmentation: IPromptAugmentation, now: number): boolean { + return ( + augmentation.status === 'active' && + (augmentation.expiresAt == null || augmentation.expiresAt > now) + ); +} + +function normalizeMaxActive(value: number | undefined): number { + if (value == null || !Number.isFinite(value)) { + return DEFAULT_MAX_ACTIVE_AUGMENTATIONS; + } + return Math.max(0, Math.floor(value)); +} + +function truncateSnippet(value: string): string { + const normalized = value.replace(/\s+/g, ' ').trim(); + if (normalized.length <= MAX_SNIPPET_LENGTH) { + return normalized; + } + return `${normalized.slice(0, MAX_SNIPPET_LENGTH - 3).trimEnd()}...`; +} + +export function isFeedbackPromptEnabled(): boolean { + const raw = process.env.NW_FEEDBACK_ENABLED ?? process.env.NW_FEEDBACK_PROMPT_ENABLED; + if (!raw) { + return true; + } + return !DISABLED_VALUES.has(raw.trim().toLowerCase()); +} + +export function selectPromptAugmentations( + augmentations: IPromptAugmentation[], + options: IPromptAugmenterOptions = {}, +): IPromptAugmentation[] { + if (options.feedbackEnabled === false) { + return []; + } + + const now = options.now ?? Date.now(); + const maxActive = normalizeMaxActive(options.maxActiveAugmentations); + if (maxActive === 0) { + return []; + } + + return augmentations + .filter((augmentation) => isActiveAt(augmentation, now)) + .sort((left, right) => { + if (left.createdAt !== right.createdAt) { + return left.createdAt - right.createdAt; + } + return left.id - right.id; + }) + .slice(0, maxActive); +} + +export function renderProjectFeedbackBlock( + augmentations: IPromptAugmentation[], + options: IPromptAugmenterOptions = {}, +): string { + const selected = selectPromptAugmentations(augmentations, options); + if (selected.length === 0) { + return ''; + } + + const lines = [ + '## Project Feedback', + 'The following short notes come from repeated recent Night Watch failures. Treat them as targeted guardrails, not as replacements for the main task instructions.', + '', + ...selected.map((augmentation) => `- ${truncateSnippet(augmentation.promptText)}`), + ]; + + return lines.join('\n'); +} + +export function buildProjectFeedbackPromptBlock( + repository: ISessionOutcomeRepository, + projectPath: string, + jobType: JobType, + options: IPromptAugmenterOptions = {}, +): IProjectFeedbackPromptResult { + const enabled = options.feedbackEnabled ?? isFeedbackPromptEnabled(); + if (!enabled) { + return { augmentationIds: [], promptBlock: '' }; + } + + const now = options.now ?? Date.now(); + const activeAugmentations = repository.listActiveAugmentations(projectPath, jobType, now); + const selected = selectPromptAugmentations(activeAugmentations, { + ...options, + feedbackEnabled: enabled, + now, + }); + const promptBlock = renderProjectFeedbackBlock(selected, { + ...options, + feedbackEnabled: enabled, + now, + }); + + if (options.markApplied === true && promptBlock.length > 0) { + for (const augmentation of selected) { + repository.incrementAugmentationCounts(augmentation.id); + } + } + + return { + augmentationIds: selected.map((augmentation) => augmentation.id), + promptBlock, + }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9c48ce76..cf6b793f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -47,6 +47,8 @@ export * from './utils/summary.js'; export * from './analytics/index.js'; export * from './audit/index.js'; export * from './feedback/outcome-parser.js'; +export * from './feedback/pattern-analyzer.js'; +export * from './feedback/prompt-augmenter.js'; export * from './templates/prd-template.js'; export * from './templates/slicer-prompt.js'; // Note: shared/types are re-exported selectively through types.ts to avoid duplicates. diff --git a/scripts/night-watch-cron.sh b/scripts/night-watch-cron.sh index 34734a72..304b3c4e 100755 --- a/scripts/night-watch-cron.sh +++ b/scripts/night-watch-cron.sh @@ -724,6 +724,11 @@ Follow all CLAUDE.md conventions (if present). - Do NOT process any other PRDs — only ${ELIGIBLE_PRD}" fi +if [ -n "${NW_PROJECT_FEEDBACK_PROMPT:-}" ]; then + PROMPT="${PROMPT}"$'\n\n'"${NW_PROJECT_FEEDBACK_PROMPT}" + log "INFO: Added project feedback prompt context" +fi + # Dry-run mode: print diagnostics and exit if [ "${NW_DRY_RUN:-0}" = "1" ]; then log "DRY-RUN: Would process ${ELIGIBLE_PRD}" diff --git a/scripts/night-watch-pr-reviewer-cron.sh b/scripts/night-watch-pr-reviewer-cron.sh index 0b0f9259..15d1b695 100755 --- a/scripts/night-watch-pr-reviewer-cron.sh +++ b/scripts/night-watch-pr-reviewer-cron.sh @@ -1226,6 +1226,10 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0) REVIEWER_ATTEMPT_START=$(date +%s) REVIEWER_PROMPT="${REVIEWER_PROMPT_BASE}${TARGET_SCOPE_PROMPT}${PRD_CONTEXT_PROMPT}" + if [ -n "${NW_PROJECT_FEEDBACK_PROMPT:-}" ]; then + REVIEWER_PROMPT="${REVIEWER_PROMPT}"$'\n\n'"${NW_PROJECT_FEEDBACK_PROMPT}" + log "INFO: Added project feedback prompt context" + fi # Build provider command array using generic helper mapfile -d '' -t PROVIDER_CMD_PARTS < <(build_provider_cmd "${REVIEW_WORKTREE_DIR}" "${REVIEWER_PROMPT}") diff --git a/web/components/feedback/PatternList.tsx b/web/components/feedback/PatternList.tsx new file mode 100644 index 00000000..e1ee9105 --- /dev/null +++ b/web/components/feedback/PatternList.tsx @@ -0,0 +1,197 @@ +import React from 'react'; +import { Clock3, PauseCircle, TimerOff } from 'lucide-react'; +import type { + IAugmentationUpdate, + IFeedbackPattern, + IPromptAugmentation, + ITopFailurePattern, +} from '../../api.js'; +import Badge from '../ui/Badge.js'; +import Button from '../ui/Button.js'; + +interface IPatternListProps { + activePatterns: IFeedbackPattern[]; + augmentations: IPromptAugmentation[]; + topFailurePatterns: ITopFailurePattern[]; + updatingAugmentationId?: number | null; + onAugmentationAction: (id: number, action: NonNullable) => Promise | void; +} + +function formatPercent(value: number): string { + return `${Math.round(value * 100)}%`; +} + +function formatDate(value: number): string { + return new Date(value).toLocaleDateString([], { month: 'short', day: 'numeric' }); +} + +function getCategoryLabel(category: string | null): string { + return category?.replace(/_/g, ' ') || 'uncategorized'; +} + +const PatternList: React.FC = ({ + activePatterns, + augmentations, + topFailurePatterns, + updatingAugmentationId = null, + onAugmentationAction, +}) => { + return ( +
+
+
+
+

Active Patterns

+ {activePatterns.length} +
+ {activePatterns.length === 0 ? ( +

+ No active feedback patterns. +

+ ) : ( +
+ {activePatterns.map((pattern) => ( +
+
+
+
+ {pattern.title} +
+

{pattern.description}

+
+ + {pattern.jobType} + +
+
+ {getCategoryLabel(pattern.category)} + {pattern.sampleCount} samples + {formatPercent(pattern.confidence)} confidence +
+
+ ))} +
+ )} +
+ +
+
+

Top Failure Patterns

+ {topFailurePatterns.length} +
+ {topFailurePatterns.length === 0 ? ( +

+ No repeated failure signatures yet. +

+ ) : ( +
+ {topFailurePatterns.map((pattern) => ( +
+
+
+
+ {pattern.signature || getCategoryLabel(pattern.category)} +
+
+ {pattern.jobType} + {pattern.providerKey} +
+
+ + {pattern.sampleCount} + +
+
+ + Last seen {formatDate(pattern.lastSeenAt)} +
+
+ ))} +
+ )} +
+
+ +
+
+

Active Augmentations

+ 0 ? 'success' : 'neutral'}>{augmentations.length} +
+ {augmentations.length === 0 ? ( +

+ No active prompt augmentations. +

+ ) : ( +
+ + + + + + + + + + + + {augmentations.map((augmentation) => { + const isUpdating = updatingAugmentationId === augmentation.id; + const successRate = + augmentation.appliedCount > 0 + ? `${Math.round((augmentation.successCount / augmentation.appliedCount) * 100)}%` + : 'new'; + + return ( + + + + + + + + ); + })} + +
Prompt SnippetJobUseExpiresActions
+
+ {augmentation.promptText} +
+
+ + {augmentation.jobType} + + + {augmentation.appliedCount} applied · {successRate} + + {augmentation.expiresAt ? formatDate(augmentation.expiresAt) : 'No expiry'} + +
+ + +
+
+
+ )} +
+
+ ); +}; + +export default PatternList; diff --git a/web/components/feedback/PerformanceDashboard.tsx b/web/components/feedback/PerformanceDashboard.tsx new file mode 100644 index 00000000..84886527 --- /dev/null +++ b/web/components/feedback/PerformanceDashboard.tsx @@ -0,0 +1,256 @@ +import React from 'react'; +import { AlertCircle, RefreshCw, TrendingDown, TrendingUp } from 'lucide-react'; +import { + fetchFeedbackPatterns, + fetchFeedbackSummary, + IAugmentationUpdate, + IFeedbackPatterns, + IFeedbackSummary, + updateFeedbackAugmentation, + useApi, +} from '../../api.js'; +import { useStore } from '../../store/useStore.js'; +import Badge from '../ui/Badge.js'; +import Button from '../ui/Button.js'; +import Card from '../ui/Card.js'; +import PatternList from './PatternList.js'; + +function formatPercent(value: number | null): string { + return value === null ? '—' : `${Math.round(value * 100)}%`; +} + +function formatDuration(seconds: number | null): string { + if (seconds === null) return '—'; + if (seconds < 60) return `${Math.round(seconds)}s`; + return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; +} + +function getSortedEntries(values: Record, limit: number): Array<[string, number]> { + return Object.entries(values) + .sort(([, a], [, b]) => b - a) + .slice(0, limit); +} + +function getTrendLabel(last7Rate: number | null, last30Rate: number | null): string { + if (last7Rate === null || last30Rate === null) return 'Waiting for comparable data'; + const delta = Math.round((last7Rate - last30Rate) * 100); + if (delta === 0) return 'Flat vs 30 days'; + return `${delta > 0 ? '+' : ''}${delta} pts vs 30 days`; +} + +function getTrendVariant(last7Rate: number | null, last30Rate: number | null): 'success' | 'warning' | 'neutral' { + if (last7Rate === null || last30Rate === null) return 'neutral'; + if (last7Rate >= last30Rate) return 'success'; + return 'warning'; +} + +interface IMetricProps { + label: string; + value: string; + detail: string; +} + +const Metric: React.FC = ({ label, value, detail }) => ( +
+
{label}
+
{value}
+
{detail}
+
+); + +const PerformanceDashboard: React.FC = () => { + const { addToast, selectedProjectId, globalModeLoading } = useStore(); + const [updatingAugmentationId, setUpdatingAugmentationId] = React.useState(null); + + const { + data: summary, + loading: summaryLoading, + error: summaryError, + refetch: refetchSummary, + } = useApi(fetchFeedbackSummary, [selectedProjectId], { enabled: !globalModeLoading }); + + const { + data: patterns, + loading: patternsLoading, + error: patternsError, + refetch: refetchPatterns, + } = useApi(fetchFeedbackPatterns, [selectedProjectId], { enabled: !globalModeLoading }); + + const handleRefresh = () => { + refetchSummary(); + refetchPatterns(); + }; + + const handleAugmentationAction = async (id: number, action: NonNullable) => { + setUpdatingAugmentationId(id); + try { + await updateFeedbackAugmentation(id, { action }); + addToast({ + title: action === 'expire' ? 'Augmentation Expired' : 'Augmentation Disabled', + message: 'Prompt augmentation state was updated.', + type: 'success', + }); + handleRefresh(); + } catch (err) { + addToast({ + title: 'Update Failed', + message: err instanceof Error ? err.message : 'Failed to update augmentation', + type: 'error', + }); + } finally { + setUpdatingAugmentationId(null); + } + }; + + const loading = summaryLoading || patternsLoading; + const error = summaryError || patternsError; + const last7 = summary?.windows.last7Days ?? null; + const last30 = summary?.windows.last30Days ?? null; + const activePatterns = (patterns?.patterns ?? []) + .filter((pattern) => pattern.status === 'active') + .sort((a, b) => b.confidence - a.confidence || b.sampleCount - a.sampleCount) + .slice(0, 5); + const topFailurePatterns = patterns?.topFailurePatterns.slice(0, 5) ?? []; + const categoryEntries = getSortedEntries(last30?.byFailureCategory ?? {}, 5); + const maxCategoryCount = Math.max(...categoryEntries.map(([, count]) => count), 1); + const hasRecordedOutcomes = (last30?.totalCount ?? 0) > 0 || (last7?.totalCount ?? 0) > 0; + const trendVariant = getTrendVariant(last7?.successRate ?? null, last30?.successRate ?? null); + const trendIcon = + trendVariant === 'success' ? ( + + ) : trendVariant === 'warning' ? ( + + ) : null; + + return ( +
+
+
+

+ Feedback Performance +

+

Outcome trends, repeated failures, and prompt augmentations.

+
+ +
+ + + {loading && !summary ? ( +
Loading feedback performance...
+ ) : error ? ( +
+ + {error.message} +
+ ) : !summary || !hasRecordedOutcomes ? ( +
+
No feedback outcomes recorded yet.
+

+ This panel will populate after executor, reviewer, QA, audit, planner, or merge jobs complete. +

+
+ ) : ( +
+
+ + + + +
+ +
+
+
+

Success-Rate Trend

+ + {trendIcon} + {getTrendLabel(last7?.successRate ?? null, last30?.successRate ?? null)} + +
+
+ {[ + { label: 'Last 7 days', value: last7?.successRate ?? 0 }, + { label: 'Last 30 days', value: last30?.successRate ?? 0 }, + ].map((row) => ( +
+
+ {row.label} + {formatPercent(row.value)} +
+
+
+
+
+ ))} +
+
+ +
+
+

Failure Categories

+ {categoryEntries.length} +
+ {categoryEntries.length === 0 ? ( +

+ No categorized failures in the last 30 days. +

+ ) : ( +
+ {categoryEntries.map(([category, count]) => ( +
+
+ {category.replace(/_/g, ' ')} + {count} +
+
+
+
+
+ ))} +
+ )} +
+
+
+ )} + + + + + +
+ ); +}; + +export default PerformanceDashboard; diff --git a/web/components/feedback/__tests__/PatternList.test.tsx b/web/components/feedback/__tests__/PatternList.test.tsx new file mode 100644 index 00000000..33a3fe8e --- /dev/null +++ b/web/components/feedback/__tests__/PatternList.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; +import PatternList from '../PatternList.js'; + +const now = Date.now(); + +describe('PatternList', () => { + it('should disable augmentation', async () => { + const onAugmentationAction = vi.fn(); + + render( + , + ); + + await userEvent.click(screen.getByRole('button', { name: /disable/i })); + + expect(onAugmentationAction).toHaveBeenCalledWith(3, 'disable'); + }); +}); diff --git a/web/components/feedback/__tests__/PerformanceDashboard.test.tsx b/web/components/feedback/__tests__/PerformanceDashboard.test.tsx new file mode 100644 index 00000000..db70a733 --- /dev/null +++ b/web/components/feedback/__tests__/PerformanceDashboard.test.tsx @@ -0,0 +1,134 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import PerformanceDashboard from '../PerformanceDashboard.js'; +import { useStore } from '../../../store/useStore.js'; + +const now = Date.now(); + +const summary = { + projectPath: '/tmp/night-watch', + windows: { + last7Days: { + days: 7, + fromFinishedAt: now - 7 * 24 * 60 * 60 * 1000, + toFinishedAt: now, + totalCount: 4, + successCount: 3, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.75, + averageDurationSeconds: 90, + byOutcome: { success: 3, failure: 1 }, + byFailureCategory: { tests: 1 }, + byJobType: {}, + byProvider: {}, + }, + last30Days: { + days: 30, + fromFinishedAt: now - 30 * 24 * 60 * 60 * 1000, + toFinishedAt: now, + totalCount: 10, + successCount: 6, + failureCount: 3, + timeoutCount: 1, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.6, + averageDurationSeconds: 125, + byOutcome: { success: 6, failure: 3, timeout: 1 }, + byFailureCategory: { tests: 2, lint: 1 }, + byJobType: {}, + byProvider: {}, + }, + }, + activeAugmentations: [ + { + id: 7, + projectPath: '/tmp/night-watch', + patternId: 1, + jobType: 'executor', + promptText: 'Check flaky test setup before editing.', + status: 'active', + createdAt: now, + updatedAt: now, + expiresAt: null, + appliedCount: 2, + successCount: 1, + }, + ], +}; + +const patterns = { + projectPath: '/tmp/night-watch', + patterns: [ + { + id: 1, + projectPath: '/tmp/night-watch', + patternKey: 'executor:tests', + jobType: 'executor', + category: 'tests', + title: 'Repeated test failures', + description: 'Executor runs repeatedly fail in the test suite.', + sampleCount: 3, + confidence: 0.82, + firstSeenAt: now - 1000, + lastSeenAt: now, + status: 'active', + metadata: {}, + }, + ], + topFailurePatterns: [ + { + key: 'executor:codex:tests:vitest failed', + jobType: 'executor', + providerKey: 'codex', + category: 'tests', + signature: 'vitest failed', + sampleCount: 2, + lastSeenAt: now, + }, + ], +}; + +describe('PerformanceDashboard', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('should render feedback summary', async () => { + useStore.setState({ globalModeLoading: false, selectedProjectId: null }); + vi.stubGlobal( + 'fetch', + vi.fn((input: RequestInfo | URL) => { + const url = String(input); + if (url.endsWith('/api/feedback/summary')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(summary), + } as Response); + } + if (url.endsWith('/api/feedback/patterns')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(patterns), + } as Response); + } + return Promise.reject(new Error(`Unhandled URL: ${url}`)); + }), + ); + + render(); + + await waitFor(() => { + expect(screen.getAllByText('75%').length).toBeGreaterThan(0); + }); + + expect(screen.getByText('Feedback Performance')).toBeInTheDocument(); + expect(screen.getByText('Success-Rate Trend')).toBeInTheDocument(); + expect(screen.getByText('Failure Categories')).toBeInTheDocument(); + expect(screen.getByText('Repeated test failures')).toBeInTheDocument(); + expect(screen.getByText('Check flaky test setup before editing.')).toBeInTheDocument(); + }); +}); diff --git a/web/pages/Dashboard.tsx b/web/pages/Dashboard.tsx index 6a4462b1..b729fb86 100644 --- a/web/pages/Dashboard.tsx +++ b/web/pages/Dashboard.tsx @@ -2,19 +2,19 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Activity, - CheckCircle, - Clock, ArrowRight, Calendar, + CheckCircle, + Clock, Play, Pause, RefreshCw, } from 'lucide-react'; import Card from '../components/ui/Card'; -import Button from '../components/ui/Button'; import { useApi, fetchScheduleInfo, fetchBoardStatus, triggerCancel, triggerClearLock, triggerJob, triggerInstallCron, triggerUninstallCron, BOARD_COLUMNS, IBoardStatus, BoardColumnName } from '../api'; import { useStore } from '../store/useStore'; import AgentStatusBar from '../components/dashboard/AgentStatusBar'; +import PerformanceDashboard from '../components/feedback/PerformanceDashboard.js'; const BOARD_COLUMN_COLORS: Record = { 'Draft': 'text-slate-400 bg-slate-500/10 ring-slate-500/20', @@ -72,13 +72,6 @@ const Dashboard: React.FC = () => { const boardReadyCount = boardStatus?.columns['Ready']?.length ?? 0; const boardInProgressCount = boardStatus?.columns['In Progress']?.length ?? 0; - const executorProcess = currentStatus.processes.find(p => p.name === 'executor'); - const reviewerProcess = currentStatus.processes.find(p => p.name === 'reviewer'); - const qaProcess = currentStatus.processes.find(p => p.name === 'qa'); - const auditProcess = currentStatus.processes.find(p => p.name === 'audit'); - const plannerProcess = currentStatus.processes.find(p => p.name === 'planner'); - const analyticsProcess = currentStatus.processes.find(p => p.name === 'analytics'); - const handleCancelProcess = async (type: 'run' | 'review') => { setCancellingProcess(type); try { @@ -343,6 +336,8 @@ const Dashboard: React.FC = () => { ) : null} + + {/* Board Widget */}
diff --git a/web/pages/settings/JobsTab.tsx b/web/pages/settings/JobsTab.tsx index ca7062c9..4cded263 100644 --- a/web/pages/settings/JobsTab.tsx +++ b/web/pages/settings/JobsTab.tsx @@ -8,6 +8,7 @@ import { Layout, Play, Search, + Sparkles, } from 'lucide-react'; import { IAnalyticsConfig, @@ -115,6 +116,62 @@ const JobsTab: React.FC = ({ return (
+
+
+
+

Prompt Augmentation

+

+ Tune how feedback patterns become prompt snippets once the config schema supports these fields. +

+
+
+ +
+
+
+
+ Prompt augmentation controls are read-only because the Night Watch config schema does not expose persistent + augmentation settings yet. +
+
+
+
+ Enable prompt augmentation +

Requires schema support before it can be saved.

+
+ +
+ + days} + helperText="How long an augmentation remains active before expiry." + /> + +
+
+
+
diff --git a/web/vitest.config.ts b/web/vitest.config.ts index 9e243f1a..6621be37 100644 --- a/web/vitest.config.ts +++ b/web/vitest.config.ts @@ -14,7 +14,12 @@ export default defineConfig({ globals: true, environment: 'happy-dom', setupFiles: ['./src/__tests__/setup.ts'], - include: ['src/**/__tests__/**/*.test.tsx', 'pages/**/__tests__/**/*.test.tsx', 'hooks/**/__tests__/**/*.test.ts'], + include: [ + 'components/**/__tests__/**/*.test.tsx', + 'src/**/__tests__/**/*.test.tsx', + 'pages/**/__tests__/**/*.test.tsx', + 'hooks/**/__tests__/**/*.test.ts', + ], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], From f18a2cbbef79769e5ddaa122d9511bb222f42572 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 18:32:47 -0700 Subject: [PATCH 4/8] fix(feedback): close PRD coverage gaps --- packages/cli/src/commands/analytics.ts | 54 ++++++++++++++ packages/cli/src/commands/audit.ts | 44 ++++++++++++ packages/cli/src/commands/merge.ts | 42 ++++++----- packages/cli/src/commands/plan.ts | 56 ++++++++++++++- packages/cli/src/commands/qa.ts | 36 +++++----- packages/cli/src/commands/resolve.ts | 36 +++++----- packages/cli/src/commands/review.ts | 10 +-- packages/cli/src/commands/run.ts | 10 +-- packages/cli/src/commands/shared/feedback.ts | 67 ++++++++++++++++++ packages/cli/src/commands/slice.ts | 49 ++++++++++++- packages/core/src/__tests__/config.test.ts | 35 ++++++++++ .../__tests__/feedback/outcome-parser.test.ts | 24 +++++++ packages/core/src/config-env.ts | 32 +++++++++ packages/core/src/config-normalize.ts | 10 +++ packages/core/src/config.ts | 19 +++++ packages/core/src/constants.ts | 10 +++ packages/core/src/shared/types.ts | 9 +++ packages/core/src/types.ts | 16 +++++ web/api.ts | 3 +- .../feedback/PerformanceDashboard.tsx | 51 ++++++++++++++ .../__tests__/PerformanceDashboard.test.tsx | 70 +++++++++++++++++-- web/pages/Scheduling.tsx | 10 +++ web/pages/Settings.tsx | 10 +++ web/pages/__tests__/Scheduling.test.tsx | 11 +++ .../__tests__/Settings.scheduling.test.tsx | 7 ++ web/pages/settings/JobsTab.tsx | 59 ++++++++++++---- 26 files changed, 688 insertions(+), 92 deletions(-) create mode 100644 packages/cli/src/commands/shared/feedback.ts diff --git a/packages/cli/src/commands/analytics.ts b/packages/cli/src/commands/analytics.ts index e200bad9..e76c2f21 100644 --- a/packages/cli/src/commands/analytics.ts +++ b/packages/cli/src/commands/analytics.ts @@ -14,6 +14,7 @@ import { runAnalytics, } from '@night-watch/core'; import { maybeApplyCronSchedulingDelay } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; export interface IAnalyticsOptions { dryRun: boolean; @@ -58,6 +59,27 @@ export function analyticsCommand(program: Command): void { const apiKey = config.providerEnv?.AMPLITUDE_API_KEY; const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY; if (!apiKey || !secretKey) { + const now = Date.now(); + if (!options.dryRun) { + try { + recordJobOutcome({ + config, + exitCode: 1, + finishedAt: now, + jobType: 'analytics', + metadata: { + missingAmplitudeCredentials: true, + }, + projectDir, + providerKey: resolveJobProvider(config, 'analytics'), + startedAt: now, + stderr: + 'AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics.', + }); + } catch { + // Outcome persistence must not change command exit behavior. + } + } info( 'AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics.', ); @@ -84,13 +106,45 @@ export function analyticsCommand(program: Command): void { const spinner = createSpinner('Running analytics job...'); spinner.start(); + const startedAt = Date.now(); try { await maybeApplyCronSchedulingDelay(config, 'analytics', projectDir); const result = await runAnalytics(config, projectDir); + recordJobOutcome({ + config, + exitCode: 0, + finishedAt: Date.now(), + jobType: 'analytics', + metadata: { + lookbackDays: config.analytics.lookbackDays, + summary: result.summary, + }, + projectDir, + providerKey: resolveJobProvider(config, 'analytics'), + startedAt, + stdout: result.summary, + }); spinner.succeed(`Analytics complete — ${result.summary}`); } catch (err) { + try { + recordJobOutcome({ + config, + exitCode: 1, + finishedAt: Date.now(), + jobType: 'analytics', + metadata: { + lookbackDays: config.analytics.lookbackDays, + }, + projectDir, + providerKey: resolveJobProvider(config, 'analytics'), + startedAt, + stderr: err instanceof Error ? err.message : String(err), + }); + } catch { + // Outcome persistence must not change command exit behavior. + } spinner.fail(`Analytics failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } diff --git a/packages/cli/src/commands/audit.ts b/packages/cli/src/commands/audit.ts index f0d6b9f7..2a7e84c6 100644 --- a/packages/cli/src/commands/audit.ts +++ b/packages/cli/src/commands/audit.ts @@ -26,6 +26,7 @@ import { getTelegramStatusWebhooks, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; export interface IAuditOptions { dryRun: boolean; @@ -130,6 +131,7 @@ export function auditCommand(program: Command): void { const spinner = createSpinner('Running code audit...'); spinner.start(); + const startedAt = Date.now(); try { await maybeApplyCronSchedulingDelay(config, 'audit', projectDir); @@ -138,8 +140,32 @@ export function auditCommand(program: Command): void { [projectDir], envVars, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); + if (!options.dryRun) { + try { + recordJobOutcome({ + config, + exitCode, + finishedAt, + jobType: 'audit', + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'audit'), + scriptResult, + startedAt, + stderr, + stdout, + }); + } catch { + // Outcome persistence must not change command exit behavior. + } + } + if (exitCode === 0) { if (scriptResult?.status === 'queued') { spinner.succeed('Code audit queued — another job is currently running'); @@ -186,6 +212,24 @@ export function auditCommand(program: Command): void { process.exit(exitCode || 1); } } catch (err) { + try { + recordJobOutcome({ + config, + exitCode: 1, + finishedAt: Date.now(), + jobType: 'audit', + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'audit'), + startedAt, + stderr: err instanceof Error ? err.message : String(err), + }); + } catch { + // Outcome persistence must not change command exit behavior. + } spinner.fail(`Code audit failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } diff --git a/packages/cli/src/commands/merge.ts b/packages/cli/src/commands/merge.ts index af1f9c13..4b2f7a13 100644 --- a/packages/cli/src/commands/merge.ts +++ b/packages/cli/src/commands/merge.ts @@ -5,12 +5,10 @@ import { Command } from 'commander'; import { INightWatchConfig, - buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, - getRepositories, getScriptPath, header, info, @@ -25,6 +23,7 @@ import { formatProviderDisplay, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; import * as path from 'path'; /** @@ -216,26 +215,25 @@ export function mergeCommand(program: Command): void { if (!options.dryRun) { try { - getRepositories().sessionOutcomes.insertOutcome( - buildSessionOutcomeInput({ - projectPath: projectDir, - jobType: 'merger', - providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'merger'), - startedAt, - finishedAt, - exitCode, - stdout, - stderr, - scriptResult, - minReviewScore: config.merger.minReviewScore, - metadata: { - providerCommand: envVars.NW_PROVIDER_CMD, - providerLabel: envVars.NW_PROVIDER_LABEL, - mergedCount, - failedCount, - }, - }), - ); + recordJobOutcome({ + config, + exitCode, + finishedAt, + jobType: 'merger', + metadata: { + failedCount, + mergedCount, + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + minReviewScore: config.merger.minReviewScore, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'merger'), + scriptResult, + startedAt, + stderr, + stdout, + }); } catch { // Outcome persistence must not change command exit behavior. } diff --git a/packages/cli/src/commands/plan.ts b/packages/cli/src/commands/plan.ts index a52f6bdb..56c2214f 100644 --- a/packages/cli/src/commands/plan.ts +++ b/packages/cli/src/commands/plan.ts @@ -18,6 +18,7 @@ import { resolveJobProvider, } from '@night-watch/core'; import { buildBaseEnvVars } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; import * as path from 'path'; export interface IPlanOptions { @@ -85,9 +86,13 @@ export function planCommand(program: Command): void { header('Provider Invocation'); if (plannerProvider === 'claude') { - dim(` ${PROVIDER_COMMANDS[plannerProvider]} -p "" --dangerously-skip-permissions`); + dim( + ` ${PROVIDER_COMMANDS[plannerProvider]} -p "" --dangerously-skip-permissions`, + ); } else { - dim(` ${PROVIDER_COMMANDS[plannerProvider]} exec --yolo ""`); + dim( + ` ${PROVIDER_COMMANDS[plannerProvider]} exec --yolo ""`, + ); } header('Command'); @@ -100,6 +105,7 @@ export function planCommand(program: Command): void { const label = resolvedTask ? `Planning: ${resolvedTask}` : 'Running PRD planner...'; const spinner = createSpinner(label); spinner.start(); + const startedAt = Date.now(); try { const { exitCode, stdout, stderr } = await executeScriptWithOutput( @@ -108,10 +114,35 @@ export function planCommand(program: Command): void { envVars, { cwd: projectDir }, ); + const finishedAt = Date.now(); const scriptResult = parseScriptResult(`${stdout}\n${stderr}`); + try { + recordJobOutcome({ + config, + exitCode, + finishedAt, + jobType: 'planner', + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + task: resolvedTask, + }, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'planner'), + scriptResult, + startedAt, + stderr, + stdout, + }); + } catch { + // Outcome persistence must not change command exit behavior. + } + if (exitCode === 0) { - spinner.succeed(`PRD planner complete — PRD written to ${path.join(projectDir, config.prdDir)}/`); + spinner.succeed( + `PRD planner complete — PRD written to ${path.join(projectDir, config.prdDir)}/`, + ); } else if (exitCode === 124) { spinner.fail('PRD planner timed out'); process.exit(1); @@ -121,6 +152,25 @@ export function planCommand(program: Command): void { process.exit(exitCode || 1); } } catch (err) { + try { + recordJobOutcome({ + config, + exitCode: 1, + finishedAt: Date.now(), + jobType: 'planner', + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + task: resolvedTask, + }, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'planner'), + startedAt, + stderr: err instanceof Error ? err.message : String(err), + }); + } catch { + // Outcome persistence must not change command exit behavior. + } spinner.fail(`PRD planner failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } diff --git a/packages/cli/src/commands/qa.ts b/packages/cli/src/commands/qa.ts index 360e6529..1a4d5d7c 100644 --- a/packages/cli/src/commands/qa.ts +++ b/packages/cli/src/commands/qa.ts @@ -7,14 +7,12 @@ import { CLAUDE_MODEL_IDS, INightWatchConfig, PROVIDER_COMMANDS, - buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, fetchPrDetailsByNumber, fetchQaScreenshotUrlsForPr, - getRepositories, getScriptPath, header, info, @@ -30,6 +28,7 @@ import { getTelegramStatusWebhooks, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; import * as path from 'path'; /** @@ -247,23 +246,22 @@ export function qaCommand(program: Command): void { // Send notifications (fire-and-forget, failures do not affect exit code) if (!options.dryRun) { try { - getRepositories().sessionOutcomes.insertOutcome( - buildSessionOutcomeInput({ - projectPath: projectDir, - jobType: 'qa', - providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'qa'), - startedAt, - finishedAt, - exitCode, - stdout, - stderr, - scriptResult, - metadata: { - providerCommand: envVars.NW_PROVIDER_CMD, - providerLabel: envVars.NW_PROVIDER_LABEL, - }, - }), - ); + recordJobOutcome({ + config, + exitCode, + finishedAt, + jobType: 'qa', + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'qa'), + scriptResult, + startedAt, + stderr, + stdout, + }); } catch { // Outcome persistence must not change command exit behavior. } diff --git a/packages/cli/src/commands/resolve.ts b/packages/cli/src/commands/resolve.ts index efae4d7e..18b64749 100644 --- a/packages/cli/src/commands/resolve.ts +++ b/packages/cli/src/commands/resolve.ts @@ -5,12 +5,10 @@ import { Command } from 'commander'; import { INightWatchConfig, - buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, - getRepositories, getScriptPath, header, info, @@ -25,6 +23,7 @@ import { formatProviderDisplay, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; import { execFileSync } from 'child_process'; import * as path from 'path'; @@ -240,23 +239,22 @@ export function resolveCommand(program: Command): void { if (!options.dryRun) { try { - getRepositories().sessionOutcomes.insertOutcome( - buildSessionOutcomeInput({ - projectPath: projectDir, - jobType: 'pr-resolver', - providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'pr-resolver'), - startedAt, - finishedAt, - exitCode, - stdout, - stderr, - scriptResult, - metadata: { - providerCommand: envVars.NW_PROVIDER_CMD, - providerLabel: envVars.NW_PROVIDER_LABEL, - }, - }), - ); + recordJobOutcome({ + config, + exitCode, + finishedAt, + jobType: 'pr-resolver', + metadata: { + providerCommand: envVars.NW_PROVIDER_CMD, + providerLabel: envVars.NW_PROVIDER_LABEL, + }, + projectDir, + providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'pr-resolver'), + scriptResult, + startedAt, + stderr, + stdout, + }); } catch { // Outcome persistence must not change command exit behavior. } diff --git a/packages/cli/src/commands/review.ts b/packages/cli/src/commands/review.ts index fcaaf768..04980bfd 100644 --- a/packages/cli/src/commands/review.ts +++ b/packages/cli/src/commands/review.ts @@ -32,6 +32,7 @@ import { formatProviderDisplay, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; +import { getFeedbackAnalysisOptions, isFeedbackEnabled } from './shared/feedback.js'; import type { IPrDetails, JobType } from '@night-watch/core'; import { execFileSync } from 'child_process'; import * as path from 'path'; @@ -142,7 +143,8 @@ export function applyProjectFeedbackPromptEnv( markApplied = true, ): void { delete envVars.NW_PROJECT_FEEDBACK_PROMPT; - if (!isFeedbackPromptEnabled()) { + const config = loadConfig(projectDir); + if (!isFeedbackPromptEnabled() || config.feedback?.enabled === false) { return; } @@ -151,7 +153,7 @@ export function applyProjectFeedbackPromptEnv( getRepositories().sessionOutcomes, projectDir, jobType, - { markApplied }, + { markApplied, maxActiveAugmentations: config.feedback?.maxActiveAugmentations }, ); if (promptBlock.length > 0) { envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock; @@ -507,8 +509,8 @@ export function reviewCommand(program: Command): void { stdout, }), ); - if (isFeedbackPromptEnabled()) { - analyzeFeedbackOutcome(repository, storedOutcome); + if (isFeedbackEnabled(config)) { + analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(config)); } } catch { // Outcome persistence must not change command exit behavior. diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 4b861158..d92a3727 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -35,6 +35,7 @@ import { warn, } from '@night-watch/core'; import { buildBaseEnvVars, maybeApplyCronSchedulingDelay } from './shared/env-builder.js'; +import { getFeedbackAnalysisOptions, isFeedbackEnabled } from './shared/feedback.js'; import type { IPrDetails, JobType } from '@night-watch/core'; import * as fs from 'fs'; import * as path from 'path'; @@ -334,8 +335,8 @@ export function recordRunSessionOutcome(input: IRunOutcomeRecordInput): void { const repository = getRepositories().sessionOutcomes; const storedOutcome = repository.insertOutcome(outcome); - if (isFeedbackPromptEnabled()) { - analyzeFeedbackOutcome(repository, storedOutcome); + if (isFeedbackEnabled(input.config)) { + analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(input.config)); } } @@ -346,7 +347,8 @@ export function applyProjectFeedbackPromptEnv( markApplied = true, ): void { delete envVars.NW_PROJECT_FEEDBACK_PROMPT; - if (!isFeedbackPromptEnabled()) { + const config = loadConfig(projectDir); + if (!isFeedbackPromptEnabled() || config.feedback?.enabled === false) { return; } @@ -355,7 +357,7 @@ export function applyProjectFeedbackPromptEnv( getRepositories().sessionOutcomes, projectDir, jobType, - { markApplied }, + { markApplied, maxActiveAugmentations: config.feedback?.maxActiveAugmentations }, ); if (promptBlock.length > 0) { envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock; diff --git a/packages/cli/src/commands/shared/feedback.ts b/packages/cli/src/commands/shared/feedback.ts new file mode 100644 index 00000000..57ba0c28 --- /dev/null +++ b/packages/cli/src/commands/shared/feedback.ts @@ -0,0 +1,67 @@ +import { + INightWatchConfig, + IScriptResult, + JobType, + analyzeFeedbackOutcome, + buildSessionOutcomeInput, + getRepositories, + isFeedbackPromptEnabled, + resolveJobProvider, +} from '@night-watch/core'; + +export interface IRecordJobOutcomeInput { + config: INightWatchConfig; + exitCode: number; + finishedAt: number; + jobType: JobType; + metadata?: Record; + minReviewScore?: number; + projectDir: string; + providerKey?: string; + scriptResult?: IScriptResult | null; + startedAt: number; + stderr?: string; + stdout?: string; +} + +export function getFeedbackAnalysisOptions(config: INightWatchConfig) { + const feedback = config.feedback ?? { + augmentationTtlDays: 14, + confidenceThreshold: 0.75, + maxActiveAugmentations: 3, + successStreakToExpire: 3, + }; + return { + augmentationTtlMs: feedback.augmentationTtlDays * 24 * 60 * 60 * 1000, + confidenceThreshold: feedback.confidenceThreshold, + maxActiveAugmentations: feedback.maxActiveAugmentations, + successStreakToExpire: feedback.successStreakToExpire, + }; +} + +export function isFeedbackEnabled(config: INightWatchConfig): boolean { + return config.feedback?.enabled !== false && isFeedbackPromptEnabled(); +} + +export function recordJobOutcome(input: IRecordJobOutcomeInput): void { + const repository = getRepositories().sessionOutcomes; + const storedOutcome = repository.insertOutcome( + buildSessionOutcomeInput({ + exitCode: input.exitCode, + finishedAt: input.finishedAt, + jobType: input.jobType, + metadata: input.metadata, + minReviewScore: input.minReviewScore, + projectPath: input.projectDir, + providerKey: input.providerKey ?? resolveJobProvider(input.config, input.jobType), + scriptResult: input.scriptResult, + startedAt: input.startedAt, + stderr: input.stderr, + stdout: input.stdout, + }), + ); + + if (isFeedbackEnabled(input.config)) { + analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(input.config)); + } +} diff --git a/packages/cli/src/commands/slice.ts b/packages/cli/src/commands/slice.ts index bdc33550..9886bf98 100644 --- a/packages/cli/src/commands/slice.ts +++ b/packages/cli/src/commands/slice.ts @@ -30,6 +30,7 @@ import { getTelegramStatusWebhooks, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js'; +import { recordJobOutcome } from './shared/feedback.js'; import type { ISliceResult } from '@night-watch/core'; import * as fs from 'fs'; import * as path from 'path'; @@ -152,7 +153,11 @@ export async function createPlannerIssue( } const issueTitle = `PRD: ${result.item.title}`; - const normalizeTitle = (t: string) => t.replace(/^PRD:\s*/i, '').trim().toLowerCase(); + const normalizeTitle = (t: string) => + t + .replace(/^PRD:\s*/i, '') + .trim() + .toLowerCase(); const existingIssues = await provider.getAllIssues(); const existing = existingIssues.find( @@ -358,6 +363,7 @@ export function sliceCommand(program: Command): void { // Execute planner with spinner const spinner = createSpinner('Running Planner...'); spinner.start(); + const startedAt = Date.now(); try { await maybeApplyCronSchedulingDelay(config, 'slicer', projectDir); @@ -395,6 +401,30 @@ export function sliceCommand(program: Command): void { const nothingPending = result.error === 'No pending items to process'; const exitCode = result.sliced || nothingPending ? 0 : 1; + if (!options.dryRun) { + try { + recordJobOutcome({ + config, + exitCode, + finishedAt: Date.now(), + jobType: 'planner', + metadata: { + error: result.error ?? null, + file: result.file ?? null, + itemTitle: result.item?.title ?? null, + sliced: result.sliced, + }, + projectDir, + providerKey: resolveJobProvider(config, 'slicer'), + startedAt, + stderr: result.error, + stdout: result.file ? `Created ${result.file}` : undefined, + }); + } catch { + // Outcome persistence must not change command exit behavior. + } + } + if (!options.dryRun && result.sliced) { await sendNotifications(config, { event: 'run_succeeded', @@ -414,6 +444,23 @@ export function sliceCommand(program: Command): void { process.exit(exitCode); } catch (err) { + try { + recordJobOutcome({ + config, + exitCode: 1, + finishedAt: Date.now(), + jobType: 'planner', + metadata: { + error: err instanceof Error ? err.message : String(err), + }, + projectDir, + providerKey: resolveJobProvider(config, 'slicer'), + startedAt, + stderr: err instanceof Error ? err.message : String(err), + }); + } catch { + // Outcome persistence must not change command exit behavior. + } spinner.fail('Failed to execute planner command'); uiError(`${err instanceof Error ? err.message : String(err)}`); process.exit(1); diff --git a/packages/core/src/__tests__/config.test.ts b/packages/core/src/__tests__/config.test.ts index 927ce4bb..6f7cdedf 100644 --- a/packages/core/src/__tests__/config.test.ts +++ b/packages/core/src/__tests__/config.test.ts @@ -73,6 +73,13 @@ describe('config', () => { expect(config.reviewerSchedule).toBe('25 */3 * * *'); expect(config.reviewerMaxPrsPerRun).toBe(0); expect(config.scheduleBundleId).toBe('always-on'); + expect(config.feedback).toEqual({ + enabled: true, + confidenceThreshold: 0.75, + augmentationTtlDays: 14, + maxActiveAugmentations: 3, + successStreakToExpire: 3, + }); }); it('should return defaults with provider and reviewerEnabled', () => { @@ -439,6 +446,34 @@ describe('config', () => { expect(config.reviewerMaxPrsPerRun).toBe(4); }); + it('should handle feedback config and env overrides', () => { + fs.writeFileSync( + path.join(tempDir, 'night-watch.config.json'), + JSON.stringify({ + feedback: { + enabled: false, + confidenceThreshold: 0.5, + augmentationTtlDays: 7, + maxActiveAugmentations: 2, + successStreakToExpire: 4, + }, + }), + ); + process.env.NW_FEEDBACK_ENABLED = 'true'; + process.env.NW_FEEDBACK_CONFIDENCE_THRESHOLD = '0.9'; + process.env.NW_FEEDBACK_MAX_ACTIVE_AUGMENTATIONS = '5'; + + const config = loadConfig(tempDir); + + expect(config.feedback).toEqual({ + enabled: true, + confidenceThreshold: 0.9, + augmentationTtlDays: 7, + maxActiveAugmentations: 5, + successStreakToExpire: 4, + }); + }); + it('should handle NW_REVIEWER_MAX_RETRIES=0 env var', () => { process.env.NW_REVIEWER_MAX_RETRIES = '0'; diff --git a/packages/core/src/__tests__/feedback/outcome-parser.test.ts b/packages/core/src/__tests__/feedback/outcome-parser.test.ts index 70ae97af..491850c6 100644 --- a/packages/core/src/__tests__/feedback/outcome-parser.test.ts +++ b/packages/core/src/__tests__/feedback/outcome-parser.test.ts @@ -23,6 +23,30 @@ packages/core/src/feedback/outcome-parser.ts:42:7 - error TS2322: Type 'string' expect(result.failureSignature).toContain('ts2322'); }); + it.each([ + ['test', 'FAIL src/example.test.ts > expected true to be false'], + ['ci', 'GitHub Actions required check failed with action_required'], + ['review-score', 'review score below threshold: final_score=72'], + ['rate-limit', '429 rate limit exceeded by provider'], + ['timeout', 'operation timed out with exit code 124'], + ['conflict', 'Automatic merge failed; fix conflicts and then commit the result.'], + ['unknown', 'provider exited without a recognized failure marker'], + ] as const)('should classify %s failures', (expectedCategory, stderr) => { + const result = classifyFailure({ + projectPath: '/tmp/night-watch', + stderr, + exitCode: expectedCategory === 'timeout' ? 124 : 1, + minReviewScore: expectedCategory === 'review-score' ? 80 : undefined, + scriptResult: + expectedCategory === 'review-score' + ? parseScriptResult('NIGHT_WATCH_RESULT:failure|final_score=72') + : null, + }); + + expect(result.category).toBe(expectedCategory); + expect(result.failureSignature).toContain(`${expectedCategory}|`); + }); + it('should classify ESLint errors', () => { const stdout = ` /tmp/night-watch/packages/cli/src/commands/run.ts diff --git a/packages/core/src/config-env.ts b/packages/core/src/config-env.ts index 1c39f752..a795686a 100644 --- a/packages/core/src/config-env.ts +++ b/packages/core/src/config-env.ts @@ -103,6 +103,38 @@ export function buildEnvOverrideConfig( const v = parseInt(process.env.NW_REVIEWER_MAX_PRS_PER_RUN, 10); if (!isNaN(v) && v >= 0) env.reviewerMaxPrsPerRun = v; } + if ( + process.env.NW_FEEDBACK_ENABLED !== undefined || + process.env.NW_FEEDBACK_CONFIDENCE_THRESHOLD !== undefined || + process.env.NW_FEEDBACK_AUGMENTATION_TTL_DAYS !== undefined || + process.env.NW_FEEDBACK_MAX_ACTIVE_AUGMENTATIONS !== undefined || + process.env.NW_FEEDBACK_SUCCESS_STREAK_TO_EXPIRE !== undefined + ) { + const feedback = { ...(fileConfig?.feedback ?? {}) }; + const enabled = process.env.NW_FEEDBACK_ENABLED + ? parseBoolean(process.env.NW_FEEDBACK_ENABLED) + : null; + if (enabled !== null) feedback.enabled = enabled; + const confidenceThreshold = parseFloat(process.env.NW_FEEDBACK_CONFIDENCE_THRESHOLD ?? ''); + if (!Number.isNaN(confidenceThreshold)) feedback.confidenceThreshold = confidenceThreshold; + const augmentationTtlDays = parseInt(process.env.NW_FEEDBACK_AUGMENTATION_TTL_DAYS ?? '', 10); + if (!Number.isNaN(augmentationTtlDays)) feedback.augmentationTtlDays = augmentationTtlDays; + const maxActiveAugmentations = parseInt( + process.env.NW_FEEDBACK_MAX_ACTIVE_AUGMENTATIONS ?? '', + 10, + ); + if (!Number.isNaN(maxActiveAugmentations)) { + feedback.maxActiveAugmentations = maxActiveAugmentations; + } + const successStreakToExpire = parseInt( + process.env.NW_FEEDBACK_SUCCESS_STREAK_TO_EXPIRE ?? '', + 10, + ); + if (!Number.isNaN(successStreakToExpire)) { + feedback.successStreakToExpire = successStreakToExpire; + } + env.feedback = feedback as INightWatchConfig['feedback']; + } if (process.env.NW_PROVIDER) { const p = validateProvider(process.env.NW_PROVIDER); diff --git a/packages/core/src/config-normalize.ts b/packages/core/src/config-normalize.ts index dae83905..56a9bbef 100644 --- a/packages/core/src/config-normalize.ts +++ b/packages/core/src/config-normalize.ts @@ -94,6 +94,16 @@ export function normalizeConfig(rawConfig: Record): Partial, limit: number): Array< .slice(0, limit); } +function getBreakdownEntries(values: IFeedbackSummary['windows']['last30Days']['byJobType']): Array<[string, string]> { + return Object.entries(values) + .sort(([, a], [, b]) => b.totalCount - a.totalCount) + .slice(0, 5) + .map(([key, summary]) => [key, `${formatPercent(summary.successRate)} · ${summary.totalCount} runs`]); +} + function getTrendLabel(last7Rate: number | null, last30Rate: number | null): string { if (last7Rate === null || last30Rate === null) return 'Waiting for comparable data'; const delta = Math.round((last7Rate - last30Rate) * 100); @@ -236,6 +243,50 @@ const PerformanceDashboard: React.FC = () => { )}
+ +
+
+
+

Job Breakdown

+ {Object.keys(last30?.byJobType ?? {}).length} +
+ {getBreakdownEntries(last30?.byJobType ?? {}).length === 0 ? ( +

+ No job-specific outcomes in the last 30 days. +

+ ) : ( +
+ {getBreakdownEntries(last30?.byJobType ?? {}).map(([jobType, detail]) => ( +
+ {jobType} + {detail} +
+ ))} +
+ )} +
+ +
+
+

Provider Breakdown

+ {Object.keys(last30?.byProvider ?? {}).length} +
+ {getBreakdownEntries(last30?.byProvider ?? {}).length === 0 ? ( +

+ No provider-specific outcomes in the last 30 days. +

+ ) : ( +
+ {getBreakdownEntries(last30?.byProvider ?? {}).map(([provider, detail]) => ( +
+ {provider} + {detail} +
+ ))} +
+ )} +
+
)} diff --git a/web/components/feedback/__tests__/PerformanceDashboard.test.tsx b/web/components/feedback/__tests__/PerformanceDashboard.test.tsx index db70a733..787dfde6 100644 --- a/web/components/feedback/__tests__/PerformanceDashboard.test.tsx +++ b/web/components/feedback/__tests__/PerformanceDashboard.test.tsx @@ -22,8 +22,28 @@ const summary = { averageDurationSeconds: 90, byOutcome: { success: 3, failure: 1 }, byFailureCategory: { tests: 1 }, - byJobType: {}, - byProvider: {}, + byJobType: { + executor: { + totalCount: 4, + successCount: 3, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.75, + }, + }, + byProvider: { + codex: { + totalCount: 4, + successCount: 3, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.75, + }, + }, }, last30Days: { days: 30, @@ -39,8 +59,46 @@ const summary = { averageDurationSeconds: 125, byOutcome: { success: 6, failure: 3, timeout: 1 }, byFailureCategory: { tests: 2, lint: 1 }, - byJobType: {}, - byProvider: {}, + byJobType: { + executor: { + totalCount: 6, + successCount: 4, + failureCount: 2, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.67, + }, + reviewer: { + totalCount: 4, + successCount: 2, + failureCount: 1, + timeoutCount: 1, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.5, + }, + }, + byProvider: { + codex: { + totalCount: 7, + successCount: 5, + failureCount: 2, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.71, + }, + claude: { + totalCount: 3, + successCount: 1, + failureCount: 1, + timeoutCount: 1, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.33, + }, + }, }, }, activeAugmentations: [ @@ -128,6 +186,10 @@ describe('PerformanceDashboard', () => { expect(screen.getByText('Feedback Performance')).toBeInTheDocument(); expect(screen.getByText('Success-Rate Trend')).toBeInTheDocument(); expect(screen.getByText('Failure Categories')).toBeInTheDocument(); + expect(screen.getByText('Job Breakdown')).toBeInTheDocument(); + expect(screen.getByText('Provider Breakdown')).toBeInTheDocument(); + expect(screen.getByText('reviewer')).toBeInTheDocument(); + expect(screen.getAllByText('codex').length).toBeGreaterThan(0); expect(screen.getByText('Repeated test failures')).toBeInTheDocument(); expect(screen.getByText('Check flaky test setup before editing.')).toBeInTheDocument(); }); diff --git a/web/pages/Scheduling.tsx b/web/pages/Scheduling.tsx index 8dabd928..c8459d1d 100644 --- a/web/pages/Scheduling.tsx +++ b/web/pages/Scheduling.tsx @@ -32,6 +32,7 @@ import { useStore } from '../store/useStore'; import type { IAnalyticsConfig, IAuditConfig, + IFeedbackConfig, IJobProviders, IMergerConfig, INightWatchConfig, @@ -97,6 +98,7 @@ type AutomationForm = { qa: IQaConfig; audit: IAuditConfig; analytics: IAnalyticsConfig; + feedback: IFeedbackConfig; prResolver: IPrResolverConfig; merger: IMergerConfig; roadmapScanner: IRoadmapScannerConfig; @@ -155,6 +157,13 @@ const toAutomationForm = (config: INightWatchConfig): AutomationForm => ({ qa: config.qa || getDefaultQaConfig(), audit: config.audit || getDefaultAuditConfig(), analytics: config.analytics || getDefaultAnalyticsConfig(), + feedback: config.feedback ?? { + enabled: true, + confidenceThreshold: 0.75, + augmentationTtlDays: 14, + maxActiveAugmentations: 3, + successStreakToExpire: 3, + }, prResolver: config.prResolver ?? getDefaultPrResolverConfig(), merger: config.merger ?? getDefaultMergerConfig(), roadmapScanner: config.roadmapScanner || getDefaultRoadmapScannerConfig(), @@ -369,6 +378,7 @@ const Scheduling: React.FC = () => { qa: form.qa, audit: form.audit, analytics: form.analytics, + feedback: form.feedback, prResolver: form.prResolver, merger: form.merger, roadmapScanner: form.roadmapScanner, diff --git a/web/pages/Settings.tsx b/web/pages/Settings.tsx index 7bb0d7bf..e2ed741a 100644 --- a/web/pages/Settings.tsx +++ b/web/pages/Settings.tsx @@ -9,6 +9,7 @@ import { IAnalyticsConfig, IAuditConfig, IBoardProviderConfig, + IFeedbackConfig, IJobProviders, IMergerConfig, INightWatchConfig, @@ -100,6 +101,7 @@ type ConfigForm = { qa: IQaConfig; audit: IAuditConfig; analytics: IAnalyticsConfig; + feedback: IFeedbackConfig; prResolver: IPrResolverConfig; merger: IMergerConfig; queue: INightWatchConfig['queue']; @@ -158,6 +160,13 @@ const toFormState = (config: INightWatchConfig): ConfigForm => { qa: config.qa || getDefaultQaConfig(), audit: config.audit || getDefaultAuditConfig(), analytics: config.analytics || getDefaultAnalyticsConfig(), + feedback: config.feedback ?? { + enabled: true, + confidenceThreshold: 0.75, + augmentationTtlDays: 14, + maxActiveAugmentations: 3, + successStreakToExpire: 3, + }, prResolver: config.prResolver ?? getDefaultPrResolverConfig(), merger: config.merger ?? getDefaultMergerConfig(), queue: config.queue || { @@ -374,6 +383,7 @@ const Settings: React.FC = () => { qa: form.qa, audit: form.audit, analytics: form.analytics, + feedback: form.feedback, prResolver: form.prResolver, merger: form.merger, queue: form.queue, diff --git a/web/pages/__tests__/Scheduling.test.tsx b/web/pages/__tests__/Scheduling.test.tsx index 79e7ccc5..4f05edfa 100644 --- a/web/pages/__tests__/Scheduling.test.tsx +++ b/web/pages/__tests__/Scheduling.test.tsx @@ -113,6 +113,13 @@ function makeConfig(overrides: Partial = {}): INightWatchConf targetColumn: 'Draft', analysisPrompt: '', }, + feedback: { + enabled: true, + confidenceThreshold: 0.75, + augmentationTtlDays: 14, + maxActiveAugmentations: 3, + successStreakToExpire: 3, + }, merger: { enabled: true, schedule: '55 */4 * * *', @@ -144,6 +151,10 @@ function makeConfig(overrides: Partial = {}): INightWatchConf ...base.analytics, ...(overrides.analytics ?? {}), }, + feedback: { + ...base.feedback, + ...(overrides.feedback ?? {}), + }, }; } diff --git a/web/pages/__tests__/Settings.scheduling.test.tsx b/web/pages/__tests__/Settings.scheduling.test.tsx index b56627a0..865597df 100644 --- a/web/pages/__tests__/Settings.scheduling.test.tsx +++ b/web/pages/__tests__/Settings.scheduling.test.tsx @@ -96,6 +96,13 @@ function makeConfig(overrides: Partial = {}): INightWatchConf targetColumn: 'Draft', analysisPrompt: '', }, + feedback: { + enabled: true, + confidenceThreshold: 0.75, + augmentationTtlDays: 14, + maxActiveAugmentations: 3, + successStreakToExpire: 3, + }, prResolver: { enabled: true, schedule: '10 */4 * * *', diff --git a/web/pages/settings/JobsTab.tsx b/web/pages/settings/JobsTab.tsx index 4cded263..7fd0245e 100644 --- a/web/pages/settings/JobsTab.tsx +++ b/web/pages/settings/JobsTab.tsx @@ -18,6 +18,7 @@ import { IPrResolverConfig, IRoadmapScannerConfig, IJobProviders, + IFeedbackConfig, MergeMethod, QaArtifacts, INightWatchConfig, @@ -56,6 +57,7 @@ interface IConfigFormJobs { qa: IQaConfig; audit: IAuditConfig; analytics: IAnalyticsConfig; + feedback: IFeedbackConfig; prResolver: IPrResolverConfig; merger: IMergerConfig; roadmapScanner: IRoadmapScannerConfig; @@ -121,25 +123,24 @@ const JobsTab: React.FC = ({

Prompt Augmentation

- Tune how feedback patterns become prompt snippets once the config schema supports these fields. + Tune how repeated feedback patterns become prompt snippets.

-
-
- Prompt augmentation controls are read-only because the Night Watch config schema does not expose persistent - augmentation settings yet. -
+
Enable prompt augmentation -

Requires schema support before it can be saved.

+

Adds capped feedback snippets to future job prompts.

- + updateField('feedback', { ...form.feedback, enabled: checked })} + />
= ({ min="0" max="1" step="0.05" - value="0.75" - disabled + value={String(form.feedback.confidenceThreshold)} + onChange={(e) => + updateField('feedback', { + ...form.feedback, + confidenceThreshold: Math.max(0, Math.min(1, Number(e.target.value || 0))), + }) + } helperText="Minimum confidence required before a snippet can activate." /> + updateField('feedback', { + ...form.feedback, + augmentationTtlDays: Math.max(1, Number(e.target.value || 1)), + }) + } rightIcon={days} helperText="How long an augmentation remains active before expiry." /> + updateField('feedback', { + ...form.feedback, + maxActiveAugmentations: Math.max(0, Number(e.target.value || 0)), + }) + } helperText="Maximum active augmentation snippets applied to each job prompt." /> + + updateField('feedback', { + ...form.feedback, + successStreakToExpire: Math.max(0, Number(e.target.value || 0)), + }) + } + helperText="Consecutive successes before active snippets expire." + />
From f903a619ee0f2863f07b11ae9ac1e945ae3aec0d Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 18:58:44 -0700 Subject: [PATCH 5/8] fix: restore feedback build compatibility --- packages/cli/src/commands/init.ts | 1 + packages/server/src/routes/feedback.routes.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 4ff45852..d1187aab 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -378,6 +378,7 @@ export function buildInitConfig(params: { }, audit: { ...defaults.audit }, analytics: { ...defaults.analytics }, + feedback: { ...defaults.feedback }, merger: { ...defaults.merger }, prResolver: { ...defaults.prResolver }, jobProviders: { ...defaults.jobProviders }, diff --git a/packages/server/src/routes/feedback.routes.ts b/packages/server/src/routes/feedback.routes.ts index 609db736..6c7e5bbf 100644 --- a/packages/server/src/routes/feedback.routes.ts +++ b/packages/server/src/routes/feedback.routes.ts @@ -144,6 +144,7 @@ function buildWindowSummary(projectPath: string, days: number): IFeedbackWindowS timeoutCount: summary.timeoutCount, rateLimitedCount: summary.rateLimitedCount, skippedCount: summary.skippedCount, + successRate: null, }), ] as const; }) From e86b5f32d2694776d622e043935a19fda2233259 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 19:05:35 -0700 Subject: [PATCH 6/8] fix: order cli build after workspace packages --- packages/cli/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/package.json b/packages/cli/package.json index 371a445a..ca79e7b0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -71,6 +71,8 @@ "tsyringe": "^4.10.0" }, "devDependencies": { + "@night-watch/core": "*", + "@night-watch/server": "*", "@types/blessed": "^0.1.27", "@types/node": "^22.0.0", "esbuild": "^0.25.0", From ce37473f566a92ac17a6f10ab6bbbc8c0694c09a Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 19:11:23 -0700 Subject: [PATCH 7/8] test: make reviewer smoke fixtures hermetic --- .../cli/src/__tests__/scripts/core-flow-smoke.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/cli/src/__tests__/scripts/core-flow-smoke.test.ts b/packages/cli/src/__tests__/scripts/core-flow-smoke.test.ts index 291838d8..36ac4b88 100644 --- a/packages/cli/src/__tests__/scripts/core-flow-smoke.test.ts +++ b/packages/cli/src/__tests__/scripts/core-flow-smoke.test.ts @@ -105,6 +105,13 @@ function commitAll(projectDir: string, message: string): void { }); } +function writeFakeClaude(fakeBin: string): void { + fs.writeFileSync(path.join(fakeBin, 'claude'), '#!/usr/bin/env bash\nexit 0\n', { + encoding: 'utf-8', + mode: 0o755, + }); +} + afterEach(() => { for (const dir of tempDirs) { fs.rmSync(dir, { recursive: true, force: true }); @@ -1926,6 +1933,7 @@ describe('core flow smoke tests (bash scripts)', () => { fs.mkdirSync(path.join(projectDir, 'logs'), { recursive: true }); const fakeBin = mkTempDir('nw-smoke-reviewer-score-threshold-bin-'); + writeFakeClaude(fakeBin); fs.writeFileSync( path.join(fakeBin, 'gh'), @@ -2064,6 +2072,7 @@ describe('core flow smoke tests (bash scripts)', () => { fs.mkdirSync(path.join(projectDir, 'logs'), { recursive: true }); const fakeBin = mkTempDir('nw-smoke-reviewer-needs-human-review-bin-'); + writeFakeClaude(fakeBin); fs.writeFileSync( path.join(fakeBin, 'gh'), @@ -2333,6 +2342,7 @@ describe('core flow smoke tests (bash scripts)', () => { fs.mkdirSync(path.join(projectDir, 'logs'), { recursive: true }); const fakeBin = mkTempDir('nw-smoke-reviewer-max-prs-per-run-bin-'); + writeFakeClaude(fakeBin); fs.writeFileSync( path.join(fakeBin, 'gh'), From 30f50d20be413e599a62e709b875c95a782270f1 Mon Sep 17 00:00:00 2001 From: Test User Date: Sun, 3 May 2026 19:23:58 -0700 Subject: [PATCH 8/8] test(qa): add automated QA tests for PR changes - Generated by Night Watch QA agent - UI tests: 1 passing, 0 failing - API tests: 3 passing, 0 failing - Artifacts: screenshots, videos Co-Authored-By: Claude --- .../server/feedback-validation.test.ts | 191 ++++++++++++ qa-artifacts/qa-feedback-dashboard.png | Bin 0 -> 98252 bytes qa-artifacts/qa-feedback-dashboard.webm | Bin 0 -> 159144 bytes .../e2e/qa/qa-feedback-dashboard.spec.ts | 282 ++++++++++++++++++ 4 files changed, 473 insertions(+) create mode 100644 packages/server/src/__tests__/server/feedback-validation.test.ts create mode 100644 qa-artifacts/qa-feedback-dashboard.png create mode 100644 qa-artifacts/qa-feedback-dashboard.webm create mode 100644 web/tests/e2e/qa/qa-feedback-dashboard.spec.ts diff --git a/packages/server/src/__tests__/server/feedback-validation.test.ts b/packages/server/src/__tests__/server/feedback-validation.test.ts new file mode 100644 index 00000000..3e3beb57 --- /dev/null +++ b/packages/server/src/__tests__/server/feedback-validation.test.ts @@ -0,0 +1,191 @@ +/** + * Additional QA coverage for feedback API validation and aggregation behavior. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import request from 'supertest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { closeDb, getRepositories, resetRepositories } from '@night-watch/core'; +import { createApp } from '../../index.js'; + +vi.mock('child_process', () => ({ + exec: vi.fn( + ( + _cmd: string, + _opts: unknown, + cb?: (err: Error | null, result: { stdout: string; stderr: string }) => void, + ) => { + const callback = typeof _opts === 'function' ? (_opts as typeof cb) : cb; + callback?.(null, { stdout: '', stderr: '' }); + }, + ), + execFile: vi.fn(), + execSync: vi.fn(() => ''), + spawn: vi.fn(), +})); + +vi.mock('@night-watch/core/board/factory.js', () => ({ + createBoardProvider: vi.fn(() => ({ + closeIssue: vi.fn(), + commentOnIssue: vi.fn(), + createIssue: vi.fn(), + getAllIssues: vi.fn(), + getBoard: vi.fn(), + getColumns: vi.fn(), + getIssue: vi.fn(), + getIssuesByColumn: vi.fn(), + moveIssue: vi.fn(), + setupBoard: vi.fn(), + })), +})); + +vi.mock('@night-watch/core/utils/crontab.js', () => ({ + generateMarker: vi.fn((name: string) => `# night-watch-cli: ${name}`), + getEntries: vi.fn(() => []), + getProjectEntries: vi.fn(() => []), +})); + +function writeMinimalConfig(dir: string): void { + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'test-project' })); + fs.writeFileSync( + path.join(dir, 'night-watch.config.json'), + JSON.stringify({ + defaultBranch: 'main', + projectName: 'test-project', + provider: 'claude', + reviewerEnabled: true, + }), + ); + fs.mkdirSync(path.join(dir, 'docs', 'PRDs', 'night-watch', 'done'), { recursive: true }); +} + +describe('feedback API validation', () => { + let app: ReturnType; + let tempDir: string; + + beforeEach(() => { + vi.resetAllMocks(); + closeDb(); + resetRepositories(); + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nw-feedback-validation-test-')); + process.env.NIGHT_WATCH_HOME = tempDir; + writeMinimalConfig(tempDir); + app = createApp(tempDir); + }); + + afterEach(() => { + closeDb(); + resetRepositories(); + delete process.env.NIGHT_WATCH_HOME; + fs.rmSync(tempDir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('should reject invalid augmentation update requests', async () => { + const invalidId = await request(app) + .patch('/api/feedback/augmentations/not-a-number') + .send({ action: 'disable' }); + expect(invalidId.status).toBe(400); + expect(invalidId.body.error).toBe('Invalid augmentation id'); + + const invalidBody = await request(app).patch('/api/feedback/augmentations/1').send({}); + expect(invalidBody.status).toBe(400); + expect(invalidBody.body.error).toBe('Expected action, enabled, or status update'); + }); + + it('should not update augmentations from another project', async () => { + const repo = getRepositories().sessionOutcomes; + const otherProjectAugmentation = repo.createAugmentation({ + projectPath: `${tempDir}-other-project`, + jobType: 'executor', + promptText: 'Do not leak across project scopes.', + status: 'active', + }); + + const response = await request(app) + .patch(`/api/feedback/augmentations/${otherProjectAugmentation.id}`) + .send({ action: 'disable' }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Augmentation not found'); + expect(repo.listAugmentations({ projectPath: `${tempDir}-other-project` })[0].status).toBe( + 'active', + ); + }); + + it('should return stored patterns with aggregated top failure signatures', async () => { + const repo = getRepositories().sessionOutcomes; + const now = Date.now(); + + repo.upsertPattern({ + projectPath: tempDir, + patternKey: 'executor:tests', + jobType: 'executor', + category: 'tests', + title: 'Repeated test failures', + description: 'Executor runs repeatedly fail in vitest.', + sampleCount: 4, + confidence: 0.9, + status: 'active', + firstSeenAt: now - 10_000, + lastSeenAt: now, + }); + repo.upsertPattern({ + projectPath: tempDir, + patternKey: 'reviewer:lint', + jobType: 'reviewer', + category: 'lint', + title: 'Lint regressions', + description: 'Reviewer fixes repeatedly trigger lint failures.', + sampleCount: 2, + confidence: 0.75, + status: 'observing', + firstSeenAt: now - 20_000, + lastSeenAt: now - 1_000, + }); + + for (let i = 0; i < 3; i += 1) { + repo.insertOutcome({ + projectPath: tempDir, + jobType: 'executor', + providerKey: 'codex', + startedAt: now - 30_000 + i, + finishedAt: now - 20_000 + i, + durationSeconds: 10, + outcome: 'failure', + failureCategory: 'tests', + failureSignature: 'vitest failed', + }); + } + repo.insertOutcome({ + projectPath: tempDir, + jobType: 'reviewer', + providerKey: 'claude', + startedAt: now - 15_000, + finishedAt: now - 10_000, + durationSeconds: 5, + outcome: 'failure', + failureCategory: 'lint', + failureSignature: 'eslint failed', + }); + + const response = await request(app).get('/api/feedback/patterns'); + + expect(response.status).toBe(200); + expect(response.body.patterns.map((pattern: { title: string }) => pattern.title)).toEqual([ + 'Repeated test failures', + 'Lint regressions', + ]); + expect(response.body.topFailurePatterns[0]).toMatchObject({ + jobType: 'executor', + providerKey: 'codex', + category: 'tests', + signature: 'vitest failed', + sampleCount: 3, + }); + }); +}); diff --git a/qa-artifacts/qa-feedback-dashboard.png b/qa-artifacts/qa-feedback-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..5acecce9a7b57fac4e62830bd03ffad66ecdd793 GIT binary patch literal 98252 zcmXt91z1z<+XqBR1xaZo1*Ai|r9F%zf64KJmXauAi9Njg#VbUOsk%JNA+xx!X zf7i8Z&z^J6bMEup_qqEQ^-*2nIW8qG8XDSjB}F++G_b76Pn3h*&K;T0wgES_idM2Z!F1j#}b`FH^wlCNd z9ECX}M3nT3h!8#NIsVM|d(oeTDJzpN{vie}XrV9BaJc^) z#$qdmqKHL(%;lso@c)}+z#uaJZ}jTr540!$4!ggHV!rxsQXZ4B`@a!4JvMeXVfXC# zze||6zCnNX&wC{5LWGQq^56^Zz7-JtN7Ki~jMT zLjT{zDC*GT%zFrXnZ*BD>-5=vG-1%&{~Zg(*;S!h(Idk28Pmpg>EnOo6%&PiB#ADsoftHv1OV zr{Zg(DCk2`>-$?O%DBNv^>uEW-hZ}V=9BNnQPx>RaI@Vb#;=CiJOT#dXV|={n<g z9{989VOt7D27Vw4Es!J7xXgM9@*tt3Q<)X4YW^lgemncNZ9OJO@_VI|efjY{e-Cw& zc}k8PJ7yie3VyR7sHrL0`L~_8+owhihH9di0shYmhh8)^xqV5`_%Q6Lah$)2vh-5u zV8>ia#8A#l}8<&#QN&{%w%m^TS*YSCNw^=DsaW#%C{NN zwiWnzi{tkrY{hTr7%Ge;1B3lj&`EP{df3xf_yJ*s;W@wJoE=@up7t^p-he#aRDv6R zrIRamQL&8XQ*ikn=0O8q4vCt44YfX)?wa>F%fCu(n3?Dg>E(N8U;E?bh?1Qw2K5O2 zEsP;mQPqYRMIH2A^M#?w~IQi-l8Zo*j1;WA~f)U#UrP+)>iEJ&F-rtKq~l%mnm z)z4CXQyektIx=Z6x#Hw9cK$)8_okz(MX$1|s#5%9F^sm$1Y4O_yuGNXW{iG~f~{}A zJ+|m(aeDpg#1H3t_m!po@9#z59v$MjKD2jGoS;Qfzj_^7SV%%&r&vRfs*;2DIc^VR z5tTt(s#;ng)PrYmsaU01d2kZ$!?c2_pV6dw!dJFb-qVX)Xf1xH@SG~A;np>^j+ zeXt0ZDVROPLBsssV_rOvcUh>}Pl|D;{F<9Ya?v4)Lk;FIg|$5_HIi%285h zGyC`Mi-R!lPP|sCil_LpLrR2k9eKeBs=h(-`uzi!=#yWDhDMRUsD-wO%^QWNS5B{S z!s;mS2xxBZtl5EH@K3DCc>5t-_H)Iz?OIsZ4{9Z}$=NLu9KrG0g~q>+$-Tn1Ke#qG z>8R!f$OiQa-PRr7`DBPEGFEJN-hAKiV(lJ;N4vIg+dB3MRGT}a0XZD?zfymj( zN!8O6kAg^ADRi4i9%o={(C~yrSVT~|Kzm*{b=Jk&>z6W(mY!!~)BZJm9V0VmkA+}M zi=Zg%I+?Z5VmsL`a>Bvh-i4p)`F#yb)3xi*s%C@n#-W2o{32<{ivExWlU_wpN<~_! zeD=VnP)?{YUCi|Gb^5TCdsNZ$fM6pn(2>Y2X?&Cli3+3IRr)))reC>C5BSSsfj!L5 zm5I=8eZP~N5aY{RCg(kc$Gge=pDDYRBeqR$un+)iifc-u{ zFARZA$7wP*J3816^(!5be!@{m_)YK1(RDJ|V*0ROzgrnXPL+N(u?d*Q_kWn@dc1&n zn(PZ&?)louOtN%?(vRTtsPl-3UamEm1^yi~;f+gcIKw?bQsgGu^AX0R>gy9>BoMxO z9dJO*Bge>y%Oh&2*7a=m7XvgP!rSzP>9H&N6f#td2t zl^+_tdeZ_7=3;mXy{PKa4^|osvL6^DxX@3^s55MO*D}_$4Ym+1`tLhk?@T*d*+dN2 zHzY{XNxvqcC1j@nMZFh{xE@h#<1lvr(EBrm0CL|^+En6(JfAK1enGoV9-7VA!$vKUH+qdHx{(1_wvH`bi8B29Wl8+RS3$HVU0TxIZ zjqtXvjTOh}sOkA_r6VP!I#2|;(I?5Vy|3#LKpZ^ne$H&Rn_~pqZ4Va6kBOPb$=)O1Dlh?~fS31~JJ0^)8IBP3BGd*i$*}w|A+ANt3C&L=oUG zZL)%O=|{M&=cb+X*^3_L)ns12!fyCVCQcz%dY!zg~>PD8`#rumLm7ghZG? z)un8lJehDM^^szn)1N$t=@pza5PHGS{1YKyQaW~#RxM6FPGkRp-d(-lk1KeS!c%fB zpeFaTzm?2t**ae4k@G88QydYwf`Lj4HzOU*L337IpVh%RQVkGi;BON^xd>pPcV`Y? z0^Mg)VRog_?kd6nk4qGAYjmqHOr36I@+m7TF*zyIM9WlpC@M;$a2d9&fC#dM| zbNGE{?k2tvBmv%s!jx=Vkt={@r!^?2X1OwB)ZaknNPEfGF%!<#8`mx1Emi@nNl&g(z$qV^(&mxF97D{V`o^m|jdy!~Z zWF*Xu9xDB3Qts0u#p|hK9skIS&nw2u<>KC6{_2=@HLubGaKCy&GdR_&G-u*j3nP#-(|wl~-M@9PglX+{jLbFb zA7rtP18)=mnTvJ0MNV`=gC_Q?fSn9_Uch*kc-o?m9)RP$2$6=Dac`f|MSp$Z7qomc zfltmEl5+BtSV66r{PExfJZPA!hi)(JBD=0+D0jS#4^DS7@EtJQ+Nx*SseXX0zP}JZ z#FzBDhR4UZhpr%1DT9klmEPvz{iJQA3uck2KAsweHePDz$+cJk`ybVu5gB0^MPATz;jsuQh3`2muBP6tsAK8qrA_-^jjG>SaOD7t1k1Hvtov>nI7B?i5Ws)uS8_FM4$C&k4e{Po(Z z@|Bq)P{olDe-YyARQnh2l1~X_J*3X@(9jt26zL5BeKcMECnZ41t1{Bcn&Oeo)aaiC zT7%I@eL@`oZ#Iy z&5iHE*(A}TFM1tMeEGpmOlIWa?;;Wp!*1F+nLp-D7$!z0JUoE3xTbj>JyVp91NrHA z7?ze+mB`M@N6sXf-VSK1-_l6EqWEbPtLtRdE`MSQzyjUBa?#Ae%n^ zROZW{rDx#&n$Pj9B+Iqg8G-og4S1X5S)(jqv$|k*%0L42lVo=s(FieBQ~79iyE?wG z5Dk1N%;Sohm{_g#Lexy=E#GfGo-o1XS20d}kDl4cDj3E>T} z)X_NzZD5}h#hjY;{hG~0rv|f@^Xhr)$l9`}-*solz6YdutO?L0*&f_v3USeObQH$H z0!0HT1PwUsju|5oQy$LoZA$BA1q}y`oeo#vpRn<{@iApyircCvQWkxzSTUrca&XK# z!npV;hW0A-rufsf_>I1?%*?>~Oi?79W(|}J4`SqOzRqaeXK7d80*0KcO1#urW|w5) zW4C17zG^x zDU{{B6PemrDL$Gjwb@Ha&Dn|6)ibTtAID2K^S>XaHS`npzs+gSVpDi(nRb_Ft4>{K z_)8BG0BJP9OQ1cN{4xdx7983IEQI;1EQZ|dspvMaiZj?Q^rlAoge)&CSk+}gQ>!}w z<>E$?(~W}XlJJAZy6Cs_Es@uAW?P2oCciyoQAUp+z1rP?qCoMtIbwGnK9{xs3nNZ9 zKN9@}_e5hEQFXa{e{)z0yPzRw_vyOdI!1*FtA?CHj@XV(>4Kf^O-DOm34zgVCou5g zJ`=*+4li%5y`TeR3kO`z?Uiaa3>=x11MX&bHpC%-Yuy%!`!%u2H>~!W+S-K;s;$=^ zP_wEjXr=$X&t^~4jh@t!U0b&W6AiatTVDeT#cs?((FngB++N=Nd<~UT4q- zJ`0l@pAa?*5l2NO-q&0}oK=_-?f64CCm1V_weNpOCYjoDY?{#{R^c(J@>n%grM^Opw1gV_tTKU^2RFe&>bE zWCbspY7UErz~Z0PY^CNV+Jp75zS zc&(OwI21!Z8JC^Hl{OmNuI^$@Rdl3^Y$k_86Jt{JjI7!iHfI&fM;MON7c5rpRY-K!62|eYL zzD0~$Ge7w^+}%^(=>%+0UXUZ3vTqPPk8fS=Rj!C<&-s93%_{QDNU(AM;vZe2S*QJ( z=WpDz;M(^)KM#ZgB1V9FS6@!_1~qaGwA@Y0oMGbp62fY1sb&>I`^OB4qr4s$VBmde z0vS8n*JAU@`(N(Fo%cJz5_dEfL1#4|84I~^qEvopY$ptx_&)kTaMsoVXxC3V#OQ=R zRS`g6mnVx=e*dVc`BFeh{9Kj=R&>3mXlw#7n+SXps4FaNq~8tMam zs_bM+pfxetx0|h0c(4sX`<|Ap|5R&z@94Lt#ge_HlHoh02H0Sw;X#bBt`a9g5Gy=o zLa8vkTGblw+V3-)@~FDHwYsioUVF+~_c+UsF1((Ng@GQ65*y5o*S_jKDer$o zARk+u9Prh?W%`Kw{@5|SFR?ftM86jdnEM(3!t6*EH>@{RZ+&55#_RNG!sQzyFQUTn zq}8wbraw`<-pmqDSEtdmOy_M;TzkAHOTw%BYT#ky*Kl^OlpH(9<1)}-Yw^z9 zan#T?VAp*pNi^70rLQC+3wJRu8F%l!z=EK*$!j!tAw`MiC$mMVudw#jlgtLULk+st z*=6IGW*EQO1-zS$_XN-1fIho>H;LO{f-+fZbnld2%Z1IRk&XAJMX9r826k4CfV!8$Ay&St!OJ!XQeT zHKKA5&(oZ3lrE3Acdhn4f~@JMr{u_^MXY7aHSP@t*uqt)ePfdL4aL?nMy{=%1P|!b z@3AosMVAV#bkO%tzaSfH3H@+frJpz_#_U@D+8LM65+s#+VE0_5g1%fUV~J;N7daQy zQ;!wy>zwj)$g3%8!0r9>RihD$_Psu4UShj4eUe{)*BJeD^1n@hpCwJ@XY1hwQj(+0N_f_4l(Vk&a#r5ll-|=!+oU zMrkjs7(+I+T@NeZs=m}-FT8$C=a;GYO1Skay(X-%+;)T#FdeFqrknFp#eqV?n)hz& zwwa?!c*R~4M=WJD*4f5rw8LK#o_t9M9*ERbx6R%0C>oboNWYE&v@zkfQH;t(q5Sab z>+ALn0x{?3i`9RpHJtgX33qBpkAo)$o1jjTVL*uE-JgB30Bx?{(r$*v& zHK$I_HrPTTVS@I(b=tgzy{o;lI(RZiXhH&)&;537+Ma^H%-OkG1)L|uCLROSC8f#E z6xCtV^KxiSdnDH48`Q|OZ!$c{7&$Z`E}d}C`;{>AiX~McQ_UvpSppci9$1yaQ|D#n zT)EFLA;PINyQU}>^zb4}rE;4HRiKDgQ!FOWb1=yRk6J?8=+%7F=jUQOKdPHGej5ux z;PXzk#%5WU^oCWlyy6KG=2ppJ>R!8>+m%WF5_IKyqG~gWTM$g$>CAGs1h-H4?dVu- z-mrIm4e)2fvQcd0PUTd8i*1ocS?c?b1|IWB2r~ zZ}<52P}M|~b!&x^agn@UQeRz=#u8cg4Y3OEw0TMXfzqTR%gn*HgkIjdD^-!+Nh&&?Tc{AQNe)nXpEanrdnhU@D8h5=;RRm(Xhw@nCJedDbVSHRAtK`1>k7`+l)DJIFs*EkO!~5q+(w*n9-5c(7 z@%rYaR?O^)h2`?5jB&UvEAIWTH%Hb|V1}9>fmX~^OiD)%o0(i#%t6An-KGp8ft1AA zs*^O0^Lk5v$CfV3Rqypn5%fnwBXx_pdqb`@3}+N_!dZ5X;w^X+Ox?QGIv6cFxaMz8 zhm}G<_e=4vm9hFKtCInl2@7A*Rr1$fbG+f>%K-i!I);TfQ5o=lxi=Sfw9edbz7%lx z#%r^Q;A^pBDdVPjR^934!4Dv$*KlP9bfw7I{AsG82FLRd-DD%Z0@Rt z+a(nnvV21t3*HXn4+6Ev(Kf~V8X*jEn({+T%yA>buO!=UWt2>IpN6v;zD=6ixEx4O z8uKX9o~Ci0$l7}rF2c+stn-?1AVFC$yMrp%M}knUT6@;3rKxkFj-;XdQu|s$VezoF zy{sjHD@hSmLCexJ8qaeTe2F^{{kExjz;lz^nnD9wt3mtL`HQN(xw9hL$Qds^ zS8g%n+(jxWIr5?9kwLm|#g(0mTeEZB26^AJyvJvT7jWyqX-2A)c_sPM;Mi#C(z_SR zWniKlO0ykPyoQjmWe1)I*pZ983SV;hfhH?Qh5nwe$jmmV-@iUg5U4{a((LPyY7Ef9M#5A#i1d2Cw2xIB~7m~ z?o9EyhkZ*RpgECH7|G7K%N1?ge*%=yI{yoRc;)UaL&V<@MH#Ld0u4xq0G=Hj6UV&e^otKC%-7>dqY>-)4yg@`wvI2`(7E zBU3YCR~rrle`HLX`pT8v!a^i$I}Q^M$W>a^XWuSyu=bLnS39rL1t0RL2|ha|c^it( zZd}XA@*Y+N=vo@e-ABc*vjl=0Ui-m(d+&q-+_=uZ(M9TI&!YnHs|EQt$-ThZAGEvO z#yDIzR8>_R zE4ePZ&d&wXYm9VEO*c6#y-Mp{JFiY!Sk&8}rz-XnAWtP$-VouK5dU$kZcBn`>>p>L z&0_qele|ZR-gtQetur1te7ntTGu%W&1}zxYKs6!9XUnj07WRu__Kvn~u=9m}OgI9D zM6w+5Cz*w)hSZPxJiI$u@^1W!Xt~O&kCN)Gd)%MqEewhjnQEvwqO68mj^LQX(c`n{ zdW{DHEUxkcZOIYU^vlhI?RCP*cDKrRQ&#n7Gap*WDz@a~UVfKjfd^YR?Ppr@4hE`* zUeT=WX!@tr9mQK`%9O*7PwS?Jt+*A7c@w@_Y{@qTwxee##N-Mka!{pxaVWWp&`e+>IwJDf5JC=6|^WauA;4!JY=Od2Snza?amY z2iK#Jb}o+kiCiXamZWpyV<3!wh21t882nm^b|`ja7nm+LsA{$FAjg=NR9+eWD>Vp_ z6V7A`Bv|VaJi~*6EWT7c6)z+*UweiRn*7P|mflVY1_TBMJP5qM=9zpV8F1O|C`iq5 zyD)UTNWLtb8N{X%lOM=ZxY`VzI=b>`3))zdSwvp!i>xq@jE@hGj^|s?-4B(hke}0t z`amhhJ^H?R0<8dZ96W~KwdF1)_%YYR5jRHFoplQ#&Nr2OatDqvFUJdbJ)n^FUZ8Mr z)UN|J@3MjEMdZ3SA>X6@b=xn57KAt;ufk08&9zqTFBPR1J{xGVJx+qq+xv&uGk^ct zy`PrbQ6R$7y3V~L4M?1tVpf}Z_(w)It}}apBM*hWw4=>QV|2sN6%D0*iigvrl}Y4a zeo-dd?2McJ@y!V7zWVgE1(Aa7p1OFjRn4*$5jRGnstxq`$A|JQg}8#?Y{>w@$;Av1>Fj5UjMnD)0Uh!S!j2@!OQYB$}v$J3&Sk|1N+_8*H6fQ z?WjveU(c}L#8XZWbT}8)hdoWln}qJ4FHLV}OmDX)ZARc7@7cbyj&u&fo5A1QpDLnm zozNXf#W#BBIM^dkLF*ePV@1@?qnZ&WjuxtaZ?;!L9w3n$@99P$KxW7pg8WIjBYE{r_Yr>`j&i9Q zY$%L_<}wcC{{o~r`a3W~4pWzv$r~;!B>V9hK6n3t@WH^vz$FuzN@1bpN_Mt>n&@+9 zS9_9Zrl+3i(|u~Y{y@qkA>S?TMeK#~rx;(RY#QthDJN+qhspn>0EKxz-{yRM%8uj9 z>0HSv>BxU@P}z2(iF7)trT|qJf!f|E(;T@^k}|O{t7h|go*AwEwQXV++=FszN6M(b_ZIL6&Fz8=HMV92fBSQ()1 zRL%RrDl2ixXuQy#6QalAXO05(4{{ zm9+vuZiBt1zA`z!pcUB?;ZQRJ++`2d=(j|Z3DAjMPOv}vKO}2(-N3WOlP{Rh8JvQ) z%`UGFEW1LGV6Jxc)<9TD78+i7FY1L1x;(R4u{|q%%0{YJ2OX<2t1|Ro?tSW+i*vfR z+;9+Re;7W)TfGa$+4ju4o#4VP)u0g%(ALtrsB_imd^o}4?}G)lwXA#eL0dM2tj`(4 zWkNbHd*gEhAJ1FTD=I4Q9z?8OEoGf4XmB7oa{+jTh z?U@rzcwJ*_>!xP7)Z2QN!gV~hzy5c1^A4;?n;JI34F{g1M(CtrVs+8<01fhXcEj<5 zf=fk}I%V=n8ro1lll@ATiQddSsxqeLEwB-Rfl2My@Ni8BpqJjf{xomSU6C?sGv-2d z_q{u>>72vxs-14>;^GULDy_t8)xT^d_97#{eX+0uY%bAdAs5QAvcRkYt{C>(iErKuAt?0n5XtJ4t@e`0&rlcxIy7j7s2<;-J2>thq=i>y-tkG|>ItP9u18**UOnTXhq zFS}Za359B$?T&ifg-Ez9iq2k5O44Ph#5ng0S8am^Z>kdQOJXkkurSA-?Ojsha7%^# ztf~TB1L$VsVIlhqh>N{TQDl)Ma{A{drAEuK^i{oIZ2^17C5>)ZrBu~tLLLDDD`7ao zzH3*YUuUys4=|(tyyW{och{=fZKr>aT(^hX5Y2&d=a?;9W)%lZEfW*QCH4NEwr9a) zOM0-??Xnkin?!$NW9tkuDZ(o8Kq+*aM>HLWcMJp@HQH=38o-fUv6x8%iey8t*bdR7 zG884T%PL}=*YT?^?R$UO?T2iEt|o2pEY{imha*VMY{K@Swd73^?CQ>Yc*^r+1R?Y1 z1%ofz_r&BxR(bUr|5>^M1#21-v?o$h)93p|9bNf{Eso&kL6NMuKx)X(#o&euPl%(z z6U-+QA%M%JWXXX07J`TSa=M4(k(4?ED{P9`weX4<8kh*LPGH*D3UKXcJhH=MyV^Lt zb+_9I>{~h{x}5=U%@lR!d-b)WEv_5$6^j;O=8nfPEmFAXP`_5f&)f?)Ie;)iq=+cs3EA zYaFBdboN3=IrZY%pKp;}%b>unk$!O}%lXkUMLmXj-O4MJ=9!ImbZHIt`Wxn0-IZJO z!kL>=5P)QYeosgk8>K#Sm-gM5Y(n=rvtUw7=-Q||QXOf7iJzYCfjf&c8>(zQzkWlA zd>Is>?AgNmawNJW^fWPMM3I4fmTPj(z9R;JD(0f0byrsu9*>V$+FLeg&hXg}m5h4u z5GNptc8uA?cLY1-QgE5U9l>F_)?I6w)c#y!@OS9VzIfET-OYKsgK=p| zY(0x!(eab&M5?mx1aA4tmFO6D-`RAYvBJfxIuzfw>l@roB><3v0JAJ!{~0dY$1_*J zHI}OpotoUx@&$;)s7tV2Tk`I*fn&Bz4Jsa@qQVXgKc7s=0gsLr4>a_@M|nvgc-ECn zf!k(pn@z)qf{#;qWdcwxJqLM*2CQo;o zTE9G&Z5|n1dhCi3;o2MK*G0E<7G&;v{e&;?1WUr)FFp@#>glvhOg`zGKGhtCdHn??x9i?MSa$A3jU>c($&k6eJ|FIgh=FWRAa9UX(%b}4HYAtA?dV14tGEOz96k8OFGMQAJMFfsUx&(2fWd0jodHLegZx$)X}6B%h*vjyBHW zzI;WlgiE$VE&Jmwcepsrqc1)*3fy~Rm!6mh>rwFhkDA@WAr47&7i1E!sv zoXimqdJEJLQw=4~_RDU4UC0n2I~UJ|!`+S})eN zomAnGAsa-?(R@PCs9rfgDbWXHMu7E##e*j*aa9hZv?7iy_)&ds~}cfwk>o_ zvh=oPdE?|i`~uFdi~WOgX59CmSSQuB$GuXXtimwiH$mnKgJbKSN4hVL;gfT(;>qg3 z(^T^yXK zzpA-lr=+DxUffBFBl@@B8@CkHO?xKO8dkyY5%uod^?EhN@+xY%q!Bg0EuQ1Vqk_>H zOd(gzYHd^-ukTZ>z-!tmJAQ|z+jt|_LOT2FJn=jGp?ieq4aO!GZO~;=g5U*1O{0)fPGktVrk@!7Ni_|ehO!l%uIf<#3uH9tpiwTh6_}zAsw>Vv^t#B9vRDA_DR2GV*boi{11ZX23?{hTm**n1qWR!iTPp-TaB)6RFP4#oLH+N58iNy@OBa2By zdxA+ev3!8@jNJdAbueou!%{t^u;+bRfo0{U#!br33rd#9UqI4_f2jyikQ=ZJ$oPw}um#^k+_SYLg z;#Ze+>F-QG3RQ+O11~=u`z|4&1AvD=A&+^_yzXyU13rA@On>}Ko-ngml<;tikOVCS zHdYz=W~ee}h0nMcsvDWC2NRE-h~FVd^L?PpL%!4^mlGu_P5hFNt3beGuBb}^R`z{) zvI$dpsp4T4~J=4V(|(bNy;9 zqE(Xln4lpJxNqqODedwaeq+_0<# z6wMx{advHulp$eLn?;ccB+j=x7v4v%Vp<+)H|0%DSCIq{v7HxmW$Ky7?)+IMbcnrI z5RfnA&7$;b=gQ$R(qc?-|L?m0^%JZYw4103vghDsSJ&lV;;x5a$XJQigxT*s$r|V5 zgM*Ki^Fi%t79Vl5V*s=h&Q60)*+RTRsQ_J#h1Gt7Rb5l!mBTc-P?Ck#;#7o&;m3>S2`Z#W&(BXIM485w(AQ|6RjM>8pGQR8 z8WMevQDrO9TzzR{glU$gEb74mX5-+v4wk9Y7qc8}+}qo`*ys8Y8XXsxD;_vllC?t2 z5)*932?YWrt;aJyK|%s6>g%J!NQVaWB3jeW+6NeCw}FIv@7BGc`wPV7TvD}+{X={h zgXY|Y)UV9B52Tfulb4*{U1P>^Zx1!Y+!cBPD5VQN$|+PL7<|8;p649ED*7pcaB91e zcIMK0!rYdyzU##dwn;m)s?U;}wkj$bxg5>#KuW@&7h$1H-IhZ(6caY>flR1pVor|! z(EE2Z#tH@4&@*k@+vtGB#>bm>I(+Nt77+fCBNhOTM}>ET3kwS+*6PgMzl>-8oB-%| zGNqiroW-4J(xsZLhvgN^PR)k)n{m%GxkF(GFO^@LhsmIWn$@)gs+yC-C?1=AUGK0G zgliQUHRV5yka{5Ba$3)9g#}+)qr3p4Pskz$Q@zdHhp3Ek*g}4Od zb`~|vBHk*SO=4nrHxt&%*D`9BH)|=N^-_onf0Sn(9?t*4u3N1y@5_V=_SY^C$=%)S z9Cc0PV~h^-$x#j(M&CqRdJCNmbpp1J1anjv`$ARnU6x`9`d8ITX#NKhkG7n=D&-O) zr2jEm4>?Y{;eh}{%z*L*f7Wfp=T*IsN1PykpDa)6+lI!bmf3S!Ch;$6?6Jia3+L+2 zL!uzE_2A+%y+2+e!M3(#j(0(_nGZ=)sc2fnENbPIt|*~#x%^q2G+Xvclvk+0>}ddW zB^4Fr!|LmK5eZ&gKIe0X=|}_+&~^Ll3n5u&$@{m5D3H)AwkKjnrzM0-+jc}z`pn5H z{TA|4F%$MZ!oF1#gENn<6)emRN;U5BrYzg+qS5R#$9K{-2pF_Kj7YTwlOhXfGr%)-X|eBabU-KlWoDzN=tC2<`r zu5~>U{sW2$9p*Wuo-pFD8=O@QF6V_hvr1F8nf(u7^Ziv1Tft@86VbH4vdq-D11kT6 z2hq!-)kD6d9zA1idw%0IjsF_86bkBuiG90X{?ZNb`u`C_p|yk01n-_Fmb!hdqb$&T zD)NR$k4;t0Ba`|je0Q(0LJmL2C64#T1 zS9WHU|DKz#p_p~gcdwJ5JT#ua49l*mLbYDBU0y95Rc>z$C8+1A-@kX=$U$XA+Tfpt zNgFkEb(x`LxZy49KC^SI$#=QnB&HaT`i3Yl^RAkg(aBm{!#)^zv=SN5ZS6rzK@HIR z5AtMg9#Kc{bxOuzCFene9Tin1A|e`0Cn;*XyXH@zmglkE>h8F6-Wt}AL(F3G!7S52 zMdxqIbkT+9VAbSk=eUDFOS9>zdtMFluEeo=d1`stf2*IhQ^R2;;}N-U);>8Y8yR0* z9P7X73T7`PS#1HVUk6Ft|8b5PXa_t>w;)^AJo@I!lQr_*`5Xb(oAe&H_9fT5VCKQH zPab}1RwA!Avh|ER;Q(0h&&4BqU{tb)8DbH#Dvi~ZAJT-%9! z*hW1>V(-6A{4+wR6GFR1EwmIvl@}PA3>C`(vrgP=&_J|6Ra8&a< z=y2u(a|0sR7t(bW;dpdJnHSh8DsJkF$Vwec&zwlRmc04CR}Da1(DCx|**(681YL!b zn|e&xB+!U%tjweVVLy#cK5|$6zAyav_HBlQ^F@lv%2Q ze#)T~5$vx^mR%u5230n;CD7-MpO_3ki*jn+CYL_USTd_e}2frDhmLyliW2v zK28|Z54bz>4Ck|rpHTFSZwo@qWV`&HwBzM47J5YN15vrVxkEmtc6FLynn$>mkx}+) zPC{^C(8^qS&~@Yez30A)it?ne)>gR|3#vz)lLQCi_W*%(z+93?NQj9q#`!cP&a#2C z1)%-=KDzbCAxW`ZNpA-Qh5hR;04xypTbj?-m0kYdr!lvph-j{9^(j!Q#WxW4{3-T~ z^`o4{bY)Fvr`_~d;QdX3Y-Hz|;7SJ12&%Uhf%3)aZhB-dw{@th{kEcLnFgz>oLe>m z$m>{gzz=83!VP$4AqWWnesM7JbV%|lZ!^&~I*r3fOxSfM9FLTkId=7YS2ub3x5YsH zL55wEa%xIStNV6`X8C2V@1yVEokQ0IV1DvF@>*je$3RMSZSPlXbUTXmort{yUl>y2 zK?r2NvhKSs>~pN&u1u?UysZW#`u9mfU*pMtIwjPrqebfGKT&y2=;h@jaWw~W}udI{|XZdgo_3Hl640(_`)P^B^(#<4bR_^;MzNY zY*GJPiZh|8I+?*`=L1#~B{bW$Q}*JsVfWpVrDUFH$4BPmtGxMNy%e8NEwvK&VUK>l z4p#5z{7zS069}9J{-x`6J^n>e0?J_6eaT8Nqp!k5m5}ryj}QglHTNULMG39K4@IWS zb*p2Sf)=eBPnsT^hBKP`-MElP?$lHrQd06V_1vVSERVyP5PI!pt6Z@*P%$&wSy zyr-U#^1klpfl+UhW0Pu??!%wRgtdSpldhZjWN+^)+I`QHW8qr~=KzVvPbIqRfiPb_ zzM=o;3}N3Tf9T5y&3@+YmH7DmygL=-94l>P9-Bn)x%Qd3FJOPM3yGldT|2+O=z>l8 zT#ABA2TP}m6sE-%wk^|M&>@-qFMs|Tk434})8H30VmWzv>VdSvz3Xvnb91T?+wqL* z*l0t*?k;;87wYwGumuAyCZqA2FMWs9A3W2_ovR|d% z=*h7bQ9@f*W~jaQbIhlYCv`oa25k{1jclDb5_ad?k8I-?tz4{?WO?&}yy0=zMtS)-bY(GM)hzxqQQwVbAL!ry;xNfaM7UbE zAn~iSl+mB1gAt)AXevxZ4a@TMxE@YuXxM7Q>TkLuggT%aKk6Z&HPU}V$eMxRCuErk z(&q!^TeH`ar?1h`B>!Cg1490azW?gwYd=80)TI9ob2n-R^#R3YY(akfJ|WymmHB@> zbl=nCJ`sd5HSMP;9 zae(!Z0?^L?pwkhwqak->bI0$kI@yn|L!~*Nqq5;U<_3-V*kAaES-w0c+M>X~JHt<8 zq~8)Ic^@50`j`|N)Rm8vdvkGP=JRvgzZQqv#3Y0Y)mV-3rQ0T^wk)rASjI9#o);qi zJ2I_vQw}#GqD2w!c;Sm*L;>onNQ6A2K3sN1i_PCM(?}e=!kJn2>sAhZ{_LARBX5=} z=4+>@0eZ?2#>+pkxH8d3Qn!IYGGeld(qgiVILUr5YL0#N7X-sX3jdHj!i8I;2OC;4 zv3SwbMt2lL-~5V_<40de{_*944I6J#3VJsj<`+>L`p;bh&ZrBi)c-UI#(4C4*sG+$7bKyB0Iz1#QtX& zFt;*kq?cj&v8Tt2EZ@Exxm0vBcBhP3IoP|z$LkV@o}w(=`pl9Q?TI12P}5hpeagP? zw%ESkZND@2k$?-bZ;5Ig2P16ZR5u6$R8y@Pg03$<@yQRgivjCPP+ME#(yuz6X$9F9%R&CNpFnfO>ahSL2~6=;c=C^|69Ylx5ZVEI({9+|Jfk|fVxXN3PF zr)40J8Y9ubz|i!)^lNjs?$D3&7p77A@6yNS98ya`Rd7uK_qKf5XKHMT855n&G4UKv z>7yLQwijOgN24q$__LdCAA!pj_u=iqU~C&=onDGm-1ZV4~EY z_bf;g&U6KZA>|U@C~f77&lrSBsN~AXu)V9Rz(HGc4TjNur6!xVNDP-H{av`?jA&jW zpVSSCos2jkVv!RFe3T^$ofHqX41Ael1c;ad7 z1AkG{!~HUQTiY!nn3Wj#&Uam%p7U-%5F1H3$2_GVhP6eF5^m(b1Wowam~8YtIjZw~ zh3Rmp3TU7-2&B?DJ4WJRwuaO@{-wfQRFGw@?3>^EA^2{S^5B@fOpDtOR5ZM(_T&t| z0P7#|DBcn_md*0M^IgK^zI#6U^{d(a7-m4kMlDDZbN87^i!sKB9-ns}e|`{2lP=W; zOMeu8;#8X*>l%)ak`lIlKDng-W9lt{;%b7mVS)t-1PK;`Yk~%M2)?+x+X9OO7WZJm z-DL^x?(PtDad!>w9^l)&_ujw$t(vN>m7Y0sdb;~`Kiy3c1vFf~c_%NH@L2PPuou|d zivDqDbd~n~j#aa)fTzJwwq>@p8+&BTsACLj?*_}e%{H`PEkmG=&_L}Sn6Z!j-PeC) zHu}MY1h*|FnfBd=qvV6<+E`g`eaL1XI+Ka2(E0kXp$0!CeQ?Jpw9mt)j0oS6x$^NK z1pK(T2y^`9V)B1$$qofJX#O-*4N8+P5B*qH$@yuqa`553P(=H8-?#tvR7HS);DQ$d;0ffA6*XrXz!)3(#)jTYCd!LBgvE$LX8uvME{HMjR`7zbbU2f`j~ZnsvLgp(A1Gy z7|Ce&5Z)G1Fx(aq<&ens;@Qp|Db%Eo9WikX8wxYr%-GUeD^?L{^i5#(C)n5m&j-Su zrOi}6=Q{C$&R!6{yqtVRZU0513G)nK3m#7jBCQ%$xge5H5b}ErQ0<_|NroE$9Wl5( z8$0>G&YfJCh5!ja{yV!J{a0`n%cj`3ZunFXiiMS}MN3wgpV=dEkas28y*Y!cwQ+3p zP8|M|Nz6N`s4ki=!2tr(r@c+khL{#uxT=?xmFO&O?d=rwc15GD&)1j8R6Gr*nz+2R5feM04UjsY;;;(o3P z@ErYN5R%A{hnyR05JCed7F7x{tNrppr6syXYsG&exjl&r@_+yn-oXU0tpaEYj99vo zZ)u0YX50zcr)O5KPhnyf`zsqOu!bCg$df?v7SsL~ap7U24Q@q~Ha6^oGF!Vr^pX^7 zgysFDDuYK<|GMWK_*bQ(`LDOsee8koFebxEcC&bn0485F{o57e1>P-Qneme>Prd02 z=Y6?0>MnS;p1@g`(cr~LQN#OA+SMf6aXS{4c|c2OUk`o$s8QD>N-!CQXh63#f(pGO zf5h#~45oxoIGtaVsY!&=AlNi7ni57g;TRdX;g{!#@QhwF zzPU|)+A9f(n?g6j4-bM1gnK!Fr_Am$J0yv3chxw(kb_phNb&t6*RrMkM^^y1F(yi7 z7&Zmprwc}$)Ck(-HJ>~QFN!Kq!avHS6_(%JvUQo6-f*SDRFPTAm_@8s-@Pv(O#Ptz zV`(CB0e0B=;N*bxn^gZAylQNt0SnQ%Q*peZ{r!U?uQ`ZrMEHjvTp*5z@|<C{Uca$^cJ?0F@$1(uL1=D$Vx#@v12pgDZYiz;II zPc#1;2P{0#zxQL{_yYl$5o&@F4=w;aUlWw~LVfH`9IvgJ-Vim;mrB&>fo47LuqcJenaKUWa~NyTXbc??Mhf3gOD|7SnI zNc`@-eW>=8_7g;TScN0z>Ys&!JFPnN|M%6uQPqoSR3)9ShU3TjA+(&MO6V2=g%?6> zay>&pfsy_GpZ>gfHxVzrJo&Aw_51Id2?N6rn2h+{Ro%`9Cz^Vt7uK(U+Cv6AIH<5O zD-!05S|W1CAeYuJ8ifd#J+O^ov7r4e# zJw`K21iiGMHy)-{75FYljz?u91X18?uMCnn{f70)JBz0RY;&qU8`DuP6HF(!2S+?i z3JeZ8L=#Cyx`+_$5ES+hKMU88Zi=pxch7TseH$io4Gln9NPFx!N;0Q(uq(&ihNKm4$Ct7Z1+k_Jn2zXo9 z#x3)Kiu_wBFikC-Fm!xLVI&aXM*-Nrjb{FB*vB3IM+mb&q6l1#p+C;XH?*uNBfWEUI-5?xzqIPqw2}ac zYc1RQj~KQXI_-&wNR8yp#rgI6K;j&{^M<~2ItzG>tLFDk*t8ZgL0!j6XU2gwe|W0Z zb5B?wxn5%Wh28p5`s-qn5Q+2=O~1aY!Ac?Npehg{Elt7|^DH%{r>u)Uflrqc7LVXj z;>>yyAuz5vc&m@H;wtdI2-Vzfro6o@}IPb(pK0JCbR+<(o z1VR13*)>YL@$Y3Y5Pl%?!1WZST9UGD#!AHrI4pAVmYk9n?8TY3HfSPxb+Q05nvR37 z_r?rugh78ik(Ok_?`-)sOQ`~OY(qDr6W=LgkZ61{Vl;MkVE~NiddqtZHn22iOgz+V zk@nipCxIh*&WQLTEJKEC2&?@$t))|92O@M;ctDOenlciulXSOsEa=uDx7GTH_R%Dt zgUf7^_PY2$zqS_LTZS>ai%U(8!}3QQsq=P^3nJHV$%dxx9fCgRU5`F2{W2dB#HR>> zusToX6on7$k;J-OvcidOV)fT5 zxBiS~{QbKa&R?H(V#5_nFyb)rSA00s2hD!X*GxP-K~$`$<;QN z!~0Hibj~cB%4uA}k9q-6vAR4X7W$0~Mm4i?gE}6m1+T+}j+{e_9ECAxdZIA6e&%tg zAnbPIltR#+KtnO0YS4wkESMgGpv*8^{@2t}L38#(yBNo`)qpTrK&ZN@x+SYlqbMXx zixomR%rcV}@WDioz!>y-=?)Ool#8^;f-0Bt(DW<2?WlxB+7IFLyAiG4LLBaQX|ppE zO1j;NDe9VkK3I)h#hLE(Sl~W0s^=^G9vu!%Ce|&LWnfNdxTs&H4*k4+Ijy?gR}h?r z4g{5gnyq$;rt$HHOQYvY9NBX*cHcH?I!SL2?wOmJOKKB;fzG3O>-?Bn;yNhl2Mk?I zg<+C3!kb>z)Uui2X~?Ssw1*bCgqZG{;QM?Cud2kYl|I~atQ^90-scGg!vz)%nG%`PyBEVa2VHlJeT^;@;Th7a? z4e*b;9A@uVXBsEJt9$*l<1#q`iWwpG%oDfC3T8CM3YP2HAo#(?#I9Y?TvaGwVeDgV z(3aN{C0w=~J#rbo{63LmscDjSR4>C`(6b3v1}Tp^Hs%9!2V42&29VA=5(1UG@9Jpk zjN37bj%1%F6;@aI7KcW=Zy|}wpAIDx0%ynauiOc_lZS36SB!)8#dRG|%T75DKn_AS zQkzjkHP7I4f}L#|0Cm?lY&$EO;lKbo>dG+bIMuk>yj;EB%tcw;eZ5W_$2fJ*;%q8D zu-$3k5RIqsLs@X%xR$G!LWG2>Zg%Y6sNPpG!v^=G(^}+gwakR1jEt0`d7|s&5Rq86 z*I=aW+~%Ph-s$9hbcwo^uEP57v=yOXI|BjjNp0O#iv-$TLkOf`t@=bdePRR+tEDep zkZID=lKXf3)j%O>Ig?fXAiIR4kA;qtKEPAUqOn9gxn4D*CH48AEx{Qz8{!K@gaHDQ#QJ)&2m&uYH)6~yS5IOI*@JUWE$L_Gi zixMZc#N^vN+I2=R2I*F6o`Rvbd{j+)cFPMjLd5rNgO5cxEYE?AdgtFK9QLP3P-#C*8}Ofs1% zh8?!so8f$CPta~#0tnAzR7S|jG#uoPpO!YD!_c~v{Kd%M^D~EZez&N$wLx56`(IHj zeZ94v=F|f~Sc|pi+HZ+HLg$}qmcF!tu|=jh@#O?V`Af+1jwF=M zmv`w<2iDJbP9-IAi{HzGIL_+zQ>}DUdiMI+{VN808wmr+l2N(zjre*vpYOtzd|fI9 z$<0mY%k^So<36a*o!&50rP-Uw$t4mJ6EC&c$sUCPb23=hr}vZ$GY2H}`x{93+xnWp z;YxdBHy@y$*6}7y)cdviZ$Sq`8lwxJ@H8L%QFb6t(Pjte_G8*lN(Vrw=Bamh1e~Z< zow(U#ot4Mm&GO7l9RxsF9Pjq1&hr>Bonfimzo{5~u3Q?HU!4_FC2eb^pE` zv@gg-IY&nM!*VP^mo_>zF*AjLMtxc+Q&Qf_^rARTOjn`ZwmL{0B=Un!jSfhMUf-WM z#us@jF4lcGxjkhzyJlZ+&VM9}Sy}@&1soJpN!6`X@C;t14k6h&fvz_7mlgkn@R>f? z*2d=#3NLC+nIuN)*E>qs$;7qHY!3yT+vpD&J4Z`2mH9Q|Qao+_U5cVi(Idqr&JyEi zw9prOwgSH9DaVW7O0_m}B!9fNHCy;@Refek^@bE}QyWT{dVX%=+5SE)%HZCHRa@fH zQr1Lg{1{L6U6$7(eikkZCr8HT*(Ip4Q~hA+vLkpZ&b*)QaQx`Q5YaY}rgX;vV453J z{3KUao81;)NmDMP8qRklb3atRbhsVak}+wJ#1a9`YUj&3E11`ojXo|qu96wB=wlFX z_|w+L%X`Z^rHZJmrczgx*RzdwT`#U`JKLX>v9e-j?$FE$6TW%8>$yrams$k*3#VB! zI6Gx)Js-HynXR<~67%R6{~|8yyU&$^spq{UrD|z@FKFim8tPGF4->`E+6HvTQ6-@u zd0qIU11`fDI%<1(C2rP8cCWv5)C}&hv7`scj7aFq4{6&wthGz@c}4;EM8&{wQ&}Gl z^DlAn2u6slWlQeUW<6M*rK<9VVByNGgk37?H@C{bn~RU0&GF^=b!8p~j*e+MDrUDh z_M)Ef0_P{9J>KU{gAD+z>W<-%tnaT=g({}CeDX_&NmJoW`&Q6uhi%6=+Wo@u%&gkI zev-~IJ0>b@sb8a`(jS~5EVk^}>GdTw-7y&%=|uv%vztes+D^jE37SjU_oDEhyn6=5 zF{!><-O~=sFAXTjR)|ni77PfD8z5Kl7&hdFBqxFwFJ~3eThbhhDg`elUpvG`A}c#K zBm43~y%f{O+HPv47=~Trv)RwLD5<-|=xpp8`@MU5sVAIIJ5L7k_g{^ zj|EUuO9ph5(vL0`vulTle~E&$SgwYdSrryEx7U_SOxr1YT2m=?sVCK@WYEBAES)$b zeUdMco}wLRiI`7L(%-QjLk(Ps=c4{<8rPC8Z2QNi7Ud6`-hnaFqh3yZ{c!?qC95EB zMK>RX!GekgKE(C)ao}=Q;C2tSEE2E7Me=-)XkgcGbU3g9@4!ajZd6dWrswf5Wn3QC zai#fEz|0?_<>yLM&F<^A5Q{U5pJfjWia)vZ)gL50-ux~B6_u2iU*2mGcyf3weL7YK zIxB1nj<@%-e^n>2U$$YOmj*DUr0QEjIfQtWrYx`KvLciyQ&lp#QeDIg<*`T9E!vab z1yDo+ODiPov5R3AN6x`Zt89*`I$O@XlOy-_(o;%Cevn4#tZ3khcGJ8Ym5O5crF|>x z-qVI?tuv0aA67gF=aSfy{+$NWtviv3PY2hl+BI9vy@#}WY1oiLWvYvL z|C774Vp>j-lZFxu4)2b`jxWNWS<=mmp`-aCQLx12a5+kCJhBMo^3fg9q#TyJaYFfe z#Ay*XC)mu!8;oD%f{BPm~z(C9Bym^w&2cKQJ~B$&uuGl*t{-tPX!9` z>@S$3s+;ax493s((u{{q*5vKb8*^$Z8{#B?Ad{EFLoU5@Q&l*3IPkV1AiY~yk?#pv zc**PE^#Z;q>hcjomT!Oe?}5Fb&Z_qHi)Drx9_i+bP=z*aJ@8I!B6PKW1DGFKSfV2X z=))R_99N)hnKFHf;?J4)!Jx6y3*Mq6u&T@Q8(|7vRDR96t|;8K)wF52O>aQSQMvNs z^+cW6l0@q`zEip3zV?z-93LihXELHjYU`tz%5>MrmbpQGUCm$!j z>ER{Ph(!^=6P!D+prSHo382>!6lBoGP8M{3nNoup+JJn8%G$6GhkfEf;w2v&oje-$ z>GIOkqm#L?d>qwR0uRv9a39y7w9}zy#WRxm_#bFxLQ-|##OYOWuq%9tp(_||@fbT< z|NM!e-b4F`Pm<-VTJmgMU-9Bl-icl@t>~4Z21Sh!Pb9N0-_}wXIM_2`#vg-23hA7&!dvhv-ZcigDc1%~cVIS;ipgLrbfK3A9h zsXcqSgL*O=L4Y833;B>xh4GYnFR!-zgV76%c-Z0mF=HAX8FG>=P49DF10?GC>#*EA zew?hsXWUz}QkqG|kOqBmIO>I$bbF0kN9(1t{gX`2%q?gz<9=cJ&=0foo{oAZr*s0S zKXYq;rY=Yta0wUi>|c36vf8%58;62FEtOxV*kga0R#8z^$Ej>JAzLDDbmw>ft{YhT zjS*9{2d)NNZy5!3Q*Wrrbtd~ek9hOighT|!ZfnCr7RQ8E3n<)r@i4=jdzqNqvRI%g z^|le!HLIw}*kThy^8*e@*UDTSXwg=fn)VTTvP9bI(d_Lg)?19Q8yEEXxgxCNaP;Cb z0m)r;kYrtXwA{~R@i0|Wm^Gf*Vlrddu|%hk%0S~+7LAVcY(K`_sG5p_PI>5zj(fwd zRfJ5Bmkx8$=`~-J;1f-?%H~-#TBAD7dC=_Do(Ok5cv+}wOovAZG$}WwqFvW%@i*+l z*}r?nbu@`fLxfIiuLYe&t#g_q6|QRxVHg8lPxLV)& z2i}fd&H7WoqLsqypv*>q8a!8HD|MIo-CMyExVX8OH(N>@-t_CsvQN_jgxE8bd&)32 z+1KN^8oT;A1;XVkC%5+`!J^5Pb>=CTMr!|fJ6l??F39OTUOn7SP1$F$(-(KE^5P(l z*)o7WTM!Wuq2|mhxOKLQ-Dx86*q0B$z^WxU{cB1$tlhV=eOxBPHutzejJAkov_q=MYdoo*T*}h+wA-Cj)MN?r3~12o9UZr=d^VL-+7$A}p`Xmr{Oi$xvky|!#-h}Xd45`T68L`bsW z9M0WVVWHUtU)@;aF!=;9^r_tP>=cit;Gw*nT=|-H{xOH@$@`XA=xLYZ4^efDd>lI` zh5qTs7P)$$i!A{(liyo{`Hly}RP15xeUv_7Xte)3o@_0yixxWSVV+=Y@#xHgVu3{+ zm%v+lrq|&5v~9;dwNr<>Uks57GN0S{_)I^NOy}7OJ$D)|GE!}Qo$s@r;m~8_CRU#0 zxgP2ZyKcJRmk~M?3`rU;G5cz2!p2;$-gGN$=xZ{0&#)6mPX@MtBNU+S+N2(hH{R*O zGJ6cmC41hb4|VdQsHpQiV`}<^E#$oL7%n=Jg%6%x$jSFFe2?B)ataZ*v_G2&(>OvN zUs3V8uHE__U17X|BAY&@QoWs+CPr3O_PnPNpIzQYPYXgGiz&B2koPErIg-?e`)QANU_)m14UVd4B(xq6V|X2r*zPt`A8`18-LR6Uuq6=I8WoS-U{ zLu4hYcjQfF7Td*uP-YX&_&MT+g0v*hE8U zq(n$;T*ALlj9Mnz_lI%(h2y~dm}v?HleZ?y}nbkNF1)lm5+)XWLN(RmsSxA@!?N%CN zrw}>1un4_wWzRnu5z@K^vvzf%HfXT1#h!%8-M#!I_5gOy^?#;TrntYPgrO}+>i5So z%l}UMZ4W?mS4o`n|7RWsoDfa9mbGP+{^Y1*f!5G71NH|CK1YcwZFo2#6zN;p7`Wal z(U0#~vHb6#@oAXLv%EHCWoKV)P1WbJ#>okm5VMkT{k~7D)o8}15M>HU#IiOVj55Px zT{dJrh=6QboyP+X9C*E>Q?Z3N2YTM}MVvU{z{7RVQf#E>4i*`TVN#|W?zWh7N{339o2C~53^$w4Wq$8o4 z%ZG?SJ^Ng9+8x+?-fWTUzYS!KN4h9*h_RkIFz5TRgv{x0N?hdg%nhf~Kl5)Fc!-h_ z6hoYehfDgCT4}X?xucb35G(P~qqwpCSZ5qx;Ec7^hs7V)9AC)apuD96VLk)bYcG^@ zAv&*1EuR}(Fw6-pp6mF1TVLw6B(_RDvv^f?#YGXvd%DomBR$cUQ)W`H^Hu0@Ppb_y zI_RE%-|c(GDeX{{O`_ljrk&fYe>D1yipd>vx^DG+@@XVoHieI*xybkn2enL(LL{AO zGgppwt@{0;)u!h@s)!rgQGME>iSyFV?eI1}LfTMJRrSsF>-Zd*uhGGoUFOvUC(@ms z4XA@v9JMiRLg?bX@6#CnKjYU@>1-2kKfEJDU_)5aJbZPPx#Pz?8G*) zuGh#io%I6kaqDT`fKO61YX6rM{3le(>rGKhmXE*4{LxQFGnZ|`Rc#oi7X7K&=oY{J zQW*SHg4bdArD+BL*Q4D#WXti!j^_an#$Fu#-p9S^6`${YH5FRfU9X*BWOZ$m-bX(l z<>qKIa-kE!(`)#BV-UJ9ke|hFri^k~F8h~b!8n36H}Gmel+3C?zA**R2daAN7I8Kn z+{C@7iW$I0@r`ZOMc=kBP?J1xve=jM0dA5Ut&Q<{)?9=-kdXW~lf;K<^!0nuU=o7f z1ud2NA}*gG{f6G@x@lzFZ z3*{hC`66ULR+5?c{$(=4eVPuS+>BWiaa-0{r%kn5U_!^Ww|h%TB;a-nnkN{~WF$7p~B@$qlx1yI<%eecYTj+$zF? zFXPGqh6wz=ak}QR`(})O@rlbuV{WEfI)OUHrU26feeA9BBjUv;W}C8EqoWv9e_|*y z=qHmnS=k{0;BW;r$C8;alMB2(nSb7I&k=jmmX|vY(T~Y>u@M+J%s~KtY~{ua<>TZW zGhfMQ6Tbvtyx~u(*I~>wQU1QYXmUyz_ZI2NX)of-M66Zb9FB#uYK#5UF$m&)^WF}q zDJZVkxqrs;Y5{=%fz?H5LtQjf%+wa{iw)iEw>YK#){eViVLWE6rx)yo$$|_5Djm^H zr%Fj*RB|{BJn{nCJ@!XiGibAaeU}5 z-YIO0#xbP9E&Dh77O*#JiP1x1;iMRMe18Y77SpZW9S87}v|h){ zw>&QzRwyQ75xOvr)aiy$@O-tYb6^J|aDwN4HUvnMa|8Oogf)k{T3{tem8hyA9~g%f z7TPxanNK6l`z4bplQ$G)s_)Pc`qyjj&yY%GR@| z^>+9Et#qHp+-gsyCA?_i8XjzU@j&=Agd@g?IZ?%s5KfU;SyjXGpo2PO<|h)5kbTzv zB{>qVG-TcDvRQ!z6f1JiL%}RrM~ttwJPn=0y!6foj|V>(5l)l$dd6SdueTV|!-->`J^Ip^Nc2A1 z%r4s$d=sh(QBVMi0wsXbKz`~cpfLUqPd_TX6k!SOA^|(P*eBKE!gZhJEz5nW-fDu5 z*YG<9{-rEdE>e$OVCQf5*InY4VX`R=Z{Q7j>slAh6qd0C5xcxE)rLhC+&6)r>~j_t zk(Q;4NL#!alV!6e5(1tS@)?aDQS1%N;oO~?`4Cr6uGg=&r@Kg@%N;hyAmqd2MKC(; z?oJ*xAQEGiGZMpG%9ZqR@nZ&cy^(@J4j-NTNy|!GbGW8e(@EidSL3WCUv615(js#s z3R#{NEB94KMR7#hRF0{6nnAjHG-_zU^~Mjj`G!{N(D(vq`q$5#oHU^`9|)512uK*p z=Ys8@t<%SB&_^1spFwX>QO`rVDBs=&oDszI97w6}Gp41_KUY6@4a|po)^%8x3EdAA zm>!X(FmDS>De4Ha+_cxAHvy1EvHllSlE~pa{@6U;Hlx*cwJ}*@h0zG)4-1_BGm*d| z$Sp|&(082Qj`jrx{3H%4b!|Hj&l2ivO@7#y7tc&r#d+S?& zIj%V@ci5Kh`#Pu38`TXf5UT4sPZxRUfA;qU%FsGo7%AoJeN$hVanP&{sF{DgoCRji z7k+H?Iy=l^6YAWZJ0BW6)6Fb&(e1q5Qp|d~v0vwz2xQ_g%SOAF{^4otjaw`nNBz}4 zZe+|nEr%tVv*j$MKHrd(@#LjwJUdwcRpdW#6bucM7ne8Q+V-o}PE1?b!P)uZGzS@& z)#rXx=;=XSS+a4O%-wnO2N^nMme1_H(Mk6AnI9=&6+ymgnnsmVi&i>;4aDLzm@DB{Q=6r#<*=)v1$>&VDKmjsu2 zBZV(bUIsayxptwYq|aCO_;feCXLYlke+O&l^DylgST)F2>S~uBlr^eM>|CM~V+#}v zPvU@@C-=WXN~@Rv`ZpuO!fRj*j5Or=+rto#H3Y?H3w1QE=?&uF4=xk9Mn9DDqC!>a zXDey{6^E7okSQR!wK8u9 zO4F$g4qT~zhV{0{I!pSTpvR5-kQfw;5Y3}vr4lzHAtPq09#9y@D=D8l;>}r;mtc9^ zOo8}+*XeMRrC&1`oFk^U2P$KJtQp-nPUiqVuRyG9wsNFMJ824^z}qkLzE7D9_NkpVdEdO7PmHC;FZ<5ay>>bDmHAaZLXW#hPvu806t}MfxFStYA(qJ<@`f#G&0=E2*1|O`auoUM9~y z|3t4~0jnjOCBtmB3vdb>=nV*cyU^qjQ=*$HBH{4GjKjm{x=nfds;zWT9F778;r)jS zqwq(y+@`g=$>QvPtFU~;l~NMckmv&JI`qE*>%}R+G-$eJYhGvm>*LTJ=@T?rg43yW zBEi<@5$MZ%$Y!fBZUiFY#oF(Sv@%=F|>-knt zs@*bv?|RQr_#U?Z65Un|_FDcP6WR13sNWO5&@#BnIQKuS>I(5RZL_ym1X!g1{RIo* zJVG+khP;hZ0w+IX)X$Db9Ct;H8G6^mb_|bu9c2)&*H#oz3VOm*VcqiMYBIA6ND4P_ z&9c3Nh5qP2gw1}{Q~qxANfYH$dd$p}5;VdO$A~VkdjdVp#_#`tb0Wqb?eOwxE!V>p z7R&Tvi&42z8l@chgP2Vp<|GO9gespV=MLLur&~+$iKE(I*1pYaj)r*UWBo2=2$|EaOQm~91um0r?t z=-_dfAWK08kbYmmzOh90f7RvC~a*^sHonPCUU?N7N07r6z{KAt7XpQ%4PrmxS@NXsqKp7 z8_P?K1&e|H1W9%9mhIaN^;pir#$%RNI_L!}HIAV7^VIR(@bMUnEG}3@zrpX&w0~BR zft484;k6}Y7za0nZ@;AHb(crvRdYJk$EW$)%s(yIU^RHK2BIH%8ZXtYbXdlpWu_{^ zZC<`1uC7O3Fhbx7L9J4vw$LUX^LdE2Kqu(VsYrJ_0x?I8FvJYwq2{WN(Tah+%fUSW zBv@f5)3+{l9DI-a@>ycAD%i8?1h58UNe1iyhxAZ}Mxv=iYkRARP+23w9}Tb+gKQU@ z@xBzT(~pX(y)OzzC6;HK5o3q7LL@sVTq&s?)&3~vgwv} z`02H!9F;+|z)V={1%A)lueh4+z=b8XtR&7K6^gQ^rn2DGz+(1;Pk+#eZ}`}1czwT_ z^~ClsC~2%J)U?`}Z&nz}ggyl<1?hfc;`ji^d?F%svZg!IOcBIxFjmhO0v>B5(Z(~@M#HFF<~Cq zOQUTGc8$r`Ym}N#UGu_%<&uSF5o4+1(+Z{i*ekRF+41MdB6GPvjBF z^H}Vh_*oT)F!g-#T{R4*x?ObcAUdi#Y6TQ<6#r6L{-u!wm%e7~E1rHyuk$#dwu8?t z=+3tbVEjzuK!5rRW^mtc_+5Q9o#eQYmuJ%Y3SFE4_g2WiZMj^BfEKED>4LOjA+Biy z18oMwF9Y=@6S#ed3pN}Off}hViHcdZ)b;~ynDV+8)d&?-95ZU-#X0iGg7OjiPl{hs zj+W5s5PAunGCA?~r)%k?&1!srTjs&r_d;n67U7Gqb}vh5&ALar=|@4L*kk#q*MS^7 zmMKDp)EkR61=g<0uKYi#ZHLB~Z50-Z>B>9o6kI(cnNQT=0zdx4rNah3U~LM|{Uy;$ z=4!6;cW!ej+?vldNwL5PKgi4|9X-o`=Y4>zJ)+z9`;Hn|75-S2TR#%+578jCQ zj1I1eWDEGO)=M(e@A&N{PMd=Pywi>oztsuROzbhrh|8`nG+*#6N5YJ+KB-TJ75V zfNc-Nh{GRNn>dVzo;$tFf=42`CDPx+%IpdF83+9jm|0r1a=y&5{MG9*2xRZ^=+`iB zS?hcCvV7;Hew45~DY~5;hG`kr_{m1gS7LswF}|YnHB4TDI5VgyRMxJ;+scFkkP(|C zXnL?!cc{;=2oltP7x$>?xwKH{J6%z76IJ*`i>KGIma@cI<}p}c%FLuZ%_%Tf<&$rU z0j}`gIUSRLewu3XsvKPRdT6K{nA)1|?_WP%xPD14tVa};`;KcA!umU~H}$s5^L5eT zSyi?b4LeRl_{M4+wv1WH0JY3Ux88U28lOtI+iU7S?4ob{Tq}==Gg!6Ra3TGlOp}OD zHkDD|l=sWDwsc;b?>C}M6p4Q6>P`g8rW^8nFY?%zWjQwdo0kjt5?6O!thqkwu^B0b zA!#+Wl1|oK*lPS`>ic>7^LPoCq`Y(P4>Nw8vp@aO!cVd{o>6OoXCN`fIf!^`;KOi`uer9|(pi+YQ|`5~DP^CY zorHZ8L7kSPNcUDq0{)Xf%*!Z3j*BTZFnuQcJet!6)Z}|lT9eUFN}Nyc{oDvWDeHc~ z3uOBrHbl(2av=NG-YK70Ykgsse7QdLyc{ODURA;L;;4BbxH!%V)o!6Ydk}le0bIMk zF-+=smEFJjRp;*d?6aodPCI|>_QImuQYs2FL!uKu?(q<0a-1d6?XQZy8}xDR@2;5^ znn|(TA$4-{9M6MzM;12NxEQ_;tU?8!=h`&w4|Kpc5l3YCm20&&3zx~hB$kv|9{z6c zStAe`PjNBn3bwd?97h7ql0QBYN`bEp@7DhIajh62Q~WPPzMDM|XnlA*E2|IwT|;6B zxt%jgFK}CqGD(K=GRzZDW1V_T=)U*4dv)Q!I^%dUY`W{d8*j2hOUsFbdaIlWmM+8B51lvDhQ2z%<($+MZ-To-Xg|@` z^}l*L^$+v=-mFy{Na35#Mm73)oV0)(T#$_a*Ge;1%xpAqhK%e_1?bAOYr-|m`@1}3 zv^dRDdpi57ZvRL=)p5TGcwO=+nID)iXuEN3ENk6O6^pc6_OIR>$y$BLBydr``NJSD zqw?g&Usd3CRREFaY9z2-+j{Wvw4Edt)~UXyjRsj3@!eja3pd{;t@~(4dfteA_~+{L z+aTP;%S)8Wuc-B0AFLT)k|M1pZ6S%Tx&XjDeVNo%d8mu-@s2nrgGi>PR+9&8u(oX* zr4?=k4y3ay6I&=Y12+&rgC_ULYdR|$EOQJACW-Sj1(v^7xHjBH0S7-z^%UKA4PI?X z(ys^k{5Tst{9`!>$Vs23M6T3M|iM)toA_sV7LZmvu*Mnto$oiu*Fr!yx!G_i??Zraej!1K zgu!HJKmW`AH9~o31%G@(Dm~A)2-lvAc=10u!2tv-h)tauz5>y~UW6;;G|2y``gNnr zz#p5gEuZcD4(uk*>-+a1+(soFUDukbq2Uzn-1bTgasZ2fB&q1z|GvwBkB6K7q|O*8 zn=ccqMQIcW%QNN8n@>5N_K-={jt(cMBDie&Rl8`TQMzbdPZ&Xh58fvS{g+lAD3o#9 zg|g!ce>{p#IH4PNY+_)tNb}oQAu798a#Q#ygK;ge;!*JNyS%?d&heyBP z<6RP_%H@Lr-kFP5S;h8eLN=AzjKXhU@Qvmj{Id{n^SVW}j}w$pk+a1M?P(B#7%KnU zmF(O~#D(eJ&`(y`;y5&kCGU`NPe5dmFHq+RhYaniqq;nn3Nih7MP`gs{SN}vysVwD z{3I#9fNuI&`f`oh$(e7+7(@THT<)qeX{YUCUaSNijYf`zUroO)}9G%l)BP#Vti%X!Em6KvYf zIKEq8Jh<9fCL3D`Qq_t&&HjSuQub?P%GeBN^0!p(#DBZBSui+O?wttdD)=XaI8xEu z>RY8+NsW%}Er-@`0(fu$Qk0|7{FdInR0m2UL-^jHQ-?rQmzxa4(>vPOO@%=wK#IO} z_DihzKVE<(b<>>^>5)YmeHENm7MeE~r$hqbIzb~h(@PvVw9Lr{Z|1ZIXv%Ng=ve0m zwbwvdsKRdt@+mAEKpHlhtrKEi>{SdagaV?WfsRy@|7sBUp>((GAk**t2-%^i5~S|Z zze#OfZ#{+XqS>$*R_jhHyHs4)w*5jr2lUU@#P$ws*<-+n zZXy9$SlMpy9@cP-2B|_?28=_Vk&Wg*ICNkOhxs>cLoC_+)b=~!G^&daW zq!Q-KkqhFQHm3jdgyw-X`gXUgR#1XtD5N#5{&XXA4*>9uU-dpXu*roqxJjwCQ@AyU{wj)0dogz<=}uW_-v0@V;@ejai)RHLzN|;n1g6!IAup#lUoF zPI7mADKW6BsNl#jk0+ZmXYlegQys)PD*^xG`(xSi7Vd!U!!gjJHp2E2Y2y{8HwXhQZSj!RBE$ct!Xktx8`uP~Ts@Ti+J}21yWfRI|57a(^^+1+ z?+ZIWDavTk%+UPWJ=&O0xT4L{^$P-7`<>uYEQEJ|DgC8fjfS^o_q1+N+fU9zBk;7^ z3nwh5;y=Q0@4jZ?RQup=RE)+uRKl-vYHf__cor1tn|I#A%K5B->|g9{^!Ch_(v+a;yQqjcu-fLEI1y0MwH&fIoAqX1R>3C+_yUK#awM0J4peY!}} zqkLcF14E-zwDD9{Do$^KH*hGESGfbK%jELS$Bx#n#S^JVrh&(pM2}csN|tN>(=0?#~>09_@QGx3$YX2J!9B2!a*) ze6llm@qb(jOWM<6Q&Rl92@YS>Qt|rb>YG06xHsdoRr)PaS@`eOD{a(AU8j3l`0Q?* zgL9BqvA7ABE`Xdq=L2ng>bnGThq<_Va3JTtPUNO@1+9EWi-(Dtw|7(dT?l2Yv{R;s zCvcu~l(bdoeQP(pc}hQc!S35eV|MgDbJ!%fA|KX}a%!#^ zkfPh~Jj$pS2MTX~n;=n27mdfEKARrQS^!Tvgs_C{u!bXk>vnpLGx`i3BSl)1aul#{ z1broiRp{B*@9=WoB~@f{l;dv_vk!+q;J@+?{QmH``VMwEytv_q@?NaeVtAnuTY;ZmCY8FvTTju6R}0x69( z!(D6oiksuX{}Q7XC%3Lm=M1wMvM9@ysq9m;@ zOVG6~Z_y{>K#=<_FL(U7i}GdD%(&|Eh>n0Fvr>VD0wfm4)~eO5?b@q8DoZaeB~CXc zmXTeLanf;+_=mO4H~;#&HXYOLj&Mo`y^6Bzrjq^+3NR>ztWr()3RY$1^OyYX2_*Af zU{qvnLk1K7u2zg{G+xKA&;wqkgLa_`!=3BR@vnl(5=K1lyao7^Eu-JsvI8AOASW`fje&x6BRD=__tRp*hUV|4KJp*2#cqptqLGbtGn{Vk( zTk6I8XA@lX^Zc&8EbP6!xSsd_#LhdB=r2~*ns6L^dDlRIcG2XrorVed>K)*|>I-XR43nPbTzO*&NPQ z*Dw>>H>G>#<2de*G0r=h(Ry3M**LAn>pM{MME3CW3m@c6*mOMYX8yIPHW&6FQF@R; zrvTZ{*a-vqoIgvwDB$YnW`mz^guRX%&t$1t1&+ z!c&2IjIl!Z3#DO2mhf08@a~R#J8guh6vL>~$$0;dr>lT!Gg#JvQi?l;;ts`&7k8He z#ogWADee^4grdbM?rud>G&sfG-CyXv_i;|n;s3Ll-JMzAneQ9eXBYU(xma02ztdAOEZxy$yRxH$Ri~)JJ|V^+J*^Z2BTFrv%Sm0N%`XLR z!b|nSg=)6aTUy=3Vx&+psk*kIw7Fu6CWg54a=STc<8nrg%4=)e0Ofc&7;}y#__V+K`jFe<99Qe}$nA6an0M~w z!MbYB0_huhsr7N%x3D=HL#@M6#`mD~f2GIFE}gt~`d*I^+^dq4~PM#2X9)d3JtxgChj2UXDx%7ZwSy zk|zD{vpiS^PT9v?1l79l(5e4RHwuoz!py%M7=>BotyEUVS^8B;agsF+tP;>V^! z@tjor!|EP);{?Vo#oJm0_J?Oa%S4$2AC|l~Ly87mS{stx9Py<3he&7~+BBUnYn@f! zajV6Cw7=V|qcf7QaP}`MxIDEmnB7JEY#_KXeDx`0*IJ1ahf!9E_m8GsqwemJ${JW`$fwHvmFJ;e-y zo}lxGSgOkTH?%PvQpeV8mJvgA9~L9Lt7^jy0)>mQ>DZ^#m{Q*9xDpRCC>%_w&ZzBh zKFLopOb*|8Ed<}S#$F{wJm@Nty-3~^pY#pnE#b`m;SXXU z`fA?T{#x7Huwj4$=N0E-9YpNd2j`i*y-(S#c}Jn`ad0A^hFSaCQqHAWe^s){-EVzw zaNzy|71bhL-viHMCdl0n))Bnb%+D~VgKCwp-1t!DK_j2(*FS?UZKj;`b7Eb6kdwHg zs4%zEcx!7_t!zUs3O$>*Iz07&mzGgLr_W>hD=`10beHFOHa!#t0v$Uug;Iy9Xwb=Lw8A1AWUFZraA0GvYu6o$}WW9J=zHA@Z zKYV~epwp3)(b8ENEjt-1rmJZCqU)p#X^sE62aSwwDsF1(VPLPgWF@C!EBsoPoVedn zRIkNEV|--d3{xFt;_Pg(MMFy~HDO~+d?cFtrRXbiePIR7gDZ`KSeBv!-{s6!eQJhQ zNsYOQc4={Q@siv!RSTKDvs4O|s+dXeCw4(?X(B|&00Z@D8^5IvgpkzSn4RO$OkSNk zKrO)-&AR*8thu#=<;I0keo+I^w4`6O(Jt2`Vqmzu(r9&MseHpF#-BTH<=}9U1wbxQ zOu|Pl$;>9NUtANFM>Sbcj?y2B?FR|E#<8L>suD==T$HXlbr-iic{Oy}6u?f%=j*yV z!u6GDSFf!d>YL9nr)xTNIO?wh0}lMkpG0tfg>FV+c)#mbv0%kF6@knSQi02ynn}Gn zNU{XV#|`RNFLAf>aYxJsU60o(9-H&urwX9=DzoZ=YFDS|;l+ z(NX_SN5GxhDj3M-&|Pz|2Y)@l(8=3$W8#ltKs4rds^tf1`G5i(AG*3ntL}5kYSqmx zI5|2}7|+eTrdg;x!F9ab!%xt(&I$0~>-7c(*rC0-1TSb_Z^@a7me7B6IDag2Q4w0~ zXzi&0Lk62MTW=n9(^Ar7!UlSM8gZolm@8h~ZLm_gZsv)|>JiEU2Z6_xPBY2)4H6${uRe_PjF zUR}jERf6~R58mY3d9F9Jz~)N#0J)tmQl!O0_~e`g#=TLLu~$`@sg{O_fWhHKCXbps z-2uq>Dm_h?F_vOt%}=h&#+H^GKevnMqvam5i#-a4*fqewSsV225iaM__$^I81jRsq zua#iLb+*U=U;YoLO-R8)FsJTp8)t2AgNC&Pd4->zV>Qz}M3)kkQo)k*#|c_FY-@sP zGduW#qXw9T`gM=++AVU(4nM)#xe-y%BefncYePCKk2fq?SuG5or*yTcusZMds{-y$ zhYg-QuL}1sY`B*ePeyA8Jyxf4*mHl>H^-e+wrKXC67S&Cr_c`+#8p*%JTQ5?)S+dA z%w3jUc{fNK7pWvq4uGMK8_D9LRg#EVSlHNMZou$LrkZF+uWld=aT=1bhQTZycpszg z{PQF%OeL0|mU`U3Uka4aYAK^#_v>Z}PG2#WUx7K{H?Fvj{^E~nOo_+2Pizlc>S8X< zlAx>vs`{#<&BH!Af3)q?L|MP}+aasjg>`d3aI+56H8%TTkoMN+jllt=rfuf)nk1Qv ze#zRKDi*SFt56ciAaJC{a1K?2|3(6oEg%?KwnHg1j$d_L-my6gpE8IsGkK-Fy%j5m zgPluJPe%E%^d`|ycW0$D5P&FWi9NU#m&%;<;@2%@Pk`-U*sL@k@<$ zF_9X}r>Em~gc_Bt(WzU{Cu0`3&m61F9|fgVcT@5%e%6_pUrXGw+++luDL=&*lK!gHLxc}(2ra5-G(Ec#gyGb+E zk6m$B23g^%s?ArOTC&<69*(GJzwJA#YnJOwvS!n+aLu3oeXFIZV@=PIWFf;aV!cfI z#nQUO1_PJ5UBS=jVI`P)Fpi2PJpeKj%_wSI&a|MbYje19aB$i8`RG}dWH$V=zC5a1 z=pGy1qzT$T?^DN{wnnOOCsY77N6+m$I(8ERQ9Ml&H%Q8aAMeZ?q0qPYS1$ReYEor{ zygm@ND<`19BW&?7#)5GTJJp!PGT6S_+IoazV{oU`hVLPBRA4fbcZ=lOP|%KYc1bW* z>`m3`U%1`0K>_@;xw*0tC-;|TeX$3g54)-WML?S`a@Nd<7c$&izVsjiT1m24x13Lq zx!Nv$ae70j^Ce}0S)uOK7%b*dhyT-uo%9@gz2OC04ty;_;DGVQ{>1_jn3;kR$Yb#c zjVA}K+|PqY#!r6u)eh=4I3Fb~3cjdm?Vp-HB_pY6CV7I08&qmAEx`=vlt8x#JUT9E4e(Jh+pwL?zwY_eBxVr$nV?& zYf84VsO6NE@N=8pCx$;8kPB^x9gu2QA2WXwwyf122Yx=kwo&4m)+=~2@IBgu=;*)v zJreqJoZh4iel#0=a1eZ$NvoQAUKDy!GIxJ&imcsjAzrxIvtbe1e&qbR-*h7INI@*y z>IOV|@wvExZaJ3*k(!u8)6$b)*M|ftqd=yKY47Jmn*(OOTgWO24F-YhJ#XiL#>W&O zaCtrr{R&U>=D3pJJ)+Fn>*WJ$NL14%T}gdyv%1Ck?BGFNO;Z7#jhs#EgybV|c4|}9 zO1Zv6U`1v zfkIn`nfv@)tlgW}Uyfr)i_FKOpa3l+t|Pr?8KGM~*Q%vbEOz*FeZ?c|<*4=Je`f(V z7`V~}eWnJeMu)dEkb7+Be5j@VjtbQ;nyNE3hz#nF_AD=3 z@8&m0X$E|&!Gwe?~3WE2;PbV7n;y&tG5>e z780C9AFqxAEvD;z%7iC&kbHDVPy0^~3&TEv+CS}UTR)Hva*9)zs6r}~WTBq}_<*j9=HAZ%9T-{c}*7SOkP=0icf1 zMK9!#27d=L6XGMGDc5mOjyJw_)tar6n-+wPc|o;o&E5;B?ij5vCNiTSc_i%Qr3(2@#t;lbW`(~igME%)91 zrDj_vLnVRl{qi=G$66My4v9k;;8|=8X=gaL2T+KY#=u^p{>NVe^W(1CkE0KaIo7?!V>pDrD5eH|c zF}%zB_i5WuFU(4t>jO;@w#nv|HW&Z~4gO&#dC==!9Q4BoNAa-Nic}|uH2ljxE3_fM zM=+h`!OTYxE7HwYl2Pm>-^xzGtxSO2oVmHG$&VoHs0BE8bZp>H%|9JrULEPIx!Lu4 zR{U3J*5{qDC6fI5lm?}yuY!3G?kO)pld)c^cgGJ=zwnAV^k7~2w|u=1hMio5RY33W@$W@Aw;NOouPbWvd&-sdH*#em5n27%Y&4HoFD<%ciDOj6~CQeWget^wz z4AiodWWy*=J6<_W3rYY;So|yKd&eM{1dA`q%TRqXhj!_r*>-xQRcpE$C*=9SNlAIE zG36xij(m%hBan}P5Pn>)S*mnhV0t(L5#FL?I2vkJypap^+acL6K0;p`>V%?Chj2mN z1fZy&P^xNd!;bf^8ZPQ61}_;WhFV$u#yCjSkLk>6ne&x8L0aDeUqs*-(U*b$961Z|%ZsnLQCIZ!T!g2WgwBTWXrzZk-2u({;G?3{fp*_VMnK!c9S7`el{lZ5UpQ#&gd>#a$BKzCR~a7{SILo1 z-og2H^V1b|i03GjOp4I5nDXe`{E|`i{?O^}O7bojxKUgRAd1+FaQKdDhosjIver!U zas7iRHq;GU#)IF(qFEpe3g{W*+V5f*AuWWwvEm~XlXlImQ^z&Sh$+R6;$(Fj)s%d8 zEgkH)4Wg%fAV#Bmzs)?DK5EzP@GwmKK3akdXMxcZ2Rr2bH(|s2dp)5ZV@ZCoe-2}7 z>+?`BLWBu+(7Pl&x{a3j4^u_O?8TFdg;V%8vL_6>S# z+=ecxX{T7xNvB?4o>x`(98Tb2+R?=p%tm$2yeX7O0*>Jo!nki`A}%h5HV`OOz?(!v^-@n@Tb|-DGE9wX?t{Bu z^MvLs;}@xu7kKD#dzsT7%Hl)ma5>qTAjuYMK`y-O;)XoFT8pLBikz3L{Z<-jK z`bNq}4Y`a$)<|Ruhjq!4l>rCIo1-QjII;WLz8z{yYcZ%@|0gu}42r2-ugiaNb@zm9 z4!LuDaovKt{00JD(7$#pxF|xgs2`ybO2HQOntShazx=LHX8=vV>xEu{{UMt^xby~g z7kZEmQh|dRbzkvB@%8_Q@9E2b;L01kawk6djiyUr5XBL8sU2Slo}10t5P`iN+tA!G zv@660G+|t?xry`^s0PRhhyP1itttPeHNU;rkhP`nLKscj)fox|%<>;6BylW3Q2h}> zaJLsHE$ciBM2aky+UN}wyB9k}Wq#hs@nG*F47Hyf*mP=6&ccj9I_M;1IU7hBN3RXo^P5WiJljY zmt0aX)BWMLnt6PIJHJwK--;s2-~0W4DqqW+ACRcaU~_V?kH_8r+=4DLhYM-P{@X1G zZ^iTL_ctSPXFpvSjjUeL481bVn*{9*Yo^QCGb;=+Zvn{oP5Y+}0W>J?tLCewbBVxy zQYRSwTwJ+EoWFBcN8YsHPg-0b1PekKh_DTVi0~!DR^CxH3c9}QZn<>)s(H@F00{Wh zE_fx_YW$E{@Nx$Qy#4(;W24CN*FMG0`dI!7(U~Mnkh$;bz?pjaQnV^_pB;&QfLnyf zx_9r)V=HWWLH&-#7nM2gjZV@B!`zL{<@H zR&kCZFyCMsQCUl47>M$RIE<&D8ppX4T)<7cpYE&#e{bNcI&TTU_72@F8tTRsy(gH}~ zSk)YX7*Hr>Sg31N{Lw|e>MCY0N?t}@N80Y9?T4x|ypJ+0vVL#+U*uAQwK=(Zpl${KL7Vvp=qK&RGW%XHPjIvaR@e8q z@+MMp9jDWdA_K&QA1bT*+yFkBxM{d_yy712z}!JJL%398ay~Nzo_LSAE4~hsY>{`M z&Lqn5TnA`}PychLbLn)yFU6YFm5qPP{chHTHYAq(w#-X2$|WpYd#NjBm*}UD#0$Bf=I(q17o}j`z5Gx*>>&^bYP8XT(Ic7F!Nyh zu7<=-X(2{3?y$fv+bR|pJ@l11~aThJLL@=>^0TXs*yoUpSKl+RbW`+CL zT$n+v>-e~Vlu8l4TIlt_WyQb`<^Y(JwZ+(MXjhhLf}Ar4BQ`R>!>53YuHT}vecbz8i4P>~pm*$o6!GWKghx|nsZ5C?DaRP4KJ z$ls0mMgO^#)+*TtN>-R{a?~&k+|96ZjEjFOv{(3I{Bp2B@W;N5<{RRll(N#>kIz+K zSFIdLYw^C5$q@k?4Ew!HBx<`vqFA!HuA0=E}fpYX=8KZ$q6xE-M}wPlb$jc*$M&W z%?>IGPgPJvcN3H-3=P_UscP0gn6>#K-{nvX+w92uEnBO4XFXwX2D!PuXk7XY$9UOP zHJgltR}rFmvr*9~072Yt=C1O(^UYJ?Hz$XOe|X<1}OCl0sMcAfMr5W_YaoOZjYzWdT?Zx-a@M=xVhwPp8Pm4As?YrNTOq$ zg>7&LzH%qlVyghqpXC^?9jQ*$PVKV>9<%w;8P`+k&7 zRp-%*Oy6X~Y|&dejj{B0!_~?ot_SC;)+3Oc#;Q2W`(568o60t3X2A(#u7HOzm=-0^ z)9RmWHG>9>EGZ-TspV~L^(AL2ZqzWR2Ayg@1W0^>e3jozp&{@>Fd7V!sS)@Q4E{~d zJJNW90b|1DC4;=+zVJbB)IF2$`^;*J4O%Ismeu0M_36^Ah!PZ;_&H31XXgeW=#Aax z*6Kt4pykAIF$ppEoA%l9T@*ox5tVEDiG%^oUHwHB&o)1&J(57FiD!p7`@uI&>pEPB5{ zSKH?F=vLaQVZCO9?`@F?L?npOLKGxlIoIh!_;O}@b~bRofS6SD=eYXA{G}h{dhGE> z9;J^J4BP7Re6C%2S-b7ny^TXNU%){2)Xxqk7J+CTWHNl;&vUmfX=umaz8P(p{J6&k z1A5_KTMp6s2U^x(Om`t-xw&tRPRCe3%WvM!tv>LNk>p_+WKIh%RikFBPeBaep}oU9D$ELd*LU13j7Xs! z9~1n3N#(FUj0Tx(B5s`JDB5Es&>5C#g=x;Sm=tyVY}E3RpZ+`G2wXrSO|k*EqvQ3eCIG ze@UCSjZz}=+i4fl?l`7TgG=9e^h!YN1)!cstgD>8q1DF_UjgUCLW2`igB-)UUSmNJ z%RD77-+c)w>_7uCFR}WM<=y-e)b2&{hUK@GzmK}&)?!MnAay{|zb^Yi4{Zi~Lh2f+ z+?>-;02Sx{7F4FO*qIbujb+Dhaf;89luo(8Ytv*&>ao;If@ll|3Vm5@f`v=V#yN0w~Buu)^s;t@~33&IbT)Et&m z$ZpL}*|KqR!ga*C{#2iupZQ%oB_{L&oUy($p7N>Nw4F@eb2U>W=36H!wX(@&VH38b zLk{Y$j%CQ(bZ#CsWSvBognP9H+4>)2lCqgnm9i^dVkV@9n*+rvc&$ArC@ov~^6sa- zgUXgq9?pVH#tsfPckB5c=%jNJu;@^a$oEhpg27v;Wee3Oo&eT-ucT1IrukcqNTF?F z`JDJDzUb>v3K>zgA5tWyJ(LN9aGqWBRho=ZamrIitJV_pEU(#%3u(J2ir5q}MtQoN=EP=?LhzL%qVWvEf7>gs*_wuh4Fpm${ZFjbbC?%^#oMPNdh znU*#hV~Q+L;P+oL7!vp2A-MsrS&f>E|FmKlQmxH|RT2V+fc5v!H+g*Bs|dIR>4+VY zSLfd@1k?J3KI3Y+co8#s=|C|IwzUY(zk_(6P=clYF->(Lh${sP2>$s!XtjJFfe^wB z>3mO8z>(KvSNvvm`=9@Ck6u-hk$cx2rfgvE+o;f9k|@W2@59m;Wq6?P(4s=62nxnt zYXh(^?0CZnCkab5n_Ys*wk91EgZKKHjP5%?3SQ6*3McdW2>6Q&jo=T0UHct8Y54C2 z3E!>!J48qE9>1P{&lvOYP#6%HI8VCwXe@N+uX+F=yr=4-4A(Ia^~L{}M)LLfeW^@1 zdNR^KnlDFqA3|0vLozxdG&O18e`SP~5e6((3mC8-!ry3okI}m53lMyKoB|7a->vm{ z{%gCeD$qa)e{xbZhs5N!8Wl|0=_x6kPTdQH=h#$KyHsb$BtCb&@mzu)Ga=-f|2Qj~ zzW-$ExJhN8`K{~y(Z!){D~|f37R2}FEF4J{UJY3jN2BRF&W25mN}e1BJ^(72B{8{Y z1{9;?!b478oXxyD*2AEl)kQvN`m3tHIe4%tz_rg!;MGuMWW2KI24sy&W6C+7 zi`E1Ow-gm7^yW0JFsP&ZjHTe|NCm1LCA$0I4@t#uSg5$%o{o#DY5Q0+DP^%aNObc^97FKOct3|Re*82E zYijje7n-J5f4wY4;^~gQjJv(9DF1c&c{I~X%le`G92sxBjFVfye{RDY+WJNVatx%> zOr|*q3b8Mc(;PH;&R&+!`Z{R-wM3Xg7CtXY4+W9R^SL!NQDf&KS{tR~%8BT8XXG8F zxqx#=;AguPVj((KQeItGc`m0R)SE&(@4Z;_IiIPu zeAXQ+eAHf>gfeA2)fHtAl_URB46siqpx{QM3hvLuMwl zgVu7x0yCLK>>pzo<}W8#1z+|r(Dm;ZAqz`hs*(&=N>@YZ&(}S(PkbH$b$i*V7;iC4 zCXHpLGW_`Bj$R&v@jx$!o4?FD;i$V<<-K>h=t!@PS<{fIGLUB%E>?SeM(Sx4h9HB4 z5N4%pS@yr+krAs8zj!@F{3*yY+gTnjqMciOCjGB^Iy+hpGe&NS($Rnt;asT(Uv~7} zwV02lT#J@8({t3)`L@M6{x)ovLG)ir3o_gv4}fNA0{X>@_+mfXr)?zUi;8G6F=@9~ zZo6)>^dD|d&sh9j!2fA(-9{y!eMMh_gR*bgaaD`rtc98jXD5LVk;zU&pFR#j_&NHY zl^LT|IZAOU6o&S~uHJs$Q`>iS*=y+C$4z2OtpB{18U>6cDHWQ^po-8|RI@r9D(`40 z71j98M!%3vHks`lQ{_-v>Yy~`#Ng6o)O|1Y^2Kp`GcpfI8n?yBK}ggah@*Zg09m<+qk^cTsE&DH{;%>BEvn4Ia;24+|Qv^pn!Gm zDpHd1Wm0G(5$>y*1~FUHr?XKjaC~mqnrrrkuxUj1=N%N^K8~d|a9+_x;i>_P_4wV+ z#f_O)^;JkTG}Dv#?33?$ex|@HAw%i;#LJI|Y5Upg(|1vIm72Agi+cAS`<$EGs~P2H z*3w9L853zY8JiCG!;xK{pC$)Ph)q4oV8XtK5=Oq0NmSeY9iWN?S<##m5C(Rd8_f=G zQPU)~TUqAGN`H}yNR>0Qid#~9n~65#C}z{)z18Js*lg6uZmnCKIa;LjQ znGZf5Y5S4Z=Xne?jrIXAron|r>mQHKag#1-g^DL!EfqhgR{>2w--)!ZY{2fyTWeJ5 z>Hazi?scolhTf$nBA=*nHD+7489&wZ^qw4-rYPvDq+=;CV`&X1N`!9bF+J?dULOl- zd5X!_Nt=SdhrfXixGXEf(h2xCdBg3tcFYj@nnV{%j^wWg}op9cUFA1$`qgz(0aW`02dQOC#yt#ql_F>CjhcPj7+pjb-F%B zb2T2a`;5oIXnuG7AvVXJEq~;b&(=$iSlVr%i*gkMXZ?VqHuy@k+J0U2%6LV49<{P7 zt`{)_f8Mo$gPiC7EAvkiz{RUc2(>rzeCr-le<`QX?feWPu|)%@a;;RDa9^%;Q>f|U z>pgwmz>x#H{nsQ1LkdAZ99Te5jb>HXp>u{IU!U`q27qZO#9Dx0YFdph8or z_x);OKJmOt|7ACiL*YHjssyvb;DJUmJ-u#}V2gCxZ=661OJp8f7}+L5eDd`BoZ@l zH_qx>P9+(IsQ)0ZsrKujz-M~V>zWCuoVRf(D2*<;yWNqpyr$n`GsGny^T~PWrhIwa zUnGD!<`>cjY$OZ{cKp$6lM8>ojRZ5}&uAe-?3%2J$NaOd`EPm$g?JZNH+<>`SMMJ9 zEMllW1sugfs(|UjvMYXvYZ_=a(z0u)@%;Wb3z3^W*E^(5J6h8Lukn-kf~gIe$ZF$< z9)Xv3sBX*Ps}v>8yDn0awv)`gskHL8jdvrGGa!Cd8z;sCTdzGCL`Mg^%AQn9V7i@WIYEHeR;(^T_i&A+7>Q&H{3D>JVFa*V& zl90ER6frR6Mnc&oB-9oFONV7&B{`=dt(S&vu08{-%NswS*X*Q0%Ce*P{K;2#i6UPV znw~N`1sZg(j`)-WpO^F#JGVjaW(zPTJ-1Ik#JW&r{vx?64Y>tRfA!Ck!mPTUJZn$; zM|myIEH_fpn**ZAMM7llLMUQdy_h*$OsfLAZ;%RgPE3F`FJ_`yeEkrq>;4tb>C0OJ zGR?2Dg+J;4_#LRh9u~t2&|9kgh@>)`HA`b+7^PTXr@bax@oCeI-u$;65S0Ry2~)zr zBcwXY{2QwPq92Z7{)Mvw0H^3!hOcs=w!!=_5i6i8;(z?A07*C)M1n$Ibz*F=2gBEi z>a+Y?`wY>3H;?5AMvSQG2`418x`U$;+A9n_z$M2b~(@n130=_{qw7cQqUAwxlLvIP?aMt(xh(Tru~U!+yZQ=!pYI* zx~AgXYl?rnC8+X6Fv^5$xomvo#N>5hdlBl6#>P_$lFI%vzjRu zdRXJeCh=8w-BK@np!lRf%#Bz(lW_O-gko{xo96=?Dme=y9UEFhz=4%!munPE1d&|IS!JM->_#2ulvy{I{hB0G^QT zVau%?8qyVDzx}8vOz^k0tHeW+V2X`+N1CPUDevx0e@;RpR@&nektdKZIh0qjq*-;X+@Ja{hc4s$o3n6)*SUz8QeV*Ud~! z$ow;{BuOD#X>P|#;k%8(MfR{gl6P0(Fp{v6MKSLq3ok=k;pwtwj6V^M(F9l50#T6s zH9ccn^XMVV zTCJf7+8h#s0MgVgSIoav}kk?VS!Af4}J(h z`Ch0T{d5ujRqls5^bu%mJR_8Y6BZysmLe@r!;Tn2SLh6=#TrRA&OW&wEIx=(8N{rN zimU2mZSP=6{KzqO1$QqVpcA^<$rRKO@L#ck6i~5B{}bDVen6EV&C939zVnT?tK04^ z0U!)LKSt5gp(m~>0P5f~JV%uy^;5_;uLvqPs*Cij#O|QGV_UNsVpIPnvVW-_o zCw|Z@%YO z_(GL%$5y1tDE8+bO@xptVvg*L*7&UVez3D0#$P9g+%mZDhLlVC4;QzvDOO}*6V@Kz zz0QQeXxb-eqLG|c^`O;RkN@Ni)$ZT!6*6gzI+67UXhoz0VJMP>=|ndkI`TVH;?WO! z(GOT28$^Pj@}Twom)wDn>elE#S^RIP74F*ru|&b)lchsLegCycR`C|4z68L3nAC;E z*Hu@K)qni|^XA!?H`vfh{`(Whs^TW4`0q#_i0~&MpATA$*RKq_aH>D-MIwEISk2bY zYH(SP3ZlRw@Z@A>OZ=CE`wcZ%|7=;xe8A&|6@u$`c5UMWZoJ_{Ap8F4xd7!f{Febf zj>~c!x)AdumV~Z(cXnuPRhu5HlKZDChH<{fy8h^oH@u4T|mXuk`Q|{LgWN&)!mypVz8{-2SR|G|mq{CgdEN96kzl zE$O=dt&JT#aC*avB5a5h{y!G-zjbMTE_o^rv-Rp}$w1~3H?O6Hd`w2Cw}v)VKP!mY^{A!58cdcUK> zdv|0}NN#-lHwrFJk%AK9+h-W*A6)J)FYhB9WnG*H(`Ya3^?#X<$E7u@=TW!@+_u9A z*rv=}d)n4$W@G{j-cRLS4ez@@#yk^5!D*7=4B;vR7mmaIWXzU`&m@Ca=4oYdD>W~_sRH2VyQ4(19h8YZWcBTYJYt^_3ii)?SG}1BQ5~KS_3b-P z{_=U2mxr9(dUzh%9%ox(#BiQbn{Pdqei>ckhi=q%R9Y?`R2^3A3Zll2b>4NHy#Fn8 z1QL3>p1Npq{~Ipy6On1N*y;Ne5)5$-cwX2NuKA81e7)(>6o+&pGxzaBoHQ^Eqs!4sLG zCQ_p*fOk({Sm#?9+e$^ToTazcY_S?rIkD z{Ds1yQv7;Rf{1VDxYdFAyBhKR=2b=i%y!>nA;T;rna9pZ1_GELoJBL;O z5VpCw1785vd?*<9dg=;cIiY&!rrXcKv9)pyy(*iM)l;mg`Ub`1Fx#lBFQb+=W@SS2 zh=(+SxY1CQ;~NZN6kNIyzE^UM6zR7=c0|0e#IXQjO~*R(iDj|W>ht>pBorV0hf~9f zP`Vm*e}}PxqV_5W{&8xDsLc{&^YLspNjrNUzumtevHG}S(x|?Y+MQ9xzxSl8)h_XV zVf!C7mKw;fFKq}t969sF^UV$Xrc^F+!C607>Npe_X%0Fbl9o13UN(@AGyNc&&dY@t z601av>jc@GGzLF;+c#_<4>L+y99H~~EIkD*yYG5Ibi9j1Pa9dav<38W4Rt>EsJMG_ zCmj@>?FUi~FRmVT(!5Wdjc8G6l_$@r0tyU@`i?g<>|;I#0kK(Jm48bYA^mCrkH-(R zd=WfOj!W2w|Bwscsvixmxh->vMhgNc@n`eEyfH|Q0*HEcJF z+)Yyk%DTs+@)EsgFMiko%L_8KQ!})8pK=ZK19;;X&p{f30t=>?5+eB>D5mdwKj6GB$2ClI4~i!w*>7nB%${C9z65pDCXV-9F$^=7lhNWBzPE zPvjA3@L^nSST~n|95*(2g2~w|#6a8Z4-+{t#ZSUi96>25Nqj~sf-g}jrP8cs=1S1( zds@Jsq}=<>&wBTxSCFm#NV5+ImAYvRRV=(VVE@|6r>!?PcIiL3Y_6gXQ&w9~R@{b` z1TgAe!2Zsw#4c>O?-TitTBJyP9_@LVFePZs%;P^)IO=9&)5heI=F6CPX}uAEnVnHr zPu88}F+WJx%@(xiy&<_r1ZL0Cf`JWdarJjY&eiGS2=Se{m4*602|5kO3*ep`qkwg; z+qZCjE)}_h4!cg;wdy8uga+S*K&^r0@<0JE-|LXad6VfSBTgU1?m16;xT=6w$A<)o zv*!7;g0;9;!y3N&Dfc0k=ftLmiV?8SCYmp z&$qGFZHobST^?0g8T2&!K@1%}9v!unRn*upKmK4mIh=}D2Wd9TN9z;y*>-^RW=6lXYZ05>DyaCnQ@ho0{T9p>{o#0y zgkDoB(;<4LVOf80Km3wq1BoWcYk#B5RK>v;?y1Ga{oBjmA8qyyrOJtUbRlN*{Qc9S zd2tenVCd-I&suC-ZYg@TGmh4dh;s`dWiJf4cym^U?P}F0&OTBTtTR#;gZ1;I`y}HtcqeX6cW>OBBDQ zsqa+mq)zONvVBR|uvUKc4>_B|GOO4NBzxTx_1Jp0JZnR@M+7#UO8aTVR+pTB=Cu8T zT6ff(?HCwv;C_X|?`Ta<>S>bzr?_n1$c_H;h|a{pD=P!r%@fXEle2B+v6Ei(3SbW_ zbttAuNcPvfpcSEjRXGKov%yjQU$-6MpnI?l7z@Cf1aIV|=;R|uSM?_bP(df~G+1pF z&!oc-M5yvTi{8#-lZ%hEpOtSGyKM6*QfB#pqA`Q$NlF_Rk-3`f!Q3oLoXoLJ^zc~< zm>I16u;b}E-^L7$<{!3F?iN`pD=nXXuU|z78sl&oJ)A)wo0T82IcFwW0BTb=W*4-g zd^q!x`GAY5JoQMlaj+>;deJSjuc%nTHm&hI-7wv)OE&W|WP?Uzqm$-c?7L*lLP~3o zTkkLCRJvbeAJ<_wBNX0S^4*8e+cYKv|G2kYp-DXS3F+>t?Csb$7^qCqxp1e5M39V+xElcEh@m!vKK zzLSsan2H9wTp`y}k8$%^@{WkBs4=E0Rd3dN^xtF)Id|+y?$!_N(>10Vzu8)iPBv{cj87b_YRsLjC3NzTkp!_J8U;77DE?_jxg&`=GZxLS!xhv1bg z1=QDhsSg^ z`_^4|tGJl4vMiDP>qQndlOK`4D$v#?8tM`3!j)y0lA!(tLP4u8FUQtakM7fplIaIF zW>I$T!JWxX=wK_*?l@;S3(|@fd!u`SquQ3FAk9)ih<9CGzc;Q`AF`=mzrvY?cU8D>cU~% zG(vNmc_zii@0z#YX-KRr8Ta99jM_7WwdF2qfavE=6I6d$qk3sn;x_)yI*_eBXRD$2 z78gNDnnW>ARXhJEq1nNR{GL7&|T@p zG*$e6biHL*97`82O3)w)NpMeach>}Wg1ftWaJS&@790i(?t?>ccMIY}6S zxzFx?Zv4bJCV}yDqm9ZcdYuU~?Tp;ZOrN=zf-uiZ0v9vYJ6YfOM%q^u+xgn(V9i_k zMoSrzW1UF0Rr(K^L=y;o`Kp;0?u$dODFrU4WujxE)2g?*t!PHD2j0{RGZsu8CAP`e31DtBPo(IjCkL4+q(lhz&q(foR zgKmekMw4AkkE0s8w{_5F>e=AKya?=RC8>U5UsnmCb-Zf9v9X_f8Y2W^P9rtSeODj3 z(AC95Jw=gn^bg3@9S4fML$a*w($!A0vC!zKWQt?n^1If4xx=t?PFYb^O=8pnem7HG z)1bhl3w?Y;d)m9cBgIb%b&1^d`igXsg?&s*wK~}!rD+)*AI6tF_1RqruS5F1r@ z$kj(uLqugaX|J|W>4qyKhd|+<__d`D^R~2D9gkeJi-o1E>E6xmnwl4qXC?oVdTx9> zWul=ttE0e>%=AW|(2At&VpZsmyjEGOmyS8B(F1aXd=X;MA zf;f^J9G$<`yt{I?}U^*53KAJo{9i2sv#IFL&}t+ z%E`80!gVMs+OTu=WpKx!T>(76-vf`TGaDVQduMP)@1&R0g@mgwxWeAgjeb*lea)y> z{pKz*?aYd4f;aV(N?hhEhsbVw$JDvfJ+99Z*VeKq(D5Nr!KAR?UstNG10m4l%mT& zrZ%XgVsb`Ax##E%Rs!%7GTOKBkK;_OsWfm%1nY(7`e`m8rgUbeJ|6T6O8B{7-*vcL zE`RTtsQ#V>yKk>SV4nHZe=KBfv1hMAy8NjAs^Jt)CMZ+kg^Q=Np0rb1@RLz*D|SgR ztl0s`0_o^=)N+_G2R+6N88taOlK`2_4|&d6FVg#g+u#rJ$13X8^UFq_N%s#Z)63GK z{({&xZF^fY&&p(tqNDmJnKf+s`gnTQKYZWUF<~p05R{HbU4c{K1!CgJpPE!>O^Ozn zaY7Iq3Vn6S6J$^l%89sS&4#bP?JFYKS?_;MaT7WDz`aoklqsM}4w;shHeZcECQH;9)}E9Mgh6wGJ6DsBGv@C-hH8Z5_ut2=CJLlC?91!7%s;Xx zp_G4LIu0yB;2og4af{_r{khx!xKO3@YpfkxknL=|0lk=l%A{D&(pEL0+svw)mA@E| zwiO@Buu=g4c}69_`)#`He07Fz#vh0V?^=OZrwrm{wzI>k7^P`DI%;I;`I^~$fhhCb zPGT}afrm#Y+vfTigRj?qvd^ZZfo2`BIx2&oG8s!80TI`U5+c!aU|n@w?Qs;tr{s2z z^85moN=K*XYppg_6y01;@;io$i;f%=Bf1uj7=)%@E+ zjD6F?Pd!W98LqW#N(ic00o@p7Flk35#e2_4q4dq$jEzOE7k$jU;Jw83E2mlxjCEsh zS?6U*td$-m#Q>9bT>wnfYujLMuc+NNPglW!sBRNa1bNIP9Q9Nzd|&GHyGj7&Wpb=r zp}aP}p>DmOo3~|MzSBKP8B?WpJ16Zn_+ff0SXX~#Pt35zFba&hE~GLs0#&oBwB(t&0?nLf&xhg zaV@8(;p`z(>>-WFt&6U>cMT}uv)TM*6bg}#7z%iWmiCG*xNoM6TbE-iB%hM0L*;B= zF)K$TTU|}}UQ&yIePuj@O_3bid6>>lhY7g8C5)($JEErKevo;OpR!&@YcvmtS&<{C z#b8X1tXSJqa3t%AEc4J>phLN?GHD2wM??SCp)@z4P7FX%lT*?6dYaTzpQzbn+X4lN zC^JG-vxF?rJx-Oowc5mR>04(!fg=namqKTE%k665HG4+hhL?yrz&oPDqv^K%Op z73eX=b@6uV7<@)3^BS(|zC0fws}ZIX{GCz1=n|D@2uS%Q^`X+%J)?=)%E|_CHg*)v zU=H8_CQ;>b2k3Mr(4JUuwW=AP>QYBoqoAf$bd8gA7bZ$1gOE{mHE|NJQpcCCYL7^5 zJ#4!VH+=bZS^RN6-#1Kat;F7LX_xoXcsT2-*8F&}*KKUPcQE=+>nY~}uew~N6_Kb! zS3Ia`u0MLT9&d4>U6CyeNz%YGkj! ziSh1gjjEbJV;?xEPy0(s=z8D2&xNTIY$>KWq<*BQKRq?m_MADf5i$jwRpF0N->XZf-iJ6`7yO}uCMLoRyKHh3~zQQkWA<-YT~dHasJa2 z#8040!Sh+YEOFQ*e9K{OOCUdT+1P#)b|Q&x4< zZYcg-7@$b?j#oh-F8-46x82dPRcUAA-CNV5klA4SF848S%E;5BGfhoJak;Qixw^(p zUr0AWZu|A{#D;x!<&?m~7v1d7d~h$A^xV za%e@duzVtX!=KCxRB5Xw1u^ZcM}Rq?=d6Gv3jwZ}$)dsi8c|47Cg8DN++h`m3 zq{>#HDtjY4>Xwi%INT4-;YP+K=QY7pvYJp!A#L?y-$AN{U^np;6J7_xijI0(vi>6a z6w}lUl%jxJt?@>^%{u!w^EvqAi1Lyu0*}nxVw*P5>u*Vwg=M}`<>x=5+PvsY>8km` zGdSmd;Qswiq7L%ka+DlaZVVlfI z)(C=VnZLUvD(=hA3LZkhc522C;nmWErCF+^#AM2fs`hwCX!ktR1`b%j%D2a(H}u%g zphM$Xt9-;cgnLN9b{X;S@I=yL(eXGbSHb|Cy?soqol))fc5%sUM`~@KbIK4MacI8mZ{G{D{O5 zVWgMm{l@sIn(RZ}oUHpzu7iHM+ZM3MaI#+_`4W65r< zDJn@uF)!Pcj<@Umb0;3SJt$Ap_}T0utbvq_vvXovT4Iwz*B({wf-KkU!rA(zrlTP- z2N=AN%YU&b#AV}~kWqYfab0zqp;%p2M6RdM(r$B$c%(0;={UT5{VB1>wHO3_*v=EQ z^i1p-k+I&n&)2x?;5UlW^?lw0tWkN}0x!rO08TO%S^qv0}~LTqb_8cFd~jaQL+FHr>BuD9>{ z0JkI&&dCcqy9?Z644ItP^F`S&V1BS!;8=s|*s9}{V)x+moCx*`(nJjIev(g>aQ^!I zhQjnb^#|9b)XE^ZTzIkTLw35o&-xE+g_0=S_dd8A=&q{9i}Q;Cul<>_9}}M>M)w-_ zZ(>GQXQ?3vr>Qp0{9b&6O0EYhhbt2s_m`-JGlw2L>B~M(&|YZScUBF7fF^z4<*{C< z^i9Ry&lNBo;DTk2BbSSodUTzFQT>OuVwMCmle5kr> zgi^OH6P>G>5(phtn=@G)Ywd59%_LeHR~NzkC4;30^Matw!LwW^?O^r0r9?lPuXJ4-Q`ufa^$3_Za_BN zu!3$r)6^r13`=^w{V6YJ^!~Pb9|T(wOtCHeibLyoxDV&Y|K@Bs}FvNWS zogmw|N0gW6RZ-OJNDv?CizKZE>&57VB>*^s7^DVY z=k*qovBd>fypn1)`AC$vTY7V?jqOF;GzFxjRvEd*-r4-rJz<*JnELdj89u5%v%IO6 zTan4u%Tc<4+Cs5ie@*hr5 zeE;>Q33*Co`_+y8b7KGKkEMJ1yAKppI*R>2Wv#7g7k?6N<1Dg>*XP$ZS4qoj)yAAW z=+wIVN?x2`y}w4blw1Kr&K`_zpT@W%h1dTAE~8aVA30O?!*NEn2=RYsl%#F-W9qeN z$htB?Pq8#L?Ny%KeS7zIj6n%CBuUeSl`atn=p8o*7PDq9L`#Noe?ix8WY4@y!hvZP zn=3+i(<42|Ld@9+s7sK0)4-8jL>X-))I6QQBt9CoePM(Ca^>Sw>5qoj+hH*Dj&TK$ z0P_5TKx^A6@(-BD+QHOhheky~@cP=-P@0vt55@Xamqt3VI!UQ5 z4V(1dSY<%JOi9cr!-qq6Jy`@jk1c&cDFG`%dJagKKlILj-hg^i=LeSx?C;`D=Yh8d znH8+7`jimOV>=SC$i|N<(o(E`}O7 z^k0@T-u&H(e}q)-d(}o=cAVVxT>KgeBZ$#PRQ6-hB{IU43{G`4H3$^(i^c&G+Thq& zrr!?9w1B`VFqn~hgZi0H7e4n%NeL;@ukj57P~Bk z3#a+ixUU12>R!`q_(}bM%q}-|_?Qlu=jtn+9t(~EeHkf4;I)Fj6#CdNmo_8e!aTiM z8(1G|>CgDmTHo&?BVn%HL+Q%-mhr7{J)Mt#AO}fOW%`eQwiWf_E3#JKr-R$LhAGqn zc|VT|Zo`FzyL+LZTlz0e!VR)|K9|SacyCbpYkH9}xJH++uIT4Rmjh=y!R}gX5*1px zelLH05Z#8tts|z~a$?3gFhtYif4qc;l{DDT70>Y%A z)kOvb&HNl&WEL`d(P=9WZv@)hNT0pFHR=nUL65*Et5_t(aMR8<9(tVlumMfu!R zi9VVwIKDzk(qKDN z9MLKbv+9mn(^{sskCzXcc~Q_#7-JD3uNdc5Lwa57y8?p$&&AWg{r1%|+T`QGTqq25 z+5PSM8;T;GkJ!!Qi2oWoF7zbn^uV7>^uj8!Oa*Kj^r7=}+|s0pjVijl&eT%DnO|jt z{x1W0qbLES6zzn+hk^UNQU4lMRBhWwcX5_|#!=WmQZBVfm50>YPeEc%$%uo~a>`AZ+I;~6C-{(}9rbDCu8liZmNM6mxBM>n{0``*y{^#++kYSc)`M$b^Yh7*xHKQ%oaN@FZCDI z)P$$?bk8*bxKq;z)-v`j3Mz2ehg_H%U0n94t*wuXL|()(ahP6~BS3oUBHeed{>L(@ zctoqLCHnH!a;)!H;M~CG;%fjvMfo(o`Wwuu7jwm$dK9q%Of$l;nPNkLA!2eu1`6`4 z8K^${-x)WO)aZy9QI1p*j+8B&xtp{Fk6mNu%2hd`MmsRdacj>B|4#WaeTT6M_fLhG zk62TUo70}A+p)T`tK_oNQn2av?=6FHc`jVmuXuk`ihcU1f^UFMCy==clLp1!9VFs{@VR6nI|K|9T(5>)7ZkbsHE!jGsM=ZhpbDfx^eEH zim&35HNVXIdth*m0X=50%s=1#lOol{kgx4%d$^UwJBHg|8wz-$)UGU|=R=Aq*uKFM zl%}|i&~RKLt)hr#M~`%DG$j85^UsW%B+HU$UT2Pxir(&s^9J=BS?I!6lCxUM%oVYP zC5Sd(;~--Fnfb! zR~}SM{<-W_t$nnlBg}8wp9P9O6e+2w&;1Y>RxRf~9>(IbvV_5UxjN|Cf!pspKn?xk z58Y&ct|tHmm#;#VSuNxC?*JR?oPy|Dy~)BBytLOb=Tb@v)R|7PBx3@mhX|2nCVG?s4#XUs=O!GZMwmf7N8;=pv3{`YnmafE*#hGC}t2=h<&ufO7} zSO4acMWL|6!jLJJD}5>0e7qXSAC@jt!L6f2nOFH=S-BSk1*0%4(sA{F?hV7d^8Yj( zxC$GhGhk4Ij2lKvSEOS#nnA6hoM-dby87#>WT5}U+eY5qM{Y7mL(TB`1|~|s$XyT< z{T)kd6GoyuX+Np{WJlDJ$C$)J+aAGAo_8;;Hw(nKrlg$Hz^st^R}OBY&dW~1p)s>c zWgNBK$-_s9Bq!IGb*FKZ1@10gBjZL}PjCTk6fL8}q^&7SYs`$4qQ$y1;Qx^WEI@;? zV7u<0q%a1E&#6*Ze@8-=Xnj&GlG3Pi=t`E$=<}ITNYk;bjHpm4A(It#4yh%_i zqlg1z7V2=5YDL6AD?qQ#NO0`1-T;Mr0{} z`Y7<9nL}-df0gmhJgQm&37i2c{tEQ08$1Ud!kXr?>!PV>1MP`_K@}&|Mb1Xq6pEms z;wcvFw~Dk_(k&{CiPD)zLzQ2;zx})5|0wo<^`?{`7JZm8St(!|3xqdPgwu#PBdjOE=(r4KG^T8g~=#Cwzg|tgY2@?t*`7>*hdTzH8S5e(WngZutI2<~YY=<>!< zr;N-_@%}`{5$`E=3#X8Sy?wx1+}dtpb>oc$MCDNal$wgQXI7;9$!C|~o#<&uDH96= z9ixR6CPCd?^KYq!9xFpv)22gTDZzn@T1?;WxyQ{ckr*sY`jg5AHoys*s4Z~rikCWS3V0 zUud}OI|Y%tlAswELNALl1)9xX?h_H(?i+hl8}64wkfV*`lm`(7;Kfm0tTY9K$9v{tAwSzpj};I!s1pBaFn^z8;-KP>r0|~#KjN{q zwWR>}wZHmVSVXgVldEh#md+{=;|ZxU^TqBC8VHt>lI8Xj$Ff;*XeJ;54=!OyC?$TdV{s(ubkscJ!4M`36zR3K zJw>^;1G$3YbQouBK41$P8@ZFuwMq7?LnA;7DCIApZJaSuIL+zwjG)Bp(M6A{vz!l7 zGHO~@TOpn7hc=5hN#&VT9G4Gu)x^-EnU74`8yTO&(}?A6*RAvQ^N)s@n&TZAY^-ME z(EQsJ`+H~Ia?A{;BAEY5VR1ZER4pOIu0107$b`i7TCeBl{Lt3WP%SG`re}N>wlWoA za6@fP9z@CTXOQwU!l(=4%plX`eANc=v&-{z+aIjLxVN&pyy)q}xt~pWv4rnj2l{%O z2P#%JD_(^}jxPzt`nHdM3pTzx|H_+F7k1#LC9=}^m=JK$3upzo9jXWgSoWm+lDEvVb1f3zk0 zs6?RtT*7L~w9}tq0!YjwK*>ht`c%PlST?lbeaFZl#Lw!v1{ZtV8hKXsh-l*>pRDzC zYXXcso`Ul#^UIjEvWCx)lPBS0{zM;`F=-?0N_nh}QFWZve$6$kk03$LPiJN8ye+G&xKu=Gfh88MGO93`{4G$H0N7j5R zE-_$Zazj-yD<@Jc0+p0xv*LT_lLemF^BZS=I@8k$85OPi3&X=1Psc}Rhrf4LYqYAG z^J0d+BB&&!6pxQ-XJmS=g#c$u!>a{-%)1lXV1uZ}zh}$z-wB8rx-bOCMgBfHs#=&|N9Ple9!DrO|m>9v4nN*X&2 zNqtwfq*mig+Y+VnT4PpsF`!obZjDYDtK|hhZ$;lKGfJ!(sFcGWPXNw-2Mp$3URPC@ zo=HnU(w;emSllk1d<-iutz(4el1=T-)|Z_Jn!O?jj>38@*2S$bBMAw!Rr1=E{w==X z<;B`$#%)-XZ#Vfm&lYO&FfeDpAJa8$xY~Kzxb6NnNpd~ z?QsO~BKg>~H(L{w|I|uBx1dOL88{q4fVY6nsC~M_Kd>BH_dBLd&-;4Y{|9o22aihZ zWqbb!m0bGX)}G@zTx=69R)?9CzE#Y)G34?5zO*z`OM)_a5>Gb1Ra>JVuWGzIUdVUL zgw8r>x{#YC`~c=7IbuYDq;5uYnXWbWhUd9I0MV=c_~o>`0|w4DFnK?u6{|SooQ@r zL^Y4uHz|V!oOeQ8X0|8R?}yZp0L>` zOlp}YAEddmSYI#sMocsO#hdvUq$w*tLSwIf|wea94kF&Ti??(Mc>9gB*Ot%5rt z&1BnJtSnTFVPx2DokVMWiWSSAK$Ds!ovKPr@C>PNDX4qCa zIAP8_PInCMU>1bf#DHy_r3GDugjLcZ2Q@eL+bqV(~OFk`H_v2i8wx{TBO3U8{fEQ5p7PNQZD+>M_|yn zW~V9vG7@D;QWSDV=|{YSnP2RL(EZpxr+%(us4VUD6kl!l>Fi=>t;{()fAh#92_y2= zPX|`+#+zo@rGu5#)O!T_8UxYpcX_=LVoZ_rXw^2$<^mR}p9#DdWwm|AnA=Y93}6nu z1-~t%XPmogKl@3ZBg6jrw9I!`3F=6@m(xe#3BtrTSI+*gx?VpS8RV0p6+?`*!v44! zU6X;-ZnHlpc`WDO5M5bDpM;UF3rM01Lu-nRcSNtlsP30?hPVN;Ht+QwV{xle|T*yiv?ya##{28?vc zfZJbcW#o7)g^T`*x$DAcR8oB{1G%pkde?wnCmjv0=a<#|OT&BU&Ckcjwq3f@Utc&_ z--Du{3N;Ga&(bh%)o843E~5Ng>HUkf3W>t?Lyz+l}o1_rk)>IVE3?*N{& z!x^^;0wn{Q?su(MQSrB1}{51va;^UF#pl}N!iIKsd$NdG_AP)FY^aKq#gNY$W z=AZ4ruk4lZ$C(QLLHF10>5}x}b{9uyhAvUn-@n21KCRjZ!{8cF3*-Lvm?vi#^jO28 zKO7u^Jjp)%V-{9)+F^;ppfRJlJok_h|NSj~Og3m6>R{sf-f$qj{zFX|19!aNiU#jt z;Gzg|MgR76>4Je$=kN9ZWvulBdof2|P761M&)nKUYj^u<1M=voZMIT2%&{Mz{`AAa{Q@Ny zOfOO9ISIB57?t4VAruEaWmr!!^)Ya0FO5ad|>{vV7J)5 zXx{1LWSW%AMk6P9*Bf?z#;rV`1HWRqJdm2Bi-&bGVUD3_={cOj#3i!^040 za_xOKKLabQ$HgdUkdc7ki|e3t(tJLe?wB{_ez#txE8;>4wc7>3Kf{RiKI{!*7ZF0c zrYyXD-NOmIJlQZ7c_<{`T?}Aaf4p$_dc>s`7-Li^>u477T&Pnye6B+nGLJcZIOv5- zH!@PWyn8x??zpZvBSS;Ou{z^g`gB?BXu*j5MztbgMrfwfba5!J@Uo;I!9l*u?M)^1 z5VYW-TEo{3Y`*U$!>>A{glxFt8O-4RfFY<^x7krnTA=njRG{AHS*Re3tr;#EfN-RH;m9~KbL;k)Lwk{hYYknmkpDMv!` zq&04F@i&9D9=D{JBYh_yqz=4I0XaSVnGMcYvt7a9hg`~4li;{|F}by04sp_!=kHAD zFIw;5s*qZxssMt=%m|b}+BE7rq~!{23bosYv*|Y|F^n)UFHUr@c@g`Ld&eWS2?(tS z=`F=O=a?^oBzlaDOUj(&5#j_AKdj2;N{rp8mk2)?majhtM@>X7bL(f*ZQ(WU#4`mw zW-hcl=umTLcoLP>EZ9b7e;BZ50zDFxd@Xr>=CPS}1RtEY#gKgaAAK#yHhC`kZyUz8 z5JO~_mjUDjh|wtbnl`*ahbNn4fXWnht^{8WhaiK;n9OExZ~9oea*Da&NOSTlQX*O5YbUBpBFzJ>-nMi9OEK@qhu2)l`xyLxf)R(}P~1hzT~+qpWI`eGrH15WVNcb+DD|cy9Lj z_zVEB~LhmOHj9jgY5izj!qD>PsuTk*5>)*wu43( zAjJH#C_zH}eGNEF=03#VNm6BY;idU$f{AuM7NxL|hEMQmGwKMSwHd%97?sg^ZNFx0 zVEMF;?#Er;`lRk~agAOww$U)&%W31AHn`rA-uB|SaF$6I0q+58#QR|3D*?yetOMXa@jdI z2JY+LbR|`LXQuOeD|5zJ-onkxmQ5TPcGpMlcuaxGZn>v>6@ zTAKSFZUe+N@~pPL{uAZZo7YCQDnu+$z{a#6K`z>8+RqyVN5Q~=^?k^&^j*?kgqObx(>6!mZb>k=qK=EJl9^;s@DH*y}uhRK|fqk;SKeXV>iIFL^-rK1Cv;{KW z@XbpHBSk_(S`xMZaK4qaPYP|Hevq+b0Z-0v!R@{ekJpn~w@Cm_s z4`r0RMXgaRo_Hy#|5RQT}TL*X`QSfmE9J|D`+Wx&3it44l_^9f{Nj&wob_yx! zsQk2k4udy%{Z1ETArhSbvS?xk7&~rk`Ct; z>!#zeI(T0md6w0rP`3oe6u@Wf5HzcGwtPwcV`il#zgf`J(HHqqJ`jpepzU>=rUCAP zvP{rGAq1s-xb&cDe>wwzGPiMb2k}8wwRa%F!rB2n4(j2L^{n-Jsjc>e<#=z-nUg?w z_3JL`A4hMK-Wkzw-z^Jh#~!*PzH7S28lI)Q0Q0-kld8D6Iz4t13&1hwgpuU>NL3vA z6!H?Q;t(r$Cw(Wmo&2B*N@=>Ud1hbxDPL24e*Aiq*Xh^&^>*EdzM~t>GesGr$H1iE z;L(^;xeU z6zo_?DjOp+U*+_fhn|e>R;6`*b|?b#6vTc0kd8+CVf&A5W$AtEQ(<%OG2wd&t@Up# zli*=B3JGTO0H8Yc&aV~WnWpW&Z*!D#nDDvGpRbB^nl60TAm1S_GEkFbg(}Bn?7E(g zIabso-(90fyypBXQp-a>N#& zbo!7AKl$oda+P$8Gxt**GEle@YDz^RL^U}zITedRtI<hgKhF`@|512P~ju zA&3g=@PIbivtq^7$hXS#jkzjrTFrfdu}8F$V1pw4ga^d$4%Ey_p)ed9k$pDB5hm1k z20h(=cT?a}d0%6YD|pTyU_Saz=@8Tc^0U+sr`w^ldvc&)$Q$Y_7vL{q#mw{iT; z!BBRWb*{XxGlyg+6#}Lq0oHNVjp(-fP+}zE(+eoGmKC^%Z28i?`J#uWI4*uJf& zV}zB8nadM0(*9zgQ(QCho!B5jCKQU#_`STWIA!l?b;Xt2A=7~t8>M&OWxUJ>sH&Kx zZ+)5alMj#by8xD4jQf##W4CO-X-vDG@D=k7 zs$q{8!F-ay^A!eNiF@!`aZEP32a?{(Q{WMD$3VqLI=+i1%Ep`M)P;~G&xfTCN>0BU zh)%-v(}n!th{j49`M*vEL{uBapr zX7(4Y$-XJsJgbd{B1pj zIXldEdgEki-zg^?#b57qkO1$V&XRly>=jnE4{s?Fv-?-7i~e|!B61GDf`5Mh{^Q3b zHa0d_ZQXR0z6{VQB%{+{%BV=maqB=4R*N1Q$T#9N1_#<}yDS{`dPQEb%K2Efr*1%0 zyxvtr)NUCg`O3)2Pl~ofy(Y+Q(nyBbL?>he{|naKIl$wP1aoVjq_43)hnEa`koU`R zcr-MS!72C@%OvDU$0wj#H-1dwot&6lg$I8uqz`h~d)S2QgnX9U@VI|EtlP@*0ts4W z^w#;D3h6^GM_wSjzF+l+GsY%fT)j7P&e@ME)*dW=71A*Adk6)*j~Llx=j4nJ=-+xq zM0&mq|46oLmHz7nYf)$8H#0hC1G{o*lXKiIG4}2t_KvB^H<^H47D3v@AOgMiP}vGoL5_QFXZ5We!Sx&@^Wf= zlBBx*;*==psdz}uP=vX2?WMV}C?vwSEIq}B?xi&85*07U41UACCr@C1`QuIn+#Y!P~JSe!69I1k1IA#hnjJ<}!mHHZMySIzj3@ht4^Jm0pe` zeLJxmi+%+{xjNPf@tm*c4-1f>#u!zIm=fEFG-_EV5OS)ZZdAJn4Pqr@0k-cZe_GCS zu1*NKZzsCxL4o1Z298s*g84brdafF`9X@Uz4CMnE&0emLU#)pdfrwQk;H54HR&4}L&r5(N<>feqmNl+w(lyG2j7ZM=>0U=Z9P_J zLM^AWH_^##a@Vwt{!!oT@c(8x7ZzwQ3OVr4DeyTe^(FnnV=V*NH!?GY9m(c2xqC$a zYz5TX+;(JQI?eDt-WFAC9-cP2KMGv>5oHVbm1TzALt;kq{(*g>OiWFE765bh9S|)| zOUov)bI5(CAn=qdN=pkLw@Nw7`FErnI;N6gncL=zPlLyo*+u;byI-%(jYfx6Bh8xQ zduD`u9%UjH8O%-xb~oj1K5(A{Rcp_ErOM)Kbj+Fbf2%p_9&{K`@;vq5OYN>T0rHHf z=hF_$=c}`bx{|SVU$&p^W3f3J&(lu1jc+^c(6_O(9`3?0xDcOO2KV0huSlj2zVPzK zPJvE6QmaD{#-{qCp@G@m9qf8qtg!o+`aNKnS^t{v-A?>@XYiOe@qbtVWG7${(qed> zIGevmc{$P6XgyHenv&oyuzOWIM^Neq4RFzIuG3?lknefR_ah9-;v+)uV2sO)k{8`m zwHzniU+Fcw*^YJ-f?%16IZeCwBb?RF$;taVJbr;i*5!liGHzP8(<|B{!RjAST9oI3 z*j*olo*)Rf1A>u|OVROYr}4h%>J9A8=FO>G#5*SoXmlET_GNb92>}{`FTe)?qGeHX z4-YXMZl^WJ@*6p~aJXscX;YxCBi0kHc%?j-J^HgkV=l@C4+Ty7wVPKb zr$jQ5sP*d4ch~nmAh<&p&-Lih#>UJ5&&nT1r>VKct>%_4o|cNhZ&6RNVe_u?FOX}- zqg=CA#o3FSVvdet*;#Z)$W=iTXds%2hTg)`?k@@l14D_Di=UnzZ|I)AE*`LlNbhlw zi_x#bP0;u;Fd3}q^vN-;#wkbPkqce$0{n($#C4aCk3>u0I(7fs`ESwD3rat4@X-2j zrurZX#=84Xi*X2mFW7!%+NXS}(*^^vwRTv+`|7H(b67LRn5b}9<)T;7mRkOCC%)t@ zX6=CO<59V)A_md@+B@`}E-fhE6iu5QokunI+sM!X;O5W#x;_s}hrq@P{L@1nJU0^4+L(|H*>=A>xg{agqsLlrcDQ*~0G8U!@_Q?5XjC0t zCC_21Ng}6?QNoS1zjE-9+MJUy*IDf-AC7=}R#5iG#xCR*%6uw0!pI?(aeYmPS<5Ej zOZQ8TicHsVJzDFNc*j2j)^@tN$kSUu#utLz`*a@Z($C!V&BL$WFPUdjg>ctQs7ac# z)>MFc+A?zlvFT(g$?#V}TJsA8G8Jlu4sBSUo=6b-C=yjsal6c*u_L3s2KIDhUiUUHI= z897;JOjJV;xjkJDJ0D7&Ec z;x>O7{{rbf?W3E3+~nHS82=U;bPD?gXMp5^c`$_m{yR;W8`N~!Wq?hGuYl{|-Kkvk zS{SyItDD~u$;-nrw2TJ?TAu;%ynve)j3G6SJ`aa(OlRPW#I6O%W^M zzw!L)-C0{>32A_RwXNY}AIZV?|8mpNsP-Ye1uC@g zb8`wp5DU?r`rmPJjsagPD=Q_?Nu+^J6l}&j=*+9;(2TJz9S3CgKi|czCSkfEf;PJ_ zfd)bTUWnR%a@MAhUBr@+Rapyj|I2N|^>%8B>NdhSh@i!QhW4J2p+ObHP_t8u^Bs%u z#2~IIhV0eT7P4khwDvEJK3ykiMfZKOB<2OxFb9o{uM$^PGG`c0iEKlEu{}g_YzeIz zZkaJaDa)qxtI&lq@v}M_q737C6HMCwkyPd@vX8LJV!pICQzG{!i`%tlHqAhPFl9Kf z)vTzya#8-v41a}~>zip#*6*X~Z(2pV^1XU_LbxLOgPw?^r%IeAUl7w79j~z!cG=Zc zQ!&+E_f@tw7B7Vi(ChB3XJl$0v-#}N+IE72)7B=z4q?Bz%F&}nJyPfF=JTiX zUKrqDa0g73!h`Cx6H8xr#)3`H&)AHo zZV((9o@0^-+`9;}dr5@IepXfdXwFMUgPvy<{bgZd_p|wBw#L}ur35-|DO{y%c$koa zikxOF*`$7`eKIHOzOH(kW+ZfXwJ(b{%zXo~+y>^|UHtfBJm^+gT}BE<66k{2o6Z&J za?#FRe9Vb1k%uJDe~fQx9NHb@nA;^Cj7zsmI0-yT;Y@iBZfWw8jG*m163~jx6#(n$_Ai_hbS_G>wiqG<*9;V{J-jX7 zsNS!cf&%&4OoE!r5{ljr(m3}9K17R!$Vuzcl-{4wR~x#IzmCkn`#lu@dbpoeZra9c z<6Sd@#~dZqc-$67bvG%VM%9hWZRxN5-0 zr1?Dp5A-d~YV?OE?hDuJhmCfhSPT_e5-FqIe#{|T_n~OTI(_ZH@ewPzf3r?M_vj`T zLo%|-+YZl!Y8gc}XkH+qvNL40}XxSX392MWC$C zv9WYFdd#4ZO;T#vfB<7KtNhEfuP4xW|07hD7UX0nmKSQ$+n!DA1AY@jqoG~PD}&*$ zStB#*tKG11cGaW_VcRPYf|+dy2bx#3n)GMB6gaCeEk%UZ0tP7aZ2%usQaEVKq7Uyj z?-}(7d(|i2 z7|TYPRC%FkYkVRd3@esT9Wv)pj$g5{Ql$uc&VfE>g;n2+3{cWqlfeTgIwrQJHfi?Z1Qv2?NydBkftB5b>7 zupX1bd`ISi?JPR;MEF?qt!JeC3&AYo+%W4GT{6Ylg%m7RluVOWx8)AIclp(g%ux*Q z5>b}ER>^`K+I+q}2_~JLZ($!bu6NvksH43{S|fC?Q*hjXySp<~?tF{d-`Z1wzOP(U z9b&TxG}Z0R{vXcXIv}bqY8xFzK?zZikQ9)R?k}8{%R!R0TRbv9Z8Ey$%BCKE`6j{9Q;|L>Ok8Ws9VGWGjjJlRyBP=N4t^ z%5F1Hbrwa{^^8ZmD1jQksunw+J6CNqi=?TK$$YZ)R=(hLVctsjoiWAn+fGX|E? zCwAYc21%PbL@l&>v%;Eombb>%*rOQUhU}jlm*-)0EwrsLhD7*~r-K)&D+BotQg>2a zgj=Xo`vb|QGww>h49>Z1rmU~k5~jXaQ$d>*S9hkrBdKyR-l{S=-1YwKYHk(lhudTS zWTP3rTAfjOc!tL@>+@zR{KZ6+e~Y*BzR>&0`EUBP6oG_^Sg4SOuLz~ApTSPHud51c zd0fj~_C|QCL<}3Z0(>?IDl*OvfM1Wa| z_kekeNSZd-d!VABX-yb8Jl?fzLF(E0|x+5%g>LvGVgFK zT|Y=*$j8#jR&lgt8C87){ugg^)o2Ryld{4-T`b{R@`Cf`q_xGf!z_!nGF?UlVUY8vov16B4tN^oM=wCbOP z6Ytv&RIOa|mnDHPM+@yDJCn5_!7OKaO1^r4I6@iz3_48U{7Q zH=QgoZHsL{7ahtMO+&~|JoWUlWvdAsQ0>`6$lb_12KdI&C^PxpAz3Jf zgkNGODkGH;8F6Y{Uxunzd9|X}lkmZ12B!S;Q3|Az3^7eZ8g#Mfr-KXEmg5B$$`cx9 z`j+|DoZcpHZ{JTr4V=xOpS@7O75|16<$#Z;H~W)_e>7V@OTgjv?mrP&*$f`t_PL4D zH5>P$Laq3)t1xmI9y5ePkLy&wApeYq*Q^Vj+}TviTkE5$mMJ)dY52R4cIj!$hubf6 zf4)#TTM9Nk&9Y{1j`Zk@l_s}!sTX;zx1SXOn*FVRg57M3K>I*Du0tbJMADBLv9n;~ zh}$AulcdwJHtpzAa}bN%$*`h5L887tI4VDahv1N{sCzm#8YDFtWHf-^mVLqlsQ$?O z-d1MzJO4|fyQ$5<=;>MohVt67vV{g8EHMERs=#5%TmK|<#hLVfB{!N6)MiB5^PhcqNbuWie1Si2q zJ0)@kmuzRKnYGQxc^{EMZ|cy@TEXx!$b$B5#+&V=3$)WNK_;n?P= z6IvQ+wkzDd5Jf0-mIpB)&vqIjQA|I!1ynpv{yo)&(c@O9 zsVz|dWGFQKpVtLQFqe%g!<|3X~3kzQxYF}TjYYh#J1NM?iY|kY1gv9RIB82O0^0U0+pM= zf49mNH-v;RRqWe+HzIzCyQx6BJZ95qOydz_PW4(&ofLk)nZ+q)9o>)4ao^$(R`wMTD=vT^a*&yz?LY?2{&6JT|A4SJ)?HpU~p8)p*_U+@f%B z5SP=ffPgbrPu{xLajRd-nBDst;h^<#!N7-?vB~4`)*cZcF{yYHm5Ja*@TWg>#J)|C zAL7Zym9E+rv5sHxy7o%4!SgQ=L7t~Mc9O#QS5!(T8hUnce~{v{AniNFv6?M)Pg7IO zXml}JPuGo?O{lv>8(1~y(!rR+c>=l>TH!+?6UQL$L#4(Y6Bf%ObBU2QW0=i+S=Sc}s^r+^@h?JPGsbFul{mQQ)8BPL zJL6O5#Ym&xJj3)bE4R*+WjuTau7dTn99KFc-mx>gp3Ma9boUn!pii+idCzI{aUMQ& zszf&&4%;sbLVCy&$iPf@8?5A>9OsJG$+E@) za&0BsEvq{6*Te=xgRg3B3{)dS3e>QnTbWgLGWmMq);ehFpG_^Al$XBWSu0VqSaW;h znnsuQ<{y(^o#*dk5q%n%N==H)+1p7}>GkT1{k5;R|6`O2!>eSrdtbuCr@O_pe&7Op zF82;{NDD*nB|Pu5Z&L{Cibu%*^zLKwek6bIE5|szhuotA3=fcpJ>OU1lC(^~$>8mr z9J|-@Rk5ap>vyg%;Ju)N0Bfk< zL>?@C>k_tb{^B$No5Ux82gf(^mj~*U;p!JFE2_WWJAMQNJbfdI1f~U&KdFDX#LB)Y zP|l@l82Ye2iFRT+gn1zE!1|v%{2mZ}oT51oVJN`2 zf(XSuuIKO$6IW42x>U<8AtFv}v9b}qC_%rR;VEyx{ zcldodOyjZ)#LTm!(B|AEKtk4iz0a$^9oE{L!B@WfgLO)jwsacBkfr5Nc!j1nC(2RE z2R>;3m577Fc)e>3Ex8Ly2)bF_l3xz2%(*DLqbN=jo|)B`6=cX~%%t(`mOsHt%1MMo z4yfP`e=$~&R~heIbMy&DSmvMe@mW@bV?jWzSFsr|%+1-5rfhzVychW$^ZI9WPy&5n zj~Mr@B@S4Jz@L*$p(lnJkY@OJ5HWYMeBPLax9?_^Z@6t&fqKqNY1n<>dCHzPKn3r!Oncs%|Ni@bKfo7y6c)D zf60pK>axm8HF_XZfEi>St5ap{1|yaHJg_9d)d>i@vvA@%s(Uns!z= zmXX-BZ%5n|IrX>+qV64d%{OD*$D?>cu&@->O6Hn@!Gb2f0P5Y znc(KuB~zq~?O#KtK|p?3mzB`!$CsEvzpZR+inbn6-u8q(V#VuesC82=^VA%-M(bfy zQ`_1>Nbncm=eW$3e99Yxd>Iqv9||+3{|de`<%RZY!cZ1mk8vNG&u5$(`XXt}Y66v1P| zU}3$2LSb|9yn-ulY8BnxztCPPGSF{X(km6T)^Vd!GgP);unsd0)K=!b&yeEz<;FsO zBfQg+`mCuDQ_OFDan+o94W8jP7I@r3u=&}Rx7YPcBRQ)oeWwHF)7A`7DxPgkh~CY8}^u6@G*t0#>5?2d$<< zdLT9BXtrn{eke@dsQJQ4RPXcm!Pk~HHrXSUD-$~W^pZ%B^VchqPL)MBOrHllFd+LS z=$X6~`LlYKS?A<$NVEC{aiq8FKKjpAS1LVioyQKmUU(jwZRz{S-zwqnprdNmeEW(t zwI7v|+u|Qe0c6YHlTdK{tqZbIbjUU5?`0%i_u=5)iDbq6{XWWq*8NaJBJqDYb(#)M%pu9tuL4BiL_*!#AvK+fH(k-4CNncjBxwd zTf+42E6ci^poAB2JX8RJ9+N7+m{s>gR*#Du*|6-V1EYa$<*#D!UjjI?!}Cp|@GZqg z->vi2DdFbcU1Zj`w{FJQ_nTxFw~LB`0+yC1wxL=^ufO;z`|+;~7|#B*gg$F+#d%(! zY5fA!YkEV2ES&C2O@mKO&*o(o@}ZB&hqACi@HynMITRJ2M5p;#iyP}FqVKUJ9_HB^ zQlNkjpWXF9VtN5>{s{DrfFKZjXA>Wj;en`cR%XJ^#}>BtsKlC=WGs5L|9OwX=v+>u z^z+uY7FIUMysz}DO&Vfq8icwuaNv7n+o09e>iDxV6!9nZG*AXi=9O)zFe83J9R%|E zq`>x@;i)i9TtL?l=wX+1#FiLW6x`f145(3nL~>}xbr%L9(7z4D)c30+Yo4Lf4?*;E zo5?>E-#rO`^l-qD6h@5(NA^8y{gYWwN+_4@eW|2p^fggMV;VU)waC9~S(}Sr0FZkU zOq<)omxe^Cej6mMf3hqyekmr4g96$D>d`x%A!|l}EK&cY$B*rQ82Ety1)N7t6vFbW z6-w-fx#at5E$&4W8Yf_VhW}+qakiR{X8ymt!bn)zbnDYrb?y|rb734o3+R~~f|_eA z$x8O$oCFN^?VJO~zBsX@iwa25R)ebo0s`Xg-iCM`AG&{@Yl0-==E=oZb52e^3&AL7 z6C?fiSi-kNBm+4_VDGL0WwFY!n=qnmyv5ckY=0BK_>~&TjfIqs&Oe(70BmX?gJeBO zGdyBvWUD?=C2kcyUwA3&$f4p`p(3DhfQ^2N{X)p)=G@G_pVc>(%kw^VMT{(LX31R0qtlD%13e}m7aCjjua-e!0o^n7EO1e|NbI~Nvlx3UvN1(AME%q_Hr zSeG91yqVU(zN>F|%afch34Vc*_}a*7BhrP63K@Py;zH%o35L@uD=W!FEMz_?bVIVy z{As=`qQwHFMWWK zkMTTpnf!HBa%{2I13~zKT9^bgyt;Urt1q3A8-n53#~XrtL*eonTLyu)u$#Vs$IKg*!I#EgdbhjO=qf7LZ-M_cvR<<6thRt z-S4oZ%s0k!WJ#FM_ag)ysh8&VNYhW}LY%!Z_habMgL1%Jo|l3PE>(2r3oKAD>G-?or&Si?_wxS7VO%I)X8bizC(0 zu#ws4YFmTeYrt@Q{aT$1}L~L=pjHE{MAuP1?+}u7`O3JvW z?yRG8_Vhk=>EWg8FS$8Vb#7XxFn8)Hg&${GqhsGiDvioDtv)T$eYvgVee>hEj$iSx zboZONRN$q+A!ATwCjGgXu~_}5+=bI+U#N)7yO>Wv0f)4zO_iu5Bg2KOqvJm4(4+S-jt&$5LS!rph7G0AHsuVV>vNfi5E_QEj| z!YPb6sG9VwV^66i-sh=0Sb>DiGA5JcoYdCuAbq98V*II+t52(kHo#66RQyxt4Mt6#4^F zaDJWD$ZcG+uM?wv6pv7h1a=l{_cBa*TsURsF6~Y*uaPYJ zfR#)vXJx6}AmyLo=_LnM1 z?Z^WKD;80#jf_T=81m`2g#gv%m6_@^xk!=4Cr<)8)A=RkWwnzyN(@q8;~yTMhpL0y zFT`(9J|f>lmK334x5U&m`h4g!%(SUjfSSd8#@A~7rYiJT;9Q0QS>Uv~iRb83OZ@uMo#k9wHFL}*1yat9EmDLF{KO%gqvJ`vUQ!x$ImUB5seX}vG!Z70T zg%Tpeo2D;it?Tbk!<4e!E}VHOt&}-?jW$i6FQ3|T#k-pGY+Qt_Kk3Br(yzcPyWJ5A zga7uT33NjecP25M4E~7T21*R2w9@n5ZDcwyq$l*|yZvepR((^|)QA#ibiapU%fz~QciH&k{8o9`Rk56^K=q-zw@U1*U(HZ^8=bf_P4W&j^%Mw1#2$G8bf zB1L{5%7^QZui#bbn&Z>jXZH$Q3Ykl<#(U^_9gjc5^`t|yfG8YE)69NjlzRu2LKh#6 zrOoM47vh)fNHAXsiF|;tHe|5T_0A6Evg+XBYS&!|lo1CzFwAtcgljA3Tg3Ytw=c)_ zy|~MPGlRx9k_`Ml1Ya|eQ9rjjH^IkjDgGfu0I8z_I&+aM{+_X}1Cn@h2>I^Sto z%&u=JDCjTfAAr*L&j~iV&-WiF2&Dgzqb?ZDt3UyGQ4wgU6$m_H6#AA@m@EU!+q&ga zp(73mz#EqyYoD_`?0?Gf(MxZ1_A_Q20x2HgO_5IaM*Q}*9&RNqB|}|Y<%J7>e%ewY)Q0s85=- z8%I#G(SBz(u)zB%7R&BYZfI}Nk_0P2yQ?m!h2WFr6M32XLfxDbi+hU-r{@l3wse9X znfdjJy8PQq&7B>Km)dSYd(kJTmh2;{iPzaQ#Ae}1)^9oF##bL@9Df{{)IPgbD2TCb z6DDI8RF^EunWY>975GVXRhe|j>`On|T7&F*+5b0i^}-+OSQ{#B%S&1!fXt^Kj#F-g zw6Y4@p=ZOz73#XycJY?UV*Ux%++m+UJTzX#&rvO_(t`8&sxzfY#a`dZ7)b!7JuYT; ze`cKr7e$UweA3H>9WnB0nuxg&W8zU!0A8dL0X3*DE`1DKVAG{rLGP7Ek^q{_@jBC$ z29#|p(+Js|!pg|2?QIc#$8XHhUCy9$IeCS5lIs-KNmW?qA|*l^#R}%T-94eteeLY? z0A^Af9*U-wX-x@`x5QDcHA0Z#G|7TswI8fdyN+Mx`Pl5rLx5%ilmdv0 zNomp+bao#nJ#(7v-f7x$SH#411G+=Liv4bsFQ7yZ9#wU8z_8SI7h=t+*dZy*soDa{ z?~2rvM}>;`u-ih2I?SvGwLO4&H7fcbiaYD#PD6#O80k%s@|h${V?O7zImn$L^Q+u@ zEoifNV%J((45d+1y))M&dNLmKI0^r+^P?rFl0I=CQ?hWbX;Cw!Xtc-Qb~!DaYvZ9% z&N%S;Yv_wMtjf6CAluzdk*-)NZT2Kv{ju!4D8gvnbD}8~J$?A{>#pu@(R4Yqz1!?_d#5n`iC)#! zM6*(phIPAAa5;{IQ3b8k#WUQOkFZ5LgaOqqs~*=+Quyq71%#>`&5(%l-v@L^hYnKs zLM4HKjITm@TvUEguTjopHn5(b{N8?rR)KkN08d>5N&DuuJ1n0e`e0$dbOa~XWIVeZ z>j@nU%q5t7z&D}@{)cbO+0aL3)2yng?@E1~kPJ3=jm$1Ta7}`Ez2bzbhL`T5bk9WP zTJjI8XpM;p3O!C4D-hscjLB;M-sy|Srcj=e6NB@yW~f&t%6m}@kGE~sywK&O+&3Rl zC~{5{o8T0yPfka)%>ae_sfLuU6jP$`y-AT1BapGLQra}^J9TovuB@TocN=gg&$nWy zK&3zaTG)rm{w+!HB_WA`hB>@TUnY4S38Pxj9=My%3tJ?ODCl0zfr=jnG!^F%JVI%H(Fc- z}WF(|{`T_JP$^%^5k~s3m+Fblgsz z5-Ufkg)zWM#dv*V8$nyv#QYzp?N?e<)D33a&(597 z`bsnm{7t!>9p*Tb{Z$D#)M{*rbfLMC=q0HN2zU96=9EvE%>Iu=E0$*+rB&sx;A2~p z#qx4RRmPl%fH z5D=R-{aHU9mTW2^TjV9AJn``_;?k1fTB7iySYTIF_fj|VGQZtYVUSVuyz9`KJisdu zN=gpRhv`{9ZW^w-Qg2ae0`9=(2~wK&TA11C%YO;u6z7uy18>osPg?a61~rKm5NSm_ z?d-%{cdbS5%Pp8hUIgi83Nr(<=VhXNDoZj^7kSfd6jr>s-M*%_CkOg%&&O^4JfC|v zf>T8f`BQqAy?~?Ljs5Ejw9&1TM&d-9-I`s*REV>sRnSD`>zBEA7aK=1i~wK9 zf7sv+15vBQ&3_M4PRrR*Yrc|vn=AX1K~crQa&p=lo*>HY=fV_x{ARnenEMc)g`p;4 zidSQ#Y9(lr{!7~3*@XB6B@nu|`u-*NkLw{_LIp*74gM!`$*mSJWbR!1AP~?}0jQ)o zSpMuP0EcbqDGdG4cr~XId+CcS4nzdlp54byT z`W+IpfTthKfm~=|VM)sL^5y}ZuG*8UPA|qrqtlY)t|08h`R`EaEBGwS{RT5=u+hip z6VA+MbQubjz2*XEJ6_1R$y>@e_cpd_HEW5OPSSziv(ClM~#7v z6@0ssBE;SXS1m|8NqF@nwhE6A9-Y-e`)V{Y1uW#x17ryP{()k57bkT?WGH@C$!TJ> zx&}F(6FSx%csf=aNV|ivz_4CdCQaqHgj90+O03-Fhu@hql04I!VTluRbyGX%6n14ogSSrq-pn8d>;)c z_A4HA7)!(b9piD6HQ#t3EsL)#QX#IQ^OgI(mKTMptkGzC&z60K|8m?@h<6}*F^P!& z`1DiSf?j^_NRIt8-ziflZtG~&36)ea=Vki3sUA!rp3q`|okMna zKNsyq;Lj>`?PXO7?;_UT#(zqQLeg_ASX;=L zP)p-<>8teUD+cb8O)PlmRHav2Esz-^<%`_x(;DK_8{$jYwpBCPkIlCSk{=N49zsL+8>+^#ES#_3B^n=whu>V};cPft;DKZ2yr?#y}DP<00NmI}7 z$g0KF^z_`l@558CIVAYU&Qcn!r~7LY+tPA4cW)BY&&WT~|2owTBXH=6!%`Ns*xE`J z2w&}g=8Io#J{Tx}d^?S*5w*U%c-Qe1>Rgr@*X#A}KVN`Zp_{AL&$}vFoa%kS1cp%s z@~YQ^z8k`ym!eGVwdJ?(Q1YTMIg0?_Bu*=ujkH)5Z^e>4tgm4e)a%6%N$(GGcB7Aw z-Rb*iF4Af4R3HSaj><7ND!xkihg z38sA{NORX`GXDC$B46mR^k*&gxlP*rMR|`8B975#W7wh3E ztmMh3g!-A%Oi9^4jivE3%Rwd9quo~zr4#?qVE2()w6#MMyCqfSx8&7Vweag!%3 z8OT+rsA-hz!ppfDte*7w)FZ81D`A~hffT69juUIiPMT0WYn)vB(;N-8xILPFSrFC@R^#^+KgMy(y`DY7Q67xpBPZpvmL%jwOuxkj(Ukl2&?GE^_E(jwOFpj zT&szV%dVl5>^p|Kqsu_9eVSi=EPIPJrjHNPU#<5w$1K=eCW>75*(85^{))tVze;Fp z6Ymje4z;oCYxYN^^h*8V<18AdliS=~w~eNBW{5ZNiMdO&f>^tYB{RN@!;1@fb-J(tNRM;pAd$)_yTJ%ltvZU_vrHgpX-f``Y!KJ)^>-r5> zuL;zUGsUJ|T~zs`;SyZ9Q#}Tg=h2#}_qfUnXQHrkk(< z8`M>!Ayl)L<<@b0Ty`fS z-Rm6Tv1JM$wmB`EtRZbJdz%or+wkQ$>MWZ}x<75#TxMoF8BI}DnK}zc*{%v5+2CZl zjVajvksAC{%+VO697a#Y)Yn@?Bkncxi~q2l2Jq2WJhFPDqljfsfpScLzd2OkdcH$s zej_t!aJtO8qpbE^c$&T97O{#r79d+Zf^AvewNHv%UNplP@+J7L7pBWWE>U;a;&QkC z2@^T0BNg|JOgiNH_yp^q<8}+>!&r{lMGy0hEq2bO>?cTz`&&N7gQmzG9KUY8-OhSX zWx9)YE*SCk{%o^`_kHHAB~`VQxb9bv8-_js2e8_;hIphiBg9qD_lljq+Q$gf|TLO~F^^5DM@~@^m$kShanJ_#4C@8Rh zH^i{Tjb5D38HVsWR!J`dond@6(|g(XQ|%Mzb$Wz{TW-k=yJoX;`tT>XQz7=3WnLb>}}Ehv^o&-wVC7gS0{wq#ULU-{N@C^wda3sgp7)KKsL2$S7H%e~&-iob{1Mg6o*UDob}G+yb> z+WE8eU~g(HO5=fT%x|d#LXAebJPG>B;AM1@lkG(ytw8o-VCCoQGwK*uNhSkf z1ED{0Jx4Yy=`u4XxC*A>kCBVED7pP8tcNEu9=SHLx5bf`-b{yg_{1U@JQh8+s*+o9 zt_(boykZ7>wlBCNN13_a+cR!$R$XhYh)cvjM1NN`8?~9L|b*m>~W|C zOwemQ2cL(>zAXjdTDaWflTVk46divGvDu!pCs`+{GIKlMHM6}lxqlMDMclmED29z( zReuna*#&|kq6nfs2b>rm-_U&b;N7k#2;0~pU~f~rj>=EAk_HR;z$-T_@@Sd( zuKU*Ua2q{Sn#`DHj*94u-!k4KSM$obZ;_)yEM8M?ENsd9#ALz?V+TM@K#s@xJ}h~@ zg(wSg_T1v|l3ahT(z831D_LJ8a>9PBVONgc)p>FOQ1u!WeVF5|t7I*?U31k66kB_srN4rFI@Ch z|5%y%F(0LpI?Q6*b{m%wf466lRbOm0D}-AOp#=JvTS&Vn=4SJpN5|ygY&GvPz%|Z~ z&6zQe1y#&4R0)uFGJtMlFn&=Qh0zU`=6$!+rXSbI{nq)I)(AI+$NFNGK8+TuDRuv3`6(^$zI93U9?QNAVU;6<-&E;1BAb)ROU3klZ5)Ck z6|URgrC~cR`mi_1@y@sNIAM^#cIXJL#k1nJtFs#luC0~#m{Ht2qU3IBG)2{D3eL|t zHiyOSTMreN&gq8+q_8H<)hSC%mCg z!!5O*Vy!9i*J_Tb=EW&nLD?UFe}LZ#s2ZG?d>EHbfsutK^FYtPYoo7Ie44H(S@}K3 z&CA@5vY`mDWk1Eps*sLf-(M`}eQRlf8Gd&&2gp1|UVvH`~p0 zEa%4=qMWuWVr6Xgd&dteDCGoVM_)}{brl8GU$Q=FKZ?*{&t74BY9WPlBE%3&AO=e6 zno-%d_StR1NHU+UNa1oJl4TE18Gb1}5EY)2ADCZy`mD_>?llW3J?$`L&0CXimLX?EnyF`p<;8iQ7@xOpnjr{s6y^D6Rbs05 zJDARk68I>3IXs6#u8;WRTsxUC*Kr9i|Mq&;Xb!BwC}l&WnJ;khu%Z1d)XP)Wy5}?M z6F!UGh#Mxp(~jn|f#{!;NC_r4&m?!;;`{P=5jQsItxExg(Z(;#`Cm`%ZUSu-VU^TT z3VBF{#J1DpZuYXN9t-6nUYS87j)lV3 zHw$y~!Cymi{3}ynA*Sng)30aU>M%t-*H)4R(i7-=`(7EE*b{;tb_e4*$9Y0`^L82z z=nl~edmeVpG^Q4PU?G9wRVOo1%C2);&epe@fQ%5%`-6Bi?ye$Lm)tU{>K#F$l_?14 zqq0@SpUEPbE16HCDp6(y$bIN#rtcQ~0t9AzQhs_^C|*r4M?6u}dwW&Z+~e`JgQu}9 z7G3Y%`~9X9w@>s%Z>f%^%1w6E_< z6F=#qrhK1F{s(mI?iZ*k;7s$z>r}Y)rxD^|-Pfa!nnCq|Y%m5QFJG?|3~=TAYZ+*GZ(+GbX#C{be@NJThP<{X3P*yy9nWm1PhbJbQWxb>R3$dv zLQCtaCyZ6u1}cpk{|nzp`BUA{IK_Si6=wR-g9WPaFMK zEaqe>m&wDCzh|jz)#K>}h_AM#{PWmEQynkn59PjbmY>h%>**YatFeb`$g@jPmep;i z&$_FJWGVRtf?GVybvF3C2uB{~sPOVhs?M^RwY!(1n8Kt~hsH+AhuYYWe=Dyj3;XIFkLDTx~kr{Y-ZB>s<$di*rOIxDHL<{l*dK^x+oW z{g4SL;w^%-T2!QZRvGg1x7*;vxE$+y_7C@`*~>Y*=R*m%5iHo|PH)2HRoqR7tp@OI zXNoH}u#kp6jM3$OqP7-`#o10947ccM_Vm0AT`NiNhTsx5J9@R1f#z07Py$V8(dy9;Y zy=b2tOsjlQ&~9d{$t}CwyVECwSzAa;>q`e+sQai+?JaTmr!ocQ@Uy&sPj1xpb#-S= zL+97xw&ksgURc-qk>hJy`l%`^(7o zI#T9j5_v)~WTs*Spw>!IUD07lz8(h>NBneqP9K$>ml(~Zrgdp8V39G)y)Z2 z73puo@iXkv!KvS9iwGy_o`6*f2_CZqTWa4c55jRnv^b-pg&@pxUbOl@?~SjWL; z7m3d-k_lLv|L2Bjr88duOqtI;SabG}TWNjiPQGs5#F%B)$8Syo;!J8}OH zrEOxp**}|hd?A6TdA$}K1KPs> zL1X~TuzYQ5oz-}?wOy%1wu#^!#C%q5Q@+nNtQ4ld?m{+eDp`LdDT0;)vnk3W@i6wZ{FUTtAU}nS)nO~-OE<=ljd@w$UlxaQUnRbkk(uSVQ1S7 z90K>gk5{PBj6+<09zRyizf=>kvA0$Kbb)F0LUAla7Q)POhLNZX)Dn|h+FZ_UYWUvr z0?GJ#`1rms!}Q(4G}A%)UBhab!j|zV_xKEomBLhDNTDenM%Zw*wdq0BY-$t&KiZlrL3tLYDRK2oIZQ+I_|i$q{LxIc z_FkbskThU|e`5wv%6?S=d8JMl8~mq~4$YkBjxZwf{TQF~i~044|2MAGCII-L{C5`4 zJU?pvB_D#Tu6&;lTd)nGA3%o)8_{b?5%9>$S~V9)6#=LW{}nDOfP|4cKvu6}9j&W@ zn&Ugtzsi7k)LI(2vl|A&1&QQ1!r97d@o7=N9glY^2hi&VFXpT0@gPA^6I#V&Az`F- z31bynqx`R`9Q&UmEusA^YPXr6y+HwIrNgPvevE=Ux^X#G`=dmvmQCFpI~qLMZ8`kE zgMj2D|CisS^`+nc1Fhs(Kx(;WzYghHDSzJj`$s)G5)#~>flS_lnRP6fg&(N7_6|vh z;V{kI2yFU4%DuF7(qtfcM_-ujrkJ4%mKseNN& zf|8t|Ad!T=6x?ZSOVNUfRcUvX=Ex;d52;A1 zrWzZkabT{a$U)^6H0V3|Md#7i^to7ceHCN3b8|-v++;Q(yZ@4z)6GZC)uJ2 z?=xN0(k8C_Q>-BHp(ZI7o=I`IKl0(G=nAF=#U@jJ2Al!VXrq9CVZWmavk4x@WBlTj zLT$fY!ZJqwPRWF1^@SZG!TlFL0w{}EYJ8h$4|#=d3vXx@)K2jM5maC{9eiD!Zg z_sgi8^U{q64X7$4zDh`oQKhuptriQRq4kW2{A$2co%6Qfk^FjOrxUsZOBY#+_I)Rt7ZW7P=DovT({hXA2^Sd{P7fP&B z1x@M?=+h>IJ|8@`eUy*WB&wyUN-QSXlc}bySx3ThQB67Cv6@am4@8#2Tz=RxFq9^5 zWsh}8!PgUEUI3H8=PsL6$(s*>;rCa$KnnU#dr?z)xW}~;lS`&a=V$m>d!h8HmI@84 zObz13X$&legnvD#pLh8|fvS4T@Htdc5qzf~t!`G?w0xg}Wq*1MHsT!%YN{v$rBgNO z3mD$B;LbH#M-Nq)ypm1V;vgsq4@z9Nme`LAzi)&mIce(|uQR1>Sb`bs8N@1_v3>?) zVB3G8!?mZGFZ%;)5KDo|I$CMn9p|DQL#pMP+S@m$DdIp+DczM;Q(Zt?CE3R)8}oeA z656q;z#jC7=Fim}vNP`rzeLF-_se`gl`j&~P+I#=?%9Lby0cccXX1Fs)iwk;1Bj)w z74Ene!Xo;o>Y*_)<{INx5@}2U8j2}&DnF_?8_I0eRV<^EWtBl~Cdd&%1Cb^Wyjkjq zg3FkU|ML2DGAi)6&{7ti$TfN|MaYp+)uQu9F@x5kH15u{Bv1^VgsH}~84%ttJ+y`z zI2=-G_r7)e>jN+SmrX6m1{jo6ImpIpY>8nA&vqett~xceRM=ia zp-~j}y&j64y730TpOG)V1VlvwWj^76yxl(eHFw7Pmfu7D^FX7+)7%5yH{XBMFuQWj};bdFR-4EFp8kh;~)l^ZUJ9abk~;_$i6ngE_Zi$9ij7H*<6ob9&Kz%vpL3#PxV@&hd9W7DWoyIZi{{o0@Buh+iz^3(J}8L zAB4}K0tv3(0Hs$`f82a;E5~@2h`);1gCfvzL%!(MYn_gx8*lbDPs&W8K=C(ovK^`? zOxDH~6P(X!7X4GQ=1y)Nf7~$EE*K`AOY3#SNEAFi+d37|vs8b{pUtrh9($8h;SDv{ zvi@I?z&1uO4EAdZ+FgNOE)CZ z;~%Aw5D00;nfS37)iNxYMdnn|0 zYGLZ`@VqW_tyZtv`qA73bvHsre88$e8>tBO#e(Rhs_j7m8b=IfvPPwO(*tfbJyt5> z`&o0T$ifP#7wIoYgzOAht7hpi-7O_VKEfZ9I%z0?-=6>8sB_{TrZzDi)WZ6VUr|@1d!ph4jmfM5rl^M#MkL;^PJx80oHggOzEd40z8EApH?dE|8Y6PV@ zW_aT`R>aU>h{8{)`73@u1@_R13z{+F*LD!b+19pEoUieN-5y1IGH!i0BY3?$jK8k_ z1uefGN)iS+77+W{NK=qM&G#iLuUt?spX|>EsfDjFv+<7)uD{>(1x4>m;Bp+XckDit zj09h>Q>N%)GWDxOxD|(c)9C!a;tcp)<+*w*!VADURw}qG0#aQzwW`pJ3h>%qr1uV?1(aTefb>K~P`Y&Ky@$|yN2G_|L6F{i50IR3xz;}ay{~gVoDV18GP!1W zr!e!*^W69S`xk7>7EVm8SJxz|rkcEq_5cCA`|( z?lTGFTcVo|%gJ}@+F<*H5?#jG`6)L4DvEasq2epQTM#fTo&mQ}Qh)vNmFZ?%?dy2c zPml8DV#jEX4|d&Y_Mgbh?31mKagX;Yh{sI-e3`BL$P)dq8eGANsuO|wwadT zgi&@`yp#SS)Ps^w;070nn~?MK(74;ddPhZt0F!|AhIj$)yQ|}Xa+fACx?$vpdn^06 zz&inX$AWt#6e**o-K~t(gQw0xDh9sGLd^C_D!r*b64BS{tMxJtQXguPq^6e!msFsQ z@>a}+4cYn&+(QJqJYEjH5N~#C{V#Qeeh%7Mfm6#w(!;|*DPLLo)d<~(9+*z>icp^ioS(}2@WK@dv9Wx(QLl+(8n&9;KJNl_KvUM?fFUv$(+1^*!J| zGq-0;yVg2yaJenn;uD4pIe!X@I~Yp?-hFSO2edDvJU~Q*7$QOemL$Z#wSWR&%6fZy z6Gu25FpY02_$n}>aQ;RqQ33d`+tB8xz{aow_{SR#xPkJIB~Z;^6J9^@OiA{^#xjSI zT%DX6V|q;$o${QqD35%K%Loc!sv+UJq$w~GN$r}KWSwv0(t|4GuTl$T9pj~SzPa<| z{!84bR*X~a484Qb%pZvblcWt?3Rj*b0&f)pEq}@*1oe~X3MfKddiOMZg+#C2cmx=- z!_qPtA?Y7MM4~x>Pr4ESwhLx{0v+D?_f;(D_WuXn1|JXgc=RX`t9KQ2Eln#Ifc0HB z-c{xQn)msS4U5o)!tWknz2Xci%)e%w;`!dnS-Pjr%m}fNvzAteDs4ZmEUzF1vU&fe zSpv4TtK72G>=hE6{C^x<|C;4YybS3;*M44mKrLow+jJU!kudntAo=}Q$7Yl3pdQ6X z5&hJPSGnCRiTn(nJpZ#Q;3X3Qykx8*fKQ9;xATj;|H|%Ze{OIZc1Q*x>~b2jEUos| z(o~=J3aB)kZv?)T_}dU>o$@!`Qth`P3^2d}1IQ5q+;EE2FrAyApC3(TJRP?l7*ew1 zr-ozU0A|Vi;?*g7J2p3l4R|4!iq?L>)%b$NqSEn^$&iEoDzCkSnH5mCQN*^6&J6q< zitjb8;1dkfH+#3pwoQ(MJ!#vxtUJ-<~ zmL+H^xIMCbi`Dp-+CZVf;M176w@ze$T~Uf*BaZ+$6<77MpF8nM+t>m+6~GNP z>?}X%i}j29<*xyFNaEsYa{mvc#dLG~{{$yqxVS1#Gj7EIiIc8qtiJr~mNQ-%{#rM{ zaYogb5XrV^a*sE%Am9K=mp(ecew#4)`E#*w>sP&rA`(Xs4RD_f0`DKEa^$ArJ@B7+ z##>Fs=^q1Xx~^6P7i98>Hk)b`kO86f0F&iW4B%qPYY1zQaPhtJ{$E$)rMeELy)LQ& zB~*xd+Q;%RBP)-GW9jM`RFd)Cm_(!3_8!C|OcCCAg=K5f>e2*$MfOCNbcc(;NQ zXV&0+i+=WASbKuXo9XR zWGI)w1*5X09Ra9Yt8SyC)LC_6ZHkc|bV@bVHaNv&FtP(M;W^ZunnviBtVvot7KPD5JT2W`u@?2$zg8iwnrdt zBH-F1{HG|($Lj!Mk1BhBe{+&(BS@f>AVKio_@;XD|H(J?vNSD7nQ#5T;J=2eTqv!@ zA2)fEfSDuY;5#oS$8Xh+ub06j^YCLGNW`Ko)QuzrKFd_Kf)A{QcK87qnaTT}zoi zhGCqc8({4bE3f6&mzayiZUF6w8dKEx*#Qv$gb8;R_cxtN26+b2zzyx6>MOOFImvq# zDak2O5twixpv+d*Ezo5B_(ZD1Or0k9IYRxDdTI`Zwpw5b+Ax;K`?z&?&8m}1E;*8t z>f&?@uZG{&t94wqJK#zXwl*_U!nnGeIKOooMQLb$WwXy=@=m)wFvT*AFD^3X)*VJt z1jN5Nkk_-HnY$dx-eK2u(lOp`1&n`D@=JZ9jjhc6e~OVl9RNHv(dS^%s@PZfeq53E z;9%riY+r8avSb_k!}XU%zd5QAWvWAC=vL8P!)!?mXICHP*ceSCA(c*O?HI)Exb+_9 zp>8BVIm`u($9?DEVoNhh0wW$5r+1Idgu!%%9AX%Cv?JA!m+b^L+muSW>uq(#$;G9KfdAW$?KkUWCTP%7ViqXQ!dG5Jx8yBYOq-o64BBO~4de^^oiQSX5_fQdCPP zQYkcXXyV%Ds@QTe9JtOgj={heK znKniLfK2Qsh1(9YakQBn7(N=X$4p}BGUt6N(&Z)EMPAOTRP^%Xl3T>Pfzi?EIAfLt82Wq_RZY~Goq^HqEL?CQT58vqXNJ)eRD}vSygM4oig+ewTf?5 z=U>^mQggh^%0$UV-{Pg{rbVl(D3;7&VsPEb!Sry@#EPotK3e{hI50`NQ)KjHcWlh!(@?rFN_c8{#GbMu>WJq z{b#I)zkW-L^HS68!61Qi=Kk>Itf9}yZLjKKM1;ey>ntlt zQrg(QD5_5LLN-i{-avq5C#mIUKldVW9^e#1fft_SthL%73 zy8bN?=+F;SQZnbbrYMjo&5K|&v&mQIv?KM;EJ+<42RMX)_F5=!b*#HL4gDicTA7Xt zSl?MMo+1Lvp0gm-+5`gm6;Eha5G!+VC}Dt@-~!Ai4i5HjiqQq>8m|geS?NfFIezc- z`wy>v72wop7q3xr+`jtCM=H?3g+l}wlcYt-IqBs))s7|2+oqO4E4^v8*~#f-&vB(w z((LSPX?|`5-|9pZOypwiApD~Rg&O90zUE+%oSWJ(orEdX{1)|1Z>Eo+&51CWXuj*V zIvF7rpV*`-UHY(~YTv!X(r%+qw79lRb4$`JHy6q;LVGv*yr(_MXvgSy=sY=zl_lim z!q17clAez=r2>5hCruAsE4U%~FNDevH)7F6qn*KqD!R?y^!J##I`zy0gXQ1$hn^p2(~Jx}>YY6~_Q}no zH{M3w#MM91j5|Hj7u6z=JOahcSrXldT0HR72XFEZQQCbv8gRsR_RI(_xOdI!bGq6f z%^de~Mnoqht=z~OoId3LI3v25hxR^xjtG8`O^zh!vJA3(P-}5{9 zOR>O@Vr?XQL6J}DpMAoF+ZJ0i?B1+TAhL>!mLJ9itY4vAgQ_K*B1zEVKd8*r?%WCd zM!{x@iBA?M!!WhveyV)IZa1NawyBxu*oxHI1N z(7Euo^#p~Rc-Kc3ImqAPUO`^$EVB>ub}Bcsk&sKhx^;FO4i1HGFu6-SGdY_++o>Eq zC*Q=5u{CPcgQojEXT1>jxsFOLW+0g&lP1D`y@nOG%BFXd$qm34LujV0_7{FDTmi)M zon5>t+Y{yZ5|Ouyewyyr7c_DhyL_e@KVFDKv*c^^37o{ z5?sorpT**t*h&zyK&a{PJ-AY_#&4ZoE*O%#Ua1x}m~^xag=~>28IpH);}2mD?PIH6 zrE&YtDwo(H$!w|ag^zCYs8=xL?oj!0<&S-TBgDym^-F_+^6#H9v&mPPZc*%!K1sI= zD%ixW3eZ-pw-~o?yruB+ zdxthGR)@v=*=hO_%6d4R{tDv|+wKQ}F~ma(pU=R9i5yXs+I{BovgOo{q2Sq>t1(uE zs75o&>U`S!OmXR)wXOk1jUNIK-N#_>*+!E5S{>Id$i;;#%WZ>d{SSbKyd#h)Y-y{ zyh9Q963B0Pd6FQ2%u*QOD)fp<-!-Z;`>}NpAdg^+_?k)tq^3Q6NO?P#_|}Oq2^tcM zUpsRG0z7sF3+c5vOngNMf=Tw7=}u6N-fb$x^8wX)4Y#5f5_FIk&cYNTf!EDS_QD3b zWBSfcu*eI|wk_|5FJNIFj{#JFJH6M@1DoFXWE>#bi;Q(sTBonSxW6v+1%+Jt((j$x zdnRpo_9PW zji^Dl*=z}S+g#bF%cW5EcBsg#?Ri|Eca2{cZG8Ka2&Px=^PWlBAwp2`e8}ncEIe^I z{*!cX6lq@nJpvzPZT5+4rPMO$A*XzPUQ1|{9in$Ic%W?G!|Xj_*v5L>(r#3IRvDdm zXUF=jhjzA;Um{0ePb}NKQBzZ>32wOW9>4fpN6$ByaePwZvGK**NcS)nq<6^2yo|!1 zv)Cj0&M7&4w{jvK|4JLeP+0_d7}qLqRp}vp6jf3ZKFDZ zuR7`InQ-UkQRmi>3x+c8JmuO&Buag`*?{m-u~33yXl#Ch=_@X~s#v*br41}Wi7$Nt zI{{eg#p=3jQoD|rV>bI>TwGa4bsMhFtfZ^8hlo27Gn_uVcu-T(l<0=lH&2ge+G-1? zC8v|)=o32|s_uPK@nkO_iBfU4h*?r_D5jvOvJtd*z!*M$ExNbwT?H2K&p>dEvU}m1 zIr0KSJsR#6kJgTsa(P3ZR+-Wm7d<5yBSs>XbkjucF|+3~KhlnVCEn2bv>(KjjAF-? zs*q^33x&m3B-cfIy_=135cfXO%r@ezdUM9r$!D!umF5{b+*ro5! zg8%KckRX_*dM7jk4$zqJ^*)bdu(Dz-X&_##S|a)2!3P9 z?tnVNm)Q+2FXHi8#6dYp0VpSljM&c*PyE?AC6_^o+r)C>*(W*?mO+7yjcnL>OkoZ3 zr1U1B7}XINhyasuikimk=s_Ix7B4K0qfVmra26YHvXz3}-&#bkcepCw4HW?;dYyQz zgVZ$a!_;hM`WQhX&|=^;=YC^g^oC2>{G!H)SxfAX-qr&$>3zLD1T&x&9tTrydT(f^ z_9qhzmbePS8}=%f6HhwQ?P?ubN*%h-y4OG8i$ht-H|#a`gBYFF0rpaGAaS zCT7s(btc+<^Z0q5tsb3dh)twY*{Q_*^6YxoLv1m$3?He%J8`mjY~sgPi+$*DbaD2j zd$4x9CA{R3QrdA7u&V*gwiVD*G1&~m*hh1f~8C(8ln0F@Loh-l&>pF(wdb$HOPeO`w zIhAKS9=0#U91!GtGbNuNmZn6oa3+hM98_N&xjT%y3O)d2RA)o*2vOn`zJ>O(*_Z91 zh~0wKkOu*zt_}5UVUvrKVPLb=Qpy6Pd1{2GYT5cnu=R>XD@wBTE?>p!Gi_?f$ zem@Pk#)*Sm_dFTz4@v2g-ppvbTl4!>?395U=?%p7>C^jUDrWLbEM)J|DCnSFx4z*P zdnJw$1~J59osQYgb0-FMVg{V@#|v>UGj0ct3Z!j{gp}21&-+_u64s9R$j}>&)F?~h z=N>Z^`x^vSdNI~3X<9r=t3<;d)eC-1LWFB)m#5~0?^&cuMYYUJ-tgWY))o}0&#*ys zu~~Jk1$bJNGgAyAg)CCNd2N&hUX|(I>rIEzYR8+fbnNTb%bdn4?)Fs- zxztnKOy_8-E)WTIG;;q^?+vr6jLRw(gc_W#wg_^{XSXd#Y9Z z{jRj}_e+h>d(~kgsAy*KI*9nlH}fuXX+D`YL3^ILg0i$n&6xEzj=c>Tb)6!tp zahIE1WoeU~v;tZ~$iQsj5H%Dbb2b;RQyhoW@Q6b0L#eAruMp&AHWZYl?snRr4ryGh z+2k$k3bZ+ITPK2LoEjs~Mje7|=qPj^r@(AgKb=1-5Sz+_uMq?ojgEE5TA1FQzsKka z&trU?_{^43piY1|ibhT8Z4{*zrhdtPpu+frGhHZUvVuw%(+oN<;!5yUkJmEhdHbtU zZDwtXUB{3rEWwuN`(WFw!+||pf%Lfa_|_I4R?&v-h||q@9YVMdrFfM&tK8*Udx;do zVLgT2W0ZYxyzDf;gCLU2lt-%qDwr&geJLF@9)n{bzupY;>2TT%@}wuSFu-obH0Jpo z1D+3{ogyt3J++VD^t1^%Y=w-96^I|+qcSz(Udd{%x%Ev znlCX=UbA^dVgxN){UGc)-`M+@J@G(;3xD|X#IsMAE>`^x2NSFDXTJu1*Q50A&v(X@Snnn&EE)7OX+ zQPA+pZR^s~5-*%iAgPNNn^qC*%n)1ngVN3pRY^=Hb?)6*hU(g$>dK%TlV^8peGPrs z`7DF^-6!&)6dZ4Y8lorAGYNy)Iet!N6!VFd)agC4GSk+uxHeh0 zB{#>v>^jD7@GA27Fr>$Hh*B}E_THp}d%cDE3&&uatg=2~fk=W`<*NcQB zI1+(K7SS_3lZ2cO`vfMUqv;fOt24DD`30pVB^(6ow-bZ)PiF5`oIi$%hZ$j5H`bC`!`!u6;T@>ZUr#3wC#e;E9n++-g2J(iFm;b`@sPxi!27~{P=Ak{EW z<`3BsO|`&PC!bjKy1zYxI|L*gXUe}C6RUp|9(B6xQ+P{o3u{cwh2PDFC9*E~VTE$( z*&dUZS7W^4p&@6|50biWD!_Pg3X#se{uU*zn)oa6JM?@m#4&X4JAk=NMIke9 z*vor`9rjf0K9W<-;&|Khta5AFPu*@q&^h#Ck zNOv)2v}=E&o^t6(aD_E;a}38V@R$D=#5~Qj5gG=zwc8t;4<_Q{&75Dqboq>UTYdgi zHBBV&D*+}S<*(j4j{BUqFr4?%h|dr^xI7AmlpyckHPO%?e;{|LN%;EbXe35?b@IR< zuKj5&>zy3qCob1VZ;Yxa8lUxsfjt`&L{dj|yHB?s^BT%W`u6OUX6i=^7Czi^Ulq8{ zS3csOIf%6VN=5+A4bNKF|2n7)ORsm<@9ve2`{YqjX4$8BFADDK@Qk-O^`wXB=}3xq zP5d*kX$!kI2V7;Tjn96*yLJZu3|o2>I3psoQalXTA^hTbINhl{*pnq=>Q7Iajs9B?1}9nUETH#?1p6Aop~?^ELuM<|BDpC==7CS0N3SiDbZ!#1|s+ zJU(tO72+?CPEQ!VB=CRsJX*vR9!FScS2EOo;X-0B@|7LHm0sWDOTVnYUW8B0A~sY% zC~Bv;;)V)EykB6DX3-)aO5hI2BCY~e>?{fK*@fp`n#GLXaKpw43~bQUFw$G#nM>T@I3s#kx*!p?)YLucf? zfX#ElyUt$O4eDy$Fn|>l#!oatcD?ay9nk#bSZ0iv^JB-+64|T$A<wu`7cfZ&;&{s`WEmEdiZQYd8=4UI3O;gvHsr6n=sM*a z`HnxHlaKS@noKlOLWMUoR2A8cvN3x@BWQAg6a?$yE)GK3X&6Mtz>#rGLMP36^r=a&66o%Zc>R$rJ*h07+~oC;3vdJIUR70wvY$Q*!KmRDarhbCR~wzL7~&vhEUA zNUsiA{Bo#%HR~ef*Hz2+FPSiB+l_rl89$2uv^>O*?U$F|KbY?Q-`Nw`kR?jK89BXZ zvq9g+-vuy+ByRrlDOD!ya%loks!Nca;NK!JIFU;g2W08i+R#c#=RUzZ-OnwZ?&?Tp z4xX2CJDV5ocnS#fqxCJn*78C4^KX2%O^~-9yY@A^pzS)%kb%fCMQ z8isgiSDbSf%J=QUd=dryB%M9-i)ewvlC!&~aOI|ABK$n9n)ivDzE0x#+)~U81jf&k zM+wS->glN$JJv5tdfzp$DapyDRCBHlKN6Wuo5lVo(S`fH=0$T1d0UhSoFMC=8 z>7}|snmB6y!?v_CjbcoHxDu#$_lIICN?hJrTUmv7n`K}=A&gn~GtyveZ1H}2shX1~ zZ50fj@1NfS#P&^1sjoR`Tu6WVj+v{ll+7SZn+^1)zG(UbN%T$BnwyiVOl5P990j(k zd~tiUthq*Ssv{@0S!r4SDIQ;uk}`0c2T%}7-%~nNNFNHlsL9t>mHT_SF&|*1 zNhfF@MGP38K<@@JHG0EXB>yDzbFV)H`KjGxf&mq5VB^Zv*9oNq+x%+2HzLMHM?KC1 zJn2r~{PUN2ZoM}lzc(`S0B2{BuU@^2K2qki|I=)cCcECmU1Ry$IMcxKeE$eASp2j6 z4zPS(_4#g?cP;ttv#su(rQOXxCY2y!U~_!dITZ9~E-|_WGJl3TT$EBgaP5XMr2%nv zadGZ}nZADfJ=$`YWNrLhZIva<-?7!)9&d#NKIXBGXr5m)EKA-PJm)oHXSYZDv_JfN zH~w%B{@x&vK@Ge({QdbK4T+|w#uU`>6T5J-*~ih(WFf}mz6FgoXoi>xoZf(X*u|6} zRe+vzLC*oysjqdg9>{ORn$3bhBG21fQC_FGs38Mdxbh}$ej|OYJg=H~XnV3sOr%Lo zJN9_xWHzDKu51rhc0A+$#yVtY#*wj5t?f{+!s`CnLf|DckJH9xi-2`yu;hpsB@1pE zX2%6;dfVdst-^UBZWR=Q;$cDH(GU&RB~#fHeQwk(fXVS&)Ph}!xBosjt*)+Zqz2FC zwNL2F<-dFwBszy?q2J4G$SAkVmw1BW=-|Y6>>vljdB_PhDcAN}j?De*E-ePTq!JEylc(4lM@SqR>^liiCoRV5O zmG4|AgnPL)#zJg#mP(U6d8z7jjXxoxR)t(J9M?Z1upG_p#k9#5N)L8?&carkQ6v3Y zaVS5%m;gjwAv8pfTg_bc^PYl$4VLTOhU&wjEL5ljSmj`U%WRIA)Bvm>=dv;~iCDvb}zV?lfQo@!(ctl}-gxRhaW42?!d|^zv zbLaQ7P3Z@BuD&;6zsV&tF|u=F*jr$_4JqbhwI=%fpe8_o!;yXuL>)y< z%&P-=hkRVgco5f*tN6UF|GYUoZsV@|ik}pk!>Y6MOgn6YN~HwV-C1a3h3-~j%eCKj z2erKoO`4lcxX^KmnIU8A`}R4h9vMsIXm3q^gOD2VLg%mw}ym{=oQld`GoJU-$o5#!(jrsOmKvJ4t12r0+ zFZ_5rbqr?ImTQ|(!h?Ey5~ASJ+C!bhD)wf=eguV2TbU$gcs6PE!%ooC@16}zIaj80g^uyusjD*G=+(>1E)8&|pZUGoqJr3ePcLXfA4CrH># zL)CNlF-S7RfSD;y-%W zNS@4ceu>gVPRl1h>E}I1?bn>u@HmESl>6(urHidDBWB~^1E9rD-R*fiIyUNb40cy3 z`Ch`3?71;lnT!|Hsh89)+wrj=HRWGC9*NU3Q;MR%k$_1J!lHqx3k>{{RmJPF8Q(KT zqR8)$ez%Qw1@o(2hFh%}bNqrvZ1~aW6U4m)QQz0SqG064qh7aDexkX;{Q1`JI!vVD z;^hg3nF@hh9!lg7N(m<6Q0vo*e2m!BWc=RM3GwI+$Y#21Jez;t&|K;4idUC=O77HR zS|P|ulodsxv9m3!@FR00z=fAZDKhlUGF=zNTJQI5@&ngFP!o5X^Q~I2;=A=~@7y23 zi^y3S#P06aBZ#+Ns_c&LqJ+{myyt&vz#loD1 z4GhUNBgUt7-A<9P2UPFu*zfRHfa99+o63(spznm;o=;q?!(A@6RvJ4W$jdlY0el+g z*Sb!2a(0u!Ei?pk?#lZMIV*ZKvlW(SJaqWx*9Bz!)nIg7)__H2*PJxk z`^7hij7w>3U2&TFGL5hlnD?bhiz_4r$Nr{0Z(G1wGjF`H3hXcnwJ4@ zYkr>Zx7de2IzMV;wXenJR_>tk?e1#wD-07p)`Bu?ht8f9iZVN8C2GBz?_I=7hikOe z*?iBxXf=dvDnyiJrxc#+NU6~w4<&!axfI^02uF3_ z_O`6o>B8418nKaO56|HH087N=VV~<*+}|z?`g0-jv}C_D zw;jJY$-E8-N*uoxIS+h#9zrHARwR9?mr%9UY!ONx_NeJ&TLlZq_{RTb-Z0`o9r9+{ zj$-6T3+2|`2$Pj<3kT*xw(dfQN8atfgsVlL<&~2Tl!|7LXYa&+J8tA6)?vkpl7M#c zn?Gyq$<*%N_$6oiic^{KdKbZ8Lk|HeObBVt*VV84>|buC_{33x2A=Q_Ka~i$t0hZt z>&vT`r1k;zGt87y+`8mNZ6PP;c_ZRYmUy_iML?#A>$<5eYE0CgHVBL%|REY`!hrPrLJmjatH7X zxkVGS0W?1O4hR7^e^Z)t;eDgmk)&urjvCQ9{{ysI=DFZpbi&LhLMdDg<|hL=^9oU@ z6Y>!?E>3%aoUQ4SZ%@|0tv0-2aQ^^G(Esz7rfd|AA51{)ulF!b`IqP{8NSgg&Vs-? znSOIR2qTjB%9%R)FFjwpvVT>S#J7JILv4J@z9sOQ+Y6X>mMABsEQyvd{`kKD$JL0A literal 0 HcmV?d00001 diff --git a/qa-artifacts/qa-feedback-dashboard.webm b/qa-artifacts/qa-feedback-dashboard.webm new file mode 100644 index 0000000000000000000000000000000000000000..f599c9bf3463d0d1c47bca57cf624cf3407c9024 GIT binary patch literal 159144 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⁢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

o3%8|b1=R5R^&R>a) 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%0D$9D}BKM~oNQ$vmTh|d6y*N`v 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 literal 0 HcmV?d00001 diff --git a/web/tests/e2e/qa/qa-feedback-dashboard.spec.ts b/web/tests/e2e/qa/qa-feedback-dashboard.spec.ts new file mode 100644 index 00000000..d1d0d058 --- /dev/null +++ b/web/tests/e2e/qa/qa-feedback-dashboard.spec.ts @@ -0,0 +1,282 @@ +import { expect, test } from '@playwright/test'; + +const now = Date.now(); + +const jobSchedule = { + schedule: '*/30 * * * *', + installed: true, + nextRun: null, + delayMinutes: 0, + manualDelayMinutes: 0, + balancedDelayMinutes: 0, +}; + +const config = { + cronSchedule: '*/30 * * * *', + reviewerSchedule: '*/45 * * * *', + executorEnabled: true, + reviewerEnabled: true, + qa: { enabled: true, schedule: '15 */2 * * *' }, + audit: { enabled: true, schedule: '30 */6 * * *' }, + analytics: { enabled: true, schedule: '0 6 * * 1' }, + roadmapScanner: { enabled: true, slicerSchedule: '0 */6 * * *' }, + prResolver: { enabled: true, schedule: '15 6,14,22 * * *' }, + merger: { enabled: true, schedule: '55 */4 * * *' }, +}; + +test.describe('Dashboard - Feedback Performance QA', () => { + test.beforeEach(async ({ page }) => { + await page.route('**/api/mode', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ globalMode: false }), + }); + }); + + await page.route('**/api/status', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + projectName: 'Night Watch', + projectDir: '/tmp/night-watch', + config, + prds: [], + processes: [ + { name: 'executor', running: false, pid: null }, + { name: 'reviewer', running: false, pid: null }, + { name: 'qa', running: false, pid: null }, + { name: 'audit', running: false, pid: null }, + { name: 'planner', running: false, pid: null }, + { name: 'analytics', running: false, pid: null }, + { name: 'pr-resolver', running: false, pid: null }, + { name: 'merger', running: false, pid: null }, + ], + prs: [], + logs: [], + crontab: { installed: true, entries: ['night-watch executor'] }, + activePrd: null, + timestamp: new Date(now).toISOString(), + }), + }); + }); + + await page.route('**/api/prs', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify([]), + }); + }); + + await page.route('**/api/schedule-info', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + executor: jobSchedule, + reviewer: jobSchedule, + qa: jobSchedule, + audit: jobSchedule, + planner: jobSchedule, + analytics: jobSchedule, + prResolver: jobSchedule, + merger: jobSchedule, + paused: false, + schedulingPriority: 50, + entries: ['night-watch executor'], + }), + }); + }); + + await page.route('**/api/board/status', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + enabled: true, + columns: { + Draft: [], + Ready: [], + 'In Progress': [], + Review: [], + Done: [], + }, + }), + }); + }); + + await page.route('**/api/feedback/summary', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + projectPath: '/tmp/night-watch', + windows: { + last7Days: { + days: 7, + fromFinishedAt: now - 7 * 24 * 60 * 60 * 1000, + toFinishedAt: now, + totalCount: 5, + successCount: 4, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.8, + averageDurationSeconds: 72, + byOutcome: { success: 4, failure: 1 }, + byFailureCategory: { tests: 1 }, + byJobType: { + executor: { + totalCount: 5, + successCount: 4, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.8, + }, + }, + byProvider: { + codex: { + totalCount: 5, + successCount: 4, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.8, + }, + }, + }, + last30Days: { + days: 30, + fromFinishedAt: now - 30 * 24 * 60 * 60 * 1000, + toFinishedAt: now, + totalCount: 12, + successCount: 9, + failureCount: 2, + timeoutCount: 1, + rateLimitedCount: 1, + skippedCount: 0, + successRate: 0.75, + averageDurationSeconds: 95, + byOutcome: { success: 9, failure: 2, timeout: 1 }, + byFailureCategory: { tests: 2, lint: 1 }, + byJobType: { + executor: { + totalCount: 8, + successCount: 6, + failureCount: 2, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.75, + }, + reviewer: { + totalCount: 4, + successCount: 3, + failureCount: 0, + timeoutCount: 1, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.75, + }, + }, + byProvider: { + codex: { + totalCount: 9, + successCount: 7, + failureCount: 1, + timeoutCount: 1, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.78, + }, + claude: { + totalCount: 3, + successCount: 2, + failureCount: 1, + timeoutCount: 0, + rateLimitedCount: 0, + skippedCount: 0, + successRate: 0.67, + }, + }, + }, + }, + activeAugmentations: [ + { + id: 7, + projectPath: '/tmp/night-watch', + patternId: 1, + jobType: 'executor', + promptText: 'Check known flaky test setup before editing.', + status: 'active', + createdAt: now, + updatedAt: now, + expiresAt: null, + appliedCount: 2, + successCount: 1, + }, + ], + }), + }); + }); + + await page.route('**/api/feedback/patterns', async (route) => { + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + projectPath: '/tmp/night-watch', + patterns: [ + { + id: 1, + projectPath: '/tmp/night-watch', + patternKey: 'executor:tests', + jobType: 'executor', + category: 'tests', + title: 'Repeated test failures', + description: 'Executor runs repeatedly fail in vitest.', + sampleCount: 3, + confidence: 0.86, + firstSeenAt: now - 10_000, + lastSeenAt: now, + status: 'active', + metadata: {}, + }, + ], + topFailurePatterns: [ + { + key: 'executor:codex:tests:vitest failed', + jobType: 'executor', + providerKey: 'codex', + category: 'tests', + signature: 'vitest failed', + sampleCount: 3, + lastSeenAt: now, + }, + ], + }), + }); + }); + }); + + test('should render feedback performance metrics and augmentation controls', async ({ page }) => { + await page.goto('#/'); + await page.waitForLoadState('networkidle'); + + await expect(page.getByRole('heading', { name: 'Feedback Performance' })).toBeVisible(); + await expect(page.getByText('80%').first()).toBeVisible(); + await expect(page.getByText('75%').first()).toBeVisible(); + await expect(page.getByText('Success-Rate Trend')).toBeVisible(); + await expect(page.getByText('Failure Categories')).toBeVisible(); + await expect(page.getByText('Job Breakdown')).toBeVisible(); + await expect(page.getByText('Provider Breakdown')).toBeVisible(); + await expect(page.getByText('Repeated test failures')).toBeVisible(); + await expect(page.getByText('Check known flaky test setup before editing.')).toBeVisible(); + await expect(page.getByRole('button', { name: /disable/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /expire/i })).toBeVisible(); + + await page.screenshot({ + path: 'test-results/qa-feedback-dashboard.png', + fullPage: true, + }); + }); +});