From e1900f4d45406eac56851904682683f50fd9e916 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Mon, 23 Mar 2026 14:20:35 -0500 Subject: [PATCH 001/100] =?UTF-8?q?feat(gsd):=20add=20codebase=20map=20?= =?UTF-8?q?=E2=80=94=20structural=20orientation=20for=20fresh=20agent=20co?= =?UTF-8?q?ntexts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add /gsd codebase command that generates .gsd/CODEBASE.md — a table of contents for the project giving agents instant structural awareness. Eliminates the 10-30+ tool call "exploration tax" that fresh agent contexts pay to understand what exists and where things live. Components: - codebase-generator.ts: walks git ls-files, groups by directory, renders with one-liner descriptions, supports incremental updates that preserve existing descriptions - commands-codebase.ts: CLI handler (generate, update, stats) - system-context.ts: injects CODEBASE.md into system prompt at session start (alongside KNOWLEDGE.md) - paths.ts: adds CODEBASE to GSD_ROOT_FILES - catalog.ts: registers command with nested completions Features: - Incremental update preserves agent-written descriptions - Directories with >20 files collapsed to summary - Token budget: ~2-4K for 100 files, scaling to ~20K for 500 - Configurable excludes and max file count Closes #2229 14 unit tests, all passing. --- .../gsd/bootstrap/system-context.ts | 15 +- .../extensions/gsd/codebase-generator.ts | 328 ++++++++++++++++++ .../extensions/gsd/commands-codebase.ts | 105 ++++++ .../extensions/gsd/commands/catalog.ts | 8 +- .../extensions/gsd/commands/handlers/ops.ts | 5 + src/resources/extensions/gsd/paths.ts | 2 + .../gsd/tests/codebase-generator.test.ts | 237 +++++++++++++ 7 files changed, 698 insertions(+), 2 deletions(-) create mode 100644 src/resources/extensions/gsd/codebase-generator.ts create mode 100644 src/resources/extensions/gsd/commands-codebase.ts create mode 100644 src/resources/extensions/gsd/tests/codebase-generator.test.ts diff --git a/src/resources/extensions/gsd/bootstrap/system-context.ts b/src/resources/extensions/gsd/bootstrap/system-context.ts index 0a8255fdc..8a27e5fca 100644 --- a/src/resources/extensions/gsd/bootstrap/system-context.ts +++ b/src/resources/extensions/gsd/bootstrap/system-context.ts @@ -94,11 +94,24 @@ export async function buildBeforeAgentStartResult( } } + let codebaseBlock = ""; + const codebasePath = resolveGsdRootFile(process.cwd(), "CODEBASE"); + if (existsSync(codebasePath)) { + try { + const content = readFileSync(codebasePath, "utf-8").trim(); + if (content) { + codebaseBlock = `\n\n[PROJECT CODEBASE — File structure and descriptions]\n\n${content}`; + } + } catch { + // skip + } + } + warnDeprecatedAgentInstructions(); const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd()); const worktreeBlock = buildWorktreeContextBlock(); - const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`; + const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`; stopContextTimer({ systemPromptSize: fullSystem.length, diff --git a/src/resources/extensions/gsd/codebase-generator.ts b/src/resources/extensions/gsd/codebase-generator.ts new file mode 100644 index 000000000..1d4c39cb3 --- /dev/null +++ b/src/resources/extensions/gsd/codebase-generator.ts @@ -0,0 +1,328 @@ +/** + * GSD Codebase Map Generator + * + * Produces .gsd/CODEBASE.md — a structural table of contents for the project. + * Gives fresh agent contexts instant orientation without filesystem exploration. + * + * Generation: walk `git ls-files`, group by directory, output with descriptions. + * Maintenance: agent updates descriptions as it works; incremental update preserves them. + */ + +import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { join, dirname, extname } from "node:path"; + +import { execSync } from "node:child_process"; +import { gsdRoot } from "./paths.js"; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +export interface CodebaseMapOptions { + excludePatterns?: string[]; + maxFiles?: number; + collapseThreshold?: number; +} + +interface FileEntry { + path: string; + description: string; +} + +interface DirectoryGroup { + path: string; + files: FileEntry[]; + collapsed: boolean; +} + +// ─── Defaults ──────────────────────────────────────────────────────────────── + +const DEFAULT_EXCLUDES = [ + ".gsd/", + ".planning/", + ".git/", + "node_modules/", + "dist/", + "build/", + ".next/", + "coverage/", + "__pycache__/", + ".venv/", + "vendor/", +]; + +const DEFAULT_MAX_FILES = 500; +const DEFAULT_COLLAPSE_THRESHOLD = 20; + +// ─── Parsing ───────────────────────────────────────────────────────────────── + +/** + * Parse an existing CODEBASE.md to extract file → description mappings. + */ +export function parseCodebaseMap(content: string): Map { + const descriptions = new Map(); + for (const line of content.split("\n")) { + // Match: - `path/to/file.ts` — Description here + const match = line.match(/^- `(.+?)` — (.+)$/); + if (match) { + descriptions.set(match[1], match[2]); + } + // Match: - `path/to/file.ts` (no description) + const bareMatch = line.match(/^- `(.+?)`\s*$/); + if (bareMatch) { + descriptions.set(bareMatch[1], ""); + } + } + return descriptions; +} + +// ─── File Enumeration ──────────────────────────────────────────────────────── + +function shouldExclude(filePath: string, excludes: string[]): boolean { + for (const pattern of excludes) { + if (pattern.endsWith("/")) { + if (filePath.startsWith(pattern) || filePath.includes(`/${pattern}`)) return true; + } else if (filePath === pattern || filePath.endsWith(`/${pattern}`)) { + return true; + } + } + // Skip binary/lock files + const ext = extname(filePath).toLowerCase(); + if ([".lock", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".svg"].includes(ext)) { + return true; + } + return false; +} + +function lsFiles(basePath: string): string[] { + try { + // Use git ls-files directly — nativeLsFiles("") doesn't work in all contexts + const result = execSync("git ls-files", { cwd: basePath, encoding: "utf-8", timeout: 10000 }); + return result.split("\n").filter(Boolean); + } catch { + return []; + } +} + +function enumerateFiles(basePath: string, excludes: string[], maxFiles: number): string[] { + let files: string[]; + try { + files = lsFiles(basePath); + } catch { + return []; + } + + const filtered = files.filter((f) => !shouldExclude(f, excludes)); + + if (filtered.length > maxFiles) { + return filtered.slice(0, maxFiles); + } + + return filtered; +} + +// ─── Grouping ──────────────────────────────────────────────────────────────── + +function groupByDirectory( + files: string[], + descriptions: Map, + collapseThreshold: number, +): DirectoryGroup[] { + const dirMap = new Map(); + + for (const file of files) { + const dir = dirname(file); + const dirKey = dir === "." ? "" : dir; + if (!dirMap.has(dirKey)) { + dirMap.set(dirKey, []); + } + dirMap.get(dirKey)!.push({ + path: file, + description: descriptions.get(file) ?? "", + }); + } + + const groups: DirectoryGroup[] = []; + const sortedDirs = [...dirMap.keys()].sort(); + + for (const dir of sortedDirs) { + const files = dirMap.get(dir)!; + files.sort((a, b) => a.path.localeCompare(b.path)); + + groups.push({ + path: dir, + files, + collapsed: files.length > collapseThreshold, + }); + } + + return groups; +} + +// ─── Rendering ─────────────────────────────────────────────────────────────── + +function renderCodebaseMap(groups: DirectoryGroup[], totalFiles: number, truncated: boolean): string { + const lines: string[] = []; + const now = new Date().toISOString().slice(0, 19) + "Z"; + const described = groups.reduce((sum, g) => sum + g.files.filter((f) => f.description).length, 0); + + lines.push("# Codebase Map"); + lines.push(""); + lines.push(`Generated: ${now} | Files: ${totalFiles} | Described: ${described}/${totalFiles}`); + if (truncated) { + lines.push(`Note: Truncated to first ${totalFiles} files. Run with higher --max-files to include all.`); + } + lines.push(""); + + for (const group of groups) { + const heading = group.path || "(root)"; + // Use ### for directories to keep hierarchy flat and scannable + lines.push(`### ${heading}/`); + + if (group.collapsed) { + // Summarize collapsed directories + const extensions = new Map(); + for (const f of group.files) { + const ext = extname(f.path) || "(no ext)"; + extensions.set(ext, (extensions.get(ext) ?? 0) + 1); + } + const extSummary = [...extensions.entries()] + .sort((a, b) => b[1] - a[1]) + .map(([ext, count]) => `${count} ${ext}`) + .join(", "); + lines.push(`- *(${group.files.length} files: ${extSummary})*`); + } else { + for (const file of group.files) { + if (file.description) { + lines.push(`- \`${file.path}\` — ${file.description}`); + } else { + lines.push(`- \`${file.path}\``); + } + } + } + lines.push(""); + } + + return lines.join("\n"); +} + +// ─── Public API ────────────────────────────────────────────────────────────── + +/** + * Generate a fresh CODEBASE.md from scratch. + * Preserves existing descriptions if `existingDescriptions` is provided. + */ +export function generateCodebaseMap( + basePath: string, + options?: CodebaseMapOptions, + existingDescriptions?: Map, +): { content: string; fileCount: number; truncated: boolean } { + const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])]; + const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES; + const collapseThreshold = options?.collapseThreshold ?? DEFAULT_COLLAPSE_THRESHOLD; + + const files = enumerateFiles(basePath, excludes, maxFiles); + const truncated = files.length >= maxFiles; + const descriptions = existingDescriptions ?? new Map(); + const groups = groupByDirectory(files, descriptions, collapseThreshold); + const content = renderCodebaseMap(groups, files.length, truncated); + + return { content, fileCount: files.length, truncated }; +} + +/** + * Incremental update: re-scan files, preserve existing descriptions, + * add new files, remove deleted files. + */ +export function updateCodebaseMap( + basePath: string, + options?: CodebaseMapOptions, +): { content: string; added: number; removed: number; unchanged: number; fileCount: number } { + const codebasePath = join(gsdRoot(basePath), "CODEBASE.md"); + + // Load existing descriptions + let existingDescriptions = new Map(); + if (existsSync(codebasePath)) { + const existing = readFileSync(codebasePath, "utf-8"); + existingDescriptions = parseCodebaseMap(existing); + } + + const existingFiles = new Set(existingDescriptions.keys()); + + // Generate new map preserving descriptions + const result = generateCodebaseMap(basePath, options, existingDescriptions); + + // Count changes + const newFiles = new Set(); + const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])]; + const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES; + const currentFiles = enumerateFiles(basePath, excludes, maxFiles); + + for (const f of currentFiles) { + if (!existingFiles.has(f)) newFiles.add(f); + } + + const currentSet = new Set(currentFiles); + let removed = 0; + for (const f of existingFiles) { + if (!currentSet.has(f)) removed++; + } + + return { + content: result.content, + added: newFiles.size, + removed, + unchanged: currentFiles.length - newFiles.size, + fileCount: result.fileCount, + }; +} + +/** + * Write CODEBASE.md to .gsd/ directory. + */ +export function writeCodebaseMap(basePath: string, content: string): string { + const root = gsdRoot(basePath); + mkdirSync(root, { recursive: true }); + const outPath = join(root, "CODEBASE.md"); + writeFileSync(outPath, content, "utf-8"); + return outPath; +} + +/** + * Read existing CODEBASE.md, or return null if it doesn't exist. + */ +export function readCodebaseMap(basePath: string): string | null { + const codebasePath = join(gsdRoot(basePath), "CODEBASE.md"); + if (!existsSync(codebasePath)) return null; + try { + return readFileSync(codebasePath, "utf-8"); + } catch { + return null; + } +} + +/** + * Get stats about the codebase map. + */ +export function getCodebaseMapStats(basePath: string): { + exists: boolean; + fileCount: number; + describedCount: number; + undescribedCount: number; + generatedAt: string | null; +} { + const content = readCodebaseMap(basePath); + if (!content) { + return { exists: false, fileCount: 0, describedCount: 0, undescribedCount: 0, generatedAt: null }; + } + + const descriptions = parseCodebaseMap(content); + const described = [...descriptions.values()].filter((d) => d.length > 0).length; + const dateMatch = content.match(/Generated: (\S+)/); + + return { + exists: true, + fileCount: descriptions.size, + describedCount: described, + undescribedCount: descriptions.size - described, + generatedAt: dateMatch?.[1] ?? null, + }; +} diff --git a/src/resources/extensions/gsd/commands-codebase.ts b/src/resources/extensions/gsd/commands-codebase.ts new file mode 100644 index 000000000..6769c2cbf --- /dev/null +++ b/src/resources/extensions/gsd/commands-codebase.ts @@ -0,0 +1,105 @@ +/** + * GSD Command — /gsd codebase + * + * Generate and manage the codebase map (.gsd/CODEBASE.md). + * Subcommands: generate, update, stats + */ + +import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; + +import { + generateCodebaseMap, + updateCodebaseMap, + writeCodebaseMap, + getCodebaseMapStats, + readCodebaseMap, + parseCodebaseMap, +} from "./codebase-generator.js"; + +export async function handleCodebase( + args: string, + ctx: ExtensionCommandContext, + _pi: ExtensionAPI, +): Promise { + const basePath = process.cwd(); + const parts = args.trim().split(/\s+/); + const sub = parts[0] ?? ""; + + switch (sub) { + case "": + case "generate": { + const maxFilesStr = extractFlag(args, "--max-files"); + const maxFiles = maxFilesStr ? parseInt(maxFilesStr, 10) : undefined; + + // Preserve existing descriptions on bare `/gsd codebase` + let existingDescriptions: Map | undefined; + if (sub === "") { + const existing = readCodebaseMap(basePath); + if (existing) { + existingDescriptions = parseCodebaseMap(existing); + } + } + + const result = generateCodebaseMap(basePath, { maxFiles }, existingDescriptions); + const outPath = writeCodebaseMap(basePath, result.content); + + ctx.ui.notify( + `Codebase map generated: ${result.fileCount} files\n` + + `Written to: ${outPath}` + + (result.truncated ? `\n(Truncated — increase --max-files to include more)` : ""), + "success", + ); + return; + } + + case "update": { + const result = updateCodebaseMap(basePath); + writeCodebaseMap(basePath, result.content); + + ctx.ui.notify( + `Codebase map updated: ${result.fileCount} files\n` + + ` Added: ${result.added} | Removed: ${result.removed} | Unchanged: ${result.unchanged}`, + "success", + ); + return; + } + + case "stats": { + const stats = getCodebaseMapStats(basePath); + if (!stats.exists) { + ctx.ui.notify("No codebase map found. Run /gsd codebase to generate one.", "info"); + return; + } + + const coverage = stats.fileCount > 0 + ? Math.round((stats.describedCount / stats.fileCount) * 100) + : 0; + + ctx.ui.notify( + `Codebase Map Stats:\n` + + ` Files: ${stats.fileCount}\n` + + ` Described: ${stats.describedCount} (${coverage}%)\n` + + ` Undescribed: ${stats.undescribedCount}\n` + + ` Generated: ${stats.generatedAt ?? "unknown"}`, + "info", + ); + return; + } + + default: + ctx.ui.notify( + "Usage: /gsd codebase [generate|update|stats]\n\n" + + " generate [--max-files N] — Generate or regenerate CODEBASE.md\n" + + " update — Incremental update (preserves descriptions)\n" + + " stats — Show coverage and staleness\n\n" + + "With no subcommand, generates (preserving existing descriptions).", + "warning", + ); + } +} + +function extractFlag(args: string, flag: string): string | undefined { + const regex = new RegExp(`${flag}\\s+(\\S+)`); + const match = args.match(regex); + return match?.[1]; +} diff --git a/src/resources/extensions/gsd/commands/catalog.ts b/src/resources/extensions/gsd/commands/catalog.ts index 8045c85be..088da9a60 100644 --- a/src/resources/extensions/gsd/commands/catalog.ts +++ b/src/resources/extensions/gsd/commands/catalog.ts @@ -15,7 +15,7 @@ export interface GsdCommandDefinition { type CompletionMap = Record; export const GSD_COMMAND_DESCRIPTION = - "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink"; + "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase"; export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [ { cmd: "help", desc: "Categorized command reference with descriptions" }, @@ -71,6 +71,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [ { cmd: "mcp", desc: "MCP server status and connectivity check (status, check )" }, { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" }, { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" }, + { cmd: "codebase", desc: "Generate and manage codebase map (.gsd/CODEBASE.md)" }, ]; const NESTED_COMPLETIONS: CompletionMap = { @@ -224,6 +225,11 @@ const NESTED_COMPLETIONS: CompletionMap = { { cmd: "pause", desc: "Pause custom workflow auto-mode" }, { cmd: "resume", desc: "Resume paused custom workflow auto-mode" }, ], + codebase: [ + { cmd: "generate", desc: "Generate or regenerate CODEBASE.md" }, + { cmd: "update", desc: "Incremental update (preserves descriptions)" }, + { cmd: "stats", desc: "Show coverage and staleness" }, + ], }; function filterOptions( diff --git a/src/resources/extensions/gsd/commands/handlers/ops.ts b/src/resources/extensions/gsd/commands/handlers/ops.ts index a1996dfef..4ebfad1bf 100644 --- a/src/resources/extensions/gsd/commands/handlers/ops.ts +++ b/src/resources/extensions/gsd/commands/handlers/ops.ts @@ -206,5 +206,10 @@ Examples: await handleRethink(trimmed, ctx, pi); return true; } + if (trimmed === "codebase" || trimmed.startsWith("codebase ")) { + const { handleCodebase } = await import("../../commands-codebase.js"); + await handleCodebase(trimmed.replace(/^codebase\s*/, "").trim(), ctx, pi); + return true; + } return false; } diff --git a/src/resources/extensions/gsd/paths.ts b/src/resources/extensions/gsd/paths.ts index ccd3c59f6..a87f87e86 100644 --- a/src/resources/extensions/gsd/paths.ts +++ b/src/resources/extensions/gsd/paths.ts @@ -264,6 +264,7 @@ export const GSD_ROOT_FILES = { REQUIREMENTS: "REQUIREMENTS.md", OVERRIDES: "OVERRIDES.md", KNOWLEDGE: "KNOWLEDGE.md", + CODEBASE: "CODEBASE.md", } as const; export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES; @@ -276,6 +277,7 @@ const LEGACY_GSD_ROOT_FILES: Record = { REQUIREMENTS: "requirements.md", OVERRIDES: "overrides.md", KNOWLEDGE: "knowledge.md", + CODEBASE: "CODEBASE.md", }; // ─── GSD Root Discovery ─────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tests/codebase-generator.test.ts b/src/resources/extensions/gsd/tests/codebase-generator.test.ts new file mode 100644 index 000000000..782170d45 --- /dev/null +++ b/src/resources/extensions/gsd/tests/codebase-generator.test.ts @@ -0,0 +1,237 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { randomUUID } from "node:crypto"; +import { execSync } from "node:child_process"; + +import { + parseCodebaseMap, + generateCodebaseMap, + updateCodebaseMap, + writeCodebaseMap, + readCodebaseMap, + getCodebaseMapStats, +} from "../codebase-generator.ts"; + +// ─── Helpers ────────────────────────────────────────────────────────────── + +function makeTmpRepo(): string { + const base = join(tmpdir(), `gsd-codebase-test-${randomUUID()}`); + mkdirSync(join(base, ".gsd"), { recursive: true }); + execSync("git init", { cwd: base, stdio: "ignore" }); + return base; +} + +function addFile(base: string, path: string, content = ""): void { + const fullPath = join(base, path); + mkdirSync(join(fullPath, ".."), { recursive: true }); + writeFileSync(fullPath, content || `// ${path}\n`, "utf-8"); + execSync(`git add "${path}"`, { cwd: base, stdio: "ignore" }); +} + +function cleanup(base: string): void { + try { rmSync(base, { recursive: true, force: true }); } catch { /* */ } +} + +// ─── parseCodebaseMap ──────────────────────────────────────────────────── + +test("parseCodebaseMap: parses file with description", () => { + const content = `# Codebase Map + +### src/ +- \`main.ts\` — Application entry point +- \`utils.ts\` — Shared utilities +`; + + const map = parseCodebaseMap(content); + assert.equal(map.size, 2); + assert.equal(map.get("main.ts"), "Application entry point"); + assert.equal(map.get("utils.ts"), "Shared utilities"); +}); + +test("parseCodebaseMap: parses file without description", () => { + const content = `- \`config.ts\`\n- \`index.ts\` — Entry\n`; + const map = parseCodebaseMap(content); + assert.equal(map.size, 2); + assert.equal(map.get("config.ts"), ""); + assert.equal(map.get("index.ts"), "Entry"); +}); + +test("parseCodebaseMap: empty content returns empty map", () => { + const map = parseCodebaseMap(""); + assert.equal(map.size, 0); +}); + +test("parseCodebaseMap: ignores non-matching lines", () => { + const content = `# Codebase Map\n\nGenerated: 2026-03-23\n\n### src/\n- \`file.ts\` — desc\n`; + const map = parseCodebaseMap(content); + assert.equal(map.size, 1); +}); + +// ─── generateCodebaseMap ───────────────────────────────────────────────── + +test("generateCodebaseMap: generates from git ls-files", () => { + const base = makeTmpRepo(); + try { + addFile(base, "src/main.ts"); + addFile(base, "src/utils.ts"); + addFile(base, "README.md"); + + const result = generateCodebaseMap(base); + assert.ok(result.content.includes("# Codebase Map")); + assert.ok(result.content.includes("`src/main.ts`")); + assert.ok(result.content.includes("`src/utils.ts`")); + assert.ok(result.content.includes("README.md")); + assert.equal(result.fileCount, 3); + assert.equal(result.truncated, false); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: excludes .gsd/ files", () => { + const base = makeTmpRepo(); + try { + addFile(base, "src/main.ts"); + addFile(base, ".gsd/PROJECT.md"); + + const result = generateCodebaseMap(base); + assert.ok(result.content.includes("`src/main.ts`")); + assert.ok(!result.content.includes("PROJECT.md")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: preserves existing descriptions", () => { + const base = makeTmpRepo(); + try { + addFile(base, "src/main.ts"); + addFile(base, "src/utils.ts"); + + const descriptions = new Map(); + descriptions.set("src/main.ts", "App entry point"); + + const result = generateCodebaseMap(base, undefined, descriptions); + assert.ok(result.content.includes("`src/main.ts` — App entry point")); + // utils.ts should be present but without description + assert.ok(result.content.includes("`src/utils.ts`")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: collapses large directories", () => { + const base = makeTmpRepo(); + try { + // Create 25 files in one directory (above default threshold of 20) + for (let i = 0; i < 25; i++) { + addFile(base, `src/components/comp${String(i).padStart(2, "0")}.ts`); + } + + const result = generateCodebaseMap(base); + // Should be collapsed + assert.ok(result.content.includes("25 files")); + assert.ok(result.content.includes(".ts")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: respects maxFiles", () => { + const base = makeTmpRepo(); + try { + for (let i = 0; i < 10; i++) { + addFile(base, `file${i}.ts`); + } + + const result = generateCodebaseMap(base, { maxFiles: 5 }); + assert.equal(result.fileCount, 5); + assert.equal(result.truncated, true); + } finally { + cleanup(base); + } +}); + +// ─── updateCodebaseMap ─────────────────────────────────────────────────── + +test("updateCodebaseMap: preserves descriptions on update", () => { + const base = makeTmpRepo(); + try { + addFile(base, "src/main.ts"); + addFile(base, "src/utils.ts"); + + // Generate initial map with a description + const initial = generateCodebaseMap(base, undefined, new Map([["src/main.ts", "Entry point"]])); + writeCodebaseMap(base, initial.content); + + // Add a new file + addFile(base, "src/new.ts"); + + // Update should preserve the description + const result = updateCodebaseMap(base); + assert.ok(result.content.includes("`src/main.ts` — Entry point")); + assert.equal(result.added, 1); + assert.equal(result.fileCount, 3); + } finally { + cleanup(base); + } +}); + +// ─── writeCodebaseMap / readCodebaseMap ────────────────────────────────── + +test("writeCodebaseMap + readCodebaseMap roundtrip", () => { + const base = makeTmpRepo(); + try { + const content = "# Codebase Map\n\n- `test.ts` — A test file\n"; + const outPath = writeCodebaseMap(base, content); + assert.ok(existsSync(outPath)); + + const read = readCodebaseMap(base); + assert.equal(read, content); + } finally { + cleanup(base); + } +}); + +test("readCodebaseMap: returns null when file missing", () => { + const base = makeTmpRepo(); + try { + const result = readCodebaseMap(base); + assert.equal(result, null); + } finally { + cleanup(base); + } +}); + +// ─── getCodebaseMapStats ───────────────────────────────────────────────── + +test("getCodebaseMapStats: no map returns exists=false", () => { + const base = makeTmpRepo(); + try { + const stats = getCodebaseMapStats(base); + assert.equal(stats.exists, false); + assert.equal(stats.fileCount, 0); + } finally { + cleanup(base); + } +}); + +test("getCodebaseMapStats: reports coverage", () => { + const base = makeTmpRepo(); + try { + const content = `# Codebase Map\n\nGenerated: 2026-03-23T14:00:00Z\n\n- \`a.ts\` — Has desc\n- \`b.ts\`\n- \`c.ts\` — Also has\n`; + writeCodebaseMap(base, content); + + const stats = getCodebaseMapStats(base); + assert.equal(stats.exists, true); + assert.equal(stats.fileCount, 3); + assert.equal(stats.describedCount, 2); + assert.equal(stats.undescribedCount, 1); + assert.equal(stats.generatedAt, "2026-03-23T14:00:00Z"); + } finally { + cleanup(base); + } +}); From 97f4d5d2593827d92072641ed38f35ca0c0f4475 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Mon, 23 Mar 2026 16:51:31 -0500 Subject: [PATCH 002/100] =?UTF-8?q?fix(gsd):=20harden=20codebase-map=20?= =?UTF-8?q?=E2=80=94=20bug=20fixes,=20UX=20polish,=20and=20expanded=20test?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generator (codebase-generator.ts): - Fix truncation off-by-one: use filtered.length > maxFiles (not >=) - Fix collapsed-directory round-trip: emit comment blocks so incremental updates recover descriptions for collapsed dirs - Fix double-enumeration race in updateCodebaseMap: reuse files array from generateCodebaseMap instead of calling enumerateFiles a second time - Propagate truncated flag through updateCodebaseMap return type - Fix getCodebaseMapStats to read Files: N from header (accurate for collapsed dirs) - Remove redundant dead catch around lsFiles() in enumerateFiles - parseCodebaseMap: use else-if for bare match (avoid unnecessary double-check) - parseCodebaseMap: scan gsd:collapsed-descriptions comment blocks Command handler (commands-codebase.ts): - Bare /gsd codebase now shows stats (if map exists) or help (if no map) instead of silently running generate - Add explicit help subcommand with info-level output - Guard update: warn if no CODEBASE.md exists instead of silently generating - Validate --max-files: reject NaN, zero, and negative values with clear message - Emit warning (not success) when generate produces 0 files - Propagate truncated flag warning in both generate and update output - Fix extractFlag regex: escape flag name and support --flag=value syntax - Add actionable tip to stats output Catalog (commands/catalog.ts): - Add --max-files and help to codebase tab-completion entries System context (bootstrap/system-context.ts): - Cap CODEBASE.md injection at 8 000 chars (~2 000 tokens) per request - Add generation timestamp and staleness notice to the injected block header Paths (paths.ts): - Fix LEGACY_GSD_ROOT_FILES.CODEBASE to use lowercase codebase.md (matches the pattern of all other legacy root file names) Tests (codebase-generator.test.ts): - 15 new test cases: custom excludePatterns, collapseThreshold option, truncation boundary conditions (below/at/above limit), non-git directory, empty repo, collapsed-description round-trip, removed file tracking, binary/lock exclusions, truncated flag propagation, collapsed-dir stats accuracy, .gsd/ auto-creation, corrupted input, parseCodebaseMap comment blocks - Fix collapse assertion to verify individual entries are absent from main body - Fix git rm test to commit first so git rm succeeds - 29/29 tests passing Co-Authored-By: Claude Sonnet 4.6 --- .../gsd/bootstrap/system-context.ts | 14 +- .../extensions/gsd/codebase-generator.ts | 115 +++++--- .../extensions/gsd/commands-codebase.ts | 139 ++++++--- .../extensions/gsd/commands/catalog.ts | 5 +- src/resources/extensions/gsd/paths.ts | 2 +- .../gsd/tests/codebase-generator.test.ts | 279 +++++++++++++++++- 6 files changed, 449 insertions(+), 105 deletions(-) diff --git a/src/resources/extensions/gsd/bootstrap/system-context.ts b/src/resources/extensions/gsd/bootstrap/system-context.ts index 8a27e5fca..534cffe0b 100644 --- a/src/resources/extensions/gsd/bootstrap/system-context.ts +++ b/src/resources/extensions/gsd/bootstrap/system-context.ts @@ -98,9 +98,17 @@ export async function buildBeforeAgentStartResult( const codebasePath = resolveGsdRootFile(process.cwd(), "CODEBASE"); if (existsSync(codebasePath)) { try { - const content = readFileSync(codebasePath, "utf-8").trim(); - if (content) { - codebaseBlock = `\n\n[PROJECT CODEBASE — File structure and descriptions]\n\n${content}`; + const rawContent = readFileSync(codebasePath, "utf-8").trim(); + if (rawContent) { + // Cap injection size to ~2 000 tokens to avoid bloating every request. + // Full map is always available at .gsd/CODEBASE.md. + const MAX_CODEBASE_CHARS = 8_000; + const generatedMatch = rawContent.match(/Generated: (\S+)/); + const generatedAt = generatedMatch?.[1] ?? "unknown"; + const content = rawContent.length > MAX_CODEBASE_CHARS + ? rawContent.slice(0, MAX_CODEBASE_CHARS) + "\n\n*(truncated — see .gsd/CODEBASE.md for full map)*" + : rawContent; + codebaseBlock = `\n\n[PROJECT CODEBASE — File structure and descriptions (generated ${generatedAt}, may be stale — run /gsd codebase update to refresh)]\n\n${content}`; } } catch { // skip diff --git a/src/resources/extensions/gsd/codebase-generator.ts b/src/resources/extensions/gsd/codebase-generator.ts index 1d4c39cb3..6fe558abb 100644 --- a/src/resources/extensions/gsd/codebase-generator.ts +++ b/src/resources/extensions/gsd/codebase-generator.ts @@ -56,19 +56,37 @@ const DEFAULT_COLLAPSE_THRESHOLD = 20; /** * Parse an existing CODEBASE.md to extract file → description mappings. + * Also scans comment blocks to preserve + * descriptions for files in collapsed directories across incremental updates. */ export function parseCodebaseMap(content: string): Map { const descriptions = new Map(); + let inCollapsedBlock = false; + for (const line of content.split("\n")) { + // Track collapsed-description comment blocks + if (line.trimStart().startsWith("")) { + inCollapsedBlock = false; + continue; + } + // Match: - `path/to/file.ts` — Description here const match = line.match(/^- `(.+?)` — (.+)$/); if (match) { descriptions.set(match[1], match[2]); + continue; } - // Match: - `path/to/file.ts` (no description) - const bareMatch = line.match(/^- `(.+?)`\s*$/); - if (bareMatch) { - descriptions.set(bareMatch[1], ""); + + // Match: - `path/to/file.ts` (no description) — only outside collapsed blocks + if (!inCollapsedBlock) { + const bareMatch = line.match(/^- `(.+?)`\s*$/); + if (bareMatch) { + descriptions.set(bareMatch[1], ""); + } } } return descriptions; @@ -94,7 +112,6 @@ function shouldExclude(filePath: string, excludes: string[]): boolean { function lsFiles(basePath: string): string[] { try { - // Use git ls-files directly — nativeLsFiles("") doesn't work in all contexts const result = execSync("git ls-files", { cwd: basePath, encoding: "utf-8", timeout: 10000 }); return result.split("\n").filter(Boolean); } catch { @@ -102,21 +119,15 @@ function lsFiles(basePath: string): string[] { } } -function enumerateFiles(basePath: string, excludes: string[], maxFiles: number): string[] { - let files: string[]; - try { - files = lsFiles(basePath); - } catch { - return []; - } - - const filtered = files.filter((f) => !shouldExclude(f, excludes)); - - if (filtered.length > maxFiles) { - return filtered.slice(0, maxFiles); - } - - return filtered; +/** + * Enumerate tracked files, applying exclusions and the maxFiles cap. + * Returns both the file list and whether truncation occurred. + */ +function enumerateFiles(basePath: string, excludes: string[], maxFiles: number): { files: string[]; truncated: boolean } { + const allFiles = lsFiles(basePath); + const filtered = allFiles.filter((f) => !shouldExclude(f, excludes)); + const truncated = filtered.length > maxFiles; + return { files: truncated ? filtered.slice(0, maxFiles) : filtered, truncated }; } // ─── Grouping ──────────────────────────────────────────────────────────────── @@ -144,13 +155,13 @@ function groupByDirectory( const sortedDirs = [...dirMap.keys()].sort(); for (const dir of sortedDirs) { - const files = dirMap.get(dir)!; - files.sort((a, b) => a.path.localeCompare(b.path)); + const dirFiles = dirMap.get(dir)!; + dirFiles.sort((a, b) => a.path.localeCompare(b.path)); groups.push({ path: dir, - files, - collapsed: files.length > collapseThreshold, + files: dirFiles, + collapsed: dirFiles.length > collapseThreshold, }); } @@ -161,7 +172,7 @@ function groupByDirectory( function renderCodebaseMap(groups: DirectoryGroup[], totalFiles: number, truncated: boolean): string { const lines: string[] = []; - const now = new Date().toISOString().slice(0, 19) + "Z"; + const now = new Date().toISOString().split(".")[0] + "Z"; const described = groups.reduce((sum, g) => sum + g.files.filter((f) => f.description).length, 0); lines.push("# Codebase Map"); @@ -174,7 +185,6 @@ function renderCodebaseMap(groups: DirectoryGroup[], totalFiles: number, truncat for (const group of groups) { const heading = group.path || "(root)"; - // Use ### for directories to keep hierarchy flat and scannable lines.push(`### ${heading}/`); if (group.collapsed) { @@ -189,6 +199,17 @@ function renderCodebaseMap(groups: DirectoryGroup[], totalFiles: number, truncat .map(([ext, count]) => `${count} ${ext}`) .join(", "); lines.push(`- *(${group.files.length} files: ${extSummary})*`); + + // Preserve any existing descriptions in a hidden comment block so + // incremental updates can recover them via parseCodebaseMap. + const descLines = group.files + .filter((f) => f.description) + .map((f) => `- \`${f.path}\` — ${f.description}`); + if (descLines.length > 0) { + lines.push(""); + } } else { for (const file of group.files) { if (file.description) { @@ -214,18 +235,17 @@ export function generateCodebaseMap( basePath: string, options?: CodebaseMapOptions, existingDescriptions?: Map, -): { content: string; fileCount: number; truncated: boolean } { +): { content: string; fileCount: number; truncated: boolean; files: string[] } { const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])]; const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES; const collapseThreshold = options?.collapseThreshold ?? DEFAULT_COLLAPSE_THRESHOLD; - const files = enumerateFiles(basePath, excludes, maxFiles); - const truncated = files.length >= maxFiles; + const { files, truncated } = enumerateFiles(basePath, excludes, maxFiles); const descriptions = existingDescriptions ?? new Map(); const groups = groupByDirectory(files, descriptions, collapseThreshold); const content = renderCodebaseMap(groups, files.length, truncated); - return { content, fileCount: files.length, truncated }; + return { content, fileCount: files.length, truncated, files }; } /** @@ -235,7 +255,7 @@ export function generateCodebaseMap( export function updateCodebaseMap( basePath: string, options?: CodebaseMapOptions, -): { content: string; added: number; removed: number; unchanged: number; fileCount: number } { +): { content: string; added: number; removed: number; unchanged: number; fileCount: number; truncated: boolean } { const codebasePath = join(gsdRoot(basePath), "CODEBASE.md"); // Load existing descriptions @@ -247,31 +267,29 @@ export function updateCodebaseMap( const existingFiles = new Set(existingDescriptions.keys()); - // Generate new map preserving descriptions + // Generate new map preserving descriptions — reuse the returned file list + // to avoid a second enumeration (prevents race between content and stats). const result = generateCodebaseMap(basePath, options, existingDescriptions); + const currentSet = new Set(result.files); // Count changes - const newFiles = new Set(); - const excludes = [...DEFAULT_EXCLUDES, ...(options?.excludePatterns ?? [])]; - const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES; - const currentFiles = enumerateFiles(basePath, excludes, maxFiles); - - for (const f of currentFiles) { - if (!existingFiles.has(f)) newFiles.add(f); - } - - const currentSet = new Set(currentFiles); + let added = 0; let removed = 0; + + for (const f of result.files) { + if (!existingFiles.has(f)) added++; + } for (const f of existingFiles) { if (!currentSet.has(f)) removed++; } return { content: result.content, - added: newFiles.size, + added, removed, - unchanged: currentFiles.length - newFiles.size, + unchanged: result.files.length - added, fileCount: result.fileCount, + truncated: result.truncated, }; } @@ -314,15 +332,20 @@ export function getCodebaseMapStats(basePath: string): { return { exists: false, fileCount: 0, describedCount: 0, undescribedCount: 0, generatedAt: null }; } + // Parse total file count from the header line (accurate even for collapsed dirs) + const fileCountMatch = content.match(/Files:\s*(\d+)/); + const totalFiles = fileCountMatch ? parseInt(fileCountMatch[1], 10) : 0; + + // Use parseCodebaseMap to count described files (includes collapsed-description blocks) const descriptions = parseCodebaseMap(content); const described = [...descriptions.values()].filter((d) => d.length > 0).length; const dateMatch = content.match(/Generated: (\S+)/); return { exists: true, - fileCount: descriptions.size, + fileCount: totalFiles, describedCount: described, - undescribedCount: descriptions.size - described, + undescribedCount: totalFiles - described, generatedAt: dateMatch?.[1] ?? null, }; } diff --git a/src/resources/extensions/gsd/commands-codebase.ts b/src/resources/extensions/gsd/commands-codebase.ts index 6769c2cbf..305f09256 100644 --- a/src/resources/extensions/gsd/commands-codebase.ts +++ b/src/resources/extensions/gsd/commands-codebase.ts @@ -2,7 +2,7 @@ * GSD Command — /gsd codebase * * Generate and manage the codebase map (.gsd/CODEBASE.md). - * Subcommands: generate, update, stats + * Subcommands: generate, update, stats, help */ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; @@ -13,9 +13,16 @@ import { writeCodebaseMap, getCodebaseMapStats, readCodebaseMap, - parseCodebaseMap, } from "./codebase-generator.js"; +const USAGE = + "Usage: /gsd codebase [generate|update|stats]\n\n" + + " generate [--max-files N] — Generate or regenerate CODEBASE.md\n" + + " update — Incremental update (preserves descriptions)\n" + + " stats — Show file count, coverage, and generation time\n" + + " help — Show this help\n\n" + + "With no subcommand, shows stats if a map exists or help if not."; + export async function handleCodebase( args: string, ctx: ExtensionCommandContext, @@ -26,80 +33,132 @@ export async function handleCodebase( const sub = parts[0] ?? ""; switch (sub) { - case "": case "generate": { - const maxFilesStr = extractFlag(args, "--max-files"); - const maxFiles = maxFilesStr ? parseInt(maxFilesStr, 10) : undefined; + const maxFiles = parseMaxFiles(args, ctx); + if (maxFiles === false) return; // validation failed, message already shown - // Preserve existing descriptions on bare `/gsd codebase` - let existingDescriptions: Map | undefined; - if (sub === "") { - const existing = readCodebaseMap(basePath); - if (existing) { - existingDescriptions = parseCodebaseMap(existing); - } + const existing = readCodebaseMap(basePath); + const existingDescriptions = existing + ? (await import("./codebase-generator.js")).parseCodebaseMap(existing) + : undefined; + + const result = generateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined }, existingDescriptions); + + if (result.fileCount === 0) { + ctx.ui.notify( + "Codebase map generated with 0 files.\n" + + "Is this a git repository? Run 'git ls-files' to verify.", + "warning", + ); + return; } - const result = generateCodebaseMap(basePath, { maxFiles }, existingDescriptions); const outPath = writeCodebaseMap(basePath, result.content); - ctx.ui.notify( `Codebase map generated: ${result.fileCount} files\n` + `Written to: ${outPath}` + - (result.truncated ? `\n(Truncated — increase --max-files to include more)` : ""), + (result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""), "success", ); return; } case "update": { - const result = updateCodebaseMap(basePath); + const existing = readCodebaseMap(basePath); + if (!existing) { + ctx.ui.notify( + "No codebase map found. Run /gsd codebase generate to create one.", + "warning", + ); + return; + } + + const maxFiles = parseMaxFiles(args, ctx); + if (maxFiles === false) return; + + const result = updateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined }); writeCodebaseMap(basePath, result.content); ctx.ui.notify( `Codebase map updated: ${result.fileCount} files\n` + - ` Added: ${result.added} | Removed: ${result.removed} | Unchanged: ${result.unchanged}`, + ` Added: ${result.added} | Removed: ${result.removed} | Unchanged: ${result.unchanged}` + + (result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""), "success", ); return; } case "stats": { - const stats = getCodebaseMapStats(basePath); - if (!stats.exists) { - ctx.ui.notify("No codebase map found. Run /gsd codebase to generate one.", "info"); - return; + showStats(basePath, ctx); + return; + } + + case "help": + ctx.ui.notify(USAGE, "info"); + return; + + case "": { + // Safe default: show stats if map exists, help if not + const existing = readCodebaseMap(basePath); + if (existing) { + showStats(basePath, ctx); + } else { + ctx.ui.notify(USAGE, "info"); } - - const coverage = stats.fileCount > 0 - ? Math.round((stats.describedCount / stats.fileCount) * 100) - : 0; - - ctx.ui.notify( - `Codebase Map Stats:\n` + - ` Files: ${stats.fileCount}\n` + - ` Described: ${stats.describedCount} (${coverage}%)\n` + - ` Undescribed: ${stats.undescribedCount}\n` + - ` Generated: ${stats.generatedAt ?? "unknown"}`, - "info", - ); return; } default: ctx.ui.notify( - "Usage: /gsd codebase [generate|update|stats]\n\n" + - " generate [--max-files N] — Generate or regenerate CODEBASE.md\n" + - " update — Incremental update (preserves descriptions)\n" + - " stats — Show coverage and staleness\n\n" + - "With no subcommand, generates (preserving existing descriptions).", + `Unknown subcommand "${sub}".\n\n${USAGE}`, "warning", ); } } +function showStats(basePath: string, ctx: ExtensionCommandContext): void { + const stats = getCodebaseMapStats(basePath); + if (!stats.exists) { + ctx.ui.notify("No codebase map found. Run /gsd codebase generate to create one.", "info"); + return; + } + + const coverage = stats.fileCount > 0 + ? Math.round((stats.describedCount / stats.fileCount) * 100) + : 0; + + ctx.ui.notify( + `Codebase Map Stats:\n` + + ` Files: ${stats.fileCount}\n` + + ` Described: ${stats.describedCount} (${coverage}%)\n` + + ` Undescribed: ${stats.undescribedCount}\n` + + ` Generated: ${stats.generatedAt ?? "unknown"}\n\n` + + (stats.undescribedCount > 0 + ? `Tip: Run /gsd codebase update to refresh after file changes.` + : `Coverage is complete.`), + "info", + ); +} + +/** + * Parse and validate --max-files flag. + * Returns the parsed number, undefined if flag not present, or false if invalid. + */ +function parseMaxFiles(args: string, ctx: ExtensionCommandContext): number | undefined | false { + const maxFilesStr = extractFlag(args, "--max-files"); + if (!maxFilesStr) return undefined; + + const maxFiles = parseInt(maxFilesStr, 10); + if (isNaN(maxFiles) || maxFiles < 1) { + ctx.ui.notify("--max-files must be a positive integer (e.g. --max-files 200).", "warning"); + return false; + } + return maxFiles; +} + function extractFlag(args: string, flag: string): string | undefined { - const regex = new RegExp(`${flag}\\s+(\\S+)`); + const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(`${escaped}[=\\s]+(\\S+)`); const match = args.match(regex); return match?.[1]; } diff --git a/src/resources/extensions/gsd/commands/catalog.ts b/src/resources/extensions/gsd/commands/catalog.ts index 088da9a60..c59464963 100644 --- a/src/resources/extensions/gsd/commands/catalog.ts +++ b/src/resources/extensions/gsd/commands/catalog.ts @@ -227,8 +227,11 @@ const NESTED_COMPLETIONS: CompletionMap = { ], codebase: [ { cmd: "generate", desc: "Generate or regenerate CODEBASE.md" }, + { cmd: "generate --max-files", desc: "Generate with custom file limit (default: 500)" }, { cmd: "update", desc: "Incremental update (preserves descriptions)" }, - { cmd: "stats", desc: "Show coverage and staleness" }, + { cmd: "update --max-files", desc: "Update with custom file limit" }, + { cmd: "stats", desc: "Show file count, description coverage, and generation time" }, + { cmd: "help", desc: "Show usage and available subcommands" }, ], }; diff --git a/src/resources/extensions/gsd/paths.ts b/src/resources/extensions/gsd/paths.ts index a87f87e86..d6f96bc9d 100644 --- a/src/resources/extensions/gsd/paths.ts +++ b/src/resources/extensions/gsd/paths.ts @@ -277,7 +277,7 @@ const LEGACY_GSD_ROOT_FILES: Record = { REQUIREMENTS: "requirements.md", OVERRIDES: "overrides.md", KNOWLEDGE: "knowledge.md", - CODEBASE: "CODEBASE.md", + CODEBASE: "codebase.md", }; // ─── GSD Root Discovery ─────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tests/codebase-generator.test.ts b/src/resources/extensions/gsd/tests/codebase-generator.test.ts index 782170d45..c698fc65f 100644 --- a/src/resources/extensions/gsd/tests/codebase-generator.test.ts +++ b/src/resources/extensions/gsd/tests/codebase-generator.test.ts @@ -70,6 +70,38 @@ test("parseCodebaseMap: ignores non-matching lines", () => { assert.equal(map.size, 1); }); +test("parseCodebaseMap: recovers descriptions from collapsed-description comments", () => { + const content = `# Codebase Map + +### src/components/ +- *(25 files: 25 .ts)* + +`; + const map = parseCodebaseMap(content); + assert.equal(map.get("src/components/Foo.ts"), "The Foo component"); + assert.equal(map.get("src/components/Bar.ts"), "The Bar component"); + // The collapsed summary line itself should not be parsed as a file + assert.ok(!map.has("*(25 files: 25 .ts)*")); +}); + +test("parseCodebaseMap: handles corrupted/malformed input gracefully", () => { + const content = [ + "- `unclosed backtick", + "- `` — empty filename", + "- `valid.ts` — ok", + "random garbage line", + "- `a.ts` — desc with other text", + ].join("\n"); + const map = parseCodebaseMap(content); + assert.ok(map.has("valid.ts")); + assert.ok(map.has("a.ts")); + // Malformed lines should be silently skipped + assert.equal(map.size, 2); +}); + // ─── generateCodebaseMap ───────────────────────────────────────────────── test("generateCodebaseMap: generates from git ls-files", () => { @@ -86,6 +118,7 @@ test("generateCodebaseMap: generates from git ls-files", () => { assert.ok(result.content.includes("README.md")); assert.equal(result.fileCount, 3); assert.equal(result.truncated, false); + assert.equal(result.files.length, 3); } finally { cleanup(base); } @@ -105,6 +138,41 @@ test("generateCodebaseMap: excludes .gsd/ files", () => { } }); +test("generateCodebaseMap: excludes binary and lock files", () => { + const base = makeTmpRepo(); + try { + addFile(base, "src/main.ts"); + addFile(base, "package-lock.json"); // .json not excluded + addFile(base, "yarn.lock"); // .lock excluded + addFile(base, "assets/logo.png"); // .png excluded + + const result = generateCodebaseMap(base); + assert.ok(result.content.includes("`src/main.ts`")); + assert.ok(result.content.includes("package-lock.json")); + assert.ok(!result.content.includes("yarn.lock")); + assert.ok(!result.content.includes("logo.png")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: respects custom excludePatterns", () => { + const base = makeTmpRepo(); + try { + addFile(base, "src/main.ts"); + addFile(base, "docs/guide.md"); + addFile(base, "docs/api.md"); + + const result = generateCodebaseMap(base, { excludePatterns: ["docs/"] }); + assert.ok(result.content.includes("`src/main.ts`")); + assert.ok(!result.content.includes("guide.md")); + assert.ok(!result.content.includes("api.md")); + assert.equal(result.fileCount, 1); + } finally { + cleanup(base); + } +}); + test("generateCodebaseMap: preserves existing descriptions", () => { const base = makeTmpRepo(); try { @@ -116,7 +184,6 @@ test("generateCodebaseMap: preserves existing descriptions", () => { const result = generateCodebaseMap(base, undefined, descriptions); assert.ok(result.content.includes("`src/main.ts` — App entry point")); - // utils.ts should be present but without description assert.ok(result.content.includes("`src/utils.ts`")); } finally { cleanup(base); @@ -126,30 +193,119 @@ test("generateCodebaseMap: preserves existing descriptions", () => { test("generateCodebaseMap: collapses large directories", () => { const base = makeTmpRepo(); try { - // Create 25 files in one directory (above default threshold of 20) for (let i = 0; i < 25; i++) { addFile(base, `src/components/comp${String(i).padStart(2, "0")}.ts`); } const result = generateCodebaseMap(base); - // Should be collapsed - assert.ok(result.content.includes("25 files")); - assert.ok(result.content.includes(".ts")); + // Collapsed summary should appear + assert.ok(result.content.includes("*(25 files: 25 .ts)*")); + // Individual file entries should NOT appear in main body + assert.ok(!result.content.includes("`src/components/comp00.ts`\n")); } finally { cleanup(base); } }); -test("generateCodebaseMap: respects maxFiles", () => { +test("generateCodebaseMap: respects custom collapseThreshold", () => { const base = makeTmpRepo(); try { - for (let i = 0; i < 10; i++) { - addFile(base, `file${i}.ts`); - } + for (let i = 0; i < 5; i++) addFile(base, `src/comp${i}.ts`); + // Low threshold: 5 files should collapse + const collapsed = generateCodebaseMap(base, { collapseThreshold: 3 }); + assert.ok(collapsed.content.includes("5 files")); + + // High threshold: 5 files should expand + const expanded = generateCodebaseMap(base, { collapseThreshold: 10 }); + assert.ok(expanded.content.includes("`src/comp0.ts`")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: truncated=false when file count is below maxFiles", () => { + const base = makeTmpRepo(); + try { + for (let i = 0; i < 4; i++) addFile(base, `file${i}.ts`); + const result = generateCodebaseMap(base, { maxFiles: 5 }); + assert.equal(result.fileCount, 4); + assert.equal(result.truncated, false); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: truncated=false when file count equals maxFiles exactly", () => { + const base = makeTmpRepo(); + try { + for (let i = 0; i < 5; i++) addFile(base, `file${i}.ts`); + const result = generateCodebaseMap(base, { maxFiles: 5 }); + assert.equal(result.fileCount, 5); + assert.equal(result.truncated, false); // exactly at limit — nothing was truncated + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: truncated=true when file count exceeds maxFiles", () => { + const base = makeTmpRepo(); + try { + for (let i = 0; i < 10; i++) addFile(base, `file${i}.ts`); const result = generateCodebaseMap(base, { maxFiles: 5 }); assert.equal(result.fileCount, 5); assert.equal(result.truncated, true); + assert.ok(result.content.includes("Truncated")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: returns empty map for non-git directory", () => { + const base = join(tmpdir(), `gsd-codebase-test-${randomUUID()}`); + mkdirSync(join(base, ".gsd"), { recursive: true }); + // No git init + try { + const result = generateCodebaseMap(base); + assert.equal(result.fileCount, 0); + assert.equal(result.truncated, false); + assert.ok(result.content.includes("# Codebase Map")); + assert.equal(result.files.length, 0); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: handles empty repository (no committed files)", () => { + const base = makeTmpRepo(); + try { + const result = generateCodebaseMap(base); + assert.equal(result.fileCount, 0); + assert.equal(result.truncated, false); + assert.ok(result.content.includes("Files: 0")); + } finally { + cleanup(base); + } +}); + +test("generateCodebaseMap: collapsed directories preserve descriptions in hidden comment", () => { + const base = makeTmpRepo(); + try { + for (let i = 0; i < 25; i++) { + addFile(base, `src/components/comp${String(i).padStart(2, "0")}.ts`); + } + + // Generate with a description for one file in the collapsed dir + const descriptions = new Map([["src/components/comp00.ts", "The first component"]]); + const result = generateCodebaseMap(base, undefined, descriptions); + + // The description should be in the hidden comment block + assert.ok(result.content.includes("3&db}^RK4TAv7TFmSIZHv`qYw36CuU6a3`slUY zCgP^iMYE-@lKFNxw6PAce!y|=GrhB&$to_<)=WP8hn8xy4@&ctlC!9Id6EjAouR&9 zb(5EthUJc-gzH=&0|UL=d~Ch(#Pc}mo9BIg3JTQkVB8QcS!(b(G2pSE_7P(L&;agK zt0cF_hkS(wcTfOI)W8DXhKU-Vq8iCgAUQ6G)W)XH1p4Ebh56B%@25@QDOpo zTf;rJS?KbPXyXEOB3)31sx{lbO`A&3P)@jDZid`yjtSGNZC&GCIoDiXQ@PZWkQ$~j zR9Zpfl2aiK9l$)I1Qe6jr~-bfx5H#RhtP6^5H1S-Lnz2U+@esR z7+Pzj#}{(^ee&7Ys~aV8r-K%e3+mp-nTG6@EpU<#p zb9pAHH;t9X{E5OS2Lii9R{1Qm+SLhD*2+ng&Z*p42agXJQ-&FfW67?x(O$Sg$1|li zWelN9T+y%dqcM>{OKNU-<#x29WlFRnSG~Dq$%^u($ipQ=u!>Ic61RIw*B=^C z@mJlO`iep%fPZj)*(#+bKQpuX!$&amRtDGU&L8b69H{VYawp6%P4n@8phV3bLxtu3ETwEohoFhl87tt0y$ z2Janmj8|I>^_vS-PMB5c4>geXE(b*(j~ZIXZPZ0q0_goo`}ld$4Q$O%I@YODfiqdX zo{s%w_v#(C9QDJ8g=jR~=sBk;afq|Be(j~hAt64;1t>CqHrf)_2KH&=(~W&4t_c?_ zsq$IVB5f9>J7=dX^BId_bX=$Vj_Tz5tKxSuQciu4{DwEm>_Nq~3C`*(Q)+%Iu)%y$xnngf5(ST4N=B3Cfe} z7i3E~RP$px7$K3tnu|2GTb*oWaSb(Q2&RpHPeNRCN0C@II6lX+zx3_A63aDY<43Vv zszLQ)Cu@(5t64{P$A><{IKNvSS)$*4JM)2-S;4Ntim{pveX2Bw;A5|ikp$Bd1$KsE zz?QMB+Tde~{E=rG5-HTG*ENc7|C;H0m2kPbe4!MA3sT9g`l`2jr86ojI!DtAHdPqY z_pGE3^@8;E??W?gmTW zQZak~>xq4JT&;C_RxpM$x=vY;M!@El2TR%JWHKa5d^|VjvTso3PrY4sA|@tJ6VIP3 zt*kgMww~moL7NXjUj{I;W$0DEzfk=2;BoT&D@t7(%o=L5$)$@BwH1s-QiAs$zLvwK z&#gqfBx-$fxBi{wO6i~qK#rC^{R1@sJvhQ5|!S=nbcPm#?t1~`I((R zq2Fpb;?rft8!Aea33B1e&5D~l^i(D!H7SmU{d1uvOH3i)+{znkij4*Caourx*IOYQ zn^QVUtbUQuhb%35C-Z$Lz2w?~lAs5-GOOpWpGkSpG<)DfPC6kWo*1vf#2(`sPFNQf3SKJ$WCcUw@I zt|Z~>d*1kZ;<@TYE@kAyT@~hey}Ycd3T~HBva;*%UIxYok2MIbF-=Z`d~ngkaZpJE~|pE45jKXgM5`qPPB~&sOGL!EDS$ znxsn_E}0 zq&zVB<^puqbm(?l_Map{-KTSezrHf0!o(p|o?qeF_0T z?}oOvb#*X05V?6{wfLfdE#5UZ#_@KW9fi?DL3CMhG@M({jf>`TH9)>F1HEN}yZ{RS z8Kp0kQn>!9MLr?@NzF0W7jmEd|F~mhCs6hy#!Yypl&?Fr(Osc9LvMlC&^ERVrKnuDq32FU=%%PUzxu2B1S%-oyStY$zt|d=C zdfLI5w=J#ryj04AHbFI0^eRxhztZA7<@~;uiBdE|Sx0DVzOorRzWEX9X?tm(#Vh>} zVI-!ipzQNk3w4a7=pB19V2~4;Iw@s*utSPF^utVNmoW+T3Yqf zN`!oEvl}Qei4+qm4S1Oq1|e7*{YpRDaDH9ATn=UB`t69S-S~7RJ`&B+w?P>RnZCVj zcTV#$hmUL7cB&=0>)L(>H-+`OXxuUER)UxhqomKVF?PNC&_&sjr z+48-RB>oWP*jFI^)?V^-b+aHjAYHAE0hxxlMPYZIlU<+TuYS&VvKAA5xJ=<(RGLqcBAg5xcDNR+?FX^12iafE^; z5QapgOlL!$g@h^E?fQ7oo`b+!6EV~OsRnfZ8%YEoGdUqw{{d zUNc17%5|{4&Zb~FLJ#WDizPi@m?i)^Z84H5XY)>*L!AW;Bv`d_*5w=s`rO`MIyeq# zXWYDq?*MICGHFkvOM{1A@S>PAfcg<3rRKcfr!$A{DUnMBbSd2D5{dN^gD(`C?X7|S z8lSmngv-H`ehy-o9$VeM;*ALpn1Ro3Y((YWd zP-Sx3rZVJpw#DXY<3`aXGY1S1J{`<#zHL_G*u%LsnZ!~#zrA{`EJ0{a_=KkQhJh}0 zyfZ4lei;0dSlx#9Ev9pZCzZvv+Dq2rr3S}llUULZEVv$Fp{r@XC~Y-AsiQ?cnAy=M zOHgk+>K0}}i9NZ`TK_h$GFLIsqSa+z*F&3j{+@+Q>?F`oE-N+cE^z&Pm;de##}FQP zNGHT@2SphzW8$A=L*K+UYog=QRWpCCj}+p|hUAjSF;5s=y`RD5Y*mtDCP}wZ^m4Ee zyYk2sxQ-Sop=RKpohP|eef0rnjNSaj{qVmcbG1xT!}k7TI!=ts@^@7-AurDvv0a(f z{JN%!Pn!i>nlhR(CZ#nS4e9<+ssTDa3(XGucCD61{jENOQrCj9GN5bLU3_}3 zGp&AUz4rrO9Ai1v9sl$Swg8ryrF-bA@>(MvAJ95H%9E^V>SOB*LtwYD@$;v%FhZ-7 z!My?OnLm#wL{l*E)*s6L-PaSp$-RenYl$M<`o)bu&%J!sxjQSdN*7_0W_nWN_E;q; zU*T|GA9aw3=;nvJ)jb&%j==m8(__=&gd#x!wLa^p zZX>0TE~a#~i0Agf=$)|H-b}CYj`&cY|D>i#06uUeC#Go zg;BKov*S;|YfWZ~a^u=sS?$hpkLy*{s4wZanKJJrM6d-B+Csmmp`dwMf0teoUCo`g zYaC^oua4*yCqQ?y*Aj9wYrY(r+;7kpDrB?^b?YsVdQ_(7Z8LITh_D|GHLfl)LtW+t zwjT<12wh_H2aJp5hzqKRs`s93PurNZz3wC%dM&MPgLR}&a&$euuYGKt95EU$m^Z&L z7^REmENFkuJM(hhkBFUkY0NUQ`MHn-z`4*#$%x>Y_uElb4fx8%e4fzk*+q9h)C*@} z2MEDydI`*-_YD2QkK^EtcnT6?siAc({<-2MK9+1sNnc0o_S_{=N9$eIvwW8Wi1ESO z-ToGWWFmC!;gfGrLt}DI8Dx*N{(A#GxQ-_B0Hen;ssfG{*NyIBh=+RdgRqdoNs0;E zBX|dK4M%pzt6vebh0=<2rDID%1Le1104u>zF9uRUB3maFZn-+OXSCw&fi&YF7c z+P&}94MMVKIc8By28e~U_-)@O!p_7ezdAnV$N19&sL~5xy>ox60N+W9-#JfuO7hD) zCeB0t4$zV+Ef`ykKmE6VR*{am3^1v-an#)CXM6p7ZTBDk5mQJA>u+b8D*bYaJ1_m; zE%D!Ve0f6up2xrE@s+0d;&Op}d`f-br~}@(RXwMauZZs<*$`aKyT@oiOc4Uz>+1)3 z`X9bL!#_92fAO7|`n^fF?#$;81(5jC+gza-N<0}z42dTq+&W4u%boX&)qK;F?}*D% zBx;dcBFfy?*xpPJZayGx{_IqI+oSL*#^j!OFcDc>k`habdQQ^2A+=0BgL|k8|21lV zYLNbB^(VZTDNkquYR+kPW6V#u2o>!lT%}L_J&*eDeiZiZ_F9Pj4I}_XqIaTYcnp{D z_A>Vuxy!#<*MA@OSu$eci4+S6y+Zec?_eDI;k+fbcsiHlNA@1`K>5V-z&9^s$I?9f zi4;2ty=cnVq`u#%+!6l(u=(Q_$OOc8MW|8K;amW0d{A$wWr{pD$kNXX1QJ#&Jnde< znKkD6!1^zwc*m;#^5sE=P|;~`1z^E^{WwZ*XH|NJzwCvKt`+p*TQ}?CQAtg5d7%6aRS>eoUkCv0N-9K3MFxJ{F(`u`28vkK=}A29 zL=-z5blzn-To=e)ssmmVMq+02EXP}l994m>|2dDV0~KU=*B9%^gKZ{?ri*Pmpc}f< zp*x2V00aFmcxsGz-D@7szh_DJ0fIF>zfqf=j%DkOnC86)l9{rKe*T7iLoZHb(4DSb z7*f(4lh*gzR!Rr=(&JGXn=3Okf)3+k^iK1!%*Q4CJc@x{KYpzXaEnbqL6f1Mu8l4q zMvNj>)!JgX%mCndSQoY^hfi+0PF7whQj3jf*qPCf;P+ns_q*F*^_s zzo>jbzL%wk&T%@gvT$Xp63X^Ey26~oP6N9zzALqYee^=SKq|uH_RMRX%Oi_WLsdX4 zAMlg9?;e#;OgD<)B%bqGYInc{bcr#!$O^C^v_RV!g%4wG3SB1R>CVR>sp&PJDU!Ht zjHpz68s^RhhrF2S{+i3IJR%~XY$aFh)f}*F%2dA43i=71p^NdttGBrg;vKC7F@^^e zZ{M+c_lPkTXj5r|XMy)>GyQGVMFzRBV`t(eGYqx|#(FFOHwt?X?jQczQBW78@7c z0mw#X;nnq_iCnRNu9-c;n3@C*>d+xNVi_sU-fP9^y)~7%z6?Ar-n!C9tdM1z}@oaMg7F-*98p z2r`|@0kKX7MIrJt@=+03IdpT}C1irt#}}6vOXKAtsr6K+Olt+8c;(KI$NpwDis;1N z-o0;Gz8wd+RBxbF^cj_onI8_4Bi`7zpWDw8@?h#Y=`cX@w~PIWv7%?v^Jq|pk%dN< zi)>-IOxj2xX%+zr%Ex6FP+_K-G2aWfT1^t1zQcr#XZ`y(VFGjwb$?C!S`F(8H? z%SK*jL|sb~eXb@0lhj&1Y%Gl#6*4cE7&X#0PZ>qYd$ZS6?=b9%zY>sBoTyjXh;-~b zxDaf~GPCYB#uM-juS}Z&$X*Zm(mC8tgvi`!p*p0Z!+OJezb$!A?pX}_re`sA^Z2Lm-@*)em>S%zxgcqHjN}L%@vM5!h zx0axzT>CcU<`vgB^T355(*eRA;%J#@ZS+*;Pdl!o$usQjIVfpwP_?IZ-xZ+2IXT;&GtBKA zzc~P;@6AR}8wF~1+WDB2Jrk}wY7~oR5X_zwS*W7e(Y;Z`*9R=C&m})fUuYm1rI*Wd zwVQiZDv!CXzN!5Yx!eA>gCibG|rtbTv=X58U4dMzvVrG#iNOG&2Zn}-!pp8|eOhkKX0NLT84y4mN z#Urc%C&Tc=Q>&Gir@C@9T;4w__pG$=+(*tY;&a643}Yi?ojFpRdxl!TD_v@}scg6y0 zUbjd&wZfuk=2`K+(_-~}SBy6BEdc-_@K(`w&Uvn8|2a?fw|*m!T8zH$p@|v!=L*?( zwM5xD$>M`c#GwJE0vz$@+wM(NO%K^_EO072M+iAuKyOdSd2{sz@_l1SQL${Z0@-`I z<*M&~&Z~AV1Td23OUd3}9BW_T%u1YR#O(Z`4rYSjQUcw}jMxFY zkJTYw+R*>h>)6d^H`^?b-4k!%;)H(Tty{Bv9G%LkUjgK@%?#OGW6r-9a{l*G|E4z0 zNIam5?yVmn%ABHEz$}dYrB-P^w?;i_k_Yd8>n+@0JV0tFGujv(V$lX9VJ%7)b5Lf* zH_#D-({VP>wJX=4PN593onbf8Y*R#fbceB2u-}^`bLadK6w+g)l2lAaM=7M|{my&y zzpbgq0}3d~O5=MOZb$7Y5(OeQE;YIEwp2;TtW-d2QQa-kpr?W2V+~r(_vm_mQUkbE(i>;J|qmzJ^#|LzMU_|sVd2=66(mIUCGGf!rEb8{`R{;$QbhXE$ zkmzc%^thecE+#|Vd;bBI)4)#6V<>~i=Mko}&23ZB3ok#CT%>2%&#sxBs5nG&=6_4g z&RjxLAh0^)4py!nr^p3rxh5D0bIL8p%+DeXDh9QhA`--QKyu;?A*{{DL1^3JAzAnk ziBVI9;gTC#XN^Xy!3Ki$##%U7$z>RI4LwYkWRX9qF-LM@%>m^*J*Y zFVwvInVfiN%p8U>UTSFlLfJfg>r*%qT0-a)D3-mo5Qw~rnBTaSm4+S#?5S7b2bLqGHNB)~vBSw9M0fJCBU((pkR<&?8l z77^E+G7f|v+Tnr`{+5Wzj|yf{@$q9Xebv-VIEx)=L|(;uM;q=H^o4ODa)J|8X8qmz zD4%dktkz?s)@h-)pZoE4zL!7WvrKHd@qFF$5_l$_Bxuhh}S_M=MhE`pyVHC;tqd``_;Z zzc(|P0q_D31d3sQ*(~zKJM+Y}`BODMlmg0d9WM;$U}lTrcBMlnLv}?Hsa&+^%Xgc0 z$Bx}=#Zq6mO-8}0`lytUzIW5Lx5)7t%T2jx&X-rj&*yCpV1`hZ+tmd>?>N8K%Q08? z9T+0D8jE(cNYrqY+*J*6(QoZj*{rTP&RiB=|J)}&>*%4DW#nrV?J8Q*02l)zDO!Xo z=-i1j>D_?Cm`}B&KMKKnV+QoILKAYMms*yg`C{=RH5m&7I}EZZj!&mpsxb-5gqu`w zy+=M?Z#bgUmM}$XrJR7)VaJ>9knkWmSj-l2%QQ+Gw1idp-m(LkX0B$@$kG#l#ha>x z7%6X*OVjV4uRZY$!B7yPB~eh49X>+2$Cw4NGNVkSAdk%R^PH)Qc}}%Yvaf3{8Gev} zcOU^tV@Y6RYAHQkn|b(T6d^aud0{FB$&~o@x1OZRK}^?I^A1Zoi@44{f*Z5=^iA1Y z!}@&4Q!J;*T=cI$mPBfJs(7b)qaaY?63_p@+x=>dUedFy6x$yB;-QWuEaQw9B02`$4J0h$1d!KiYWhjcpY7q%w=adz(6Ux>Z$qy&N;?)nWYXvd zjg?SqR~}HkG=r!8EwR(=9_Q;PhKj_&BvR#1Empr*n|Bd_L;Zg50q#J()+8l#P5;)J z`Y@$J1{3Zv*Kctzh6$^hdWkqnj{&NFr76!cx(SlEczqKKF0U>?mvw8O#DTYd8a*T* z?^_#m#=BK|Ao)%11XT6=puc~$*HP68XqhV$q8mP#z6MC!LD#t)BSDADT&Ok+>)!3n zRT3TCW_v|)dMWj20~^(9GQhT8OZapCQc!uQo2k5saU%60pe{`NuitME7qnMDa$EC} zVonb)t!@2Bbk4ACJ21yCbLRmhd>(QA_>G+<;J42R6>ERe-D2E&reQ$>!w34BF^-}; z?&04VRiHG04%te;CWVv|(k4m>L7Dt2KHD_jRO-IInpdhc8H@+W6hHSmm*?O*609Pb zm!>6&ahR#d?8iDhuEWizaQiKfbaMjkWxmgKZcM1S2UR^80Q8}d^obO*CqyB~@~3&E z0ufn?7i*{Qe^L5TxDwMqikSCg(bGv8r|jNMD*C)sC07hmz(-(w1daMB+>=HfoQo@! zfQh1khTiM87C?d9xoz#z*m(-S-9|-2k9s8P9; zmb&$l{M6Wz3DYVgG3!C3#LF6g3C~Ru39yd%Y(C$HdZC^1b6#_Gr{zpkWi#P=|2{n@ z+ls3DjLL`1K4s`8s&JIqhuB%dEs#m(C|zlLK7aj4Qi_>5j|N&f|}RfZ%P4ia0R5$Sf&;i zwN;==swI<%lD!^7yZaCBDLLb#jXA1l=w-d6*~jyTkAIVao7{W#Vk}LuLsm0S9$05? zl;nSKyD9h8DH@zTe-n`Fxm^bQW6GgPf?oUc2%q|WgQoaO+{FDU5^Z)>P;Y1V2=90k zUw!LumFc@10AY|nTe(A1|BbKvhaVGHf+5H$SLN%}|NMA%c&vZ;-{03K9t8v&&JxG} zLcspfwvwIzno-F`z3u8h`pMsa`-?vo0kv9x^!@oA&d;}=jY2T6WhFPayixvvhdfGv z%|t2q?DUbpaa$fqr-1P%IOH;^EFebOGnd-ohz^5S~<#VLK;ktm*q@=DTjc1rLM zJY@JBOhjUOs{8c<|5~R0@MAH|2LTQfmtc_k0}nYN51SE={R3R+TaH9Y8X94kMc7PF z|Gj7Vmny;;usx#(4XD210KOKW#H(=YlqQVTSbpFk|EiF$UgEzhEyq_|z=+pcsF^ zQYa{ahEARg%Ch)@hh!WEt>!<#|I61Z;v2kyq$Irkb+>8z5q4AFpHGPAAEx#F+oSty zjU}!`4h~6@d_&fB)yB#3a0`U8z;$ z`{sviAy;RG``#z@F|1P_di3+%`g;#{KR7Yq#qG)cGBnHBq^y}M=Yl1q<*u@2xn`xf zwp5*wwWX-8Pv*Z9mUTAl=$S-;(_75|RM<+Zi_PkfTn%Q>70PcvUiwib{i@vrjuLLmNC9$yH=zslnaf%wu*`B!;- zAuIoS9$(1Hzn;gx%Hs=x_*Z#+DG>iEkN-zJ4-bmg!UcPSen3j;s30En??kKp^$~n| zUH;EalD+bl#7lPkyqWi(e*A?<{iU>BczTEC0L@h!lCe-8(>ChTi5>Kg|9OJ=&;e+& zDz&C~L-lx|D2PrMg-V?Ns8t}${vn>2saoJnk#0(+SReCZHSdE&2-!fl6_;di7F5i zkUZ8VlN#y1IkEo15)s~7w&0KSOU>=!VkujE-#=tTsre{aUT~qR z!#II%$x%7(QCl?7=x}qFPql0yAI+6}B)E^3k949pAgsL5n`6(p{lrv{ zNaQiyNZhvG*7`w%bW*Ae+vT}-4og?-_s7uXnv&np@%n}Edb|){(kpjtq2OPB)L4b~ zrK5%(BK0Bvc*ho6XvXM0@X0-Dr<_o>tvt_0_T`G1?|HVzI*YDzxni{bk#|b4&BL!j^1N^cwjWGq-eF zI1p$egF1hDcWE`P@3%S9V6~JM9h(6CHx8{Mw9Zv%&e59i!R@tvozzI=jqg$POPw87l_X26U0q%Ilat9qK)U@7 zfzo!JJ+8CaEbXk22Gb25w}k?GVvqBf)_)9@w#a`_O3XKkZZfntfYtnH5(Q1OgX&NH@T9>9Y8I>g>{=sRl~uY^|~zvmvwag zxv6E+?>0tH4?NW|S8k5&$lhatZDCu0w#-q+ies!AtxW#1<(0?oU9){v(6)SB)a*8P z5)i~gc&_fLN+0^?A=iii1Z`=|!(KeDVvU&@(u1*!YBxbUY@J2`KbwL6_1o+s#`VxO zT_*|(gj7DB>}D_aScOKg5zxAAWZ1b5vhUtEyrOtTx1^bAFTKi0=)!6@%k=Bz5=WyC z>aoa$!Rn1irbOt9<)R0TXzu|dKMJ-tH1zWP*mXVIS0_f}LbRW?lmDTBvs2@JA>>~f zN(FqCCh4PB-9&uJlqFTr-k(}#+|H5ZGc#5IC4EUyg{N`Ah4wJf-TT=|TSZnr1ue{< z3>?0(GS@F=Vq&uB!faMhO<{m>vwr{7Y5Aj6UCpcaejIZ5UvrckgYCW()vzeI6l*^t z>ewq%YUTknby+E;7>lCdRuuGjDeGti+Yt@6s^|pC_a`YO^KWHT4nm`8Ag_7X@C=f9 z&qaJ=(GTse=|%R7p6tC37%N@|2>%RIU5-p(d^q*>uv>dqEZnwNT0VG6{W8`NNpng# z{}LmyD)r5xu|AC6X11|4XaJ)rKFdB~2Nthcdc-=~4d|Db-_^aI&!faDD|4hq*td1Y zwdQC?2|L?3wCE6y$0dM6w-E-d=?|eBW^4tRysCh=YiYBKy~nv-%X&Q4_ZMh;c@uZp zpZa)Q-m&ly%Ks~0YjLT;MK=Av#M;4|% zb}h15c(w;xAdRlW%$DIiihESO0hY|SaUI_yRaCpMSeJeeX3P(76aG_C`-88X$O5vb z@``ZMfU!a@axn2_z(Y`6V?y#HpV|tVLhkys_n$x0EX1%Pokv6wO{wj~_>5gEOo>U( zt+<1>dv%h$nr8dDK4EFFC{e$Ts`_?E-EUGxx_kd@aO)b(5__|{aP-X6R+^WKpLB54 zNE{}ag%!BixQF0+8hAU9eD})FLIs|+HSvXw@x6>2r~qq4bZ+@E+WHDlZyOWSck?&r z`bz37-r{>OX^~sjokl^jg=01oZKW4I_l>ML67n5;hQAmF#w+?9@=?M96zrPxCOziu zT3I41V95HrA0Fq{gZ4`25>6t@K>Csjo0l8~x4e13f5C|?3J4L+UhS1LYzoAO>48(? zBbZ|TLaV8pE`q>t7=tdqFqWY!*`k8Ao9#JwnV2D}{Tk8!=`y6u`an%7(5ZzuR4a?* zF(-#8`#`^O_}chmo)4^hNTI7;(|AptMLz7pxz!H+Kr^gAUmt&Nmr325jN$Dyv5dAh zY{vb5ry>6%(){Od z7AjedMLMV(+-J+}5H32%S@OtYJvlY?L%-~j8FV5!m#ALhV~W}u;XvI3GP=gJqUr0? z>Qnu{NsRxE(#7C6o9m<%j|}|%RnM(x0F?V=n%TMUwJ1qyu*6n37l!E)s-L&&%cC3A z!3~T)nZxrbYbZeI7l3CkO*E4@;T*fo!N~0+rXs^kmpJz2Hj28UfV4Qudw;acWP0XO zLmk1ra}U?I=iK=k1ThgnS=>3}1#J@kwhf{z*L4Xo8dHr@yVucT{AMju{SB*OQ7?~xx+^b}t*>3AiAa3jFS3S~NFZo8p*Qk_xw zxnKyZ+7akfun|pjU7N)d5d7v+tamoDzq2@5zyBEFxhwb~HgD!{5|Se@-Tmk2+Rly? z&C=`42@WyIB<5+I{BCItD}-3;hFEN%Gg4?K`^E*!(yhi@gH>9!16X-#F-QV-{AZDapX$Dn-HJ$<&L zHi!v@Zy60Ug7$Mu)i?Z!cBRcWnMi~FYbyGU#sNU8h^uK{d_USQ_ZjH&Z7B_pQ%zY3*iNwOlhJo*Zr3o=ue-lFoTCfA?~iqfA49- zuTR3nnUwvPj{d+x5*_gPAF+#=Z|Q3KI<8-VBvG5^i~HXuE?=x#u?Pe|Cpu>PN6>OE zkRzJ9`0)qc?4Beo$RTveH(`GS9wq|sb|vQa58l*KtBTh92Ini^7E; zLJv}ch^-#YMt--ZB|D)I2=7+pY}lV(gnwqq#dGizlQ9Ro#J-T4uLbdcA6B9iyj%KX zvj6*A|MMQlbm8$~+k0ld*9FHf|Ire zT@@s}9VbL>=gT|$VBQ9!`19y6f-m0tKUDzYIrzS(?)yB8h;GL@D&Ht~GqaA@=3DxU zQ`S?+{tRUY1xU}~$wXg;&o*0MUuE-avsfK)o+RV%} zaH<=i%b^wg5+S;z?B+#Ym6_(ajfiXd&H|R2o)U#0n!aJg&lkH8x5qEF-I-_7Lddd{ z=g!28v69O^8tR9sG0^!RX{{(@5^F`2_$sr7UwYBm&I$5B>% zc~=FbiW?hY>JoySRBX#iTi&~l9V7WFd_N_J_AwP5cUiUmFlBtC-hmI^8&}%3%Hms` z0>9IMdt8R(uh}otCh8_b=6wE*ZfoP~pY}~vf0EL^XI+*rKopmn+JD}xBje3$jXZ5J zm$g|sj^cR>dbYB7t)%rzfzl+nhqr;r(w_Da0>bE zl>wd3yD7Ca!aXk283wd2tDI_1R*NO_D09l5Y~@N%d~`%F!drI>NdBnKQH4Z#UiN=q zDmKjkxeULCNFga;c6k9KY8ZHn`>=;*x}_lXuV2TOdruyiB<7x9^#}K}p-+A(WhUX0 zpFu%C%)u6efqRROLrmY5BxH4%Wqo*|l5YM2>I2CodC*ILDO%KBd8!wGt!ro;{p`5) zp(Foge7s?BJ4_z5JGZs%^qqDx#gxF}gl4gFRw)# z=ALk8;1VTUQSV%QroKKjA1@T>2l|un^_*wOb6Ob8-wZR=>&d^{yT8#0=3xW??~fi2 zMilY^Kf5>ER+A$iT062bMDw(5pp;awoY%$E*I)I!tthD&wl0SU3SqrcgsleN^3Qy_ z@n+Fk44aN0Z1H9>Y*wTsqOEXwt*mae;AKW*9TA4jowxIq>sEKD(Pd~2dB4hU*|(KH z{ibfP+?Sr2u2f>3OK-*Ps>3ra1#C6AX4m?MvmmM`KxdZYs_A*A3<3QJ%WpR`z2W z!7j!b8JBA5O|RayH!WPtMoet+@Qgewf{cVw)W*si&T5ea@d4w6Nb2rm7N@o<8Q3{^ zL)=M^bMY71XQX5*Nar1fuW=7Di3y0|qu9dU1&01jLx-tby#d+nC96H$T{-5IW83Q} z`y%}TZP#i_Y(GvT7S8yw6#ZclBDB(XCW1WdFUANT%x2&ExlGr8j{{ZawQ2XkCe}Tx z1HnfJ7cxoHKME&ft`lzLz?cr#8IS!8^`-bi0sqD@htaMIbfr%#&57yubRD$=tR+h@ zG!zASeLWan@3#%4lfcE&9&BNRqI0aT^qd$i=(Z>liE~$h=+(eEqQ7l0hY3 zdhWi&K&mC}T7f|j&COx}1;!4140fzSI)=Pc7G zkEunLAUlegBCOo#>1l0;{5wDR8AqQ}U9aXqFbbh|lk%S^BDaAMnnk7^IB@Xe+}m)& zH{=cS$ROeOI1}gy4VL168m!e(b9kw@FmkL(2Dy7envWd2(WarlxD@o#F}lxsK0z$f zn-sI!%PxxBI64Sv-m|3?koUfKLo7d%lSeLspA*kqdj)f~yuy98Q^=vuIl-;?`)v46 z9wzu}GTM@ywdRQ(x_7(R!ZUwmi<8rG)z#ibMZ0%p-XpU;6LU*9-xa~l9#uMYKbX{F z0I$*OjcEC115%1%mN}j^`z4a`MDE6DxY4dkNC*rMU1!|;?g1FVelkAXgnYG0@q7i* z!uZR;YPEixcAs*d)n`jDK4{$}nB=TE)AI;y?p2@6Z z-#wU#I@1>Ub8>qL-CTtqJF{1NmmxaO?x6B6N;#)8{T(r_A+e;%pvA53pF&B!&+j3bEpNevUO*mBRkF8V^w(@N_G@nGP?!s zr>QD^N$YAF!uiBX7v}ow@4kGE4lcuq;QfI05gb-NC9ltg(`>vce>BWAE5l$?Y~t0? zrHr9S$6h4|mcilLprP7?sCt4hG^Ergm=hJ&et^-dbz`E}RJXWmtZy*LtA6J8sjrs*f z>ZEEImO9vBbFs(6{reDLQ;ugC^VuO^!G13fk3MN8UNNwhrKYvczu}aM=tIl&mp#+h ze|NSi`|$}r?5LLW9Z;PnPwDXWGpY3%3Dez9A4B3gt6Ar}Vl>y|?pKwqM+M3fUWzn@ zKMtPROG49eiS3nnHs!$^^<0OU5n7xbzF>4Xl4E-|Ld8s=Gd*qr5b5Z0U|n4$G475^ zZ77r&zn0045PGoAe*a|s`@@|drxmroP?e2BJ(wPhLj~;fk&x@^Bi}pt+v=^;F(>#V ze17g~C!08J@kH_q8#Od($^O+r#g|pH(VcSwHPv?*JZ3-DE%dmT+HX%!Hv~)UxMju> zmc10{`;^``WOlj2-ZuZeD zd~1C#fhGz6lhgKJHKU)^$%2lIzN0W?XPlS-RCJ}4{n1dCY?R>6Dxj0jIxR#YhpRF? zF5yNpjYn&7Mm^YzGlK*M%6u`L!fjKtM zqZ6NLXdTxV@QYP~>#_@G3z1MJI&KrWJ^7mbIZgebQ9FbfgZE-eB>q@yy)(Pa4lFv{ zEwvjjGN1724KiqNV`ID8v_E1N=iLnGxX)25)EUn&qz}^P{3dvtd>`N|{BwO;{>aF{ z3nF$sE?Vz4;&Xr;fZC*Jpg1?^6IzhHbfiRca4&N^FSrc9*GQAQ)1xmhP;0AxWpTt~ zA%Rudy<6(*^mMSSxv{=4Zr9re6t>%rtH<%ITyt7IPu_?E*05Y?*e zlr93y{xD~0uRc<2=~#`>?lz(2AQxcE`;$RaSM@h>S-(aAb=i1tmsA@8o~{itKHXjP zF{nVIT-eGwsxh+$W@0nw8yqcRuKxiyHL$tI)`vRsS=O0+h1(AWD?;bMTIm7bi+b2S zOwq#=lfVx^p+PO_wF0*cK&#P$ZS<$EeZ;e>fxTPf8)8ZbtT*z5cb0vT2`W#oXrfO= z;`W5W3xZOBi-B*!tXXZZ4*SZ-oE-rTx7^>oDjDoRiS=1!;r1$=_tB8c%9=y-erSu? z%Aj>bG`2Iod?`N1cu-=wA_j9iw?a6d`n2~_0;S54T6>a2%>=5(xmP}5a#;M6v?b{I z-+@|MlvV)r04E}oG};S&aJQMJsPldNzzysALnC*jllcDewue_uHB6%PkC(w;R_1Gb zmLN?hMNtyqKO3_$NJ^sf!c>^^o3D!_XYzev!V+By0aqQLCOKnG zyAs7Tg^LM$IL(nv|NpxB|0G~Y@)7gqXLO9LEE8Xqd9z-9sGK%wc+{S_2OZl$vWX7@ z-cT$LD3yjoJ;1Ii2o1y2(~aW}viPsEU(OKOmA`yC;RC>q>q!z*2Sc7KiCW1UKq?c$ z81LjdO#1e4Qu-wTbqY58byiN|?&5%TT<=>E@!Dy%d+y(Gg&=geE zhT~O4wt9jw=Z;7lD5j*)!lEY~``*RIMz5lI{_a(4ex;?3S8iyr>ghZAHF!_z14Y05 z6O&bh80Yw{eIO0_c{dJvR;2D6ZqF_Z#v*=o`LL_wE0);8nRDcw^D0w$SVs3r8I*!CaBPhUsO}B%1FOWn_Bw&U@JY@WxY2C$ z<({fTU<%wz~dR<(!ccC>Wuhkbtqk?}}bpXoEpAjnJH%^F3S8;IY-_+TL zu}!S*_pAGc+*i-!A0p!dI;Wm2r-h+P^DeomUf|lwitX2%Osy-gaEdx_rmng@-4pfG zGBYupblf;}{G!y)tvE^+fW#oc`pfaG{@)~Fo+kjpVSrxC#m@aCFGDXstuZ=LmI3gA z94F;A=3ox^=hOQP+-d9pxx;myKnL0sGqv=TG9w}NrTa_n=NLm6nI0XMF!&Uy>dL zaB(p3UK=vjw0}BQp+MKy6xevCt&a6R(0_NP!2t*q$;}3q+1xy(`lgg zZ&{9iO-XjYIU7q#D}GDyoEUDWPsu%QWnyr|mjW86QU0m3=!oe4yCK*5(f|rQThZ;+ zqGvSrY@PfT-9My%*bFNfa1pt!!gilbY?eYbMcTO=B zaQB7XmYH=GHDL27Adh!E2_Fad@EyO>tA17#_wf7I0b-pKh#C18B)vD&iLZ(rfn_pe z*BRLA>9hi7x!@=KFK{k|^E(K!<_Q3CtN4_7m;Hg`Z1a$HmskV zm)4B{pg(NxGQsnkmOPRrCyO%L)~JjW?K$2?BFTDG`dVuNCZjT9H%~zAYf}8MzS)W0OiV8mzCYG zKm*FA`XBnww{2VoDSD^>Nml+-JREd6`I~TWP*Z5I*p_l;*jy#ppNxh|2U_shdA(X5 zz!;Qv$X&?O7-7EN4u4?KGcC+MnfcoksNo#oK-kK9)Xx$DpX*@Y%AP{vY8Cv~Z1}go zkA<$^d zzkvVuJpSMF_;3I8Vf&0{SNkvt~ zZ`Nqws7xAz7<3#>ZfMJa_L8fySswKj=byl1cZP7^Y6~#Q)mq=a6S@;-QgYUUuC4j< z8!e5D_<0cXiuut4PjF8=gl!c8kA1i|9~U=q4)gBcJ^FtgpCwO&Z_CeQrXG`>@mR@u zs93!;;Tog2IF?~QIBq735O>|nck0I~f89%S@4K#|7G>oGa<`_quA0F+r9kEVwxRnp z`3Imt|9Qhef(LB&AHK6^O1bh;@(4QuY~zfUdee#CtaKI}G5_8RVFMB`UE=o2AyG_& zz)U6KSjI-=yWJ&GAN1mg(~RKs*vw42EPfq{JRz1~ZFaKelEtx;2#pE2I0?)>(xfZZ{q<0SqfY7gJV))jMh9b88A% zfk&D`@i)uMpv|#)TE39js68mmc6qVKxCl_sawqAP0k+c<`Jwlp*z)~X)u{?6YU|Y= zsPw|>!~D`O`+7}Qluuhn{glx}(*{$se>fp9n+RL8*VW1s?2-H{`S^I!jYi;5v%O&$ z3*AVrC4FGs7nka_A7UIT=GrBWEL|F~pdABP(UuG7cSkK&2K7MU={GEXj!c)@EO#`2 zq8x13|M&s;8&Tn5w4cY1hs$$x{(*Vde|>gILtB8B<=IZ%6(&pv1RGC>kT=d5(9?G* z9a!_SjUAWuNg)~*xWxIeQ2oAgUz++rQnm=IaZlVL*ln*B2L?Dv**4Y?ZBTkRT<;1f zE3AiFI(}~gZ=>KMFH~XGm{7s5syT_&a++B)7^gvbZ}+;-RA?F_NeA1Yx1I`Ifg+nq zUEbzLdRAV}mksSGtT<`16r-r~~fhtNdAe1{}}OC5EBh}%c7=l(G`DH1mBNF4o0 zStvld8^7q-TuuU}WfKo0X@IFw0y1gdvON=s*@r7Fae>~k*ND50ADNsWKkq1kK@mGCq`_y&o;hS zA(0Nb;beSr8_G>~8nm`K%8rHdkZYCGVaHxS-%UBpUY7lgOcKs~8bfmpx2y?9bL-E6w<*0i;mWoxQ(1V5oz z7uCyC3LXc~5lFbeWJP0W5`}NoR;%b6k3lVT=tRJV-qLn(l0ldzFdh4FGQQy;Oz?pE@-Rog zSl#t@Auzde*uLwL)DdBTzrghB52F9>7xRoCJv%)yhu)$Z_s{aT>)npKmKQbXFCv%O z=A~fM=jk}NjcQ9>Uugai>cci?#T+-WFy_?ph&0P~tX$qF@wkY$8u`hBs)|9ZvaQ&% z#!K&;FTsb~X2{_Ehb0$HU5ASPvTWiJ4yNQq6}@`@`nRM-ndLB+JG@^$b%}E9k9j50 ziMn^+*e)RoakU;!On;4&ibP9-nN8i!h*Nh{w|x=LsfY3LB4qs zpPyKlOR-vV;6rd3nL+R*w12n$(d|^7m=ih9DM;|HSk7j{sF$sWg;7)h4D62Z-H|f~ zy60gqSi2=D+rigT;-p}19=D=xOc&M3_H827TLH9V1nn_G>+b5Y6zY^HCw zYjDv;c`UcEJX&xsJveZmDTQHOyHdCl_Lm3LQ@^H~9=@jv|5B<6czbhJek=D~jN8OR zYce`P)oiWE>m}LqW^6wehj-iz`m&j_UN(d2_UM-|MjHNs{ftvD;6H{6O)mmXP~O$q zYwyn8%f#UktK$fiZAEnS^`+sjZ?kTFs}VxpeR%U(YU}obP5-xYAhfsl z1E&@_{C=NGctQdP<=v;B&t9Zf2&V2Z9Wo%135|d!rv?5jwq&jk=hais!CC591eA*( zPs+$k*h>PpEK?6%w#$%!pTG=I=Ujp*&=72GwGkdxYwCHVXDxn1S)!9CZsqg2C)$4ayhIT1Vuy;)f7BAzu|b z-6DXUYHUFh{EsPMGHVww+#7FAa3$cB-5=b(Ept~X)@C9&a2A*^mbSd?^IG+*R^!%0%Kk72p}E{yw7WL0BV25^uA3nRA!W51XmXo?$#QdZClov(rHx&= zW&ODF>;=q1Uk&Z)PLVEnZ#o{?ZJ3M~_rZ3EJDm4AgnFK{aiZTEF`wKE}ON& za{I1RjrOyW3lze3e*cSunLG_jPe{CNy1}8L=a;7*Ua2SgPJsgHcCv3Nm`` zA%|E&hj5|Jwzdc=HqG~e(ZxnF=Pv2VmuGy9mD@E7lu$tV7(PiBylgey*`@ z!S?lh`Cv4Ec$>}2X(3z_gbd1jknP2$v0_4O-c^n^WDpnRCT3+ye@?6113PI`mw9(WUrajO!8)b@MV` z&SSRA=X75F{75enZJ4%ZS3q^k8D~pM&;65_JR@_xEKlclg-}KB5Xq=jL*v~brT55d zr~kWb_jmu>92bzvA1i4Uydlzt=NdQFvHH`OfQUWK7W0TDsL#fUM>5!_GKmZmVcG@H3H7Bf*KyG9s2ZCcp?Jdx(syw*7OMi_k# zdWb3bEqi}jzf#@b10zOMh$r&rX!tD*Gr%3Gd+9efl7kA#FlqWjkU8$6z$$2Fj6iQ|K>>Qdjws7t7Omw_;X;FmRRk)IX<3_rkW3O-JfD7A;pB1fw(gE2T#8hXJ}fT=`l?ERGo057H7WH7$It8` zNTO||2C1q{RCRUr)|MiH|I`zBbh$|jWB8{}&!AjxuC-m6bcIM(Gv(*cKOYWC#J-T?M`Q(*WlSO3+n-X3@9^t?sV*)? zQe9!_M=m17?I&N2mI8ww6QVaGS~(Do{8M$7H3a=xG4>)n=MH96^Z59Yp}(pLYvWj@tjsTC4Gt!eYv;$h3h=v31uW@DjspZsyci9cXon5bCsu z?zhMe;C;l>($Zp9^|HQ>)y30jKju`Zgs8vDo_$e(ws8AL(p%9mlJ2*m%pM{pIeIaNLuDHl4QC zR_;@EuU>s6a6QVgwd%8f=}=RXAVT1+F|f1lef!@R)_*J1sN#ua`xxhH_}L)ReoqqO zcIO=9qPu0ki7!*l7`5I8F_G4V%Lglm8XSpr$8Kd{ki25wDC`PP!z zz=)WECp2*|I9SAU+HrQ>cgH+GrEI!RS({WP=LmzYLC-C#dwugl670z3z(BF-MzUWo z7+1}xkOcPNl4_kqZrofrhTHC5S=<q{;$ ziL6o43DM5#ekAwy4B7-2&quZ84jjTy9?+3lRcZu6|tzZ60D1&kK&5X3P z_kp9uM0%-;7kZrhAP^UEJ6qs))|+o&z{A_B?F34@iHw9&*wJc{c^5w;Tzcr+GnpHL z_Q}o#(g9Yfk!S3Xb7mf!Xs*bE8=TdPw^&5WEqaVRunBHsuOn#Iu#5HN3RU`Ack}eh zIxB2|kLlJC?brD28G4(EDlH=;qo;F^VEE~5iovWIWbEwbp!x9uk%+pR!uU|-6|1wN z!PY%JJq@Hiq5qo`b^`se{ZYcrapTHmR=xZsj*d=SQzF=K%rN(Q&eB%Xo3r6bn#HXz zJjCMYC;N7xRZC{-Q=OwD0%+%mcrU0O@r4UvdzjI-*Jm%b7fTS1`_42Lv+9E}fLWA* zuMKHGa5-viz=!61VK|6qvq!xFp^op$sy z14aYOvGPxcdX?ge?(Wf9;Y%Y;5C7XP_-|d-SCS1R2awgRT%SskgY!pMsHd907S z&8{Kv+9p-G^c59v6BOh4Y?tYxW+i1l*?qiWD}Gd!Xbawo@`aEBM97Ji$t zClc^_#lZXeEjjI78cy~+%VafQ>j`bGrYiVj#@~PH_WZ=|D%SBzMCPMK+A-;O$Kw?D zrxp}Sn(eoDXy@}j-C)GAV%7ST-v?To{s86cZuEY?WXw5yv8OKB(2)i}yVS^Kv#yxi z1un*O;lW(|DDvwMBzI;;fSEUkyu5s#UgxIQdJR|ExMvRW7qP7_-`3d{i8k+?$|(@@ z(v?;Q9hcIh1f?p4oEC&p35&HKF{26004%%<)Wk*Etel@YAiJfxx$Nfcqkh`Yv(z5* zJ~d4oAZbvF;npaY;OB=xL69)-;xTJ?*15&Z*C3?HBViz+g@D4du4HPQkIH{>6FB*C zGZ2oO(5A;_=$6J1+VqN&CLnXl9b;`;LiD+POY&N)(8*9vU=?vqYX%ef?f40geKC)z z!-bzHlxAMl7D-TW4jbULBkAwo2eJKh#Zh+7om;o++DmetEz3JP79JNkXIbn@kxGga z#%fG}(O#kZ<`wmhjgy|aLBVNC{>V5}gPDh&U3a;0Ffs%3!Wjg@`OaN9)8e7b+r%`z z_^MG0IhQeknOSZ&n{26C>%401tu3-(d^HgfNe;*SEM1{bjmCQ$)`vHxz+y1(J_*-V zfz*LZ7|7s5tY`J*-wyVEMOv@^n9|zWGrnRx7rz2$irj_UE(BhU!PjyX!c4d1H5Lbo zGAj(Er2L*HJ)9}u9~1)8ApR+S`1ON670Px#8eWp(*Yf`7$+`V_yl(34m!UJ7qYPlo zc1WSdHCHSix4)#uR``XO#A<*?Pevk9=`<98()+8N+lLdyyu(H2`TefbCc61=9)v%d9Y82GHBn(vO4W22;>!tPx8g!!=mfH{|Za|8&TA} z0+jGP8|TB^1XcE+qa1w8PZW&y%?hozm6yC9t627at?(E*jp#ES%v3ko-ITOuWcumtGoNU5!{v||Qgk$I0d9}QGl;iM}9&Esxp+t-eH5j!y_nAQ4=T47S! zN`MLOwbUefO=M5HrRz|1S7RDO;$}fT_2n(2=Xbf1+To&wW*s{uGg#}9sMS00W0xUu z-8Vs7Y|dyg?HV|}Zqi6H_U zBiUlB1nWN55CROtw{n;-Zh5M%zds@MCJNMp(GL>nsoTXrb4cJfz8;QykdQD%9PZZ`Q}_nD=T&1Gzu!ildYipB=86IwhCHJ zW7Jb;e!pc9R6Of_?h>5|_al(5#T1E^JB(%Nh|AKosFqk$U8;%^&WMb%(O#kRPA0Vn zT7?I1D;xzE4r;~9f>nwrNpA&eT;!3eFl~#{iR@UtH`Wufx7ee%R*rM+1ACMRpUz&q z#z4O#_&!%l9hC0x17}L1qj!gghZ%TU3H@EH9T@|El(0_XaacvzX#)d;ppcNr$w@=` zQt62Ea6^50?Z_XJSu|Hoh}>zGw`_oL=L(};Kt(Qvk>l3jl>kvO)V&rD0C0KO#l*zi zZC=8}c6Y0SZ6_aZ^k)0htg_2rEKF3Xw>}Gr-ZnvY8DrSGxl!mBM}DV4N9d5773m6M z(8Gs4s|KgzCVcu(`XY7D@%;{Pisg}={oD>6ijrL8ycMSpzgBcCcMSw(7SiYkFT?_ewz57rE4DKQ+y)IMyhp z;<`S3`$#B8iETcTmtw)zfTp;Y31T^VGe<4O=e@P|upLrk?t5xn*{hfZv0X;}lMQ)A z*zyIM3|)llIFPI65^K{t$||(BRF2=T2!&8$(E0fXhlg<_$?Luwestf7B1U!$c-Oj9 z{E5kz4?%Ue(+2mrNH#**Dr{!ZCGfu}8A?egm_%jJRSC~Edty(yb; z49?Qe!g)f=c6M-uj&rQo&vyUnP5q5a<41B8>_6};XV-He(&pHR)HH|`Sud9%A|kpY zC@2>m9{zsJerq;q&L0f2_o|#Hy70N?M|(`sBR0iER87sn(NR!M%?cs-ewfP&strsx z-)qXg_X3YFdi?vJX>ygyUdb&UNh0czel=RauA1GzC-V}$@FSZ3d@h&epW1%R5};Ii zD8&pHJu$>qIJB{$@oQ!M?tMtIvOY&beDPvL^r8iE?v;C^fOIJ?E^a-NTm8GBLvr|q zM|X=qmDV$a?bi5k#Q6}`-ne0A7FaZ->zTa>6O{UGqeRvCQ)*#JcCf7~(!RfT+>#YQ z@5O%Y3AK<05uggTz>2)2>`^BJ&?P=$5);$gk&*ekq{gHP@quO*Yqbyl3dL2mY@I;` z8#ddw689?3^O&|43B*{Rel4B8UvJ#*Nt+2{Tk*lCKDVY87;ZvoYU_D~g0DeM_>pZk zvMRvvW#@B8gPvnM{@uZyCHKA=M?n$AjB0zt?7}>qlaKtl(=v5w%5jNTu4tgn+}pQ9 z-}4&eio}f$#{KwO#y5>13%~0xD~F%bJ)CQ~JdqK z|L3_s!D)UGfMwh;x#vXw50An(3O{Y75qTS|6LY1OY>|(D;x)OFXm@!%aoJL?c*k}x ztK|F!Q@8h_p`kYM{1Y)cMb9pRNb+%!TCbHua>#Xv5Oj_j0|EWsA%v=~u(X&XhuI0` zN?cOsBk!Ld8Hq;f4W>V0yH#2_4Q=YtmxIynS3Gsjw`TFV6=#m`VsNa+5#?N$(VKC$ zt;XZcLMtT<2tU*dm3E1PZ7tRkpl5mb?6nv;y0bsu(B)~&&0P5ljHr2v=uI9C-R-}G z!~fx5CDSik2=54Ip-c@w;j98AaE}Dm*@Gou&q%hm-naLBe6pa=>Q|eRaWqLCWjNNU zu{|egRd_}t^z&|w_tNf2q4N`ds4b^Rxm^_d;jx4o!^?bAV{}-a+p*%b@Lps`Urkgl zQbZBSN64=2#pH8EFO+a~wmgl}%_)4T>aIk|*&k*aAyvZ4w?|Jr*!V{g~{8r6kR z#V}<7akF#tNbKh{TWu|8wHux^dzGrw2vYu=T`i&ddVy5se@f1sCgAIvZ;j3k|LHXR zkD*9V<}6L3QvoGIdrNEBD$eUpu4|RW24*A0|BCMIk?cLxlM(#fDXpEJ?Z;P(kCO9J zY@5QDpffar@w-R7XkvT(skD=nTWO7r-?a6Eo}n#mi! zL5bB^WAndJS^nuqBrp4oz`dfYS9f?1aSbA-eZ`ye-q_SEZk)uv!pOYFylxKu@ZgB5 zqS|Z*qMXd-(fRY%O!`tciH}D1W4i4tgzTH0?nfU(IvpI&SDM3Ly$COz3B6&a-SLD& zfqUFmdDk-t(p?B&Vr%Y-sxW47xJbVI0}o#@9oa5xdzo`9rL>YB8}x#TI27ef5cheE z2}UR{oI`!sJgR<=c!GtUCTepYFBoLUypU8~bnrc$uGfc>kbHeBRl!RupdRi zZx9eJ=QzsIJvygEuLtUdduf#+OY-O@aS927q!zA*oY`aTj(HU!BI(7jkb#yswD>t( zUl9bTt8jcK{6}Rzs4RyBo>D!+^d%c%FOwtAt_h>Jij@an9_}u!B;c$K*&TlHU5V+= zctBw;W)33Su-#d*GOl?FUYAtGeQ{{7(-vn!OQUeL}b4tl`(>CA)j<=TlBwh8H@U zWwG9|dXP72kR)*G$8lAC#!*@s`VGtgJKB}qJam-p^`Qb{-k)*v={PpL%fQt~^JM{P zdKb!tnw-s^@}!@fTCoXx(4 zbA79y$zg0>I6k+Bxu)7cLUQ!ZcYvwPhjhs@cwPK?omCSeWa;=@27Ou^Jt6!CgzAy4 z59D4qnfq{rOQL$MN@$4|I>F9Ep>CP?o~XoDW+2J!Wxj`uaOZNGvuI(lyJ)0Hb) zO}4pKAZn_%71f1hDVkS=JpRcdkGM>J!`e#{l|@Z&E-nR;b2>bD2so?p@~*kwZ(Zq~ z6|m$;rWKv_yQw}rG5ejA;dVIXj|0WrCZS#i{5MreIXUsOk68}fa#yn741}$@As4Cj z`wx%uavJ)J29HDAefRHgbMDYn@6&Ew_o16d-C? zZ@fx6Sti>ytDxd+tmPy)ss>@8#%QXTUi==KpG0-(5LLqp=`!Fb=*3%omEnUyTX>-WnMlm4`38$!Ir@VQffrm z$}^SnOX~f-_}L?r`h3Gq-;Ya&sCmhYu3J&sE7I`l7Q(&ObRVL0XO3T8IC<84A znU(2Jy)RB3jT}nus`T~nY+q6@{d+6E*pceoX1AB_8|if$$r?i4XYjdb4S8+cnt#H^ zq?I+g#py0sZYe>@Wb4u}J|7c)E;MZTE1nY@j*3M~KEmCx#73QO-wKw%l~Hl(+G?=3 zJ3&$3Q2{_aVLHq{e2m=>w-qzMd>K!3Ah2~Hdf)rIzOu*6cA3uf+l6S1lfl}Bk97Jn zBH2`mBj<0Z?C9(M3ACqTnZzfNWR+vTbN|Cv&ZG(@^7Y=#Cf-XS0P|@7E}%4qHf`Lx)KeafEqbB z`;$hzBJcGqD*BQwv}hZ2<=Eg3i*b#|-buQg$gfsGnbBIvLHEA-Ht#)F57B646z)s8B-t`J5%P+RWN| zxeOxv=bTJxcHT~VWN?%ur76sBvYsg~DzC?L7>zB5p(>)ux(E%5K1uK(N&wi79(NmZErx*ICYU9^t z{=a@>jx%`*aU*|%ix`)86)u-K#fxCxsZrR8D0I%CGV{_|eHAID&zGA4yZ4rC)#;<6 zQ}U^rdQQIwNJ!4zy4N))SfLynXZDimwH8-z;9oyTV@BGP@zY9W!e+OB1z zZpx6C7Meh*>Fwi-IM(zsH%6Rv0Dz)bT=5Gej|Y32+KDy>y|C zv4dMhuU$6m|1q1fgjOutwtMhWm6XOR$!D~-iB!EwXPv7f_az~P3dd`orEw`Zr*6T3 zS*{X|3xA$P{c^CsI|bs!uGEJ;hkD1qejB7>%HyJZ6h*R+!YIFv21~HD_CP_9hM!uT zc-%P%TcyTN9>`9j7UQqg=zGwfvs+1%|HG^BoXGEZj1JU_#@jvF&f2i)TqHj`V;iWXBadBDNQk>gz%u-#}`z-lX1=8-? zew=9#`j)YhdfWFnx;!fU*j;y_g8ZJ8?r1?S9tHH=jWhjJh5PHS)LYx+_8Lc}2ExJ`}Fs1p?@BL`X`&As^7y||hMn*;+Q35hkk)EOdVuZQM zXVCr?DX-+LMb1NZe-wA*nC{0m6v8%(Z>0=O@2RDsM>Jnxj?{`A*s`Dj9H*tT;VpSI!L7DTe zP#18z4d0&Mjh(+Xaf`>m>&HrUXh{a-`VOtYNH&RJ-Se6q@?6%EE2GgWz1=G&`<2Xv zvwcxd1L}?8Gr}2>R3hVBpWpODi`yCUQWsNBgX&7dM*i`J;e1WV%FwXY<7XcN_MGViDK;uYjTC;R$NOC<~GLr8fEXn*NJ! z(l6qUDpH@)(qjoT4=u`lHR$)bHWUn zQ0TCHV3gTZym$+qUy1sVf3Rjd%uhfWEil+gqPsxRu9mL-X4Z^8tlzXK)Gg=+I;v_g z=GtX6kVkb3Fm=DV#rHHOq5vIZ$HNEv4I7<4dt!niNDt^jjnWe=tqu)$IMY;yE-m#Nj`80q8r^-m8_D z(w{;HyfPxnu3VTY*>Z238TWViwMhSV{S^AW))$RmlgZzC(m+Qmrzfn=E0+oH7}(dk z2@ks^pg>iq~=Av2+&OQF4MhR zt?~0_rsb}T`|jHx7!!m*3OWfy3uC)KY81kFk&}1((rc?Q!5NigQA>h2IlkKFMtFnH zJGiIr>*9)eyZNuDL~IW|-agK`Yh~@;*>HtIw}Vc^_r=Y2s4q;a(__+wlEV2&kF5A8 z<=pj!>ceju2hxciP$(&YyC+ok&f8ngr1&1kAN#T*vTQf`T_ z3)1nwqB}qCUt^MVZ#6hY`%gRK-U}A**iBoUo#gF9ES7NH7M4DD+%;>O4tuxl2qi>r zr#lY(5_I4{V!Wt?Nkf)sM&4g+kExB=c?+3T9Ia{{gFo?~@7>y}QLuQfx4F3+(fo$L z_psFbXBP?Fa`b^|{NdhlJ=vrw2G{4b7`;H_u9(Qv6Vo?!Bv7f7k_|$~ zWjrO0GtQiQ8|%@s?L&>oPH=j8gN$DpJ9tKFS6^@4TRAn(?Fwn$;lZK&Nk=#n5-RUp z^bzC<#g-P)9VsMZmX%Cuvq(W-XxIFfH7Pd;o;IGOz>V0Ew z!tq0!8BNSg-!^U_JIb(IPEL2SEzf85l}0(2uQ3`)zZu*ek-lXeXZV%DdakxzZ*;f2 z7~obInV@xkx#H|JO#GLsM&bs3(S0?Zj0nD0-=-R0tmrqC_*=@NJ$l1dl$d3_^pJOL zx2>GA#J{eg$Sy;Ob8aR;ed?ZD@8}|OyA~_R7JiM(fTTE9!BG#L`iJSCNmba-6#XH* zeO%;D3PD4;Tv@X_?D@tgD{m3a&Zu;Q>SdQHSc`S~6MwTS45QsE2Q6#y1UyeKZY}ER z0sJG?P+zEd-;eRV0CiDBx|KWW<}TZ@rnGug2RpY<)yDw|*c~YdMG?eEXLFZ4%>HK2RbCxNpKhXJ62GOnrZVCX4`(8!yT(@t6r@~ML;O|H z+e_GHL_lrFZR>>>8f+N*Z*7HYU(B76iN?Q1EkUs}-z42VtIoUn{r6? zPVKrg+dcc5P8WKVv6n4;QNX+(&FpeWmt!>DW93DY+*X`@CJ#HOsI|X+ZO^%EebFqT zqoQ*h6t<%;YV*~ zc3q5vCcZe1o}%9ghlC~&a*8y)p5~}`-oRW5-#M_g6jUYMr5Ea0Sgg5~yKcQlOMZ=p zakC6HQE=dzBkULm#aA%L?c-KP zMybUXJ-XA=)3d<6Y^(M>qk^*XM<9^W1NJYRK-&B60BlTT7Q-$224&~cR1ZoRo@k=e z^^t;y(`{U)GmT27dab&dUUY(nuY?l0U9wllDz@JYvULUHaJac%MXt+?D+Dmfa&KiY zpQ~$5>*C)i#s0q^VU=U@A~3X!A!WE-p&UK*ycC%l3s!uVEDi}r39hH=K<6Khz#W|G zel=-zmm-54u0hiFH62h+M?y;-*&p4QOr5fzIRoS+L`go%8hx)aA&Q{GQwS1z8%!=@ zwx)c)!?9C$Tv6FuvI)$ zTU(`Oc&exv5y__6nzCwBq+h*jaK2E-k4~%<+qwlGxMca)&;M_FogXoG!y;lhX)`|x zgbpmfr^J%yB$330X&0y=Gz6Sh(hYF?&u#p?er*gu+>f6Je1m9*L3h@eKKxvdW6xJj zMSJ&mHd=+xe7s-g(usf8xv8@ZWLF1A3>`&#t3B%C#~WWsDo|Ov#mWgIV0WE)Q9uZ- zKi58igzWV&G5Nx*RI`P9JLtr{*Y=t`F*5!GEWE^R!P2}f@Gs4b zG7_|m>I5?7Hu`?3nkMJ*LZljVT6S(yk@j8uf)1rZ&bJP->kod|E=^7{$tx&?59DYf zcAaji)%aAsy!V}z6l@XAzt93^aAs@3QphyIBjGK$?}L-EjD+SZy21bMnMW%^A(OjmhjR z#ktI=Mhy%12S66TJP^#|xV_MIjkM8{R@mg-q4)$NIBaTQmcX(nHQgU9lc7zvRB-GZ{&U@-*rpDFyzFa$TE((u|a;qz2>q4#|-USCo{QP+a zNP+L|PJq#+bTHnMUI}5-9L%G?tsPSU6zJ0n3!2l|k(#)BEBk++ZvXFc(&SUhWTL5< zy(cOg9yBh{TmEYZxnrW^xlrPZCd-J3MW!A?Zz4tu|^0aX8Fr~x*MNO+CyH>L< z9uyShFlr1Ll=2|p_D$z=wetFc=?t8w0=S(QReM?^Tt{nsYpSpl*h0fvB19WT$Nx1D zZ-3oc?2+MpWFbJB+?mKD3l#6_Ha|{A4IRNM@d~6H%x1cRMyT40Pf)(dK1BDsmR=x05`yPtu2w z`$jTP6Dqw@uZ^1H@d7BdDFlrU?1_oVKG`*f@YMU@oATZ-thn+1u~4Be->K&xfamnu zaUvd!1drJlwTn9*;}9vf;{qG3^GE6a4{2W>59Pc6U8zK0DMBIYTT<4t@1#X`g^``? zyNrFzNQ#mrH1;LRZtP2pr6~Kp4F*G&!5Cwo?9b)=&U22=Ip61ZdOiPH?icsm*L{7q z>%G7_nE`vcL-N|!?1w)=VjmuL#8rE*3u#l1N}%Xb4?JdPKS#2baZ&93_+X+u!tfF`RKfdwKe4cyQ{*iJE6_|Yl`@dsBjP@tX@2a z(aoxO{+X1batuZ3_73VO)%o&^C1NF>=YlL+h46mkPnyjG8f~lPFErY-XZ}H>b-Z$` z+HxRwC<2M(0O|(dsBi0GhT5gKqiVoHDq-mZBx!ptgR01RLjFxqXw>Q!S@i1{P!AqZ zo~HnZmQQb@v)IN{BWLu0L#tb29e$Bb!1J{$arir{vCs1TmD*%P#KMAo>@Cw&ZAGUP z22VS_`n|^&yc6tCaFi;gilEFrt2TUTWv(&1Y-4xH$|$|?h{(vRVbXgqD^!wGgit~- z*|Ey25B~6|^Ea#AY|t|5gWDD?1+e6ak_vrm5$Rm!4<0-SpNn1*a$rQkOr-tnm zngZDo=Em0ni!%~*B36AJz=5PS7(lDYX?QlnHrP^YE;6yf2d0u4I5S(_F`X7q*n<)ic-^ivNPqvc2UB-p!mnP zH<4}6jEs_UpHw{7(>q!S*yi-NRMgcyX6F1(HZ5!bR8wm?4sMm)0*FW4W4dhq*!9iF2C0C6{b$* zCz!(63ZoggAu(J5fT{Jk*pkL75r}lW-9VNA_;I~Tdk0_c>&%pf9s`nBas<$`mB2tz zbO(I8$|zoAK0q?`2oPrV`a(TpW75M#eFJRoC=Q5RSMeNlXEj{G0?-3Gx&*H#BX#yx zi|*aOuUKl^f9>++FE9UalQ(aV6L20sA}GpD_^?Ct+ZX)$h&TxNq(FZ1?%wu1_haBN zaShHu!9%Qx>g#-=NeM$zxPBcpMa?zto`rlIQ=|Z~a>M#`TBT zpYO6WoXTS3Dvj?$DkrmAzIdsi5F|T)yDE%|b_PG)I<=4ReO&J^1Ihj$HmHq_O%W8Y zX7v%kk=;XukIxK&20=9%vIwQ29bOf>mwEKI6R()q;LBE5dtf09w1M3gSAiU4f{5L4 zo4C95LseB|T5D8=d+g)^6?E?H+Z+OGvKr-dsRs>g80D@KeEj6e;fxu5?wwI6soI4s zP4=dtoxeu&7SR5PC(>27M`hk0=k~EK2TmH;Kmpq|YfdR^4_o1{VX}MQWRTy!-NS#* zV>s3CKK|us`O*hz(}}wK-vjOw#eELX(S9=^ow}@{3&sOabjP%)Ha6>5&8B-Y0u)0k z!(Eo?F-@NvWs&}Kx<>hZOJfzHwC|%Rh82w#O=Fi<%8N|LnDe3B*~NDQ&!iV6^~W#Livl zY-T^P(t`jo54BT}uj^ilv)cy_&W7rVW;FOA-fLptWTa3vt#%~lf!A?ti>=vE4A&)sZA&A-+Q z*N`0+u^*hxo$&gQ(xsM2?T<<_`(*_A`;RX6fQO!5?v_w(No;-R#J2tI<*BQL`_13O zm;z1}3!bSooFm}5@uO}vLUTj%F0|84VVxn!skHc!nS|iTEB=(?uu>nFQNAar4Fv=Y|&V zaXcPxklj!F)^2Oh)rXuUeLsWMjAgp8F;(;v<*#YrKNYrr{WHHvz&Br-ir4vh$?)#W zM>J+rKN|C*memp0H#bkX?A1pr8?%yqkX?^m7%nNUBTo`f0%{`zIBvAM!P^dF zsC@nUlwbxlLruVY?E<|V5CSPp;xP7s!@rkiCln8c`$JHAUhc80fZL@A47l2tZ`ie0 zK=Iy*3RcZ0gkEmehsnl;K|+DSd76Rq*UgUY?p0`~v{4&P_o`EYulCwY*bJo;#QsSWN zjPHL+XU1WsUKtGgA*hI*NE5(PjT8$-4-xujeimU{ zt%bfQlpdpG!~f3*KXMGI_@!LHA*wn`?aP^Si=#CPLSmW8@GRIgK|w|$ZQ>4!jfsK5 zg3S^rw|)&*xo<;9je z0f2$2sSC06Y64>W1Mm$??dSWQS;fCU#RRHjR+dyHf)nlUdj$ut)TPiGacypH3QVLe zN5n=wQp<^UxQJE1=RVIWYCb@Zs_^fmznJVm@2?3J&_sgT znn`2$k@&pcTwTG7GCncd+RswJR=Ho)a?`{k6~@!P4y0bAflp4XvXJ%Wqjl$4S*MzX za{0J+6dny)i-c)JS#aw|B5=9_*gY8 zUZ3Io`KBtb)dXzK>LP4qp~qVRebdn;c53uyK?qYzgX z7=tKn{ZJ})pgD(F52ICvD!%FqZXKtGAGx6jRY+abtjvtMUrAl|GhZ-1NAn+7rGr9Zz$CX3q5N+dbV_hmtj zIMi5q#CDR?)0&f{VQH4_%$A-0C(oSU^;wzA=zt=x5Q^!2IgHfO`Yk93d>ibTrXMUegRSlV1K9l-I?J3KYf6eXPRX z!6ROcL`&OfzC_gJRM_-4P~AFRLmbbd))eoDY1wxFlFsHJ)$fQBW8>&4c2 zATAvND0l2+zXN$402NpUlgg17?*Gb4Bj*_wafs`>xVU_JaPMWYdkq8EnmsO#+M2@CZH+;;Qkm^o zW3T>}1X(DDMD)#gvtyd`$Ze7Wm^N9zYP%b~j(G$-+}nMG;?@T7s&BgxHiV zGMSKvWj1+$M&hxcz+<7ypZl}3qZ1Pi&DFd-0MSVC;ltNd=N56bwN=rw9E20ckH_4y zx^t(7;fJ!w(e|LR*AfpD6yEM7_Kf3!u8fkaF-WHL5+NmH+h7x36L!nM;2zy4C9nT- zu;SNp8L+j2`}+D?3Jf=1|9i;#pU>%h@3N4P)|%`wM3J6~N^k|kzR^fC9bV>NC%5PM z4~`!MAD*cD5pqF1ucJDJ3+qIFs|Coc>o& z=l^+0!hvEz=ksV!5t$c|(>~MvI$&0|`0n}hBUES3M11=6>Cf{Ap_<+2r)fAs;+H-8 z`}^BZ&=6p8WA_3&$MNz1PJ9ZgtxX31@gVrz=cWY=w~ap-$Gd;)f7<&UKm^fc zkb{unGrk%ww3aRJt(>^}OF5w~03_05zI3WtHi5fVO8kfU*f7!P+_GX1KnP4byGNjt!I5y<4Q_sfk_LSv{9Ifrjtc(!) zsi(U+igldi5Vsj2yJTTg)C)CFudH%FJ%Vy$e-(QSeIt8i+ys(6YFjXOAq zR)P_B`yEGjUk*q_@|_7&zD2r^3t{X1<>mB?IHloNa80T0cQIbQN*^Y~nUt&fDB6=; ziUh8K^F^whOZ?)ob1px-?$F!kAO{<$ze-;Q$L}>wW5S=(?X0nd=HB7YB!ZY8;tHF^ zC<9(90xTad4j+{46MPOOB(_;-H)5#;upxy+@3)YfeJ$@$e6`9OxyF`@c#-g5_zmE+ zgN_Q^_&pUB`M>U({?l&v>s9>VPGGZ?9GbIP*?#NMl5R8Ez6lV<7F)iHNC3ZV^D>_t z_%W$pLT%dKfXdyg^9>OI>>oO5z5XTU)Z`y=zR1hn8iqFUzWryZf-<#-Ta~1Z$pB@2 zFDrl5{cEVKWz1q+h@K}49~1{L8UUpxrn}dE!zx5ik(fBSBD0jwY~Xl(3)T}9+?egH zJGL!5=Dh{zZP~0&dRYzYo1yD4l%DkContvswyK7md)rTXPOp>Vv!2_WP*ujd`rbMB zD6Kkp^Uyn{rF4|*ETjd~kp4>|`cI$!m!FBfJ(2rG!&)o%=wP8|+|)FiOXH0j@A~Ow zqVmdR?c^21AslsvIZT!9ZyaEu;+@hlH+5l;M;q2Y9Z-E3mV7KWKX=nTs?GXVGJky! zGTT&aYvkO1L6bKh0BqFGs$x{IMGr*O_s6!349y*gOY5uAGfbmT2l~w%DmT8u(dN7P z#)<&dt!k{emkIF4mmEF&a$^%~6cE01j&_B|pjr98&nwi4kkQ2FQHxHbbcUj5En)zl zu`rU_s~V=f?yZ{OW$QDhnH;;UTIY1!J%8vZFczFSQf-QgM9Ihcr_ zc@&9Xto)|aYQq6-?-m13;#(zWjZVFcMqGLy{_#lsm45@dUWD}Wg)FZ4;bX8=UoH|1P# zOMMKF=l(?-(VaU~dAistCSk1U?iq*=IqK&oWgJe|^(~*J3P#7@P1ctWJF`E(?mnM> zXS`r|y`A(B(>hXWcmO{!B&V&DuXGaJmJKNtC--zDI@2*kY$YTLz8iqq%u8H{CGm&k zwtQq}?T_*m)7fy+ciM$qSDarF&(jn?gnAec2CfI54OL2Pzwiiui5$PYN`Ie-`xW2& zLjA||^vn}v|1l1Mlv1kuUNT-=8zBO7;W<+1L};~|enW?OM(6sMGwZ_hu!`eMnpr%z zgbXUjHWTR0i}GntQHKL`(&HRO{aC@7O1;OEdI6a@SVz$W&T)G?E)SfJ6|HM-Kh(TFmcZO<~Glt za(mr;Og~Jr-eRaWK#oW@;h*|wDqp(nCYsq@@ah0nw;_Z_1G3T^2duKKuIca`KTh+< zJ99atyfVsYXu2#swe}Kvj_oNn(QHgznEUvJr>|@bvP^IkKEchbJZxg3&eUH4Ah@u*P}jN!GM~??HpvObbE9^s(vWe(JYS>P|W&X z!VS0kAQS~#;rvEd?#%|({}UFi3Ae&x5c2d{Xei zRz_B8Owl8Va zNKV{CHYdbUi!-`cx)R5`0wC!GpWew0iMj1a^DXXr4!CcsWbX9yD~_JdfmwOw9a{HP z1d+4ZrSn{>9T&}WYTRyXGw=H#GO4`MS{828zCD>;`G?ozI&SUA_tIF4f!V+XpV&Fd z5ieo@71>4`y~4n){K@Mi$EZ-(^vjtD4|~4uIbkVJ6B3`UWj4j42=an($$(>*bm|pJ zny=b+-HK)Wn}pXGEnm*pmA)8=ro+-D#omfoQJ84n zd)cI_Zpe9ikN#@+zN+QU_^uh3d@eXccK@?_e?48Ney3!mozyux+w9u|wm4;5w^<7u zFHNV@VP?XI^=5-(oP?O)~*mCEciEzPp8fKK~p?iRfvV!qhf*F(xuCnEC4N}#j+>yFQs zdEJe<`E8MP4yQA)R*>@T&FdxDv4EgadMOdtkgdeNbkcedyX<*4)X3*jq1tidjbutx zJNa=Khl3&h+4cBC@5y_(A2`E4J@UKQ(FFTrr)Ah|?wWYx!qZ#gAv(Q~S_N4a`JNyf zuhOn2YqCa9*-pKD?DZ$2{aT-}w{~IlEBYh|Pv1LTYxCel1&Zgbz89?O5qj-T#P|Fb zUpe`)X3Uj6!s5JQB~>h+d%A{74{<+_C4B{ghb|7nY{z|Q6|8t&Yott?ziYNv5=V~LW1L2HM*B=^e9zp$)12Pv+F(bH?HZP9 z7D4iq%1KdMIb;rc()Hu=kj@|+^{2$@I;A;b zCo3YnNZmc|9l>};{1a>T>nuM7&U@LMgBp2RBALYGyX5Go5`~RieOnx9C)-2>JuU%D z%7e~i`nHBzKo`jO^QEC{{vsE0k$%Fy z^Yq1XYF=zNy4`D7~XS8=wU z%%Llo)NBkC?8&bV4LSAIFU-BwR7Ncqvy`Q?KZ_?~8C9Y+`KrD`jNWE>1ley|1wk{v zB+uPcbhj_alGk~kK2cE22Ja+jp%yal@MSn8?7r1me~?itmW)>Y@Yo-b#M>{Ho^kDM zz!f-!Q_`eVx$tHD%PFaCH%SYRDymG5p&(zqi6!fj**F@aMM-vAz(sg`z^Qd~Y7O&c zt-!hx-hGt~<~-!qnK-fz98r_chyQ>5P z90MG}T!ec1BIl7X^OQvLnL}x)r4kGzi}sG|${t$TLjmAFt{`a8IJ2HZ!t|FXDK|0{ zlkXTI_uhZ$DXs%$Y^0>nDYa?apaX5Ed<;N;LgYE)X0sL4<&Q4Mu za#ct!^GbPL$;xf*Yh{n@x;1Q`XaygC6%a3H_nGBAcbma|HoLLY`09b6AVjexaCsF%##1sUHU%7|65TgS<_QLP$6*_BqRc6mz4OHl* zMg61v`Qkh4u#+Rq+_)E|NYX@*lZ2*UsBxoId25WDGxIPVLL~ zRO^*9E3VPOj`mffl{us`Bezc*48OI#kat#<`$k!%eWsREKkBR%vZD*6W;Bwebj8Am zC|9S(ayj@lDhYFTe>YvgM6o23d41F-Vp6mco5Zpu-QAM3RsBXv%dOx8Iok4A#KdiF z=?wY|fsgJ{Z(p~F4rU!SL_uQ8_`lknaX(Z}-)IFRt=5%ZGva^oDj&Ke;pMT3!e=5E zVq&AX&^r8W9PdDQv<&r@>p(2PVLa&wX#SBDHV?gu*Vc-vQdOii_BlZZoDr1>p=%!M zRl0Qd3NOvD%?%~}m5mTO?1xuk0B|iKTDu3wipIXvETNI_C%PvsjDCLngQ;^xf&;{e zZzA><+l)Q}J%pHI`V02*A+d82&n|nUiCL*2lBerFE|}857_wr&j78OQdL=U3aS1_e zdO9GExx-Z-sm^sQODvXtME=+jreBa`Mt#3u{(0gK&C5jr($l(1i-;Q8M?IPmY4vWj zK>>fP+H-u~>-#pEZasE~<)vOXB@qQn>p@P>bp)#Zr(H0hX)R3N6vfPl5CZ#{bbNMcL>4rfd`D% zcv^un!SCT7oav~wa(lJ(3ea0gSF7|&uwB~jY;I#g zw1XUKecGc%hF)c)cGdZoQobfy(@AnV$xWFeC0=#v*?o*-Fk^}VU9=|VXUdiAqG4Bj!zLC0obtOMm&tf-D#fG0}T`K6Wa z`KQZ&2+*rfeR?hX0R^elb3xn^1oWj@Y43O{7FuJ`N_Vr`qjgQp>mw)TV7aO!^+e&z z7Yuv%cTdFP7fC~~u^a^L52kikycNYiTSC9cJC~Dd2d2zF!xzu}^T+?yS1oaJJ9Tqq z!*O|NBC=j(knc|nuaLM$0k(WP`Q~Doqo=l@pmtYKkI?%JRSM;GHS{gac_eem`^Tl;0jLABNMcu7AceXik$1 z(6vt6*EK8J&|60CmI1S@!%yc$ij!aMmcpCu%Oy-e7ZBks&3B$=|3qL^G&8Z|-3DgF zDWk8&hQ)GcTrHpBghyV26Is|~&~eH6O>mCc3&T5f*vwL@V={{;6Ck^LZ(vEaA&K03 z;h-lg3KVJ^Sl&_?G4<|Lb1npVyJzKxK1ZsODy+)qdrQwmWuQIBH`@$XA^6cnugdVa z66~U#SP>!&v5};rD3^b`lHQT)maXzoaXlicFP}j+xR?R&TGL=<9L{YX75BN-DBBy$ zMu#bSI|`Adp)LD7%ZT%~R!;7<5N|`vhO|`Le#m3u zd$o&>xR zKD4s`#o}LfeoW01_p@>VSE|iSX0w)mJZ(Th!}N+hJGdM^$Ey3Y`5`$59}qw&d8}WO ztmKe1xiUO(dHf&*_U@5RQ2lWxJRP1_BwI7NkJz_S94j2kfq^bYsXV1q@m02Pnr-pM zMx)*Hp*NyOzkgHBfuo_s{H=8}`WN=L6pujD8U$f50OE}9BMF6Jtv2;W5i1NiptF=F zhCxW0S2H*qjjpm-?M^KWtJ5ezL`)wz=#{GZinQ%Cw{0g8dyScuPXE!eX_mSCHXXgo zQ5YWU{^jIVmL%~nMt8Yy@NLekddwqbL!w0}NJZq7WR&!xB1QbxN57IDM?!`#C+`-{ zoWFOY1wFcyE>};{5uhgJGH27V#aEq@R?$=pdkYr$1blFoyOI?; z%kLXedC#Tt^ja3)TGI0t38p9Ll}dx2`y5B?d_P{`JX*qwSmx2)YJWnGm9*=HC9HPP zryjlM;MI|wF&&Pv;FMWYPGp43C?!~ta<2+MD|b^>S1h{<$2>|{?-VBEwW9i(6Ybda zB4$TU%fhrH%1~r)ARX67b{LqYJV?D45#Q8trFgd&xMyAxZ)sl6Z%03^xtpY#b9LTB z6Q{g-M4EPpxUN}SNa_Z67A|hliI_wCEi@e66!c%Ts?0lw1%8P59_~S954J&}Y zwqQX=7pi)MGtYuaAL$4xbo(Quhh$b(J);iqDMLy7P1;HG`e61@*Oqf(cG|)yF}(h> zTeb9VKA!e&nz#A1HXLx*bg$29>#1@VziMnyY)K-XI1_#%R>{4xA(&#uaOIS%8+uaw zO)f-#9xtd)rz?{cO$Qfb{z zyp6oRe$gUE!*+hmP@zN$LQxW7VIJaLu^aP9P+};WAWO$kM60VUwx<3cgUxUXu4BpvtE)IE3InU-Gt-)w1l>>Qi9a1ob9IX{tMLQa;wSm}=5#*`s+z zd209>{{H{f0{Cwt?%xg&$EmIR%XiUbN<~x?44-U}l?U06OH~~?oY=t^ek3KJks4b< zX96nqSJJ45lcn;>nr{j|&|81#7VW!0$7bViBhi?Rlesyol_rpPgRI4Tx;q>3Dr*6s zo03VukgrRxoiI3TJOoS>wL-K!^q%<9KmOp_ zf=hg-Wr0tZmw%B4oFU!6`tICT1g0)G!enpD2aLF*c?>JH4%{bbrPn@G-L@<#v5Abl zCe60ExQGsH4wLoakL29F;x^wW&KC8JS?!kJQ3}2&K#FSl&SdENV)SCU|NosM|L@O< z`DEP2%4m_>Uf(e@p1r5RtZf}4$Tr)DD%CS5WJu=hgF7#LbzgjIw=>7D)T+CmSv>S% zK;L#!>n zEiE}8xEm1mk*n-X)Z*>WWPw%5N#OIsKb+i=C~>KDJ;}q!m|Xej(IW+7Wet<}UIl(Z z;v{WN3;FDJquc?)ZDZT_J3}Fln8H zoPCs*uH`h^p65D|k^kP0q4?e}e{Ot=@sv!3)BY5C+drLk7wAay0We_X0nH?jS?D1U z55EF#2FYK$rPL6@!l|gZ9V*dg5p8Je@89tX?M9flPtr&RCUUC@M^YPdGPN=uZp+L=ak8RuU{`_al!pQCLUW^WMt@ZB(3fX%C=~ zuuU^Ki~n+W|NasG`2a$zzr;1ubC^z2>%UnV%%Aybe8<}*wGB?Xsi;22eG^v3zJJ>C z)i3ewiz1)giMq(O6Au9il2+!YMjo?I>CRGp81Q(w7H#OArE)=b3~`c@ebn9tpmwX0 z?pa%Bx)P#RMbCR(G66tbB^48O`4@=<6G+rG)?vQF=GasJRciw7+E3t5=UbBc?)gSQ zkCl(_Dz{8oYn#|R`)Ux&q{>cD%>QzyM~WX2C5AU|-xg!^K0BvaBkwu*=QEqjhOKemx+MoF9o*8e zjrRaq#%l8!Hwmz%(SjXfhQnnxSI?cBKr~KaE;?|{&uvO{cH0AzM^R_i)9*nX-a}u$ z$Q=;-fE%T5U7h@CX%B!lTAO#X3{MV(-l^s{ybJv9?s0Q6mfVlY_6ByQ<#@(vFUBA1EI|C%>-x_X{>NABejoKMJMcdi*kq3#AmjGYb}Uk^zr#SV+3Rp~Eo}s>)`1E;TbQ2yN z{0I3nsi9pIcP-l3J@yiwwfD#mCS#wQ?C_mJpS_`svh7B8-}hE~6XW z;C!y;*UAj%7<(W#H#xh5!sCNq1#~Y~Y&&AifJN@gl%{%Xyi@S9ggQpXV&xjP++|vc z#bllJgK3+o$0!H$y)}DK`K_!pg|9TSi{ri7dL3v|WmeZTD;B)~EO2GecaI>s^9k1} z;tL%Pz9^{fO?l)WYxCRd>PN2)fKhL@%-?z%Yi6h+!MR<_^P?1iMwJ&nfYNVeF?-O; zne6lD$HuBh-*i`HQJ7e(;iJrt0f;KeXSKcD1pvnt<>a2zjM|*-GVUFp(DrxHx!JeM zS7H8K$iA;jns@N{IWT;t%&(m1!a1Ui0Vxewla&}CzRLl=txMx^O7hm)tZ~?l?>ih||YT%y) z_KV~u$#*Z(j!ilp$;J+!IyZ#&$lqD*lp?ys2dD~XW}5lv0}h$R8sVck0G8GS29@NK zjEH)r=0Huo$YPc801w@lW2|1J>baH#3&e_(E=Xu`L4u=_5qrA8F*cqc0jLxosiYv0 zvKeZTH#O4-Cv9)a%wl zApFFWT8=aeVSKmdtfoewox-6G9C>=e;?9@*j~j|M!$@I~z)i5T*WToF29=$oD4$c! zW)!S#buWtFm=C`$t}0qz$)DJ{CjCgHYl14Q_!${Dz-x^g6N zD{a}HY-rBPNaC_e?pJnS9Of`nnoH(PvNQc}#l8PSZV($fK^>(hF@#hr+U{&>67u`1 zKAn+_h$8H-^Uq;O^7=Q`x%=V3gpeaU<>>tkZyZoRUl=h)tRqL6L7W3sB5#Z zZh#|hsm()o5cjr8wbRkYzQU%pa2GYwn9J6j^gRlMm++;@mz1SX$AP)&pS~(_*O*xs zaQVr*JjOLu!SHR~il^g9k*aCQ=+J)KKpcXcsDDn~-=C=ZP)T1_uv6bD40&MeFz7KB zjzes#R=Sa{`{z~luq8`*75VIK-a}W9Y0Yy#M6XBbk1UP<_^zy?{oTqOlQ()@%rpIu zigif${7XQ-+h8ksI=-~M7~NuNOcBd0(7RDwKQ4I( zN*}nF4|f8-;KkcN6wxSCfef2hjre5P01fTlI>y+NxAp?wUL5msP3V34(QbU3w1LC1 zJm4n+RRiMl7v};fX<5bIt*WSYsZzWFhfCc3*$@UAx~O}S{cRU5$HE*fi}bEPyx@G} zo(p~hQz}H}4qd(OQ(;GTB*!2W)~Z^N z+-P{^#E3Da=I-ia`Uf=BGOR)&uV25j%ddW*7ek%mRO2S*nu(t)zPS8tl8$gYIh3T4 z6*0S;r;8pi%Fp&<{7WZ=|Dq1?>jBzQPqrE?HmDEcZBHwI>%vg6s&;Y4)Qi)6Hy^|coJ{PH-Iv*f8 z;f%v%mtV55^_l72Sxzi;QU+#P8y$cn}cwfO5{Qac3+@D1HEb$xa+Fn>@gWCiC zt4urv{Un+F1x=PbS56*38qSh(X#_gguzbL&t0XhJd&wlKB6@R`DTdk)xY$&0_3WId zKPG&o`i{<-_p#|)DG-h|2WGYrt87akz<|?YmofoXyu!#+!pA*xsA5As1xTbkncREGzw|HWA4*GrTh0R@zsKLp(Nj`{f=P3O|pmwZ_Nz-}PdO+CiC&Wv_v8|S`X zX_t@08+(40PG`~6R@Vo;myTzbdz>-zp9umMK?%lQw}f76Pq)~5Gt6g`_O zWlYHab}f49?VDt5-Weixo8~OQjEo5N&0uTxwpdtPd-tw~#19@xA;qLF+KoSohh*m% z*VgEq)!@E&=2riYpq>)u)V-;Sb-XQ^1qbgzx8bzHz0Gk!durCfW@B6ZtWh&?v|l7X%bD^(V5_$S55LA z+jK0gXpYSAR9yvaR?`xURNB}nRh*&oT;s0gojZ5jcjo);sybFjVomG(_Kv+ARXx&b zqxXmyp?p#djgV!DS9-lS94EY&Up?@#(pXFDQ?2IPk#gI9E*pnqK1b83N71BUB&dU5 zLw2``WSm1xQ4{@k0x%f7N-vVbfqeS(9?m}|CTQ_g;}}t_B*jn3mtonbak`Y9k&$a+ zVq(MD+S)p)^L~BgFqd|=x$L@5YZPk4!8b)sI`d9dU!N9>j4LG*^lJCF z-;DqUssZR_I8nT;G_1V?2LT*#LWTjYSj<7tsw3hwUzOakcJhN9QCnog3n7{fZ{RSR z@Lf6TVR{xFvwhOtkoiDv+cL@XCkJk;%dQ6Ncxr4Lu}eQNf=@G033tZ+12gu&_xk^~ zR?e<~9LshIFW`oJ_s-i=Aqg!{4{`?PLA4|IC~|7_{(}cfhV*+bdT$wd6I5%H zF-9=dh@{VF=`!H^;t`Bzb;PHHfO2%xeQ5W?-LLmXyOr$c=H?KMHo{dDlxNP1*-v{! z&@J|vj6YZAL%s#t63G;wAnkD?4?(0?@{2Zfk4*qHO_z0HOpw*!!oyaLeVqq23y_z6 z%apWiB_i{YQBl{KZ}GG<{y6ubmjH+uKdTdDqIZ(wagk@-=B1J5tt}l~&2v>VN@LLr zrN&*C;8t+`JKSfslJHJ^m!L}~Wh9L^BK=aOmzp@`(4x=s`)=Mn3ptHRiBGQe5({;V;i@%TTXwaRp_mRJ zsc|41MhuH95oKV=XBE>>lU~=d%RY+cWZTdGqk_0`3|FTrIyN3a8O@{n=RSPr*-#BU z5O&Z5PhpG^?qp>%vyFw?5gXt}$4wrIr@!DPxjHmhVEXY}L&I>(8|`eZ=3;77qyB5F zv2k}#oIg-r=)2`NK%H)DV}zPXF>x_!ocsUbbl=eR`x22DI@A*PJRt8t#s+ZlMeRK` z{C)DPMU?U?HYy*Vrpl44Sms`K?a76>374 z-ZJsKpgR|adPg@#x=c);Jc+T=nR8^%cAtG|0_SI>qx*r?PA7wBZi`_*d*KEU%jk6D z2jWoCz~y=RmvfxoeYfO3tbrI9l|+0d4Fv-&1NgehL3nZ*^MZ45b1Tbi??5lQ_G@nl z3JMnY4HyFwHz-dFSMAd3!N!ov7e>?_Cl97Y8~kweU7847+@KLP;184D$aGy8yb~v4 z^Jo2HrLqxX?tVUX;G#)!AyBV)wn!@JRf`0pPA|4HY-K~T@75#4_BxZ-+;=AEC4O54 zZ|_Q2b^zvi&7L%{WYe5zo^7cX8k3KSGLV_Y6sf#1=4^295@p4oMcsb`H@ zt0|G$ODh!%{6!NL{Vl;{MF^A2T)7SOj~1YtTRHrrlfJvcjiWWwQSlsnd>x}08dTrU zhL~<6cv^L2Lc~-4M;Y#k2GR4`{kK(5w9{wtJ}Hnp6E`0-oOgTQ-exHE6%5AA=CkBS zd9VJJmoO?JR_9A`X5&gFjHJ333WIEpA+SONT%*7{zd#8WA zz$H1}}y0GnB_*_6N$VfA>#=a$rt(r1$oJX(}h&I&eZ9ofMrdkI)A8Z?Wf#uxVBIRr7eanso&ocrG@MU+L)`h zJDzL;J+!vsc>Bei5CiAE&D4I8Fw0)Kj69H|<6H)GtEb2~=-5@7$|%>%tJY&WMi>d; zfL;as97kMxRYh)i;`c{y4qcDQ)#mj-1CJJHTcusr^jcl(g*BPb8x3p>V4bkpaUE_! z-fiN^>c2hX`o|6(H$RY{pp)L!R1?3yc)=Szn^6Wzv!+ap@qw5CoAwEPc)P(P|NF_l zGRvxY8gf-&I+qC;kzr#qPnOq>p$M9&8t zP!hA}R+x_w>|C(%^=Y)b-&kmdTb|#WXiRSN(!tu+&#DxL3_rcOae7|b-nh=lHF;$Izk=4Wm zNwO0QgxDwU@x2pFxa(0(gT3|yDK+l-+jt+D-#gPj+Rw(lui2zu9}^T1u&576 z2y(3UPP_>Yr@|H3y`DU%m1zl}^oTjzAE_?GQ9dFul^IB>E?t^Pvzt{7In@{Bs$k3R zBQ|!)B`Pavb|?w4bx|B7p$dEw?BL|j{3o2Oh+FLty;7pUAykqesa3C6L;N0XB*L&$ z^A60bad*q+CkP)ES}AB{rrdZ0SC^j9eA=`FE|QMrwYNR?aE7V<$AxESg*qKFI&m2g z&t6#HyAMvigpLGp%`IcsLZAoQOIQNs#FXqlT&j~R5{#<_wQoAD>z`N#^T_ac$C{x; z{?^7GUGz!Tus$~U+U;1-FGg2DYoLoIHoeneOt*n%tZrmS{CspW*48a6%ckSzhGz0) z`wUxx2X1v7*mzqs0jZO^BuoNFN|J;aZnH?Re17JF+udhGZRzG2ye2uL_Z!(Gg&x`aBlU!P ziW$~p@T~T~2etbpN^|DO$t6R#htK!cy;3Nr9&M&x9631C82#f9p2R=!{W`Q!KqB;O zMZ&I%qGBO(QD%2dP*Ly-I9nbrwX@<%)GlIZzpkESFGCx+oUmQF(4%aTne{o72##j# z{H;1}<2u20iYUF(dlbIPQ9Q`qZSwY+o6EbvXFTgkVJc8vYT6)5QcKs?NP?;%Iy3Vj z+R`^g-Zv5u_my8!GuR*D2mYRzcegaf^MIC0L~ItLGWGeDHeto152fJkF51f7`OTCrtpca~us1cC5I^zy0HMr}Fj&k(hAcLk$b0 zCC3@(@YM)zICQ^Df_x^x)!6g1J&gQ^?i}%ZdV1D_*st?I$wcFwd7SOLec_{bP&=6@ zAixGRT=B~K^gI0qSMbgNdOl;Bk5pRDzn{ObhWFH-pn)!CE6wtYWky$Nb*nUQZN21gw>SEh;IDUKZr9sRRq2^kp? zVO~yk+I_xHv1ItEvvYy<4M3=hJ4oJRLsQL>79dQax9tJxc#ew4;Ijr0hfeeAO{X)d ziQXFR3wl`=tMnlUk{{KmYEiegw$`J6T=4K_mCHKCDzqPOqz|Mjvf4U79lGmZoBFh? z8hHj4AGfT8g$goxfyA_{<7?f23*H~T9P3vJ7jgJ{LdM_k6sy%SFh@@WJ(5<%=1lx2 zQ~!X+)!qrmV+(qK{(|}UAf&)_=S=~-frUx?ejZ9cdj0WeW5l^#aApcr)|iz+m3c7o z26}39-)f#Mp81RAKb#Z}E*XzGt>riZ8K$Yxf$Za?W$keV+6B{ilO6GkbsbT5GTMjtncNfvVQi8MM8~ z-Nb|Knjxd1M|iS*5oJGhixC>%Hko|gE|YO$SmYHLimgz$ z2>YdE^?PxU{nTGFAXGJcogLx6ZF$<`R~8B{Y@7>;kH6{z`5boHSP|~|5dOyv{M()V zpC5=&VPta(s+yGxD`&^|{YuaPgeidJWZdyHX$Ro%1%54&H%XezIaR#ad*q`@;bYEOs5!d)kOE-`r_S*#Npxx9}OTzol~U5aq2T@_R+ z%=Kz&3`Vstv2%6+!o9csZ1hf`f2bW{O5S(ZzFLfcc>=((fInzVo}Hpg5OHf<5W|}^ zI2Hr`UY=^V>d+Lf?06DBxS-$i`l<8}i$SnVF?u%f11ok``53BJ6D=x==1&GS?IT>~ z5(#koj?GEFFh0<2giuTXZkVTdfSF#TR${4FI$$0KEeL;O3^p$~Gy3ps^$B1Eq(NFs z4EN%u{Wz^-89z(M?bvIkXK_bGgi$8bre^4KSgOW41(7?!Ef|Ib8B9;_#U;1SmrSZQ zr-=>5gwz`N$$2g$7!>==9j6@K-&zT4aVm3W{B3dN3+)o!>^jAl{HMcRuQ}zUM&buG zvyJ;Qu{pyiwp*C^bAwedPm# zPwEwNIaS$B@A_NGls_NH|2|=9OiFd#riVCL={Qq~J(DbJ>F%b;iZc$4^m_Y9YM>r~ zWaKe>a?MM$3PJ!7#w$I?24Mg|8n#vmzmr$`fP{E%ODVy_bpx zbG0qbo6B(v@)AE%O6~5xI;{7zvoehbkEj0DuuGzQ!FO|(b+*Z5= zuM)J5qZl@w@(PEU)}3_>oYTzS%D506s_+ZZQVPX2flMyp@dBs|3skRR5W3OMgYWWh^1cpixaPmZmm3%5fFmlF_g;knjIChnXzzQq zP$BPBySZ}SM)s1a^{(5}6V;INGr`d_IXj=Ro)U5o#Sb4kqt_NbCagO0_vGat4Ub%O z0CwZV4oR0e?+nB@YrJbiR(^uH2ErT^F02^z?RvWvyd+s)CXY08Halj+t>Y_23-4#( zPMIfRf(>xKbLrKGM?Xt;1x$7x7^JpNKH1oLX`;hI!fhXeu$8%pP1Wo~+yi7Q#~cIA zK5`AzsYyzsVh_=HVR$M0@Y*=n@hH{niq5LnTcx*NSe7ju+~%7+iUPcj0o>dbH#@`} z!8-5S`I#mJb?7^}dA#-PHH(0Z!0Eb|I9yW?_W4KIx)~!Qg46--mbtcZL4Yfj-JDP+ zn`*noIDve-2=Jn&otpfR;nCkIv1tIdzNxvdGhSlln;XCXDJ!aKD!zpB(Wiq>bHs-e z=L8DX@Aa9I07Q8juv$~NAi{V80$ zI;_}r8nqBG=wI%=8Ij%1H?q|Rn8t2o{qZE0Zc#|s7njY^-8BPmC|SYejzt!riHxrfr%g+!?q z+9-w32^q{TH5X`M1Ti_|D0G4k*=upe!dyuzWiD7NcH{l*-75V1W{c`eu@nz*bzHb{ z!+u7+i9LTu(}3F#cfvCtAzw`~)FCbi#%vCzg zhuP>dFujRzdM?|Vw(cAGrGqPV>}fm>A!Y8bP5UMGV%NT<&2qT^WrAH?!;3ogV^hTr zouKksNha-tdBOlub#nXC^tCP> z$~N+lcZt^n0kavIHS-4<$}vvpmZ$vbL2c0PUER+PdZ&zo)>chkleGaZo6FSS(jg8r_((D!^!idNWU+oTO|Y&1F$+zTw}+o1Lm^QFX;O9s)jM&A2^G` zbAThX|Nm}tL(yetthzCdY%N` zX#G5^(;dOZk62Cx8q{m)FvipJyhw)bG1*XPe(B&U=X#jg;zYe+x!_jd-Qn2x%0c_C z%a%28mraWDn50d^)XfTyYoemGg-2nJ$h86Ra`bIBs~H!ZPv=yoTe&A_J1V>HJjQbg z$7JOOI6KAHnhG;z9P2-vf6dgMyfc^$tzLXmmYTO_hihu%>d7NeY5D86#>Em(NHwcy zcEu{KzZ`~e-De*Y-QR?i`*AP4wMnG3k2@vHcfUg(TG$U6-6K|O)lBTWc&TyX5D*V3 z=r7cYqym~3X=b#~t8|S0VQmVL(Frx0sY?zxC1fPT204^w7|lA2OXCaWjWU(LS@M|?hGv2mc z&ANJt(Ba9@8Ak&)o>``IxQk>dUn*oZw?8c~>+$GM6k^^17gyXkJ-zKW>vYg$&I6gu z&Oo*kxk$=z4!n^D$f=42le3@c@Jn&}pN$IafaY;;M3#~%PR_-izI7SB)fS|_Lg zd(6I~%q$-Avp=HL(i?Zzlv29cTI)Q4nnO~U9x$N}ok3z-&p7XpPTC$RTs)u3*xrE^T>s>N(9Cnuy+n*zjQE$zl#@+(CcQzFPFJEsR?uyrM-m)?&e*uE3Ov)cxcjS4wjg0SvTO`5YM_cKrw5+T;r9;__L(3Ln=)j z@;iSFSS}tiA1LcvkAGO%JiV{Hb>GPX_UPw9r}gF&vVQz{<){77^?cR64XY{~!pY5s zq&Hgn%KSreG4H&+H$_X5q09--$2K|`*;en+H~8!!)-}sS&Q#&r&h&-R(<-Os)?EiM z(=SdJ`FV$zH053Xc!R6!jzn*4Md4^r{%gekn@an?|MQHb2qSv_(a!bL?}Vt*lO&Uz zsp=Y)OwDl!{{A|ipAaeKz*n>5vbslV$AY=48>L_4eA43}S)#XUvIct(x5gIj-8^Wm zw4e5E2xxgQr^w(cZ!poN&U3Z(DA;ebl=F<-c+5Y|*VOZ+S*JRHtaFiklTFJs-7c}8 zQzSY@z3}S?O=$)3j|+?oD#G@Q^!pju#}cXqbVdG}vJy~Ms=L7{gSaSF+5LF8f|z5| z1k|$w#b>HJ+^58I^0mWzh2Z|(B@n1EJp6sXfJMW2w(k89i(p3cH4 z@uakkoSx0{t+9mr@Wd18WcZdmb>lg{n4-4Qm<#ab8stZPVw=1sInrwu7?8G9Gvg8i z`81FneymC@KjH2ypRi=MTn^a1_hxp;kOBzqD+_ESF|!RXiwGwyQTXHa=im1#8YC$} zH3`bcRi1RQX~n*slh5)F4J06UNM7E<9rKS`1)DkW!{Wnd4^PM2I;8f7EjzNvx(4-T zl`sUSzkW)%E+}pV6cz427f}~lwE5ZewT#3?A=+GPYrKMtlkhZ-$R;y8sU&ea1CvI) zLox~tVh?NH1u-m7fk&9-ZjtSAKR$rZzcp|951Ya^g|lCJI7!*nUc689ft9(Y`n-N3pH%vsnJqQW ztr~K>E64OEH#{PK$w>fS^85-+L}U~QQK>s75Hd>qv4wY~5dG^PJi)j6@Q&nmmz-=k z$U%$j(z0VuI3xurO@t5Y1&Ng5RRE?F$S?10i;>r|6c;+Y1XI4oI;3&|%}zOf^nhGz za$_n|vaN$`rYJf5o;TwsX-7yp(}{anYjAU{SL1@*bH+*Q1Mwaliv*`E~xq67)%YEk`;O%4R}?6O^mR5zvn3h0`0I z*54~($K5(1tpeiSnmYY@sv>zKS+k;+?c+RzN4p8(Lh+&d{IehLGRCZazGU2z@|~F` z6qx+{)r>WAMKNz?{x1C(d@@czXyWOWr}Rpc_}i-`Xx0&~afU-8Zx`jnJX@VEwjMvx zW)z5uUpO>cpI)+BdkF~eY^7on1qMGyM@y`-23Go7ea1@PCc%5P>^I_8y)+wH3OZcR zhk`0y%0SM=r~r8bh1*_Hpgli(I)JYyIu6=MK-iqc(Ye#lI(XE2k?fFBjY`vXO1NbT z4skzQ@;0$t5Tg@#2zQQPIyy2ckadIP?eWbNDHkaijFy?zODZCae~$T2)_*+coY?qa zK`Ibs`FY{stXY6Th{#aLr0eps=9n=BT5njmRfHL)4P}+EpH(n@g#1Wl6B)T>fq)%pkAlTfnZbMBUu zjU>0!*WM9T0g@_+Dn*C>Y)cHw8VDv4Enb$BGh9ZrMqN;zlKos(JMs z(&{-W8b7!1FCr3Yj zj%1`i+fTyOZHVj+J2zm~!<9MtFEmf%RvkNC9-Q6b8_>81p5MesMcC53gIsgo%G35k zo92Y;)S24q`N@EVyMDMaS=%wGFjkJRT7x1EJh)YF%xO9}w7ObZ#9w4g;^I7|)A12_ zs(Gvw3*pTjoMmcXBOVc@+3uWnD!Y+xgmYzGWmLdGz}JqQ_#ZyvVdWy=)LX;~59+@3 z))m~MU}nC9amZ;gG?$e94w)<$eWToMIm{^6|{@4cVz83BX*f7}pF(V7qZY-@9kaz~=L^Mas|5Dk5e>p<-_Lbm3$3 z+ec;G5nlH|Pqq}kNrfGlWu`b@zt74W6|elNH=2&?X-3D}LhPc!tzU}kuKwZur77Rb z3zyYR$D9g1URD>V;?~@D>A)2;)6Ct9doD;HYvXm&f6!VG{61IhcP@Nepe9 zJ(zd!582-1K?b{@#VZHZAN1FkHG|f-)Pucx^Qb|glGTIM$(hqj{TD&2#L;A+D;8V` ztJ=V0>&XOaAk|b&y_qt7E9n^gWG*sqi9}krX^ye)$*VbWCV5~Tm^(qH*E8A7YnLm5b?$QLcKmP+{gnlgepDTxv5KA>4@L^01F&#WQow*CdyY4rxl#im^$}%==7&P@`;nbQ9LxfJOx@UquUm(YZM3v&AuTA_KWY4wI^kLMt~1|sGW!7 zGSqBPm+%?FJ%DW0i@o^e*JnJ#merS<=aGQm&uVOxNUWpvN7RcwMyVX^^QeiQ?w;lo z+23ioGGhT#6ktVLnp$<|A8uw?=6j!o|Dn388Py&1!$Wzuz6Bi<=-M#ip6EKSCFA&L zdLgpXm-t&YPgTnK1s0Pg1)O_pcH=SYjqA?e|6olpVxXX{YUUD2pRHDfdpyp8p++{4 z6Y58!^lk|zT!MMJ%bd8F2-w~$0(x!GZCz2QKn<8PbLv0!gqkn#=l#gA?5urySW*Zt zEuNU(5;hRmMN}z(n`K${99YotrC^{E1UO>{ycRKgHCi;i*ej-GEgH1uGTFqu=D^(D|{JF^JpjlmI z-e(yL0!!lji_N1>LUrX?rGuRsqjau=@{> zE!4CL%v^7GF!qp~Yhq=zF9i7&wzWUi-MlYAeNq?AyE>h2w}6S{XEw$0wye3@2`?}$QVY*8X>HcB95OBXdA6rG1$hX2de zojJn4xeT?^JB2qqU|~9&riZ#uh>dDX$jKq5BSM2_P^|4ndsgaP|FY+CZRiNk)-QoQ zs0zZwHe`3*l}F_dHyR_IB(YC8A#ri;16EG}!c2Ef9aU9X8KEObSX^UxsY$iaLlEFR zO+q5D7VNZ93FbGUV2<V06{x5l{)~MShAmB)ig5!|#ztM?pypechfBge zt87xt&!1#F@-0DEoU+GO{`3watI*^F1b%K}gq0;dcIg#y=Sz0aeLx(=&3X62XXKbmE-QQ-yH)6&n>WMe$ztool62>~z+LQR`k0Bs zqc@-tXbo~pNC}0CYFKIVrl#y=SeNqH4OASe=MK;o}!W`8sL?S?eyWKffeu8`}HG+Dc4wn)o06N6T6~jZ@ysN zilpH~ooN_>BU7p?*02B30x;{B@s=JRdT?To%dq$W(#1VUinP*HU3R(U`T;^^&v2J$ zKd!cguoY;T1KyI`ygn|e)ezY}1L+Cq6jS z?Ev#Z)lzib2<&#|)APFGLc^giB3>BWau>QJ2h{_Rs9Ye#GGO7Eo=US39JDQk%?`{C zBMSAa*2l}IMi;IC<-I~>D7RCqwXDi}(Vua>T`0agJhDAAWE4AJV9Ew10&?E2r{ab6jFT(Sp z{#}|N2|8WKd&u*%y-)m?YBO$HCnieGDF$^--eBTM?OAt_NU%~~_29~3z@Y(DdNJF0 zZSS0c0@??Hc4@%auFdDze4)lfII^qs+iTJ1b&`14w*}%%Hf%NVfW@e7nq8g&uc<%D zATZ@XIAWU*=Z6C;VnSEmJgqXj9+!i%SM1P5IY5=3@<0MbJn!geVs^l!)@}V-%>jJf zvQ%>-P{;eBihg{!OMe%m|LO|b8vj310lZNjzflrvSlxQ$!k$fjcD)~Sfhef!#OP85 zrOws_GND+!4@tMk6J|hHB?1&zqmNInzpj64y?W4C#;eZ1v@IM zE6K+YOrf2N;n!RyJP*>#QZK~Sq~5)V%1tQKhPkdVK9cD)a4xDj-CvpGl5}=VCY8`Eg)v-S2hnk2=l0wHi+`RWk*vb2fhS?D7Db#xW4|IpI zfk;1!nm#}Qkk_RbuHBZ%^1=F^l9CD>m?7=}R+!TG-10%3XFW=wkCmB656BOg0(&o& z9LR=LGm5<6KEV@a&C;NggH`EwRd)X+F+fgk)idlOQ-guEG!N_V>!W7OIr3ywkj~dh zl-L+6OOp<50menI-+cvEV5U>GF3E_&j_|>pR$qv5?yK)UH!v^=kE#)F%7eAS|9>c$ z*KR0q0@3?~cZ#J^$o@j8@(Ta@V{9Dk2n=CFc)NioQG5FyA#|};Q5ZSVJctE9Uwq`7 zr6NtjWkGK_US6bMcqV4Ibg;n~sq5@+p{_YXa=Jn&1rRF(+^eot+fKih4 zLSv@(KJHMA>C`PXs_7Y20GKUH#WeR1E8*jT{$j)Sdo?-(WozxOc}^|$mP^N@xqBWZ zx;+qJ$XVq!!UhmfdzuC^9oJldW3AE2H{yh5sHGl|g#uP>7%Aa&@exr`^aj%|YKw^Y zHG3BiGo|hHN7h083jFe%FODwOC$XFfjfNU_LS>}rv4Jc%T{4nIp6}XyCi;;Y^XBRr zE8F4xnjG^j*jufd_w@8quUxTTry}o(Vc!TrTcLJ`PO&xDwc}C;AucAD*g3XpEnx&m zmrakwt|C9oBo>Wol_{5{N%ZSXpYM=89@d5Wk9!QC9!@$b&kt8YH|8kF-c(hq3P292p}erBg)ycYG3BL2R2T}&g_+%Ey zR=icKv1F~UOl;>{hM1DQ<8Jy);i`1Lm-xW2d#ubwu~Uy6RnpEEjr^Ej=2W2jy5yz2 z%jWUYUP6RWom*#K#Y9Mf?$$VPgQW?EY%^f|`_A&;r(a-ZG((Q9LY)7_{5hRhz>{j} zi}BCVl!X$BsK;=Riut&fr)G4xyE))!uaw*TdR(GYYv?=SFiatbHoLw2F7Vn%5EP)d z(EEh|Z6+9y4p&A?KHnz)FF_jJ89QLYIDZ4rY_Td47Q?~_$qg#3b}$<0O{Qfl)GRaG z1@^QZdgmuRzixZF_+jFimDg;g%LRi}6Vo60t@efML3P`@p6$1b|g}u{N4qPQ&70P5EEaMGB!>ZIR6ov9;++< zO6JuNtlZYRdgUdZv(4{RY1+M%eC6x6tZOzPP>RoYh~E~Akqg5FrfmQiKLYsgKw*Qc zgS5A1M{)`xfS5;=Ef?BOehrj^VJx0YMEL@<^j&&WR)2bX3+|&d)insa>i|80`$)Ov zqf1!hJb;&%)kuGT=|4ZEJW5<<2LPPKHoxBfaxi@}`~Dj0GN~%KT~&wut19*Xel4>N z{RTc{z9!OSBl3KK0XvtNx?eYnK1=$k3xddf#6yl^#bXkU;5KsYl*6#QhY`n0yUT>7 z&M=kZ_cvnUSQKEFX~6ZpODoWSknmVppx+K87hEmvC&Q&I>AZyYE7xC435ChWa<>;7 zJ=;AJzL*_Ym~@#r-$|Fbj+W@L$vawXl*2n!Z5hi{j82^Ba`H6PHy?&N>C4kn^Yzt8 zaJmVNHd-_>Mya9=#r!%LZmI=_70kG6zXK#-~)MqS7VPlNl-MQ2>HYSGF^Q=En zekK#LK2au1#H3DN06)yWapQ*6exd#@P>_gzBP;0HN>;(&7{FoDZP)p=5)f%L%3Q;- zdk4zzU#YA9%_ZY6wvVCuy^{nSZOBuTlD;x#z?6`1EXQ`(s5J#WXvxv z3|?Mdw3Tu_0I=SJO>S>_n)pE1{?2%*)!cf`Ch2n$z6^sjL&lOElWGMC;d8-FeCB+-|Baph4e#GB9VXXtSIJe^Qv>Gv#hH?!@j2L8{o zr^_rM@eX3?Z~=+(R*S=t@>B*Odu7btxpD+Z<*!oHI*C(tdu6tH@21Y|f!8o^{{K080iYw{{Ep+pv7_F!jK;j zv(n3wH(pxqL<2mb=TJGSYxe1_+>lcTh~tZHsZ0uBp1Eby(pK|bG+EGf9KyyZPLCYEe*8;)yPpJAq90dU`zKsoPn|mpPbH;0 zVH?zpPn6S9ZYe+VM^}c@Iq$F#^EO4N+s?J@e&*k@32>0~#wvTatqx)uH8=MVDUbQk zfW-kt4I;~CbhM}$8l)aOlySYc+Hr&7gjYFBtstSbU-Dj{b8b&p*S5M|0)roPqetL0 z?lQkF!b~xn{8;Np-1l9b)+e6!d&aQ%5wWst?-B%3y9z``hp!X~F|hY#!RHRg<4_+Q z*AJOvc${J^ri&&=dumD!Zi{BQjN5(t&zIs~DvI|aE<}{?3k9a*q3W^b`11m8-Vu-SXZy!FIPUYANXy6YN1H7Z4a2*soebL8P*O zzH1OH>XRLp-pFMknq{w7RaN}Rke-0zlke>qIqRA@YF_j7Kz!%5MD zikFwpXh5$Z!e=8^YV4QF@#YHad@thYo?c)Ew$E*HCS6@!MAnZNJsQhmVq$W!34*R| z{9Co==wR?K{6{e&op@L0t)4QSTDM!RRy~&;GPV}dCYiW(LCvgrZ?42 zR#X}i*>}H-3O&Ls-GZzeK0}f3XOwnsWj_@V5=F9U4f`fGv_6n_ zLz`8y9K}N1j%@_yNE{!sc73*IM1$?X&iX#)-8^`%ic-D zjHo5s+)|=ETJmOKDLVQFT9Y+Ao*p?ZtedlNIBpe+2@GEWo0Nu&OP-}VGvgaMR|j)* zvZ?cG;&;hFne^~7#PqZtCN2s6Eh3g6X07sS?q|fC2_B24KoT_=FtR(>tZP>s&$aQ2 zixr;hfNaRDobJ9ws`i5nx3BiBxKqx!El}v3T90iJuutMUWu)aD4tq>YP$vfp$Q|S) z9iH&6Broi&QaOX4q|$jjuOg$a#@`J ze34b;ybAAlq&fPrAbxYLiLWL_4WlJXakeLb zOaBG)%(MaMgfPn=KJR{9VYu=-YdWc3+vO)=x~IK;>2hls;MPck(tJU4VcqlSm!ut1 z$0?e!*;C*!sw<9HHbQF@my6(SMgW;nR}`cCkoZQS{V03J^zr6;3Hr!-U%LFGs-2cc zxk+t=#coI|#-k!<^!4=}Spy)t!B@58^QcP&PyJeaBIa<|!n01A(P|URytNt-_~MPa ziof}N2Zc`O>$P(|`(F~5QjZrNz#du1!zZ_}m>d%m5`GPJ+6g4+x>P`T42z0-XflRJ z>rUSzjLfat`qk_s-A%$}G@Bw35)$&bE_X+p!+z*g3)hoibzo~bS zdy&CUB)?QtRB*%woPs#`Uy^}aaAdynRyBv`IXZS8ff+i6hlfY^Lq3HmcBf5kMl?&8 zsa2?uP)HO#uptfxb#*jX;6owp{k_qpME+q1gmopyxO3LFW@-{1#wgwWyE}gLu{^9kHB$>yD{97F!$SH#u zvvL_!WdtCQ%OW=?b#g}jb8!eSJ0^T}Ix&)cavBhEZ;~>#B7M{T*cp5f^S*xMr61;9 z%#R;0t?i^IerJWm6c(sFy-CTWYDEi3%USJ@Gw(g(u?i!gQ-~v6SV}aBK@+nax%llj ziM~;Pv@3h;`1^(FKxQ!L_((S;ix=~ALx8Mh8bs;~hhqStN>qhhjDSXQNr}<^@+(Bl zI(N*0QtOb2_;83pS%s{EV?fXCf3K3;F`na8MXUc@U?^WS+R>IwZ%y}XOBS%`DXY$_*LHc$wi7Vz5Yc;nlam%Y z!{JT>HOU_Fu5rlo71v8%aTPM5CJo50SY1mU)4h&C2NVb`#>;i^COaN8&b=u!6Sdpm z8vX)@!=VaGv1gM`DH_i}bX-T@Eb8~Ys&M=_+5LZ#BE9mpZh>VIfl9ZjRCXP_&_4tD zXBd)1Kc1LQczy#Zk!M=psV7!FJNjl_%I83~U{SE*wmOeaX_)Zs7%G{6bx27*t$vUB zcqmi187HMPxNL36*Zh5~pyqVFM=n~)gd7xz?0r2efBVUUVY(b#xn9>Oko`hy6of$P z0MXFh@@q{~Ypdjk4zhcrgC#!#^Ni|2U zCceil$mXbUIfK*BSa{%lMTbgpak1g);hDyT%r42`D{ssL<**db2Ow)LDu=;JuZ(p5 z&1K1JmkNI<8Kk0tL3{yqp-t?ZKUIMA>LTzOkQYwk4`*UNVeqiDaNNKrdO|B6p3tDC z2V1y-f4I>vS#7`hxTvUxhKj1OQ z>JMYyweT~0d8wuWj&&cP;aAKwMst|7d+NDn-B4@l{nwiQPg(QoU(=YC7_+}ezxKvD znElrbO(;rvpcI!%K4uF-B&;GoZe|p|Tk{)90Uz24%X`uOYThi+* z{Tt7WGWZ-ADdUT%hg5&NLNd9!S%3Sjelp6mHk>P%d$uyS zyLiaj*|c1-JN-Au=)VvASLFuQOk>+SCN0^7_a4K}&&gZ;0|T9M3bZQn$BR6ah{un$ zXsIIPr$WM`pGw{%h&LNsc&!471TuEgN=iXc_(ptK*!|0b(Sv@Bg_2T)Jr5oTTB5wR zzdu3){t&jenUwBL+wLJ=U^dl0CjE0*{x>ahkt)LNor1SR5w7(46ZyfPsBDsP0NY;* zG&s@#eI7Kv4sL~etzG&4Gr8!8PMW9un;QIalGoYs6R^m2{x(Mb^%f_+#lC@s72EYL zKe}JLWF>yXY}c5CDbvH~T}Jdg4wS5P?@UceLwg@iIDDZipqCfL4HSZCZ|T2zle8>( z9S5-1aaP3yuNKE)AJB+z%3h0p`qyvxw|kbfj=}jvGn_$H?tN$lPoPo+icB9PT>kR9 z4a#_0$pF$|(B0gdj>WwD`s%MvTEEc-pvH$?|FmzIh4m&fm z>~}T<;1J0`W`6iun4_bVuKxEVCXCunJG;dzoBVgb@sCGTM!aEqdRi_tAwid<{~6ZR z4Y|AxE{MIqeR;txy!lT*x3pK>y2UTNQ7qZl(M5OLSl?i685@$cPxo6j#KbpD|h#zOMH`P!?PXq#mXgY6$$2mWQm zxeo`Q^rv;lm%se!-`<=lm3%SGR0>!GtDOpdsI~p6$i1mT1dK}=JLBj1uI}c4uysiy z*E~Hve<|9AwE!UmApPZMkXaHiyFk;ItHDL==J14=Eh)eC5Ow0yQCZ|q!%$rP?(|mH zZBP)s{PvIqWcKjGVy!YWnu;k`zXPY<+&q=X0+_YIqWoUE;kD3IgmbUyS7?yJ=O;?7C)66Qb3K&QB<7k}t+{Hrs~!_U=xgu!61Sp~EcE{5Tq zDAzwSV8U3hvExS+oZ9NNwB zSnBMy%oVQpaF{=Oz;fR64g)Me{=8 ztvg(WHiN4#pe&rFU$wcby;>c^^VmWEQ~R}oLIA0Nb78Sg0#pzSkBt5F6pz$qm7z}o zVw}GG`44RZqbDMmO~tmX!5Xc4(~3R^rcYa6v!}W$5e7n+t3(B3Wo69L&yaI^lVjQw zTu8tDeT+v72R%el&Gycv+hz+MR#;Yyv{h&6zb1X0>iTqT8~$k4);bX@2S3gxgdmeo^K15u~$82T>#3$m$z0e z4H~>9VU25M0o03_6_EF7#V@WzAci!MD;V1?2ful%-L?X)RJw&xq%o1RY7bnv^W++p zBnw&Z%M_A9Tl3)>s511dvp6p#Cc#vwB+}}c1FB{L*6z60@DHENn_*ERFOAEI`Dtis zKax2ua(TBd{jGhMOW`s%&$lcS^irt2x?$lcw0sJJXntAVz-0RKs7x-oeVp@X1= zrM04KK5G5^@KCPNdx^bu)MCR$r{Yr;UH-=C)tfUY;=$hS+EO6g2xDk^$H!&UyEGU( zhc|*l=@esF5vpe%@=D3bEY@F)JU`k~HOvjyw-xL*+E{kGYBIcWON#A0-`&i&J?R#o zbuf!6Ba>DT+~TUf!)DwS4xU!JN8A=cT;{{o2@FoBphp%4wEo(q*F3=$x)}Dc-GRaB zmXYW`3UT2wl+4UXm$Dcx6pn)qm)X1!QA~(kiqgid{!GxlwA+*C=_oR21@dw55XOR9 zF(6^jSYKZs5*Eh!nBpIw z031pCm%GhrI^*RKS_$Db4XL69IYOIvIK&u@oUo;Z&y5bex_Ts z_Zb1{7^V`vfrEj}SK=+&E@_wu??-#cT|@#DrVBQn#*{=Sd$#~8yDnlgHn0udz#sEi#}-2A zR|$Ar+$83yE$PxO9aWijG*T=<8Mm0#XcSo-+$xK3J!tCmK+^|PE~B=fM&mkRbvjRug)@MgMwD`T`Je9`s#O%&ND!4a;64Mnca=H%@{F^~Q?WbmNxLItJ^UHQ$vyXNs>6S_`SVF9dn*w}WikOFul`ND*~0 zD5xJREn2hB@d_qUc}Pyq0tTH5!5?mdv}v^1E~V(?a~l^o95jZ_KM<6?_P%h>OTAMj zCN>Y$yn{7LOgbm3_TmoB+{P^{WKdsjW+*rfKnR#PEk=~4oaf}hCBS|ri)0nmS7lYV zKvZ$63r5>j8$()o<}O-5mTikWJ>xpA(a@c$!X3oQEk@t6HxmvlS{zVOou^CN*`i|t zsWTKx9)EKA)pxlWtw@&m_}mYChk{PiMjrsi4>~_?4G;H^<&M4$`_=qCO=55H9wFgV z|3_RQlSM_wg08s|-bW*{U_(`f9zgWzGA>H8F8`LorW z0*MmRRW5BV<8MIOvLja>Yq}m~PgGCkjm6*S9TRPv9)8R=iF39#l$W#Y@cHvFVRyh$ zCu;%+cg)Yl z&>2wn0caI;oXghOwEJFOe?}4+Zqk4ln!c#mz?FNXQZc}XMC4j$`NN(+Rn5kL1Zy^W zg#?8?&f#upCkyms_IBmw(d&b0l&d;!b)t5@J9dkduxx}@%+JS_PDO-PKyq1w>GsrK zd%wrJ<}t3qzlL>_*&7S@9_p^o9}16ulR8wWuXUfKi)xxMHv8ORF7!2q@;h%Q5vZuW z5yzuzYC@k67=M!2P%=S{*7^2MY|?W)`S|S6Ap_W$)kv@F(3p7M8v%;w?O8|PK;if* zgsJvNGF;bwFi)dptvmZVx>|%8>Tz%|yqh5`l=3;n7+{dNPfvayQ=}`Dp_2r5&@$pH z`CIY;EL<6LZcV;4JZ=|Xicp02ur?w>w8o*CPB zW0?U;5$0;t`d-fhYvB2);Z}b&=n<{7bSONWIqUwxq{niZ1G0ua_p7;XzV?ri_1;=%^5fGDZIQQsa%st-yPFsvZ;7F+5C)L*Um_2LH*ywN*Ka@(M;b&-U0lQG zKwK}FC7wSzsk5z3TQP4qB^s;jN6TigqR{cc`kHXeXdwXX(vBw^v!q@P;v&bFBj2;> z?_zm2tqIMv&^HAE@`j!H;^~U(=P!<(W#sD59U)C*w%EsrS6&UrIMmc}=QVv4aQc6< z$A5Se34&fUHq?C;&b=f6=n~P94x4%7$=I(&#<4$;<+k9BT_tK}5(}*M%B5bq27Dt? zo!(D;W^mzxfscua8I$YAw>4lY%-86B$agO+SNO$YYq7)VEo#A5!l8g%5Nm+&GzZxn*<9+@DUQny~tNkT^v46wSL_ZEAcK48WB=EZl%R!BK9Mm?jWle|OL;bE-Iv1dvv0e)v^ z8YFLGMp0QG_1!CgL`1znw^GMpC@&sE={h5FOL(Sq+SKStu1-yaed&;kz(OUq)W93j zvOYB7VRiw&F;a5Auo)tOcNL?Bw*NJuHMAK(lf!i%EHgipVD2}OtBBLvJnK3kv6Xmi(6p}euyoAWkagl#i_>}nQtcu7zv zz4fH|J#4hQqa&=&){W*#iH#o90W&WEa23_3nzR9*oLnA!LKdwQ*e^=}X+9POw3z%} zltt>7FE{tZTM+RzUnhpizq4E6Q3zR})VUg6YhF9-Ehcw3`)d;!m*J8-Ko~Cc$+A9+zQ$$Kt2|n7@Hy*ncQ+)4 z%e(u$&!|&?t6Rl^CgEKL;~MR4sq?VN*n=^0}|HuT{@egkE6-4*B|<;@g*9@qOrwNUb}M(bwg@ zpnQqzxMk~Ob)6P_=iiAjx!!}YLTOgCGxP~B4*)ElyxqU#)57mCdKS{ImJ zILzAVqC9F{I%T#uwr1V->${{Nf|yKD4%DCKQ&NUJy)%uesnnOIl@ilm=d$+KN0qH= z4v6;3Nr(4hn70l$`$xjpW4m4-*8&i6NZxtu}@bh}kXiFY~Gw`M<+-{V(c0nNKR6he`+KM^bImtXt!G8A z*R@HkSuS?6MnU*9F$s~`D(92AyNnzLq6=-oPcY&;)TV(rwsJ;0p|)TB@X^0RqAj(4fOAHc5 z9lu)2<97VsdVcN%(0BeG%NAS>ik|Z>>qT%dp~asKp^=S)Rzpz-!|mW}RS%A00qmnO za{+uOor!UV`6Zs<(fd;KVZ`R!a=r}XCud0b1x?`Dj^YyO^9;;Le{#zy?fuRI_*Yn? z)o^*0O4O|-r9R0fttcCLX_-|3U5cT2r;Rn>mSo;L(*evmE@#HM7J>3K)X&vJ zLIXR7EXS#7X-M!_Li=?E}-tVif&wE_g_w(=fzjJQqR$lY@d_3-t z`$+w;QlJ#IMk8cpi{1A;^jxRCc5U&+8ia88B*2Q|QhuN-W5CW^jPJyXMFU6;yaRx! z%F-s{CLzr&JF zhc%?rC4_R&3R&M4^Vo1U2rY%HC6@agt}v-enMlFxQ?&O$5Ml#H1Rm5d%IB|N<8y8p zW^;@`WLyYk%+-gE2qbWDwW^>+8pDcjU*I$Ez0}8$54}1Om`Y8Ze$0}bcK)%r??RQH z+*-XPXL=>2wRA47H_zDS**iSv@38VZ0A}xs9X`)ESpMl+kNR1*h+jMGdgEWRxVjvn zc4}^HF0I#>kIEq)E|tRF@z}Z5lj`v1JK;>+cNcJfd42lnPLvZMi&Fk$C@BRl+6csi zozR9)lrkc>K=@5J7*+~hS)sD)6U?_e++E=`Dq%dupRW$N*l_mJSt26gwjk{HgOb{r zct8VJvb3aM32+KHp@*$_z}^d8y)K>K78FXx+!@QJ*_(R#G6oco4ni_vqIf`yU$4=z z7<@f8$UPht6_slSO_#`4U4xyD-bHwWI7*P$;S%s+kn!Qp*-O@Gxb`e-oeybgXUcU- zF6GStR>}Q<+ZDSvSn>vj4J!m5sXqec?ov*A@l_jl6V%?6h+IlNuk?TGlklO zC5$Owa#T}BcLo(0ZQ?XRIVW^z121v8z*JRMX4(-kl;dKN*SrZ*FJTRMbKb?nEi;7R z9^*B1a#_nNd+n}_%t-d5uMaAR&+ne3Pl?nz!5$Etn~D|w9j^aROL5@4yO*xU9wu5u zV4`=!!(V~vCXGPdJq`|qqobxA>rCX!){pvQ;{+J$N)RsE12CD?8)y*;zwxGc+-TU0 zfTeoICz!=f)zJ}3r51E(GqviBF7`8cKl7sFWtSlLlca^C7W4OhmNjyr7o)bl=UCp@ zS72`Xa$?0(;-?x-ODv`%3{Ja9r67NxsTs5Pt!JF#{`Mt?S%8<>_NXvYiqWlecdQ|qA+%o;qUWmhUq z)GgLKkC&@IO$@5FCNNq|ZM!RRhHKU3wLV%z1>Q=msrOmRJMy=ZR>Pg(0M-&WrJq=T zo#^op?_y3QGC2yZoNBZ_r7M}FYyXOPqk3biQ5TONlX}sgY63DJ(|7@4=KwK-w{NMl zP65_WLcvMbu!cQKtpmTQWBfrHA=ByiuE%1+ZbaIRJn;-Hc zemqzUeK+|7Y$@UQ0#n^x`tFDTJaI`w)qQ>J8Rp&%m3GG$FaoLNz+lBPobM}Cicyjzre01n4z(8q4c zx@}{6rg$96V?B#FY5C8fwRtw&Hzv($u#vkJe8cal`5t_cbun=*gI1B!i)x*{xXcYQ zptQ(~bHXA_wv`Ur zVDMg+v2teT_!O~`%;|*6?aNg!$ME8MxEp)x-TmT!;(yMoJ`M&PsxBU_6i_=Dr2GDG ziV<#ebwB9vf7U9=ng>0y8pJ(|?*iLr{o+zeng}{|&Bd1{+cLpO=M^z0y_oAIzk^mv zRnN>0-S=I76~}FIp)WwL-;yB?dwI0Z9L`zCSNk9vbe`sWfFJCB&EQvj<_?koc}!Z7 zYQogRk^1>U03EZ0*n)Wti~%J{y*28z8|<@C>*lkdevF$waNZi76(>vg#!|DMf-?6phMkNbHUg<;Y(#f|jtYK_EKX;xqdd zk#e>?u={?#ekGT-gwg0rwoDNAj9RU8+_m++9`1fG()8i=57X0uGdj6l8y0A}y;-t2k!URQb$rCqdCQ~TpMbz-pV#Q=$(KHdIXP?tA{8mJ=8zP*_nY7Gd;dt0Wy z8F@p$!W*-t=2vD+)n(w&a$~;zeH^g2Z)kqqVYTo~p>@`WJ@krmYvvklps(Q)dbsAE zo6Tq`Thy#fn$Psn0l)y7Hc8Bx$^rub|;{Q4e>oie6(+9I=abv0PIM49K ze|P~rs^o>J2-f|$zNTXOjZsA?kFX^CRlvSn?6p+Ng$l1v4`h~_YG9QX>V+Nd@&&e_ zE4N}`4A78FyeC6O_eKbsFy0mf0NS0wj*L!FbJxUUs1U+kfv+p&A^L!WL}NV2ajIB zxMb9;RPvr{ISyd#1hTM+;aET{RZ>;uJ1m6?rHgXDFF7U0=|za)zx)Nb2`uzprqS=X zZlNE5a4EihI>T?bULtsW$+&0j3RNv!TrA&asJdVSko^{=xSN*iY>0iPn?S9WDPXf8 z$(<;4Zdh{5%AiDFVDR_mwEMx!DkHObV|xpF2|S^aYEO4<%gPo`=Ezr_cz)5wG2)sJ zNZ2X203jR)vL!NJ00MEdp|WaDTi$HMGhM!X`863{xu)f1>)!OCbHa$avZCS}LqYGV z153dcz}D*_A0@wPP;2Ls(u0wP?0K$2V96^8jg%5?|2far!1H}(25RfZd0LyG7oSd`;=Va)){i3p z>tG6#BTL||w{I;(jj~-*YHceje_T5{GKe}WCO1D0A@97FAXNUU?C8b9n{$KV!DUSi zY$lP)+$G##RGx(OI~;n^z2|f=akcv6T!D&(D+4`MIUEwd*RvYWQm6*C~cd9;+`(C4J6 zSl)%sgJ)s9!b;cbkr_fVAzm3W!%VFc)oY1d=>!-`ZoQjW5F}8(sgO@6zC7NYu1O)$ zgxYLeDmQNSX32!Lu)5Rw9KOAF$~$)_GQb-cSu>K}9C@tqFzeZj!^Kw@fqbiI&BPt;wXFYPB?OwhHWNXFXH)1^&m%aOQ7;k*m13==@RsC8EQs~>)GH~vB|DV38$VQ-VcrJudC_TpUo4`GS}=TCjulkm~dz)Mg^?|D8Vcp{6w97w~Qy|IlP%G4F2P!?6s0UDe~BEMqsQ z@%E3fUKt|d_-a@f$9zv!r&##r`1k?g=(~n78UgD>E|C51(XiUHBGLS9+G@7)6Er7{ zt?Q+-X06eN?wH7=X!>I2y|uyFHjpIN8P4yqDV`+YQ^Qwy7GuO`Uo0|;9PM|va~G7i zcuC;C13=pkOJ)i01@#C7HXQ*DaRz;r;wd%lT6$l_=k`1`%Wc-*up+LzV64tvd}y>; zS_AZ-XG#k|UYZ(HW=mTq*NG6knZv~QAO~m%dWgs;;+nY{6gOJ~n*0(Sijqjd8bN^? zJXH+1}k{&*KoQHDgW|@?Rm>GaijJ z!Q7v@7gxJXTliZg|HsbsFF#3}T~mD69b48q?$_R9P`~qLIq!CNsZry@=VQ(yx#sl4 zr^Nb(gq}(VtPJHq)+64>Z8yyI^My2QE5)!TbYepl6i$dYX%zv78$uk{(Y+)DsqO2( zY=_{Fo;jDQwx%OZSa|u4(rG`rD4Tc@c)h6W@+_VGbk+E}jn84p11BdRvxzFksaM3+ zk3cqKs=o!|gf!R-*y7?qe_83e!kgzs#X#hi^x&_O{ix+!qzX7-tHKL7vx3KVO z>56`>^&(MhgK-RATJG;WX8kOg)C*`iwp3{yop``4*?x85sn1^Xo*8Tj2EAM30PiPZ zP*GKl0Wud^L5Jzb!ryMzSP$1lS&p_f15`&Hme;co%c@kLhdg#Nyph1dlEV62YL zC4F=AcyU}WpY!s-japQN&-2BPVcy9NfKXZMvbZ?AJqL4mWBt_3VbTJpII9mp-8Jtg zY`e8~Gb~1puGItzHr5;BY8gRh6EQ0 zq~v*pUu&3u`K0{wSU}d_wQRF1Bwo8FA;N}UQ-enE1bzrQw?!E591u#2* z$kN?Nd3bzsJbR!vVUEYv^l0Q|u@dP{A{WxBMz@&VjfsQ~vkZ7LFq2@#3SFV3VF7^3 z4u~rUM2ch}mVPqt{TBbk^tfap%@SxBhYB=VIcD!^qDwRY=~FERT;z+Ne>>UVUy?sx zeyux!fUybSWF9E(igGX8xl|=-ipwXA3g7P<4E3Y=HoM zR0iN}AF^L;s}uRr!P^^$5t>|O4cJSY^wqR?{}OhHp#Zv-G@VNyBa&)+6M5tSCC=Wg zvgny4xuj#jG|SR{7|Mvb$o`a1yEgYF3e8S*C@oA(7lb zs%|f}kh&(~V_Wm*bN}~OMx%o0M7#pn#Y>BTsIOD((_zg~`Vs&nQ0${$aUno#ZR)YN zHs?|&2#tB%>ugO-ykwx$w|W}Te{Qi^B}KC*{qKz+}NZ}`i@cv0+4(V}G{kV-s z*fZMjoqY=RPIW2O+Q!6^wd)i8nj%T__0yUNREa^obKG6y%5N{GVHNAMZE_@`3n>n) z-gJ_zVjZS_O1hM~Ou5tCr0c#1ak^$dzP;Unp)(dBP^sF=^DV3-B%vieoumxCK0d$! z%!2Dt!2U^$C}J^vO{7-l?_gu+Z+%EkwjQOqe*F%3F|L_U-M#t;PxSwM3PfGre#apB z<-TNz9%RY4JqjKrH!Rg)g=_r26-xUGUW|ZXt z9m0;Y(q=6xrKEp4{eOJTcO}5uKC&{$pC>053Ss_fn7YKD9E~5(U2Uv;`GdbTaLaptXk@H8tz;L#5HCIZi3{mJm-prKylzdp4mb zZu)p3OzoA|ipt6{oCcGSZ)tY2I42v>4O-YN{cPHuvNaPa?vyXf;t!bRxKI@GoxL`QeiGUubHC|*FoapB_QyJfPSnSSSv=eBQu63M}Dqt;(vrvFLB zWl#MKpwr&4Rg|R1qM5`!-ODDjCUf?uE|d-vU}vJr%F4ism?&-3{KRisozVUF2HE|D zhLPs?wqM+Ij)hvRDW+*ZZ?Ion%0MPi$2PO0(!Iw&>8Tk*lV7jAGsYcAR z^MmK3?GxIzm+uG!0-fW^_@_p|@YhGAFU6cJ+B)qIFLQKbw7@&r(%eJ&k6ZZ{pyN9l zOb*+bf)_7b=z&Xe@M!`vk0=H_L%`GN2OUBzt~$-U{NnlZmE5rL-0bYS=KXZ{dj7bP zoOq1TmIZDD$(d|5j5%|hK4G;h^NJYN?eq(oUV7yM7=U4ya(L$tXSkxmdntH(yP5JG zT$7qz!+eFk#;K8b+lljfrf04g^HoCb3l!dZeumcmy~J%3brZXhEiqFQwVN7`34plo z!`}3p^@f}n z(WULX0QI*cxBx(ajN?zgF2B;_7d++38J{^1fdwIVUDxGw$!GryQ7wV5!PfJdVjF{r zD<9ARYEPg6Faz#iN1ru>MfHDIF(R3t9!EF<89Rz5wEy28R}M#J(ww*%f}=nijM7rF zl(F@7qq-7Lm&S#EVxk+aw*MJ?$ozKZA@wiYo>MwewF|7vHWpO{@+pk+9LmPUH;Xl|97qa-?jSZJ_z2ymbl$+u7L}vohvId zoDc2mKZeo?yK3RF-Tb$Ht`pwMacaJFVEHiohz6pqmflOA?PubTcD9=Nn2zX*qy4ck zn2_5r;`GN_^MCnCqXpQ&-JRLXQ!GNA=r-Jum#9bca#r9~{+xNo^7OP}8D)_tq7C%g zgi5-XNAAKxoxz@Lhxn_;};T=2g znC{`R{w$u`PaZx9Y(`IJruQvIrJP-`adCnxY6|(aJ)m}iA1cxfmjx#BcN7Q?COavHn9)Nk^wfgsk+@NCA17=GS#bB{jFz zccvD=S7I^QX+B%-aQ8cp;oFEoAhCsv0%5_s$Vm0K{oiN*FBbn_7dfzNf-1!@+Du$6 zj3vwb%s<)miSv1zv|Q@bRCZ@typ$&cZ!1B?pPjkhp6uez=(Cc6FP~hGpq(BsjYgEN zTkWZ#3%jes_fHy;WTWko<31szV4Ah*UPXfBR_#V~dKB~f zTe9oyC%UF_xXV?d^S3g`IRS>%GghIi?C9wDtYP4PWg!HliPE2IiP~Ru@?H{7jb$Gp zC{iq$l~z)G7^O`+zYx*U8V`g=Vb`F{t#=XZjeE?=aU-SFmNgTs4qaU$R%RT%1{@zT zC&xALQT~Dzyfn>9)N|m(U(xwl!{Gn&(BA2RtM>dXXVIMdx`-_=@$Tx_{hD>r7x9-H zr5;J9Ux{&0N{-X$iKrujEHVhT z;(JsSmf#Q>X$w0b{n(1zCD?ueu{Y9Pu@jyoZ>AMG@HiG7Gw^n>F1pCHXgR`M)@I z_JWrQwHf=m7h0bZ5N{yz1EX6;6@pm!A{jr#i0JLI{Tb-EW(aP`d4Ix<#j>$WtQ?~q zO7I%eg=W_F?f{nFC==6~@w~no6S(j0R*w(t4DvLp=h7+C|e$_ zwwXv}G_M6FBqSXDj0(*Go$ANHFn!m45CY$GfxGDrG5Ml-fa`|K-)BYU zD-uvW-_Bga4NA)jbP9L(%o`c~$svWk5)3A;9c(V@9mY67KX_!+>92;4#IkPDa@pO~ z?J+Rn;xF0!GePj$e!Rs?Ug2V+yojT<1LQXZLTqOJ#HJDqr)BYIqyn>;7-HkdG~l?g z$_~+9qDIdDnfvzb`wTwoTRXYe?$-fp*;+xkNkEz*uXDCRn%^Z#O7qbjEUiO#rCHCq zb?t^6xg^txi)Rr`M1Xd#^%%$PwGbH~iu^1hP-ly_T;o)00d$qthx9$UK#p~V!C3cs zN$)$&JT*R|>}2P%c5`jrPkvg?*FjQ|mD zQov@ksUL3`;_lXZ z1ajI1DzP`Y6}}^b?F~prW+bGEc^uHed|Gziark0y-n?n%inTl#2$q1bn2(!+@i4RW z@~3Zq1L8&l4B~cSt(-d6$y(;+c6LBgz09w)9$|S9(w_c;+to-8a)jkBvVEz+F)z|L ze$@BLLCMb)0+YCt9`^3t!lBfDmce)Xy-jgF%o2M>j7W?F;-Zt7X0r5{t*J=c*+= zFB-8r^Bp8E=l!YfdV&%Srgm)3tmxJM; zp?F;Djt71V#WJ?&V)wd*k0!Q-CZ?FTrc(1+>S-YH9uE4 z*uC5|Nw$@D9l;3B_o#yPPW~KU-ie<$2VjP-Pas+2IVt?m!^XZpnT5oc!;8wwYOkSq z`B>lRV8v)ks;ft)ThCHPx^sxxBBnQgK0$S{Is^8%lJn!raGJQ$jm6PY16GHopCLZJ zQw^9J(HGLvnfVYMmvuYX;bBaH{u52j`Sg!6&;!(xlA>aM$?_9pWBW)W&$%;K?%n-t zFV%RYN5L@ZvY+Y(6m(|n(*E)Uga;YCRMDirKCl0xBKXfJD%l0k$0ClYI|SpJ59kMH ztE^VQSjqXDlOJ_vR>fxFn%&rkd9}DWfx)TW>2v4KeF(2GH5=_sa^BUm=!#|zGn@PJ zdhB*IxNk<#8_&r=pOG?^KB*s~?>YO|ae`l4kbj_a zuIDsrioU+$yj6}K4b5rE$_e}4L+-EoDoFH;r_ot7ykR1@sLKnH7KyiTMR|F7&tP}q!&+`(;U~$-$&-FlmX`TjS44;LtJdmg z+FC+bE*uqS)zqlvRe?;8G08RkkWjK(#OtZVg~dh0{L$aRVE@JGw-6AW^OI0JXpWC} zx3aRMbtU^6;->Z8pl+M4{=g>vQYK^|KAv*$l8LEl4DbbXqh%jn^#MGXC}l~UmB@TJ zj2Yx}sAXyIExoWnbWo_@jY+^Cs9UU3>`ac1`uCO=>7*GL7(mOsTwLHTt`Rjgqy3r~ z>(+{j3f=isxB{_miggfP-^fT|dtk;1`=6iQ|MIJ_Hb`DcFDyG^s^6-rsg*h1g*$FZFFv>v5nZIs=2)AlWM^mZeH+?qnphwx@)+3tjF^3l zjg95KKX|`tLY&LFnWy#UpS+@jPlw$6`W?=r;4lW`~+VV+uIGu#v<93y(<+00;HQh7{d%|++ zo)*Q@kGWhd^IgxA#C@c_?dlm^ANk6k_*~GdS1%SiuTn>)-p$I&s(JJB?76ko)tua# z2i@J>IfaF*Z-gi)DOql5K2X%sinY9?D(6{JQu5gJi=}tV*&)$@coo+7!vp%*JFO^2 z-Ze{W>-hHE2W;GB+iZrjd%^zhWVC|4#S-$*O-RN7dPrZeE2ZCxTz_F zkGvno+a3}1;lrhZ!qQ!mOKR!jN+BtG($Q=zk918eB)2>l;C_zNH;R7gWS=fyxWq(< zEt~t$8g#t(m9f*KbuUA5?=qm6n>5UM*E;l!mbe=PRJbSOow8h%1!rFHIZQY8Pq`n@ zw0ecA@ZP9eTB1A{H}V)a`PNKKd6ly6^p}PP(Ji?kbAw~gA61WXz2{rhX1e+8CbW>- zN~k6mxLQ(-Li`hXK>?C8M~2eSmLaMrRsEXE9c0)LK$V8`K^F}yQ^Cj4BVZ+ zl|=t}ixDm$A_$UQXo61NNP7`WKiwA4HnEc4fsH2j%ZRa-4OmDlU?{Q0XF zs;QQ8LDNDd8JKtP-c>~oN8K`;!Q-M661MihV98ikwn?SCdzCjbgkt{3^Nm~IgD(i# zPuLO!s!k_F&prFf%yXkH54zRw8D&l32?X8=INzih9^CS>v;Kx?WLz_kqWY^?L=0>CcuI2&CgdvWUR&dP3zh87srT)iG&jt^! z=99oua!|#}#47D7v-n-Ze&P9xT|H1fna;;P);P?~TUH=`hj@4Cp`G^I`|Y=te0MCBuJb(ja?8|g z(+l1?d_dK6Apjdl@3PaqKb>@zu;YD?qZndhVqv!J3QDi+!J(cs>81Ae_KA5>K^9Sc zXF>Zb^vJ2l55hl?P^dI)ITn?aKxA5TmssAOi!7g$Em_XXU`&aTFwhBTYWEN6rQX6# zPEOihAhXOE=f5eUJhAsoizGN;{v@f^bjQFPeP0>It2l=NiL9LLL|_XATC_ zVf;avuwJbYsaCu7=}WUubAr#1ox@av>Vuq|{ISSwRG20ag<$96! zB$vnnslu86IY=kG#3U_zN?eV;?lYjK-M)3}tE7FyN|6x@U~6Sgdd%P?aZvm`qw`lx z`f4yT)vspxYvW?lrH}pN^&pRk10=~j*X-h&kld`p)%44)<0*RL-3~ql&jhmOqrQ?$ z#iAtL(s8Ekr0^N0jSagimo8cA!ff%1SBAa{8Kz}2lP_0diEGVz(qlQw>SP~lDkRs3 zBuU|e$3<*xY|IWIJ>;#7qJ^5GRH>qF@1`!2k?{m*B{QW6Ik=N41w__Uln4He+g|-S zN!dOY*55oa{mEZ#?Y%_+M~amH_brLg5c%%Z*Sh0B=PvU&V@mdV_yYFH#}it+w)SHR z_<&$ZUb4PXzPd{GDrc)cIzn71DK(yaG%USAtpSlav$#LT?eVo$V5#;;Ar z#|$`BzV=+w4{oz654QM8nx@j1a6NhSXl}g$s{h^5uw9shZ{$2CIfm7&Yr<_Ur=?|@ z9+pAc%*raB|NRQ+M1JV#4MSyDn^iIdN7N*~XlaO#69&7vxm8Xzh5VLuB>a=$G^;;J z@zZ@ozhb8QX=BLnUpW8poL9ZkpqC;UW~`!NpRPFCfxG2ym@CuXdhNnQvhR(`0wu%#|F z?}6Q|ac{+SG(b?ba+p1DthZG;Dt%^>11gRRKU!N1=$a2WOetXBpPh4P{uxXx)+$ch zC!}QsbBLa(__{}a87DD1<1;J%&$aBoB8S&S3A(J3>^;kmo@N~#B*5Kh8HO9Jt@Gpd zOb%j>FaZv+4zBQlYOfp0asCk1D#NCH>od6)}Y0#^#6LckpcOQErwMj1?LZJ_g$cUQx!0y zPq2clFYme^HWuyCT)DDNw~BJHw0v`4US6rZfDWFJ+cGydw=~%)X=1`53^+Tdz#%a;8&#vU#x6!y1+*TOOGHsBJTVi z=!9Z4`q9T{;yClP{?jr6@F5dQ7z`#KP%#aZ7zrkIxU_lnN2!;<;1!iR3fMxNRLBM{LLiWx!;}g0IxKFRn zp2Jn+8Bju0e!tY7OfNro-JLz6|5R7E|Dr{8^WQO(gja~r5P48Mc5Jjfek}g1)bwY) z@K#Eq@AnONk2StMFAs80AI~EqC>_g7m-)SndvmQsR{3S|p<505RRmdx-k}bm{5SrwX6*tE=?{{rAOVQ7(+yj*}? z@ABZTOSJc0R*O$s@Zo*hrCRl%XWstStBMAGgVfns4@nI-@5QHuaR{68+X{2t`g)c) zP10}l3n|vC@8iv5Z8|rVY?4aV#CvTaL#{Jc`c}@+bOKP^A%ZWqIukl)jvPp0;RPK8aFX@oJ;1a>W$xbnqP+o?s;SQN`Vw= z_s}>=Xpi5<#9mUa0YT)!YNopG+|p8W;Imto5jFC3&dQ~-nR^c6xBc{Oz52{6Qc1#J zyo`kl2|8By3@oDf%su@(R`>e`MZTauP+3~p#jH*|cpE{-h02YLj~{iXWhmatdNmuv zT^7ST`H0*xSPO%>XY`f!iJ3jBe6s0 zcVarwDUcXTwwSv&Fva~z?x3X!o^Fqyb!uP9LzG791l-$NjLcc1< z==YS5lr-lNbyhL;p=1j5MI-CxDj7?AxZErW(w;L(OojTc_WCQ1?cpnuf$|pNA%p^* zY|kZaSMx+hnA0pQ-t4tdULW{ujLf|uL>gm#3DOc1Ls^u_ZqM0- z!`8ZdFr%VWk3*=RpVFYl-esn>zk#@cC9Sd1X9(`l;DsAaT1`vir0db3m>sS@C{iql zll-dkx}B3nqp0t=XmRqb2!mtxWqQ-`KN>QyBNCkp?Y9}pi)-206s))YdAI(sKWEdc zELkJ-z~}W$4Ub2w_;YshY0m?K`nk&b-MNdyQqD(<^&^$urf?IoG#>F=j>Sa4O@kAK z?5m!M6$qI2g-Dp3)b*1`mp>osj%zzK`xVE3Zu)Kc{f`xz<9QO5WDJ{6?EwO=ILD>V zTz`t99D8K9r>t_Ms4M$bj5=niYq{5!w)XFs;q_^fR1~ z43Fbz^)I9KpJ=rxvc>P4i9D&&Y**D|;!`tWuXt3{__@ zpVYjIi4oUCPsBuW#D^E^d1Kc0xnB&Cxj5`=to~b#AUpkWdv%#CfRMNv=#`=9{HDr& z>O+k8o(Pt_Nl08KIVC74fMK^?DYjTo zr^igJqNo)VS;52ev8^gF*~(Lcvnu*ob-~B$SFYRzE&*|g%a)B1Boyt4hwuA8qW$Ll zq`bNHdDMg4Dk^w#e>^KJsLao5Z>a@s2K)&J-)4VL0*&U4$0oJG!9+?*)XxI!S;HAw z8Osj7HGh_dXJ<)m`3$`aG+Q*weV4E3E87(QP#FRrd6}uRkJdGNEEh6?o*b&=wc^=| zdP2cY?@%bcdaP#kS<49RPtyyXX0?Hj|HSg;s+t(&{m7dZ`(tCCU7!G|sXNWNyAcfD zSVB~fVUghr_H(NNM`io>bn)nQ1&Qg6d2YX(A}osg2VoXH!GlW)TBx3mh*wv)v_`3E+z zoF^SyumhsjlXWMl`|;>L`%ZhWE&)b}+tI)j?Qv?Hf10& zgf#L2s?zKrC_K3ti0T@Y-%(BJuUDuLBb~P+x-JTMurB z=@1O(USmAb>D6KweWr#U8(;>KW+cW!NlCw+pz$F;l2sBN=i60lmS3J-t4hxFfL+Sa zG%G7EHmtEnEHT(yT9#9S8KD93o;0@*|I<7c>}dB?l+9*ETY3RX=el~ykG!@Aq-NHZ z10)lquBtTz*-7dM;52og!%I|kDkUY^ER}?y5E4Ry+Cim#m4r@|otDkJmhv!TV`GDt z$K}<=(94{&=c-pEylO{BJJ%X?({`iO;RnvXOX{j6xN-o86V z#na9}ei~=`6=1@UR#(;(2fBy+k?g1qC3$)IP42l4Q+mU{@rbF}2i~Koy1+kjw)z^C zYj8|ecVBF3ttKKoTB#HPj0~V zGPW#+5WATe*tr!n=Y^GGYw|uZ945WiJy`J!}Zcfi5hW7m0{7N%Jx&RMMYYnCKmP4RT40mMU zkm4cVY)B#Q`Xy)@xiwa=NkgkJ&b8%erd5?kV7&KL?_Wv%~Q_EoD~x}@etU*nHt zck4HfsCZ6#6XA@+r(ZmxmeL8=MX<#(U+U`WzK-lSoTZY8+1oV9`ig}B#6w8?4i)r_A z&N4QeaBsQT*bp0&Xd*8NT_XmF_{QPAJ9jjq$y!6adk=Av9|Ts!)#5rbXPFMuM-Rk= zNPk#|z1a@d5M%T(hVII(KOF$vzjLfh7vQ_oz#D2iogTju$Vipa`_BJ4`k#*DyHdac=C z(GvddS($`e?d_b1gv9+Q!EgM6Yy5+uHLLG}Dt~^ZI@uExl~Hq;l$Ve1Yo$sUKdOXF z9vO*MDnc4IYdP##xaL|1?e)PiyYZgFiFvQN+;!?TEf0ee`Vc1 zJ+QRvP+%y7(NAh`nEz_wme|%_VZ@&9_li6(;Xmx+9al-f%Z4#_+5FA%?!QdQPB!hM zb97@VHa^W?ee~a#S7hmggv!6u?1LT-N15h%ZZ#lyezLhAexBag)k9j@*$H0jlfBLE z9xPUh3SqSz6A;gz=fA$cJY^E!yMpXohj1rc{>0KSGVpOeda;*TXae7{Rjy~G(SXZp3>dt&`)8*Y*m5!(2%|Lp)ne|yFJjWO+1Kuf zeQ?-PmGhNp%Nm;aK~fiY4XLEx?n*7R=n9av*ooE0VqAW*HKatVYELy3zC)BC&=ZWp zUB#(Q@UZwemho=v3r(4O?1Ei70>c{5kny|?NcI<@69{IHuk45S-m)kuL8J97{B?LO zQcaP=?C+?Iz4Ds26OLI$Yf5v!=-dcbMQeQlVd_)lVQ;84FfwVkrnDxC?qxOJs;+yY z9;ZOBYhbWOMt<=kzsd9YNBXV)atVb_&1sN^+;$zg;mmBE*)_X2y^X6+ zU)+CDn-KQqoJZtHqK!yN8_{inb?RrYT&AisGc&)I>3Q6Wuy560KI|Yt9bAywt(s2P z;dAeOvbP)^p##-B+#}AJs9^HhVa!n__aR$gbe9a>sex0xy^R$2;^tm#=@>qoIMA z+=CP|a)pwC6Sib>cWnk2D&e+)=&^fw@7_Jd(ni$f@hDWyiqvgQHG>7UlEAic3%~x& zjF#n|U9X;GxXR12NqUG>Kn&?6Yi5XY>G50AgL>k=2abw~zE~`yj|Zm7?v*`_hHuVn z>tb30Lu?s-TS?4+VXR=&E9cN7<c4J!Lc1Jj-3o+FK0s9(;`og=ADLxj>k6=w%cv7)XtCv%|};?SPTD z{MR5Ff7toRtWXnL3tJ>|Ip4BY`KzN^Q9(iVX;}#qaDcDNTwp#D>8hOk()m(S0A8X3 z;FS=mo?3G3;?ziyU9(hP;j_2{m7PFB;>J`eTG(Y@jHK`ffrF+8#$`c!M;)dJ#raG4@xc0PSe*k%%!@N zMLSaLpy1Iei(jC=n1o%8O{~yj36=jApGwd>aARt2KDUA7Ie#P@YqbaqPZI8kX#NrJ zvrysFQutP6Z}LsPXZ`Fuhx$Xmsj+&O1Qosv%y2F=MbJU;#+QihSN*;;*koR>=z{CZ z1tEE+Ro!VYva5`aguyhMh#J2}(p`~`H00dZP-T!{4MCU=4A2i-nZ_2xC&n)oJk1Yv zM=l;{rL6Hc!5c3xp^9O7USCpZ$Pgc5Zjt6b{~?|1k~Ez8U*k?N?ybl`eo_(-`x`fIY+^(iX_u4H zNNFi63A|yTgHW zH~ISI6469!_G5skcTXFwEtVh|?`2g9GIp-+Jonn1JB~d8?RG6xl3B6a_8748X=uD5 zWe4~mB{B)BMJA+uUe2!W(Tl_rKxYsiyfEn87Uwa6LZ{lT>D1-CTpG~dk=)mUZPp2zah8`7Y5IWFsDf7&F@zcRnfPh4~x1|9{lIcUaTex<9I*U_%rHlxhQ%s`Oq2 zM5#)ZF1>@&A%r4LK>;b!n+Qnn9YRw&gx*3CDWN4n0t6DuU7oY|o;`E+K70Jl?>^7H z|78XrNV3*{EK$o0Gc^{ss8=d17+iY-8airfHu+rb@z1~Sy}TxY zxrXs1smne%@Fvi%+KRTEl5T5l`_$0zAhFl0^;WAnm|3CWeJugZvvG`@ZqCkOu0v_> zek`;ntbJf^&&XAja&4wD0Ro-BjZ6F~x$dnH1v;0NFC{zoS2ZnqtZO7jeM3~+S2t~1 zdA@*-BpPv4u2v7O_@=MRC`MZ*Y%SoGZ%%DX+T^IFIrzLxU~_Cb4EHW`D?}lGVJQ9I ze?Glj3P37lgBquk9oSB%I(Knqi##vd=$W$%K}{Z&u4Fn7JO2b`MBNE9bM=sbgLOyA zI4t+E`&kCYd0q<)n%;BijZr5JY$tM+94qYBsZjwr9+m8BRcvw~Sf+R;oTV^a=6&dH z!=`AKlIyZkG4_GQ6Ynp&SSQL<+LC9c-nDtq)3i!o1&_^=qi|rmY~%X%r~+7jcLx4g zm%&>rbrJsQY^sx^7RhHT zTpegXZP0gXm=@B1&X6HHx0ZNNsK`#a=GBk{Q=uJ8GClXoszzBdEuS2trLJyjm@^MO zx9*uXmu7#sQ39K*pv~0v5ai@5vvp7H<-;Myt2zStQbI#Bm6&Cq9)e^RHY-C$be)N` z%jjeSM$8 zGLKY1a1@blVhVlN`0#mpj?-+03~Y2~tcgH=murHB#&<^f|dk< zZm=Z7J)Z-?3P1W$?=+}6_kN|u3?mbjg#AzeZqNNK9AouiO7SXhO%elH(h_>YIB!n# zQSkmjw)>JKMxC$Z(@E~az2)N3al!Zpm)Gr>tG)!DsuoGbJrmbv-$Egp=IRj+?%4b4 z?#aO$d>Y!ix-7akp5%8hG%g?W7R~g{%~+b$(>0kQRqAI)M&3(E%T*Q&q-FH=&CKI` z*4Ea{GS6PNS$T5a%|meK4#zY*BrLR{e#V~FFW#DMK~8or*kIHbfNWi<;yvLQZ^;x~ z%44X}tz(OjPnmObV!F_mI~}%mpVb0ly_j}(b{zP-{ftlgj4u|n%$36g^_0!qm9WAV z7+i6GN?aT_85x;_-vEwLtenhDnO16~BwI^lW20c)eHN3#nd~YAKSK8iBcDJ*a(Nvh z_TN^44n>-C!bES}xc^1!Z&y=rNpp!@Slq<%NDt6(8CSoz>Tcm~+@`=XjlD$EZX=Ym zio9h218y32WOqB>=sFyHkx{9s=+vU7blB#1#bugiwQ7uqqYy67HsfDxlmMOPeNkYL z^y9f9i|s$5;lFQuR&J*o-yqATFaF?E^Bqxoq(8mL?oeG_v6DauUn?YwNc+ZXWcMIX zAD>l!TVfKs-?p6naI(rF$j%5CZ1oj}*M%dGZz?Up7N1fwLk~E9qPTm#gPyD+5^~?8 z#jsQ~4wS6-R>W2EDQ35ZeJ6@IZ4INx(~gWVZRp*5>d$W6@O&E>|0C;beO7JhtoYb) zaFJm9w4R=AdCbNfC(8b)&HLSwC?xSABV?LVr~w^jw86s#46R8rfI*P`vnMik@7}%p z9+*&#^s3JMM6>$aVN51 zBpJ(fD3-$PVmTwDp3EPKtI&0Pj19YqaCdL#Af2{ERf+f2b3Vw=2c(}#E~WWc)FwlBb$Dvex zr@?7Szw}F*iSiq~)tSwvEd}lxeSu8D>b+blWArw)HYf`=@5zl>Nm6;y`>M zov!D=T}WZmGvHKgTLy(VM7S5uc~by}B|x>^l0T4fb93YNnVTU*1jX93x2ZAQALPQg z%H|qAj*r+%9#ymAx5?OMv1PxaUie9`W386zC*^*G4%Xy(x%}YTeKti3HJ;l7_eW2i zYwJT&BkW_#61$1ZSl#jtW2C-2__Axu(Y^8d66x+EfULcF9UfQllK1w# z0qkHuzK-qgbS?U|GtUWsb-DD9ABTcjD|yUiWdk|lB~@)~Y?SVl8NO1MYd?jZG$~~K ztjun>R%$v!vXB_QBhLF<0Auq2rBKGn2V_`lekpr=9~Cgqau@d4d>#?{(+0=m$IZt) zvF9809$V)&0FQ9-)UbcdIU^mx06yStZ{E4t$CYScZGf(zskQHk9FYWgM0p<7O7 z^8JX8??b(Bnj>ijx?io2IYt69_nYug9_d!VN4t2Rij+?2Ym2U)Ysts5t(sZnbN%6$ zr@xu}_GSVMzXhH;LrFog2uen`mw-tb15)Jb2uA|10$$lIN&C)ATQ)EbuLb%-w~L2N z*l69xD=6kWY{xpMhgHgDTsR@;&dIg~{Z?%Jy|P1KM@kLi|=e5 zX6cNR5LFFT%v*vqd3JFTkkv*UrVrb@($e%e9gAjfEIX1LB&I^(?h?0#S;M!5tK51F z_xp>DwpV{2G)IMaE^6Pe7x38x*mna4O_l* zb+l~hz0V`Uui|sy7`7;HWMuXrnuT*BSD8`pL0Qh*sBh^~X&ndGOwGFYE~h*!5BsIb zdvn*VfvIPAHD)Po)2aWk(*Dsh-)cxf8d}QDmX;Q+pXlMa^SE6Fo;#m$UnwA>pr%#= ztm%y%SLM2@jA@6%)=8(q1RXW?@e!2$N-1{Pvb(xSvbrbUdHHQMl)I^1^TYH(Bvfo? zO9bIQe@(}refYO=&7Ya6HFw<}(AV=;2sy(ql}qwJ-mU!4cWc>~yt-Bh{O4vQh8{kC z-1R=Vrh~xahSF7)Qc>MCOcI^EG&v9&kS6Y1S5eIrl_t5TTb+OORi-#8Gy>(+RjY2+ zYBiS^ZrR&kf^7UTcrn+}t}23xX?Y431g0E8Gb+7*?kV`kLlIoloUu!7oyQe3h96RA zd-SKfmVp@836+!0e>Ve0|Iit_zx6Wo2a$tvaM;Fl^(_t50P{08=>mTTOj&Z%bJ<2DB7hHO|I=}pO==_OStQzT+ zrE1&K>xSRzxP3|q0rY?H!Mw^x)KN)f{J^^bvf$~t|M5oaY!y(jcsh^j$kK@rA#p ze4{7!PdB}1S*`w1UcSB~FF$Vv1+yzKs8-p+Vwrh))uW=tHE|aiIo>mfx;^rglQYV8 z>d?~EtVPV)x`>7}Kh!aAlnoA1@V2Xo(HiTjD}*fNOL{5vC#BtGfipXLR{)|ucJKC= zK!oU!vq<&su1vPBG1BldD(A)ko&Ot3)@~p@-Wb5bMO8mY62Rv%{i~nH+<$@g`r9|F zXD(@J;<%+g*$iSjUF5L?N8?a7C<JH2SM$w&Dx4d_pFYa8|hberMiEWWaE4I+d@lQP9|Nfs`u<>8e^)KP3 zpgkvw?d3HC>~BEG-dWOcw^LSrw}_s&@sm>tba*v~tPAa|db{1MNx1Ih+|0!mCL`u! zY^Q>!^NvcIo;ajmXdj-$75Td4Qr9F*Rib-x!TCTIW#xw%b38~Q+wv?n1_ID61OXkl z=8&h!rR(xuA3Cb}u|+0_LB!<|8$0`wKdBG5Uhn?V)WJS>iQVW(Ee;o})l-iY#3@Ki z`!@~Sr3IfpUeG({`Weatjj$QEMb9-E`RwG^FFk*ggU=Lyaqc*?*k8ix-+$acb4I-Q zek$D7ucR(d;pVNI8#C?kHi0vz-|~IhsfGJ!J$$D?7x|Wi9BF3R^Lp`eHtp1~z{-+i zYiH-9vu8H1n2zvY9NmqcYr5&X_aipy1N|+@{dSWC9-l_Bl3w+b=xlbCIBr9cj#=H8 z)|Ceb7E-3YxBIO$MX{cH>TShfUES@}zTh*YjwZ zp}Ge=px*fHMBR6raZLw~`(wV-lPJOz^0uTXp0NSQTnD>f|MW7yO`Bw`o}-}9edg{R zj+mdVLNFr@x2_rd2-$SR4CW#dysNb$A^kh}P%5;pnwr|;MtOf(tU#P3p+G53qA$>y zRblJ3nYEo&3=|j~C*^BBc6J6yGB7_%Qg(*LDR>&B_fxkpYCu znI+5hvYrnLk6ygamD&rxLc>(z>;`Mv+SlDH$0N%<`&Z-Og>gbX+ch_S#h141M@5#2dNvp#wxUb?O=UPKYk*EB%IGG zi?cn@zs{DD;q z)1Z=|ln1!hcdHB`zV*|%Jp`!3`0hA3C!furY=8(*^e z`tq&CU4%pI+%!SYz42#_3ZOa<0%0z#YLvuvLnqs2DOuRrF7iqK1uxPwm1-G4R~;Hd zesF{H$vaQS#}NMk)j?4Cw=dv#n7&$BGQOl;a+w zx)#eq^hZc(#>x603mu5}KQ*&=qeY3LXjw+68L3%J+uxQVZrr$GKomHYa}q#}Z(SeM z0bBs>iA;qQC0o2rroPO01AsFE$+%6BiDz%0;@o*Oy|VNDyNS77`?JRiN=^|$K|x6` zOhc6tcyx{qkWDp*b?=3545J;VE4o;vR;K5A6L^^+yLM^L+sGFntaob~3-1A}Ht+CN z+MnT1i>jtbjp48#wzBw`chY>-%R)TYT@@M47 zk2m*^$bD9{N)e5ss#=Y;3g}wocopp&&zd@Wjp#J>$f^W7%S%6UM`58l(O{pP2(4v@cBGL!X6eD zru@0Ms!82dShX!cGka+HL!sWM+|tKc^1NcV5%Z3}a z7e&YD(mpyqjtKQkNT8HuKV*0ZG-fs!BjPam`dEf4dq1lc=RZ3D-)V0v5uKzGnpxQ9|wer9hO*vl>YW6I$oARAJL<2((QOUS9yBz1`O zamN?cs*+HDvCVqI5=e`V0wi9$9Vy&%ays4na3l7+oTW2(pL5%F*-fR5t38wPrfgV- z!_9O<0?4$ZW))j0FgNXgSf48m(rLy)`t;Ip;f-eTY}$;fiL0tX57JJGZ-AuwQJ^}M z886gZPn|ai;u990+(n`%=BXIBFN~h}9vaI&gPr)dHQ%55f&WENLsi+%!Jqif!;^|#qocXC3uAV;N2l+54*FDvIMTxy{GEU{f zs!1?%ZoRq@3BD)Q4u@Wz^ZPVr9sMb@aa)sPYtSQIaQ!x+eQ;Ft7ngS+uNYN0I5;+( zD$4SPQ0FgR{E(f^0%#Akv}^T&7b~0aFDeKA-?ynaQhc``E}RFM^>5KBx7bf13}gTl zDQO+|%vZ#fD`nWIr#}_t1}W}CiG3B&)t$=9sx0hTkmbEVH}*rz;HRC%x}{`nMFo%q zObiX(1e#GbEv;FUZ6Pjw6z9;ikhBHt9tau|BYBXY&}@$6;T!#}Rn@c=L~K>nu4xrX z<=XTVpv1cGZwLyyi_4Q%R952OuA@{;O-+k~f$FesAy2td+E>}g{V!GzE>uDy%tv+o z2z&T)pWb2tFZ@a&#|OY(TV2ikRPw?x?gzUT?8dE!pC;}bT-?o&AvPC@Yx)owc^x<) zR)z3sWZ7mi7@Z_L={8P5Njc;94c#zvWkEV64zF-}2E?IVL1s&c%u9@EGg8_S@%F9N^+3`a~9Og5IaV{4#G zr6Jqa)TUI~^}YAM`PGIT>)oW1MVc48!wLy?| za6L!?{GG*xubXE?Zz`&(EK59hSQ2nA%7#uUVfK~GOc}9y2{8n zYyRf`{Za#bu$MfjH5-FK=2@-38V0gebaQP6F7TIX!<@S2m*kh&_JGRcF{GaLOe_eL zKtk8_n~G&>b0w+{CM}mh+?-DYZX_4E0&3}C>P;ED3Q_a7(S^>1!mcvjL6@@5I>~bw(mhSkdPjs52kN2;oE$M}7NGLx?glPPhm&dX*!-{r!lyJP-tKMg5A8!zbV<1ub@g4V*@%ahF^4@n}9uS|TTxSX4K!XzCC# z3V{BlOPVCV`Rv(CZ0%szDcI=dDfRYN2+$<^rdpMh02+H(EXhg1gctL>)A|4)yIiOe zcZrD=z`t&Ga&omXvD%M3J!{3Az`fE#uM(mqrtdN+*IxuWeB<%&ZY3}EPXqrOW)L~D z`*K*>2<2p7c-wOhiX&`G5+Zy%$;Kghl&Q|UlOM@3A1oid7O_O`0*@__r545vDAxaM zknmVfFKNy+AmgV&0-vTRygZ|puBfR+18c`Sx<`Z>0)oujw?EmMx3`p_kBX#yk3i|G z_OsZ3tju7Yea)gZz}T&BYD!n#+GT$pCnN8wfYb&4h^0v_EjM_KTTi7$I{8KjJ7a&7 zEBu*B^&e*N&Ch9sj88rY@&rta+js8|o;tZ;cXe$ozbJ-^eQxvYz6Y_*J>cmyAkjd8 z)Qg*Uo7IAJpJ^L&Dk>^BvPSV^Ge&JK9oD;V2BR*~aqWEVlc=VpxAj!dV)E!-@u{C z!2>>MpxGmXI?6Y|E4jqD{ml9xcEKv~e@#!n4uo@Rx+WkF`ke?U9(k-USgkZQ(ItAqu&3G zvqI$w4NUOArgvp%iry4m)3Z1TM{H{Q8aKoy=bB+NcE0Nl~CY&t6k$>3|v?kU*f_1 z##yCKLsPTs=#XH4Y4bHJ17kYU#;UZaNCnv3;9VRXi(eLWtctJ5%*lh%e+S-))$C~d zY36Mffw5U5NM9pg1WW=&7y%quJucfiF)MVS=4a!Al1p_I|ZH8W@j=&}UH;eWun zSJj9*@HoUZUW;cDl)C;;w9`+*SU-s0Rpf5`qIGiww#u?1X!7L@aJ@hr772XDS`ELL ztUV8Z4`pQt=U1i-U3lyB306r7bhI<*Bx zO@>wRkh%Sz58?Eo?OR$n>q-f91uxN9pGP}>! zju`CZvs|$%^Y=ltPakf*&K2`mSUSHL+_O9t!n+wMyvhwP{4h#)KA9`^vXBE22>AN| z>;n~^KaccGb!6Hh+W~Yst9@JkD1IOEV?G9Cd@#oX7zfY-ui19V5`W3 z_|>s`!lxOZl~wXjxwV>HJXEJb64YL?OZ3J{iW8nj;0Sw)X^+CsQurKwpX4!i`S|)Q z9s16l_c?}!j(FUT2N&sOAuS?c;I;Kl#y1A;z0C8NA;E^5l93v1SX7$(G}RaFd3U;g z9A)}f3sDkfzX8wteBa`lEF;V;;ed_6^7zGz67f&|%|FS9C5DpK6&0O;J8T9jvZ^8D zVpaO@#Crcq0O^m%dwG6;V@`49%Iz*-sbIxOd!37oje_jz(7!5y|NQ?;&u>mYe`$@P zr@MP`XIy(C)E``R?{#=B6@bwpAa>Exj{2qx0ZdwpST_940{H-}!#NzLbI=n90| z*V`tB&rw5u_RyxbR|jU`E9oD~DdksU z;w$l0L_{+B#KG6L@y-&Tr*8Ehl-KGIIP|oB9?4#hqYs}xiHj|>uLl$a89;K*nCb>8 zj=L$(*eAa+Oi5I%ADb`pMBc~=bN{vXQ)K0zck(2~v%*AR7jHOt5ztkBcd|=;rQXb+ z2cTYEw_*Tl_ROweJNL{eS*E*UC` zPNBE@GQ;nF*MBUi^!alniaGb7d9~(DK?@TZMhhYzQMC z$=6mDaYx?#n_B+e6344cL*LZJgp|6f=9&O_HON>Eyt3i&N6&a!En6Ds>aQvA`^!@T~?3XW` z)zciB#ChGzeM#nr!DP($&2ryeHupKPYT|RH>;AEzajPt4bFV8~B?HgB-|vP0w0`Dy z2Fm5huiZ;;YQzm-8A0%Ac-NRcV&nZbdPWhygIZ@C1wnx3t7YbBF-ovA{BAv|ceAfu z*ewMW(W1*^AdBa7oK ziAATJFlOF=bW4w~#Inp==2hXn8pzyz>&c4L1N4u}QL!nUM@K$U9`|&Hc>5Fmj9-ym z5S^zChMcFF!Bi4Jx#*$q z+&8-G(X?+-mMj6Slf7{D*9rswEo!UThFUUmhnYG}3vWqvfCFI})Mi9nKdEA7%UZ5n zcxLq~K-+*{7QeI34kW8lHI7EGomHt5{_J{E#9g$EVlFF@9M4JyOsvwhxJPka(K-5) zQpw_fTb`I&KTD4%Ocv?nVguR3+7Dhwo-j`ET~e?NncuOQ@~{5NRE0U-xAH$g)ea7^ zRt2Suue~}xdI3wB^>Sdaq(fLAB~&}*d`jzzS$?-JWH40r*g&+f$Lh=#IVpCdi%bGa z4{U?F-(H);#>9V2FZ~6J{p(LI5_vU^N45mq=Hyg{r3v?d_esvq!=`p{aA5W$D$C8F zQLw-|I-AF=^ZEYDf~wt8Ym~%6scHKoFo85fR0$<)p{kk1#TGQB{6WfYPa|%c69f>% zScBAKkg&r3?WtixhzsD8K3dl*425nvBm1hQO=s@)C`B+hC&^aXvrGD*b=*0~Zu;)- z)BN|Y`>$Or^;nTIZQ$=-0Ovh|Nh6pf9rDQl-J#7*&`2r%4%h%`Tv3zU`u@TLK zi+uG9b(PMF(BjFcJLHX<2@QlLCr?cN($=^0X;a++bs*THc3lYJ!T8e6EgV@OYVf3~ zo`~{HGUe;HZ(}Yph&(EPZQ-<|sXhO@)ZG7cKt7Q^4Gt5Iowd<&s#YEzo^xW|0)=`A z1(UmX@47J#;pjR;xy5SIY>M8rXLy}7bi>FM=#L)0A>3{$l=mfAI6x;NNjp6Usg zs3NH;stK>f2Lg#Z>^<&bo{DOob!TQ-GWa4!6vq)6<8lOX-Fx96*Q>}FbMo&e;h!q7 z6f~sr8+XC)=%W%1VW_KVIHznv7T-sQ%Eshni*X>D{CN@Vmc#bLNRH7zIhx7BS zw5WZrQ3w0}?Gu5RNU^p65UkBB-NJ1rx6>f%^dQjGp?;L?vd_`iQ*>Ot_2?xL_$@lF zaaAt(OV!l+!QE+*f^ss7Wu1-whkON`rt&VO*k~lRcMHbhNiZeeR2JA*SJ;fI<)zd= z=jul95Q*}X74q4RDdokV-}op1z4>0NC+zj>J7aGBdtfA1)uEdV6+vh%;{YVoT~j?L zaBO@yEoN+EY%ZbM{|9dKzv~zNt2(W*JB{C6<@B1H-4HH6FCGGhFq2EP_%4J6JoT2B zx&z4B!gAGp@@$NIg6N87MM2`YN_QLY9P8WhmxoQFO{(s83+umGAK@LPwM}eJuezds z`k`L6!>qI#LMQlmbKlFeN&^j;xfnFysH** zU1gM!nD@I=<7Z-a5W78PJCKGCapP}E_EJ3#=7Vvi4SzG(6jsXZ=Udz;2Q zZoO9t#KGkptgP};(a`|GH;nDGfEX3V-L*XqAVs@#^QE{}=+1_5AL@b7*5o2@3@!D+ z4`;XnpjgWWgiX1m0h7H*kmAGSwD-tir^B1h(3k;#Z6)lNtOuU_IK@|xHA?)`9t!O% z`fTN7mGz9SO0v*hFsI)i69rzxR?H8`_5exRFq}a$)iLF}TZI4ti^Ii%DDMQmRmIxl zBZ3{(rNY4YsHpGGH5Pq0Kh>X2;7UoK-NXR(O(BPHUu2O1kcdzW-WNl(k&9blv=6rOi7Ad)<|0>j?2(8A#JSFp4l=F3?eNe?QyS*7nKg5MSnrr51f?OgEaRsW??-eT!YY zkj<|0Ty+5ihu)3fhGfs(Ez}x^K}8dI-5p@2sPXDz7u1xT8!9x;v5CMn=Y%Xzw1PmW#Kf+Fm?|)fvs~4Q3MQVzdoA*$EoC{-7PE@G0@wdRM^OBscxdEdvuD7@Mf#5hqUDO?Ic0nMa#K0?KU28E2Kle(t-ItA6DM*d; z(nndt(w#IM-TL6XAFlHA4^2yxP|s$Le{O zTvo?hNXa+FITavosT1Qvpxf_KO{-R={UJ7Gbhs6_+d5FLn#2E1J4c0Kf3(==E_c$U z`6y<^2CdrlBGIL(rjgIj& zp6CwA+LiNk{5A9Vbrd;CIkLBuhQQE!+7y(cQ6=uhN~vOBgBj&7Fgf6gX&M0lVc79e zQeXvyO5u1d>%eP=3oCPTN9jB-hiWxdw=%>lM(Bi#jlGPZ-l}hnjh>vjwzPT-(&0Ede1L@t$z* zJ9j!6Lyn$DN2i$j>L&{N!mM-=+ULp0_$5ca#4{O8JyOQX3_LBL9m-PdJH2FBi>@04 z*RZQjh1L5F!Cz$VNwp9Kkdl|3tUZ0+33H#rst?3j(H-g3yBSF5cY3Q^D`D1Y#mq{` z4ybBOF4I_DdXg0UZd>9$8JUg869Te7DbDdqqb~+THkIPYMb7USijc+2#XrwT4X9#U zhKazlPx4zDsrVXD7K~_9@-FYGN;=n}>KOxONd>(pp5)r2Gn^Bh=f2M@AANbbF#91o zM`cp3&d6YyFULyy$rG}i{7%g3s^x7Eut9QFyVx)qlPYd`os9hA{WR3kVcZ^MkOY<}m(X1HS>$gb1blSIx)}L&>f6D}w(EqGK`G_#+Egb8W{UF(so$}#u zl_i{!Ae*wc=ixj!JF5SjJo?+W=Ra@ypHXXuc%1L_!n^QrxBucd9$>lcDy0poeVbra zW{rl^*0Iv?6ULx&n%PX3X!%LJqH2tE7)Xa*pcQ&l)N~{QdFp`?cc-~}ePr|7`&*Bf zZ*H;!xcD7i7l>c8k_i(zB3^zhpW3m;=yI85JLELfDXPk_(MzE|-VLpVU%YNF&2RGv zyWEGWFl<-`9LY42gNoJDCF-Y39A-q&ZeTO*;sRAG^nuh;>Q( zsfsGZXs)`RZiU?qvNgdMaigCvo2Au_6dAm2AfO=*O)3@k!(;u96E%bEQY*=66191@ zaa@Lo6>^$Fy`=LS#Z4ao4d07%*cpZz(`&@P3Xu7vBg?B})0+D9eXS(D-DF+RiHtZ3 z-v3ly{x%eOLhql);vvv2_DSN7(8{Z%Q9+!6b^Ss(g1e?jt31i^l}v~j+lax7Of_9; zCj2bOha4n@t@o7%@f)aO>=C8Igj6oL{kB9E^Rn?~0LjG)suPryl;Sirwz|X`%87hc zhyug%F_`0Y&0{Yd+8jgIOLu*w+d-E>7ISTcaCWEVpxzx*irXfm6>4rwqzajQ7VMaFCh=-xcGyBiHjQ=91y6f!T&LYl%cxe zM(eurV15Jn)t6(9`m>EpIZn0o{2e;!QfsRh_QxU4D5u7&u8F++Lj)|F5^x9oxvXN}}W{$zp=b4VltML4=a?gzQ zwFpMM>8o(jaSetMrt=l6UvZt6Z$TaCXmxvnNhx<(;+e#$r^3c7hHFR3cXEaB#E)Jk z29xsg@|yKc@qZkp21FMx2P_+p&Eg}1Oek-vR!jrnZJ-JgH+WPr3F4MjGY`2VA-J`r zf^N}Ym}I5^Ke{q~ceL~Pi;UoBiuJDO=`J5!g;bDF9G}={BTl_xdv*}n3$ z7e<^1FJ)lo$QcxKNRu{mxrSX`41Plpo4=m@l$G+K5rQ*x@feuQr@T4sc;q{xo}-qG z?%7Q_7Z`_<*DfPH!Z8`KHi|51RSmyKHXa9#U$lT7#urT2X!~rzi~EAI#y={i?$2P3 zBQYlYUHT18$1S8=-^hKQcR12enx?WicdDo+^F7|yd@_6y_r?aA+$qM~5tC)YfRl0>c^iA4Haoict3lfmIY4MpVweiwf0K zAF0)A=mG!qpn0Euyr_>XEH9dxr36Qwh%pAufZbHZzS{n|8QA+=zR#Ro4iMOhG!lG; z5xcg?I+e(?ljl<|WfaO<5e0w{#By689SGf*Yz;Ww{96Fv6`61O#Z6r8Q$fXf<2QAi z3wZd{1Y_sbIsBbhrixZi%dgJfcwnP(KBmG26E=U|;aTT7T3W@&KT^IJR68`*n`>x% zD#Rn*l9L9Q?j#D@vQ*lPmKm2tCf*n-Hj;F&Jb^X4k>hFjY7DM+Eurx#`fem0 zOOnFogqE^+5b)Y7Ex9Vm5!MBx*^ZzUioFCHSE&cHA5v2t>U%i@vu4F)0&Ql~eW11a zZ2_U9rKY5Wgf$m1>>fuJL8t1zxZT}3VLanZ^?*G7Tr@0k*Vb=92R<$$w~D=R`QkOP ziJ8F7bj6la|M;X*cY$Rh$LWzC&(8IJyx5f15kbcHc_)!=9{XA~o#8;^Lset0 zrWQrEV|_U#tJ>;Oi)?cBv#nU*2fA;R+UaV$A-HRiKvRzh!cYPzKu}p=MH^S zm0Nw|bDg^kqUY%Q$0b~gG` z`n$+@myMpdi95W0@$JN9)3G0iQE4mEGiK(p!9l8tC{inBvBMZNcM%-|Y zb3h_&tad~KX{ftt&m;nwZ?MARK0IM1ZMp~R{alX;O}+Q@4$&G2G7ySwN8L}*xh`R z*lcQhRYGut^l+VcoqOBw@FX!_5pdRe6GY)iAvu)Ycu`~hT9Ls%`?V^Y(QdABn-GvE zP&xc2`?Yi!HRtVKjhiEc6P59PmcyYzo(N!{H&f)uq6)0`FVPD&rRAkOF!&p4$M3B}E{RXo)PQ*;(Luz?pDAJWeXI)|Jz$6r zIh4v!mGj!_Vx4x~Qn4~`!_E56!T8e00BluzRJWcb9e{Tm6~#2jPy=i z#%*0y%-rxAlwGT6a}6#!3`!>JxuTQ;{YnKV46eKW;ox$TfIZu2-jWuHH|CIhab+-b zUtnV*Bj5s^fW~%eDVXNIAt`a|zi ziq%Mb(NNhKfEmCB5`*Eo)%|4b=qV~Alar0t%bTLkr3oXQghNPo=z+4vLRAjFFgkAN zuF|u9*iKg@m?m$#I+2@6cU?*1J>8Rh&3rA`H!E3Lb>PL^cWU(W`(nuP(IL1~GKMFe zmpy`>?ugC@K2_v-;Z;jQN>%KTBH~0qnJSxiVn4jMM<@jD>m6;E!02)!I~U;>&g z5(RP6k(|f>fFb|X@I92Z#H>qgFwYU-Rk!DYf3~tvQTJ5eJzrR?QirBYQ529>43=_b zxnff3ZYwG)W3MrelnOdd-lRPmUY=(4=+vOpFSjT}*;=z*mcVYSJH_3-x!ESq6r1E& z*TsIyLR$m}ud=Vj-Cl!+8q~YI#3-M?a3My|p?2~LbB)SFBS;|vQ)~Y+8w}fkXp$+h zS}J6>R+H6zqti&gS;u)Nk>5>?IPE83GVFEyS%6dRzSTFWLAY{~)|YyRZ7WgKdfN8( za8>=wh4y{4!Ayl`xdAmj{cT!Zm-Z2NvK*Tx0ZVaF=b*y{42HYk)-TsEZ(f>A!^eUs z06%1nq8n&xs!uKVF~(?Ly_NG1Q;wLh6I5qKhv&K=;7eV8Q)``lvnc8~D@%N|D! zpZB#_k$aFb#(HMS-k6N`5j<5K>xS63O!L(JsCjMnG#p1*XFaMeGB|N(ixaqTs|=#<@j3dgye)&e%O4L&enMqY z2?^F|v?D#=_;oOnuLWbOe$8x(RAg?Oj`mNvpkFVk~{j@gqYF@m*z~J?+ zLNInuz`mR8$!cxIeX2PyL%fZTJbINF+(avIA?_M446c19J>A}W$JSMl?ARJn;Ta9Z z?L=thB9h|o^xeGq2_P#DgC`29Y0O#;bPtte6m&D5B&DWN-yM*8J}cmk7CTFRJ`O;7 zosl;J%z;VFEjdhM=osplwH<5X-WG-RM_dH!mL4S*+U_}ogvy~I-W z6~LOOeN-8ti^ig8I!Bm!_2^-5il8b9IqhxWrxU}P<}rP!B1D?gARZtc@hhSrI!|8`C32Lj?(n3QRB6xHi{G+1B! z0+U&G?e$qICP#%K_t?vAA&B!9Gs8eSh9bW#0{#Bwtg!1^4Co)!Y56{LOCHQ;^7Pon zMJ3)eFsbjHC^ePkrhrx&)PK(cV3OM)lh3iJC$14^J_DLelP2F|pN}+xQx&%l6o3G6 zVtZmpCFk*zch^^eg~%67xS(|BZOBD{fyIS0h-iIjgMBg`VzSv5r5L-eq?^Q)mFly0 zcQEI~Y7p#AOr2!W-c%PqD13chyy2(WzqW;^bDRqEwrmVDShj=zQYT$|-tL}pRey?T zLZ~sZ8DW<};2sqP#Z5qsD6{(Uok&$Rz>Sz>TM)O)@Znw3xr>KXLVg{us#x}NZalEJ z9S80n+s`)mb9?xKd&8yR;Nb3`sFD+KXuR5O^v({Z|+F^!6i?ckZ-l%Lu_R>(Y z#6ze;9-pT)9Lcj*iC1Q4>rWJy2evC>+U`syY0#o`ACaxe8`IsdC8)bTaW}D`s1N5Bxo${8kscnueN`YMJ#zWXHS$!^Jw!Us` z+A*)Ec*Np0w2BRRC_Z>?m5?pOE^Ez;d~KCFPvKd2 zI24TOKqUqkx`q4PFTl8Fvl8zqCT@UlLMFM z0`VJ}YvI-|X5Xzqv*x#JQ+v{ZJ^K{rBKOE=< zG++};vzN!E>qQ8Dj zlX#`}bbX}5zG*;n?(p8Yl>JF4HbGNh3+PeQtS!%hzVXc2vC~AlFWQECGPVbR7rt3| z8voGs%<77ACY@$`S#gn;U~pw` z?e{K4Il)H&YTY4rD?ENaH@ig1Y!h#Dvi~45*+Znt!^4`eeyVpL)%zVpcp?b^F(*-J z<9&<-CH9jK>~}7Y2;{o`@GgW;64pFZV{i^ApFUa5JKI{k>|0m)0LCYC&rS2>HD1}PwzGnu@l(E!7*oxpv~PD&_uL-e`FU7IR(C0=A;7Z5YCirJj2L!pfI4Vz zF_G|_ergIY0s{lsCP0F}=ErFfC?W0eL4Zym9rWBp!ll|xPUw4cItp0SyKqcz?|*ut zqL7?}e3z4(OH?0x;Ys8?C4yCCc!P-X&10Ef&ldG!2f*8cnB=y~YKi^5PklS@4XScr z_lXGq-V@`Puos?H0~!nQas@ejlNb2MtJC%UZdx!~zT1P~eY;d^=)iY=zO(gpF-CjDA2b zLIRj(D`>}H&x&cD`;KfAXL6w&%L<^k35w#U6<9 zKLHxampZQSQ5>(d6cs*ri%rYT2W;a?WJP9MpqV5gBNKIgitMUdWyfg>XWTkw0~4b~ z3HsCLoE38kt&B$hPVuIX{{l{R8a}>U02CQXiujZl@U>&NakoJ9VMipvw9Eg6!SnZb9J>Fd$EHuH}LE=5# zYFcO5a7aW?2#KLwPQ&MPo;hRVqjds`g@0P>|9YzY?Je@Q=wRLs&B@@s`}ZFwBqS_( zb)j0v7d;E=jrhm+3-e%m92{^jRxR&NFrl>V+OixpH6MS&=ZUga2~5Cwr!4)XH9_j< zLW(35XL?IqQKid`fV$CBiO6iIsQEp_X(sjUOF(!X-QQwy(6~n_ua0U0OO4^4@sPUe zq=NQA`Lqnk!knE~O$HYcrOmNT!NH*P`p>TeWa+9o!eV0IV_fHxIe)nU@>e+msxC8f zv!_yxtxxIhu&Qk$CIt(@&vFxr z7VwwPX!j|6gfR&F%jPc7u+|ITkx>Q({Fx>Cy#NsoxU*gORJ_gi9~bDq{6OJgFb#*1 z^%dsdH|!th>Yw~6eI7uVMbFYO#Qzj;@As?w%Z2>^>}&ZB-s|jsX5f5hINi)_D`e;Q zko1@kWjS#a@|`IrC7C4pnx5Gj+n<$*N9PF}uLa|ihPVE{Y>!!dE9$<}2*1%zV?GuA zx4gCA&LePW>EFnrBQJ-ebDO?ix0?9OXx{{&?_(G4P+&7^DnaPs}J$^c0K4J5iUTl9C4f{y6&Bpy1+- z3L&39Y^q@jb(%oG4p4T(;m!y_86bJ4;pd-X4&9hiv$L}^M2uPqj%5@4b~*psKbdWO z+36w|OsCBAi6)fym!JZB0CSy5dn^u4$$wZu%?GHmF7l;j2a`37i8^ZQS=5IEfcNgv z4jlV}n|EuB@AU3EvHtoy|9i}fk(k>)35fJMD=QWm_vmMeS?PI3_^Z~#xBKy&X2V)_ zk@irma)TxPZLU0>Sx&V^mtf2BI)&GYidH&+Nn|$A81Ugf>r~%Zq3W1c9QLoD@xR@K z+j+^Tr>b5oy;#$0f!C2eNpE=MqAPz3vvoxn9Q^l((}N9-lJ7LdGC`fxlqmN1_M++? zVL_-rYi#18qwfN<>GIOyVUCG%`&`5lqy*meN8jj|`}r^LjS@Xrkg!+t^IeCS(oJ{R zmqv55o;PU6tUcuUO_k#dRi(&#uDCaswD;qAQ{afjM1^IXC`KCqc_oB}H#1Us?8QKp z6AP0#{ zx2gZI^#Ag!{{4j*9SSD1mh}Yg=fuwB^dks=$#Z*@HmamJ<%_80o4>^w`YazztOFG8 zlQI&k)eby(x1SLy*|2AvyHA-{liMKR;7l|d1SLPS6$yW$a|7>&^C zn~5JLsAgs*I$QC|{9Y)eXJK$W&0`uz$8?;;zGXf;P&Z=daxRj>7^hMKY|3}1U6)B; zSDPz8;4n?6UY}+Fjev^QmP!+QH0wvLt%HE*O|%SPkxhr0;AwX!DQGN{B@$HCM!-o4 zSbAs#lmS>ApOBD-bwB}7<&DPxx4710`-gZWa5-FN7!n#l;~O7JIGK;LGekaGy1^VzZ$3GseIJU_@5s=zED|V zn@;|6lfq%EAvN<_OB zygmU0BG7%|v-7isFh1+J?d`o0j1;ua&dyKM4M?z)eK;)c>CHN-si?#@A}_EoFfTyp zTgbp7cNl|pbwzyr>N&r>MJGed?G%V~8w&~i+2KvEHZh@no-F_r!=w$NKRd$Xj!MqZ z&K84VUe+DwFeY!ijxlW|z>NQ+AW|RGF;j%PEDib5WY4u1RDHqdnl2lhcYFn!&kr391I0aNO*E_aWQWq z5-S1yov`O^AW9A;Y~^G3e?z520y z`15e1i!+SWLZ$9iKikuVN}Xmph4V?N7WY627lfD{f_yU~1zOY9hG#6f+Zu_-evk0A z&}EW!rgQ@Nrgybu*bH!CneAOUgzdj=M+$j8jBXm?@hnM#)q$plP&WHf#))bM?MzB4VBGS`0TOtb91B3E zC6;&=c!85$Y|@cC}Q^;)+Rh)v;h z?gC2i#?jJf_MDl8nt&w3oa1bGFmjymN$=ApBRxdlRVS) zAt6&jqMTzXbzjr_`Tb*-^Op8uVKq>0u-&-rv^w~0ddmcOJzJx+U3CNPOO+X-uVutM zp7kyv<$MLmk}uQFYRramz)*0_<5V7Z@!vDRUv}!hP4-G|`r2XNcG{Zh9T9jtmu@*x zp?sBLOrIPaDt{KkHIr{?Mh`+y3h|>`03KzT%4ngJPTxA9sRHg2qs{b;2pK)>wApud>(gY$4mg1K^{YiGv$U!ccbqgw_ zqub`sK*LwKvc$YDDtnV$aLNLjo=!|FO>Bjl23Vm9&#(DL5)cmJmA%qNB4f)se2_K8=xLbjVBlXsv1kZ2*1DHxY?CwzlkrSmO zVmLzVBZ-u{dBHkx^6fJ;yj$beZ1nv3^T7%uDIVP#0jD#U=Edw0g%)iAZ0?bt^D?G2 z)M&b1US2i$d4aBu>wx^PpCTe5@xF?UEa(<_a~`aHBB7oO2qV16Ip&gpT7r>&zX?b@ z50B2z&y8T4)88)mZ;&?PGo77O)gj=<~3s`jGUxfUm+QBlKzdn8 zYOlB3kh6*L)WA1vZD3#Q?;AW#7W?o_$8#HD(;UH@V{VudIjHiv9~OB4PM&lU*YrjM5EU2!)!1gE z>=Zb~53`5JJ+?EXlf1kZjy!|7BK)1Y4UX8g*8? z<-Qa)+j(l&^>XrhGFx~pRKxgK=;Fe;bVFZXe<3PdsKOH*B%fN&4s5ic@LC%dJfZz` z{{Z0OL`Opd47F{bZj~QPRY0!fx|UD3mr|)hJpzkbVoHB*em?bo@`R32obx!zyV(Z| z+~;+2Kt_86k6cu}O01E}PhNDrnbK3vq*>RAs{_Gu!;o3mtdVgloqUuc?iiO^i~EU| zwbyWcmh98WEMrUnc@HyTaADxh@}K4sR%0bD$6nu&gG=S3nQ1nl%rcy5JQhUqIjnHn zA*Y=eHFDq|AJ`{VnGUpRftJlhX?-Bp7Ub00nG_ZbOiMb`E{?FSTwJY0rA-pjusL{( z4T8cl5sOy9&=HyHP7V0$w6ckYM5kEEv&>s3yj#m@r&@>AJ(Z<^1+*I{bUA@}$bafs z4+t2UdOZLX>MEYp76J+h_*jBPN!35XqwzhWabBFrkf_y?%_Fbh>OL$H3SEo%z@o>@@ zKoYMra7#_YEN*aGrR>$p^@%8o zybjXGH%Z+Q;RHS# z?T8od#Ima+g}GJMBi(^E4!XEQ>mlKB*-~+Y>cxF~!~1Wpn&4UuPPPpP<5fC^c3NFb z2+LUPo6J#6_S_O%wI z^0w92?H__@{=l34(-nzV;NG18=EI^5Gax|t7@HZ2S~UI#I_N~?k;q>jZP8-j0c<|J81 z14)?d;LwoK+OSy#=)kS^XEaTDs#>qu24cT5myk|UVPkwX#9BR>R-;V&_29yN3ku|& z(g!`V-+v0`8_-j8nGbi~-;Xqc#&(6WG#h%_kn@eII1F!8ZEZ(T@jelkrjXI727;ja zD)yzcCyLE6BNn6Y(~jL&#!Ay}5_9x}9prX8ABt_)CVr%fES4$cX4Yty<|uORewfsJ zc}CPy1-&?l6M`fC{hM~zEJ`OMsq$W zP7^lLKHC!}Y3zD6KR0VYi-L!nA1L=IhjL+MX`z7&C6Fazf;J0%G|NrQxTgaH9+&3B z*DI)Aa)dsWmA$rq=FK-*9a?^_e%`O@Bj0kvm-bU`ny-mZS#^GMesLxCq1kv}<(2qO zz1EsW$Md{e)u;4IxB|f?<9`E)<4e{#&sA{g%||+HZNj6Xqm`T!E}Q|8;(%lriF2EG zIoF${JqUNNz#-q82$06NI@TvqEpCtc zT+29p^7PLQhxr8xh)xww08ZMk=W^3LnYpK3K4YH@SDIv_G|zp~B`E__?&$ZWEkISgh2 zLb;w7$3$H6i#;~LA=rO8Hi=^Ll1J-!Ce3XZ8=i~uMd)Hx4xgxYy3b~|{KdTlil0}! z4T52FCeWsh*qm)OKmT;efP-{Ek2UEAy%#gddI7dv%fz& z!9oF|$h@%2OvhF|d~F5_hw;=Gfml}Jub4t}i+jAebTAbr-W-3GnVOdZKMFlyH+7hklxz7!Qkl_5G zoGxI4A6*MiZ7?x0H-FVEsC5B+*^}h8(zOoQ_hv&%TaLd^OEVmhvU=xU*(4O#Vtegq&p+g&-?xHk6cUF$ zWPRNn|Fo6NUg6@30ow_T+(jAgU?x1(4=0UzU)9rt}0R9&=^4IP7$w!f^&_A$Y`zbLx$>nM; zK6wjVOU=(C^*NifZ->81&EPe~neFrr`q#DAS`ffK+d(3c>wVk;mgDlJ+jHQw6m5T{ z&h$~m3H2o5a3w;6M*f9FY6e#^*)m75Pi!rx<$ddhD!Vil11Oy1<;sJ1dzxgxDnq3; z*`z>+_s4UXE(6I*L$TsMG$Sx`BkiqHEPr0cBX5&m{XslBu zm3Smzx%a&RJnYEf;VB$9!1=SSm(gYap5k>18pYe`L-{`zXD>F9eU`z^7N%-2cW`c2 z=>BNKC($UpY}(s&|BD7matChH!Wlyv2j8i-%TU~5-MST?xaCXCs9yC(C9xop-1DqF z|C#G}NsjXUP&lpUaZIrU1h8n-%5;4_PF77eZ%2?lvMdf;@!*b(4C~YYD#_zLqRz-{ zDXTmOP;hNJ{b3mLACxk`za{#kaDobjgX|M3s`kKUZ~#8mk|t!ZBE~eIs0dIklRr(# zA)KxvPUgZL?MdN-sniZ0n-Z83^SgX1Zp;CrjGEjhwm&dK;gcYz+~|EHJJfA=L*^R~ zUHszy9`kD|4hgq#(#NDhi(XbFj`?9_V`IkRz znXMwZ7)c>kG!r%P=03#r!}N(P7l<^jFL{InV&j?66<`?!YT#SFexCBk$>A=g!fK>3D_OgofV@a8RM2yEeK$DpED zX@cQnPMYJ4{2tIfWUInbot%ktoC?I;Hg)_yxC#Z~F4_D5HtbwK)~=mScQs7eyTiZKjv57@HJ)u8xo3Qu{psK^P>JYZnja zKl{X2N+4S&&yWNPA8DZcU*A5iq3AH3Zf zd0nVSDph79^^VcSIp^V|TE+8xAgYxDTre%;WxhG5g@pS8C+b%=yq2XV!UcfZ>pIFW z1ZSk2Kl{+31Q;r%O@T8wIDBTD{Kad|fNB|D40V1TesC2GL0M1pSuE@mz@#Q9GRR^s zK2CX^zIkBuE&+DAz8rwLx|=HMUw+cQuJMMY{gbWK zw9v4q+LKiF8ZYJmPwPIn<{A468GQ)4hLe*DcG!}D((_rVt?FIaF^-+Fk=B&$y}KMe zU0ph6VCa29su%C&^ArS+Yr))G6JL_@{~n9FEj*Wr(MOlh?Dk#?-p`)?8c@EaNQ$m& zATE--os7y`C?CEKc$57c8pyWoYmsGkI`t~N3oE=U6VC)1mJNTkcvJ#8G3P$Y;d|zF zRtGyD&W|E7Tpy{-Z9JSr)DX}6qG4HKNEHhct=7q(&_y_lAM;% zx@PRCdFlDSz`~OpqLrcfiq3z-9MDQJm@>)#{YLz^ucqHc?-mdq&y}-5Rvm7@mt~hz zp4lzDH#K^0HK3qS0fr-#Dp$eG00Xxgh23Ok@BC=T5X`PA*J5QI$o#3@#Z)Kzejj3A zRz)({g$c$24jCOU-!PmWj)_c(Y3Y6=sm3!x>DY)x?l!Jcx5s%195kq31m3L4fPo0n z)eV@y=0SMmHNLFHAe0l%RMmh z`Dz?nH%s;7o#^8RMZ>1<>l?X!Atf_uQt%H%tkD~$z7 z@+n^fKr^l3=FOY?6W$P~&6!H^eVgIZkTgI!o%CW6fMu%`%WKdKRfe1WUjoG-;Woqc zj~irK47s=~vpO7K7ORTk^T}NvTRS@q)6K1ZSiyn*RqxW99n?>gYwX9EP1t5n0gHO7 znQXcNZqRQ48B`b6_1Uk>PMbpR8Vp_(^hFxHGqA{Ej6-_w7kIMtZl|j}0<}Tq01poF2J2JD-YdiYK;_v|O<88SLKf#a9Wi$w5<1lQ5&DlO>`d|J`ixT4KFCl-g< z%Sp8~z}2i(X!$JJ5jcRQNZPi&vkvO;zhGLxy>EWd3b$?=F({Twv%Gibhy5oY0hshU zXaNA)Uh$O_;qvHoc71s_&HECr)ocV+aX!fiB8#4^wrJSn%~(Ew&o<7Ds7|t9*+LP0 z^iUG$!Ie-cx!eRUysw;OtyhlR_9j+mGlw|%U0~DB@V6O5aY!w+m=mqN5oV`^((>O- zrT;`^M?o*4MwvDH2pR}a-v_qx7k)5Pu9HdPBCK2P)u@qZaAM7TM30+biopk?cj^mN zS9m*7vDjzX?2JpfIbPoG-#a;OB;bDdGzJ&Z4QOVNj0z7D<(yklY9VW5+9%WOFU?0w zjbN6`6;>77erk~(+b#*o`6fnRfbyEKUJf_fY)-DQLo|Ut3iND=ij`^&S6`bARs~Oa z49Pg10=9H$Gd~U04`6ck5sdK(}K_;5(&lpVC! z`jhpQMV(U|Z~5%!Zeu_JUjUP@xReQ1yT}xqzzJF0|6@c1t?~qE02}e2U+PI6oCmRd;;*`52A>vGm|*gvI+205Pi9*`7}w zF5S0j*cinxe3+Y-;uUz}cCwmlxH$!>@V-Q;#a{t#O%W}Z8>REh%XGj^F9M|W6%MOx z6W((^R}N5{b>~j`in(C|M7hM-ebm&{;{{p7w4>J+W-m^N5_qi=K>!)Oa5lc`(CKG~ z%3~4)Nyqf`_p1+9Nh$`j>J2b0wdhT%$6p3&al1J@c=&u`Q{+WK6s>HehRnE9;q4jM z4${aN$59nOprEXs)EE2yVDG#*cIyizye0-w+R}F@7yneDDluaS?4e2lz_|Q99s8wR z`Zuov*MlqU8)zK6uasLrBU85tpP#q%fo?q36+wGWCH|r8k%&&=I{ObsEpar3fa)i+ zUUUF|V9!XMdYgdxfU{^n23sbD*Rh~Xh6Q1SOO@1_jwSu6yU~KLYR`w!#U^JRo-CiU z%=z}qwT2d8p{Q2&_6G*C7@^5$9*tXT_7~{Mymla6Asw3*Bx-}0eaXHKQvk~A1f3*i zo93PRp^XDU?+aAW((%Q`F{e=O=vSS|L8)DY!c>cU8XD{NI>N}~fXCKEe?j2F{%KKZ zI2imSQ2Ors@YVsqD`=53Q5v4|E$t`WaECL1SM?QYyr`~EQJKG*q5}kX?abaw2_}nW z*wcUuu2w~vOQr~V*^3a9w4F(922u0NY%bjM1MV2{YS38GNs@6n3im##+<=3W`fL^E z9dUU&!m?2gjxE*wm|9YP7x@`tWQ{6P=<=B57%9b~rg6U26A!npWHHIEG=aU+pBFS6 z$b<|j7i>YLx2uYA=~c^Tb#VJm9(XI_4XCUQnR^mf6-vwac@LRL$2~{GO8jJsd&SRW z5omv?X4RI=vy|N6xDmS*&DY_?pon+_uk2lf@NsEHV_GJx=risr|4N+?uWEjIXhA3L zL>{SafGMGLd3e|oUEjJ_-L z0S{KacUSIfZEbDTUH(4~fMW0N%T-fqE`=E`aE%Y8b?@@4}-V&9NNR-P|&+p6px3tCM&Oh62B z`45H_xgIY=B2)L~X`~F3u}2qi2WaW&pgI>P+={I>MXI;^CpJp7c6QHTUd#ZQ-#aeA zWmck`lr}#syx>4h@$lx+z}4h3 z-ID}|1UA=@^O@s<@5`=JqAAQP4R2K7?weOE|4bHKTXT%Ao)6L!Chs!T_3ZQV(N_zL z)hfjjlUnr7`GB%NWsDwd9*f(*GR*&N!uDITJQs5BIuHvMD5GToSINOVTdDyOC-=hz z<4^Q_(tn{)+#CQD3aN9=>XV6ZveOKbhwA3^4ovD*b;C%F@muwd8}#6!Dhji5bD@Q$ zP1;#c4fG7|8L6e%|ET$yL#bs}MOM2X)$DxhkeK(ULtxRFU3pPLqjaCK=CiuxYFmt? z0SbN7UU`yec$w{I7)-i7L&$BN)qUY=&KnT37JRA9(L21 zG}<1ccU9Qf?xA;gd-%i@mN+Gfd}o32I?2L*wG8NGa#&A|GY=N{5wA18D%9(fYq#K` z?NL)nGtukFzNGp*TOO6uX7=jUE4@PP8tDE*Dk`dt9Shx|G3S%!)6MD_AxDVH^#jWj z&(S4@_ip7-{tgr3e_RS?nJeCZY45*oiN%Hc%y=utA`7qF?z8Mi8+uFZg(a<~nxLd^&a7_}Lv5OCiw z<15`z2rg@Us!?USxv3sr{Fw0OG%o$NKPCn&D@&@D#9d0=uHfN&I2fbGtMyc>wO{T-}c!Uf;oIVe!*NslVBcwLJ!R$+zTOozvrk)Ts26c zVhewum@lt`!CAas+pfgJxUs~b8|$2Q^3w=d>(6Z7=(C7tW41oU7-J|Ttm0h&ROoJV z=fw*0Vp`t)#KLd0LSHWMmaZmexLW`N%uhd!1vh>b;{rdHzyd9JYY~23l=vb zKqSV-vv~{*T5MHy44MsyM?XLE5njbM?X&^jf`hrvgk%eK(+`X-Nw zUz3GgJ?~f9Es8M8YombtZ$H?rw*3wY(x@4yPlbsGL;~$x#qJ&0d=xx`B z3zVTB$j*IwgL~M3SqDy12hlH|neKu*_>`lsheMgY&B%T*j8wpG81O?zI4ov*aES@j zlKlOF9E4-bds{ESj_M}e9mq)bwpNM3Dc-}UDSVE0)#2paeT}Cx?t0xlfT)-VCMywt z{9;ebJ(gUv)|YhnoIm1|u)^odVH^2b&1R2p$rX;8^$t&z0+zxbu=DO~S%q}mVXC@D zzWD@r+Fgv1Uz%!P^x8;+!MWA=#jEJ~5g*k-uIhUJk(-ikj}-OxV+>T_G-5ZVaPFfH zu_IKhHs8Ky4_}|Ig?St=HfDJ%F$K^n()_dg_0R7j_}{kcov!lp&vA1Ac>>S+_C^`1 zt(U@P&$eEB4=Ct+`?MN1-RVSlys!983(Lz%2MF%3k~FGy z2@{JbSPK?uHS}4+4=WN_4eD2QQ>cLjsS$>ha`NYF|5tWoQwB&*6qM`Hq`u&pg74Wi z0PaI7fvswxcJD(0x1NQ?#TYHBHw1rbT_UL|exe50;Q%+b2t%gtOsDOMoW`A>TA9OX z`S|$q>xswtJL$Z4{n^cqmMu!-@?<1ADMW8@0>L!Rf_94+DT%p9&YSM(+XH zF8W}ok9FC%X~UtyZ&jH8D!}~H2j;s{$?H$1ZtXbJuZZ#WfUcWC$%O4dQdb2qGi&oA z^@4Zh(gBs#c<=>YLQjvF7^=dC>S7g{pvSmJxp6VOLcdCCRmo$pc{r~$)t;yOoS+=C zBk^ABCge^B!XG|OHhanUodGa%orr@xt745>HsI6e@SSvTndNPVy&=S~nIT*?!)l_= zEQ-e%D_<^1{?_gamRQ2nl;++c-xlq3R>r6+2WXEl37Pf_SGw#(W+C*S{}JKP?hf84ii7nCQhF7c~K zw47~D8StwTdG)%Kb_6YUQsiF;^nZH4{~r9)Ij}oFmfJ7OE_FtH242QZvjPjD z2DS42{Q-U~irU($T1Cu*x3sMD%H^XSrINS2u?r4v-6Q8s3zeTLsXtS(uiH#2I4(_u zUE0I?a0RX}EG)#QG&m)JZ>G_(cO$Hjv2B@uugXsc@TP}~HMlEnf9B-%T&chCNwgbX z1{%)~`Ihv(ni&d>0KTDbSj+q)Qv{FS-0#1FzdQ?cDyH6;^#KvUX&Uk&!qgQwg;#Tg9_j#LZxL(54)Q@e;lXPKb!%=)1(M0 zv3TV|)u3+CQ~9ZK`9hW6etp1D&VZE*-c{8pTHGAj2b4~TGE__9G!M{eWRDV3JgGNn zX&QRF80n&jcyzyq7_NO#R;^mi&l4had}%>4^gk#3V;%Ih;e1l_vJWf6-m+X(Mak_W zaf*3h!Unp3*KU=0U*!_1#m;ob3#>}#SwW;gMTJxxbAk~xE-8WCxK0f?P=0tqVVOMf ztg4Pov(ZJK;NWegL5D1|K&Pd7br4d?>=;P@1MTl?`R_O0uL;Ow6Lek&QyXeS zK&7gda#Kd#82BvSv&5v)1MF}!t!3H+aeK-#+NA+CEd?6*GN^lRs=}ynqAtH|$1nL= zj!cR^fKQe*;nuUw8$DDtRH1)LX{cwE)RROw&!?A&=gmp&xeXyL;c)t(dF4iVfD|S(mantrbok$U>UlbbT?p^n!XF(AyvH#+23c#S2a^S=Mrsa9KMn zM;w`I$fXIAE-~`}5*gK)WC|Sre)lj90|T_E+&Uftfn@L+QGVXD0%fd1I8gC_Ptq1} zuXpm=ZVk#<#Gh!%2%r=+G&GC{vq|Ftr|y|{J@bOb(1U843HvmJG`eB4$X{2w`vJ;< zg@-U3yaLukR~)KsHkfn<^q{VXCj??sFC`3Yr!M}-+Oqc4w)IWwSW z&@;J*XI1E)+Zd>3g4DVH=e+x;$?Lz|a`ZST4>)^LIiU_!TQjULwoXb`6UiUoqX{C{ zD|0|jAH@nta{_p8QIK7N*0$^>mJjyStcn3g}DO9 zL>(`6+bvuYE{A2GZpousP-N6ffC$*Wc5C#ry&NJz7glL&(80@^yi~|!rC!U$z5yA7 zDCW*lhQ-C2O=bnVUgW|8#tT2y9c<0Q`1kgQ0{UXy4iFGWFnVAtC0DRZ&_y=*{K)pF zW(qzY{qXlxmFq|AQ#?7T#k1ml`HC0^2M7dRKohdOqhiwaY<#DG|0UAKwDn66cueDU zF6NhWq~iL*;`~b{F{>3jetvi`1ZZ`aE<#!+^SL6Df6Hi&DbfXlwxEPJ8QuI8**V-5Xr;3{juy3pN+tq6nj4VujBI2jX_E+wn)*5bOC zlh86}fzYj)jkPij)Znfv(W-xwAxkt=Vx?g}UjOL{?T55Ee219Q;|ND5KBu!fGsAC+ z_*)Ju{jCmLqrHsk`^ENQ_BW!p!#_~PvS?U;A3tGGF0j=4&gCEJ>N$c-%<&eL5z^Z= zPO0^?-f>7#DU{MhQGfS7B3`x9SO(EN)(6hecE~qTYbmknoA&2!&jlj(Uj1}eadOI2 zhpdqbsO#ROY|WoKxGQu~*9+^LEu^#Tr{RQU_)&ON%Lv z(6w;?9BsxmQfb9%@6*Fo#BSHO!xZOPc93j4Ac)zGs~!MXD|V$~HZ-#& z$EUtOTxk1KnmfdQ7ClBe_tU=&NB?sw|I;@hZ}@tRVoS?v;%iC1u^+~Xxa&6NEHicn z&i;T;or}`%IKR=Qd6@%isGt7+`1iez7x^t#J|UJQ;};A7OK%Cjbx-5bJiNpj;*ngw zNG-VkXslW(a4qh=d>ei6=(`d#T(tRCPK&X?;KA1ufl9+MCZhTwrX?=jo`h25+y)D6 zqot~|$(5*O6*$4Y9@^H^ZqB4Dfp{cKiEn{9RV5&~+7I9?MJqMU;P8)4o~N%&R0g?+ zFNcT8k*eNUx24`eEJEW_WIIqhwB;1J{n+(fWSL@%I6*P&Ow^QPFT=AFhI^p-SW zNJzaPHF9A!)|{eYA}bq4fVUP}V+Po`#;s#I4F->0$RKUD7x|CJ5}t9#>Hjr3_QiRT zZnucKY-rVw3-^{(Mn~Y3P$?h883{Pk#2P_HsYe}`q)SMGUxor^UR#sCvVFG`7xo_& z!M_7pep~mqLU&B{NN4Z5AFgu(lP+AigXfq7iR$J%N9icjCv4OlAvc3> zQoXP?aFeW~vef}J5~|SXqXr`6GrG3!m=6#8iMx;cdBaJCyxOuFT;#9q?(LEm2M2#HuIkmkh$V8y>TGE~p$@Xk<)mc~$~=w6xY1r#8;=Nu{Ylh?N5ZNj}<++?Txt%B~&=bYaUhk>b~9oh}*qz8V0m4PeFmm zEfvdrgK02d(KpI)dt74VqL(M(=gSXZUOKm8j&zOwh~uM-J)moSVI^_+LbR@rx?zJ6 z>wjwHZ#44O=>BRGYM`aD^4a9&X6g%P-t35><6D1X)&9WF=;?jMRHSpCS(ARv49JlZ zUV-LI7ph={S=-@AQD0e`oE&ag5Oeo&C>KmQo=Ied8FpiS6Tr{m1ak4I_gLqLWP-^? zfOc#g%W^5$V=KRBFPmalV$YLnCsN|=#nXgbQJAxfi-{pj)+|V)ncSPN3njvNxJ*5@ zuirLNX+j9`A~l!^F715hWDL;b-lLtL$->fo@y~O+=3|@U_5d~Vu6om~c%ppQ#onW! z7D5KqlTy8{j&O3k)6JG-e%Cha>ome2J^&JusDK#rxaUI3jm7ag1Mxba{sv4;1k`f) z@z;|#A8I;n`?0TYX|h)}PD#*EMCVoeI-mWig$fr^@U;KQ_TTv@u;dH!rd3t~_kyVSxsA(w5bVQ71C@(H8)x+>@ zPUJ62lIf^EgTEuz7t3uUCPYRB1!|ujkX9)^(#u?nHb@I&fc(S=hj$unEjshq)KuQ` z7o`0_z9kgc1eKrxDZkiFF>dR-)a~x&#$|0Tk>evZE|hO%^&3rpQ_I>6 zAt;9+TVt$d_IFc#2%wS8(+0Q>~OI_+VoS z0$maP@76(yptB`@t0206@x5auy=304U^|Z#ntX;CBUqZymgX1rH>@zw%Durv8xcM~ z1+mX?1t=Q3xdlKNUJQktP^i{1tDNPGSG(5|++18HdOK7#dwys{a5v6O-OT^1fQ|}_t!=ks)&QfC@~s87B!?z9WTU>4fA4m8?mvebJ*y?Cf?Qx#+Z#ZCNK~~tP)p9 zi3O-sxBDDeo(&(9%H_!vh*QdF3EfiL(J|zx=|~kZW@#O-vyDpNvMO*KS>G(dN-27( zQts6rsLQGtc}}7dac(yqlEG~`zM?%j3IuakayB&-dn!80VNUa{nYQ_)>?WOp5Wu>^J1A%w3R%OoWN4rHQV(5(dWS}TSc zj{=;Yc-CnMG#^N9kwIZkf_s_Of0(9BR?BQ@3$%wli8;t*_7r?Or>iLf1NB%z-9)X8 z_ahXIxIT7_606&O#SGkh?ZsR$w%zUf|6K zmd%MR>GCAhE}{G;BVjdGlXVpx#Zd6gt-ggubolw_;B0(0ZEH6@s5>`Hp>`x)f!E4C zimfl{T$PLPq~|_*R^-6PY}M$S zUWh?Qcmp#1lNnI|ECq2|Ozne;Hy&c2YMXYf$nQ&_a=XUk7%6-Andc}fo8#uKi7WPnY;Ve#KL5KeB;4p+@z%uI^v$|GwvCr!g=MXSq{3XMD&PJ6Li7qUT zS*w10D*%}>1v-t1M&Gjbk{_2C_YyCaPgJJdmm^#U0d$1mco{I+IF|d;Zd5Cu%D_vm z;{;H}g&uk$Qw59x+*9)L3R$`WYEDa22?+%c7NYum#r5mkgDTtZ#U07d50*H=Y2^|Z zmM}_Lo8PjXjp*nAf8b4}>tSY2Y6UQaKEURiU%&pDaR54!Wyh|FB3 zV-ym#4X}wH;96&1h_GcCDF@+=Xw9IZ=(*1NRgwQU!65q1GqV8it>#nhH=Y2s`oE0knPSt@jP;VdgrYur0ow)pyZ5!rHT)w4#OtX?)}K}V@xZ<+9b+o=f1*Zht_ z2kI8~_4Bqw^1EV^S=k#Y2H(5^$Bmn%>FAw?7_&aOBD+vxM4?@euX~Qsj#GkIoHE{aIe}_ImLjZyG)|lf5Ym~ zZ7$|6DFz2?HKTvjk}%+5XZ)taHg)pbWxs7Hq#n6CeP9tnIMyBjjHC!nz8(6OZ-hXD z?}#Q+l>?9w-mB2108UphJ8=$pvmDH2ALD|{$sx8dAI93<4v6bKmfRl{@D`+tiOTJ4ZwEd%UnjF zc3t{Yu9C)<{!`Ydt7i*>oN`)|U+7wZ z?nA^xXUDeo-4vhU%Z3Y-2kQLciVrS?^JS6JPMzs0--)dM3SXtIEy(7uHv!2=D;?I; zJ^X4d45pnb63Q2%iTCGZRq$d~Tj+9)b~CtUc;}>Bfjiewcv0B@x+(D_ z3XYs68(VJC$9o0UTQ0Bput38wGwf9NM>@3ny5;u4tkbubzx*d={P?}vOaMqo9hM#p zt?9YCBVk1tHoZRmh!;9t4^qTwU!;h1I0li7&aP^BxgM@9n2`5%Vf&w^M~ybse^>w| zRP%@giJvsxE-`Qil~emykn8ZX)&S&KkshVyj0e$DNReUwgk{3f5U(4s>9U-<*C?f5 zZ1dBI-lwzhh-VyJz}ZyVILT@A$>(BV=x{e!{l&|rWrm1WHq(YT`Ue^vm)Kf-&|8l^ z9||ys_YRhgfjUpx{nV*C(_P(zje_g1Ixpw{Dix?z#OzEu|9MpJqi$qkf)K`7oOOSw zlxpmr%QmWsmp%kchEVno1>{vejQnUfup`T`l)!%w&wmT_OPQiwIR$E5Q^)pM=UD&a zb+coN2Gb4I7SR+3F6Y^N=C`_u=weYtZ;U}mwn^L9g%z*sKk5mgu8og|<}X*qC2(|r z;-0Ifa8bO^bo~*`0NhzR6RW@HicO77AQq$_MA{}b1Yl!D$LAIj>LfwDnsU>7SiXD| zGX(Yr4#3Dbi7;;S^Flmpob6%_jb4n-3{S#13r&4>T-4sEy^V>!IgJ2z$$M{~kC=I( z{tF?;%GAt^bBzC8IT{83v0~juk^(82?CXnWR4djs@b=p?L>{h2>Kqy*dC`8s<)Pg! zOSj*N;y0)RvSofeDDyR>1e^wHZJN_q((lU4yk)=-)4N@i67`a8K7gtFk0+7EEy};* zL_n0Wq0VDH)pk_ozQn!P0;qcf1YQ@sR3QoB_&)Lzpv-hysjzcoW=f8%Yt?r_u?W1N z*tLYD#r3bR@PC6G{_FDk*Z6qrqzfsU?7 z`nz2b^`TdKwI2rgj_IF$j80`!UXYzk&h=oI7?ZfT`8n@~p_h*Pv zl`4%46|8T>)S;=pv@Lt0Xzn)!P z5N5wc%3xJ(9~VtabmKpg`qBcB%-+kp_rojwb{cnx%$@ioPH|?lLrYFrnRcq)GU{zn{N9zP+))>ZG;=&0BdzkyAHM zfQ|`7F@NETbm~1>IjYt|U@nmd$>G!g4W{y5LkFapHO&6NDuro20Mjx7KtplBk0HtW zoXG5`Tcw(&4(A@vliT{C_fr0)#q_@(HuAQ2GYS>;dA!`E`$-1Tf1`1f1a4!Y+1np= zaRKuk*VCt}MWLku+IwR~2`lGEq=ahR#F;)HIi!60|M70|w_o@F1A_yIu9TdHGWor4 z)5?R5MBlUn&i+mRGWg0fU*1OlY~JW*M)oi9p#S|x_#ZEsiWdzNGh*rI+ke+XdRIRC z_>rT?2{-6pVMzXVJO7X8r2{EK`mh-d?0;cm{JTHG&@uE!kFt+;((L|ipL!XSib^h+ zR95)!euBS_JpY5=-BA23wC|O3g4{U&wogq47SAs`L#F@73jU`b@{Jb3n>Vpz^?pD9 zZJ#=v7AziKo9B1`?tA;EpX|SOFeyv0zK2~;244T$J~a%TSv_Cn7n=)zU6$s*ZB5?t z=BE8HEyb^Kc5LmBQS@~H3v0MAh#TYI_4aQ84bkhO_w=;ezsWH~7^nlA(PaLEJpUi< zZ-4(CggT-Z&Zgd9eUl5pzTRot`+JWaDpWD>*DQd4?PYkwBwH-+9_dyAy`KdBcOYPl zOW?7Mt`B0`eP}aL3a*vtQ-9~!ZSfFEbval$`4wFr_m6+@%sAeFT!=jT8A_zS(y2-P zf9$YSfp5UYP_7XH?=70HkvE2P6kwyA=Kg=2TL0U&08}&$YJ11q+SwMKSK{^{OBFP< zXxgLBkN#BAS3ddot%OJ3pkl-R>igXS(gxo<`d_jCa}@kLC&d43LqjHhbfR)V`#xQB zzxt=*^VMmIzk-DS_?!P{*WU^2?GnNU_-1yV&N|LL$bM7S_b;OVv&Z|#57EVY^N)L? zzT~-H1m8_d(desTwN98vz%98P)*cN5Ti3f^`LFC@3UTnw@r1&{!b)qdhySJ{&ijuz zSl70*hiD=NW(Ne_qOIBLaIZxurj=ZRh*esw|EyDuC8k|KJqu-?BNTHsFDH z%MbERo+wurOIKful5y<0T9;v!M)+oQo=#Rko=zQX%GL-3M)mYO9S)khTsKY0@o*b$ z0WfB~a)t!rl`Lbe;i`gR;ie&|UFrp}RxgMilmkqh{c>j*tW1y_AFb>!Vs46YLAj-# zpV{EQ%GL#gG(cKcfOKsK^riCJ|85bsuSG-H0Ilz6ASut7!^Xs*bwS^es2^^JNc$l= z=8anX0q8(XzSZHwOo5X@x#Q4H3s|fh( zZZZLfPM{S(^Aznm7NMd>1JyThgP zo4#b$-&DQwF?~?E8})Sd%5oy7+sXDzen*WWO{UM_l_22LDvcf&(NOWIN++sF=e_+7 zlY}R%TCpHCBoZxpC=WJ}>K(lO2RWZ`MEdHp+8>}e-gzJT6%Y=>n3$qQr%H2}Yl&n) zhp;|YWJyg89ONJ($Bx~49O!oCZ**u4#@{BZt?Rd?pe(itq*aZ${fYW~4r{+xK#_Jw zPyA8t+3z1Ape#!=Bihu!9o=HA(EOP~?E8F;FgJj~_4*zV^$ZzzFKWmBt3%|;-P^p+ zXRDt<_9fDhOGPoTb@fBhWUGygu1ZQ%Bh_09O?gv4H40>37Z2P9W zZy2}VkCO7(#51emLFt3YF?|+=^AM1W%WwFij0M>7TKka()5qJsYIXMG3acV(Q5nA2 z9JkTT<=!Nvml;a%E2qZ03~R;9Jg&99_N1*5ZnT-H)z2g=(JN^pz&qX?ABR5f2q85c zDLY^6c3D~e6=B{G)y^YN^uZs4{);=P(dlWqjgkk+yS>hvQl|jneyn$!-T*(;x~$Dd zXxU8qe>+S5>5cr2OIDZh4hSUh*#~V@ZS)PqLhi9oU%1!aq%hnxcN2P(qnX^Lqv|MO zpz!M9^L~l#k9o2+X?ZZ+%mG%))98%CUUi#Gpz~g$0lUIOu4$6K8@#Ph&uiR>x!9Kp z-TUJ68xs>+!@}YVP{c;)`ppmLsR2c4a2Ec?FDuKXpYkgo)S?MkyFd25`xO^RWNPwLPw8Ru zo}qV2)&djzoiH<5yl)_tr(7nn(PJJ;--UTCY$nTJ<`W(OJ$#U^WFhH%c5i|_NV(|E z4WpF|~Xj zQy83s>y6b~%Zpt@A`_hdV|4hZ4yhIo;WxbJAdfNAJTY#)TAep% z{|h=fcHM9>7DlxCS3&VH7aK&Gvy7hX2Nh7H= z#}jpKk`Bk&9BA^Gi2eoLt8bT0%nOG7ys5<0sq6%a4 zgLo}`Rm=Fu@wMe$*MzzBFP*uJI^P3H8~@Ejk|Xi3ppeWn?2m2|57q#)j9CJJH7veN zdC=>}vE2j8jF;tfjqYbK-w9iUZjZ~#)A0p0(~BiVAvif@-NT;*uxBH8wTuyaDzepDgIAu`qV`Ad2u7n{Ig<_V>kZV#mMx}#%YK-a+q_CMH3u;TWSK}X+2dNYuV z^YM&0R~LonEg$mkM+W7{0n>vVcmwjfJ5S#oomW}f#HA232f8%>S zoFi{f6bcT(cts%;9b3U}XK%!J*YzReXRAiRaeVNA3M)a+#oAweY;l4`=ygD2&iRmJ zq{g84{tC!6HvxH`VFil6P6G2)d%>;*h2A)9#_yoX)V;tdP3pX-Ch$D1)W$W7kOY;P zL`YKLW4FA9$08+UQaoE%95cqZ>*rc9g(|Q|*hT!zZX$<^eIK{7OnH7EsMAW?e5r=D zl548RuK|4;r|o$7nfCa!ud5HoEf!NPGuT0tpHJForh4@o#Rqx{cImZPYZFQbkVdjV z;UM!?-LAXoGC_|Nez~lG=t`zsC;1B%+wH=eW>bxmC+3Q0W>RANRL!b5^u1shUV6YZ zzF=hm`&fgxeTMxmI5>7FGKI+>c@Rm4{FBI&(p$8+G1k16(}nb3-Tc}^8*iH$!sS5c z_U|iP=yi;4eA@L7^<)JwpXhwr2fqEDg~A^@*Zar^kzJur+hp0Be&@S`!bF)!(Ra~- zZ(H9a(ubQH_CC^@aK}Q@Fr_eENIA}&Ddm}UpmhC?z5kddOf+!Ev5`%9wIu%PmDhgG zS|t5BI0+CWuL*+EN%CiSkPA|g5pJdl87-L1gl2U|#{u%Sj%)^aepW{EhW*tdKAeG@k)_g5535s;E=B{XfswfX zN~#1~iNFNNK&jsh+VSdQ?d$F4g3Z5c(otRrDG<;Wfym@od+1b7<2%qiSfNW6)UJg*v5wyeAMA;*Qlm9Es3AmD&)ZT5D+*l2Zn87i@Xduv zNY(8IM2sb}#ClN{ips$sYzhQbQHprHNHB#?Nl5%Uq&8eP4$ouh%x-X3yVmu<^ewI+ z?3I#2#A6iM=gnnphudW34pb;Kgh@?0Zyaj1790yH9ffb3F*$t7l2v)UpXyA}*To5n zfE`3y$_#%JH-1XDwBM*fG^tF2(O<75yid!nCd^?&#qtA zfhfabm#J*Fr20&iH?PfyMEeMLfEF|X7%d~Q?%4|FN3B1roh(+TeV|hb_Lgm2rX)V6 z8ygg7(sn&X_?+9d$G9?c#Luh`^f!zJe%Z$D?QiVOjQhA4Z#6glvhirYXzMS6)LCs? z!s)@lYTgYw$kDQ*2syUijbl-v@TjkTw;lCY3+b8~<3T&-51KMxEhpEuuUiDFbq!^pvfoWEWX0%UEl z%gTCA5MOqylB84`CG*_%?;j%FoAL8Gio3sQe1!@K3jJo~6Yz=ZtIE)(HA93pJSR{g z*Ow(C@Eq8uK56KD!91QvP;$|yvIZ-f2JDd!gmMb$@03{-YI)iBGEe?yn;E6 z;V6Lu9X#mh{<5OEkRy*ITpUzM5(un zUdz3;_Aj~@o<8tnT^0lzcr_pHF{hSonp&~;-UfOV+rK_EG{@@B~8^kYfJU%Ba zUYoEp41$ppU_Gq9K2g_&e>+XAagH8;QE13JxdxJFU6vR8wyV-)addj4C3ko233L;+;<^{(C(Nj@!9s1x4Z0RldaOeSVFpUKC^-ncxwkw-M& z%?i0$qg%KbUq!|4cm_zQ-QTYz#0@A4v=suEhgzQIsL{?`#dM9sqDC8AR`=Ox?_Y_g z$&SW!RF~89dUh$a*K%Ane#QKp z2Jf1@#Ic|!nLc3yu7IF6Z`9Ofac+-iRBf`7bX7X_KS3Y~r3KbBi_6$P<0sr)SQR<_ zioPGx@>|a$j?&Cqae=$k5jVd;{rWv1f_K{#-|lVx)@r(RY!>oF;|IJrKcQn>x_~{rouX}P}_jL&cK3yRl8tU#h*p(de4WS$6`9T!Ba!}*^0n{ zoRmN=9w%Mb5p9!H*7Zz9fA%2>L(DH~AB_6!4fn$iozm?aRda_`hgRQ5{Mc+x5H4wC z(wB(0F;OsFgi~lM2^@pH0AqMt*Y!59z#qO6C@DbP-v!6r^jgIza_*93$q2sUs{WBj zNrm!h0lwueGl{w#E##TSa?Fj9L|O8|;8!Au+j>^0vl1@O6wkTx5B3s_5WP##ULpD0 zY@5*{o{xm!XNVJRnm8iqNfp@vl~OY1+QQmG++9d$CSn$0+Fy>-sVk7{CyO!-?1TGK zEQ8TJS@NH~ukA3P!LZ$~av(1=B!Zqt%)CNEq;uB}93m}q&~>i8P7UUNVHfJU?)z4r zd0m#9LLoH;dPf=02X((3k>41Q&;Lv=zou1 z=Pl#v)>(>B%wkoxB)sf1k9ZaE<*DeEcbjV>AM_=^WTB%NflICWm%O{+m`sF>>Lo9Y z6-^`S&XC31^u5BWY}+?BNv^>UeLiXvq&6MG6D45ngNv02 z0fGlpbeenkc&_^?Fl^d(dCrFcgsm5AsL1|ll|@<0bK3piphU5^V~m&v&#tPc&Rq!J z@sZyswBpZikIo+PPv6a`C5?S`EIpD2yJic_^TxVo3bv(88i#oA_TS%@WbabUZgSAM zv9uR!a#DO_fWf}EFN(89NKK}1>>mJRwJWN7%zH2R;{e0k3z0m}@lJdh%or^wPrAl_ zsRyIgV7f1~nL|7CB`E|uc?OzELZ`pQPVV>wXMhnl%gUoQVB|$O z@rUgxxXF(%69!UAmVDWxFD%VN>}C(SMc%q#LGaswiV$0~P)2UN-ie*dt9AT^by2%n z1G&6}#U`lA<)Pafet5yZ`6Q3h)fMRYn)J%jJ1xcNN<1A?WFzxJKPUyFL=(u)Z`9si zZ}O3z6I#_)F3XG39^;+r_(d|9?rXsu6CsYNVUNaa8I{7=Yc}1w97{1Cl9_=tyhXlw z=^~EO(ATcYg<|2m4Hq_RvE%KixSSLitsw~F8L5MANLD%E*5yUFX%<$IpCJJh730iB znfUqzo!N-T#sPa1roH5-5G4>=*<+0ez8jmqG5TX^?*g;4ujhzPy-gB4T?a3|#O*f_ z4hHg7=gW2s^4ur-vT}jT=lK%<3obmBZ<Y^zGoEcTB#J0{AAEDk0*=!MmYgaeHKYLDI zcz?XNqh=FB6elgPbiLceU9HEfHy0&W$Ei2jFa)gXU(~YPpCVVJDl^(Vk@p(+OL#;N zcH8~HPcGk@4E4tQwkUTWtnU05f#U0v$7Rv)D+G|-*#XMt;~C)CX=YUaGpq=?9~ZXf zp1{BQG-fx)z4XSPw6#T>HJiv;q_Ku! zJ!kZB)C73GuqY$q3>_6pl07{U9pojG`f+{xH&h|)nj)@1V}~af&FYP`y%K|>YbW&G zXOZuhARmYG`Koru{*9fkujWr!)UfN}#0%3%y!(0p|LhG;;_m_RlZJ%So9m$A!TxN- zA`{lgu8qOwj|uu9_C*vvr)YX~WQ?V@e6x;Bd2Xjwi5W0e8GJ#l)g?F(&uBgkLL^P? zpRW?-`T7488z+CE+MEF|P#X-0Er$L`vW#*fU*}*Ejt@2kYkNJfL#j&m8FL|bA|vZW zX`1N$Dmj+}KU`DZRip@4_rhn1AnZdH$;GXDpnV^A*qLD}W>uY3`7(|zv!$(>o(+Tp zbp(u2SW{sS$E2ATLMfInf01ZP%%kqAlN-*)?u-w#E8|(GoccNFWoHJeq^S2x7RY9- zoccVQy6iNc@`HKE8m}!dlH6~`@$aJ>7%8J)h!lC!&-cY~YeFBu>X#uxD3nLm|vA+G!JHs2N7V}-dMnXQ&=x>R!R5EuDl;v#N(VTovpUR>1 z3D_WbTxFGhu*?`!Sdchl6#;!U=lH~loh?6kz_aXRekgdTwgY3j&`%`DB4?u9*ir@~ ze9d&%@G8v%Xr}ZRg^PB9;5HDwH^4^C-4k*mN{;@Y3c$b5CI0nK-_%j1Cf>WgUiihW z(anq@eumMt`L=HqJ0V0}D{aOHOHBzWdgt<9yH9+v`%|oYsOu$`ls7Kg;=ZqqBH!+a zX>u`J@Ds!_L|~_^R=|+?B8luewPsgGE81y^^{qbl2(=rDS-hk=WE;}R0>xsKXBTx< zY%D8}EZri0>B(n%431G+$3I|r3_wC$e~csOYJ%8vA*1Lwk%G684d(}9yk5flsah4F zkYTA^7>fA(O{=S^s<14bfKeJJzvmlb-tMsq#n#2H*P~|?kNJ_UU8jq`ThU&m7WZ>{ zF|(QU#VV&aN!T65thm?VoSRKm`d_Sxf6f8coRg=!VvJ4#{+J%gJA3+~hAXw_2o?Pg zo?>sNRW!Gq;(Hm+19dcTB#%+Xi-C z2iLg8SDwwIBe_=jA~R71C7wBza%{soJXpO0(zzHVLzq~Uo`2ew`k+cgAs~s5F28$X zRc*caTWK2^^Ys-5Wj41<19x>1_eKv;-;XLLSMuk#qzlyI2s*EeI30D3{$8#X3aQ9I ze^{Dv7YdL}UQfaEPFjV-Oq`FwcgU^qJJ+b8#h!@Y=jg6=Nz%CtepQ_V`v(I)xbbXI zK0lkte8tTh-+UULh*8<|O~ZDGo}1uE9Zt0(8by^N?)8=aJL}W)P1`_)!1uZKSH0iG z_0_(|dv)@)vFes{Q(MlwZ*2G2F%vJthG){@1TZ*Gd*rCQV8f+0Cnz=OA|sW!IMoU@ z)`K$?##EJQM1opiLln)}42x(~d6hv>P!X10L*5OB?QZd=0z(MA%jjePxyw$nF40kOek)9ErTHAmK6~e;zOff1ztR?P z0hq1oB)`~33?G&H^nnv9j{6sg-Vv;{ZG+;n%RXVpdcu0BdD7}f;W|(kdydMlHa0fq zPt#PumZ%uwbCAEO3o$G>-q1~7l7tX%Af7yhm8nvigD4P_ZA6vhMNZvrQ{uVZ{G|1C zg=t92@*I6~L`6#Vb%@uw{e*zWplGZH3-?gmPD(4tp}x(qRiK>lTVMX<)V;pgmdHbU zEtyP*ZLr_C6Aqmoq$ioS4i?_j%1+{jk=N-9qIi6|w)h6p*q1nub8bsfZZ+2C6uhR)O2 z^QgxB$xPrjUo2McT4xVAgV)u$yb7uR7&t=|!=}9g`wWM~12a5JfZKCAnn;`WZzN;qu=zA+(eyF)=Ti7Q>{MLbf{S!Mq- za6Qd^9gu_)y--J%<^xHWs;=!Q4P}pLug0@%F3fBKw6~nh zqTMQ)IIri%O&v*~rz?t5^-6peE_!{$Gat(o!H^KFW>3JH8Fv0NTH1ImKI>SwDRLJ! zVqISl&l1C)0740;g5`tW5S`vaur%xYrJlhxux^HO;F=2)cfuy~^2td$0}8fku0 zQnI@m#5Fuq`tg&83b|i6*f|3PrxZgnN}1F_^s0@ogd6|{ z>?TIQ29bYBn<08}yu#Cb6-7Y))Ad83PIm3C0Ry$fO@a!Q8d7mZ%*%|ixcCp}e8?P~ zWTRP{pSIJFM4?%Ksm8BLXr$~EBX$S=gjr;~Fgh(lwXZ5lHG1kJB6tLiM+MJzs|@1J zsKbKLKV#Ruixvaj0>=02J)kPGu(uY9>3H(LR~pES1EX;5W#Q*3mN{n#jqvK z(xHgoB^u^YkX&7L73`d6E=~Cr_wo@r$Zwk8t$<7@WbBxSr5&WxEpovfYBcrZbcEsL zQ6G5Ts*u<9g}d7hXIEY9PhLx{Yz~E3Z?s%4u0ptMn-3IXNwe7r&WbTcev3w=lu(vW zSDG(TNa=Yk>FFE1FOri$vHda8@wyXFznW&w=T5363XpBLrOO~j*Y>J{p+&i1CG`9~!zJ2-b&folyE%RdDSx2it@&= zO)2)PW9Sm-?CWFLH|J6NV;~C=)HJX_cn{~=u;(Jxf#;%RN+6FVZT);Nmax9M<}4vB zjcfoArF{GCAVZRpcUk~|&ENm1sJz)$658bEA8-cF0PKfktYPWZ?8JPOBPNZ7Lgk@f z_q;Kz`ql^Pd2@s)CF*4I6+g|szle;;5MGVqvY){wnR8j4{k>t}sRnbeY;8iJbq(*= z@$nK0;EvkbBN{31FSj!@ushG?7U46v?FY!c{(4CLyZx7XMr=IYkPLfbf%$IA38zfF z&s3hFw+vgdfCh4`*UwGhXZ!M4P3Z+sy~HPQ(ws#vD=X!e?0$@iWaqd6n4R!82B z7kSYm4o0dieT+v+Ys9M8hv2sFh@^Y|0Q+u~VZq+{os;$}>Fk?*P{90x+VcaBo~iO~ z*ZNJa6rU*F5&iOertOa+)2kP6H)0+WiI1wzl%KdZ!2*q`XdWurrQIgTGz<^rjgNb4 z*B!HZw!z-rDfNXB%I8#e)_u1)cjL~^Yz(+-TyiLi--? zWODvJ&yzjxD@Xhz+EHt{oO3OW=|5M;!g+awydyO{^iXGd&!OAEqV&YR4cjXG;kFa> ztAl;ngwAf{k7Au|JAF&yf4@7|*aHe+XAaXuzkJ5R1U4;vd|VkkwirgZ zfl-c-#fa3nOSFstBh~ZbS&IZd$G6tq-qJ}GfN_llZNC+AKcW*WXiJ)}1 zbYWE|f~KmRb`1A8UiCPNLUN5dY>tic;>kQ`JP~9nfK;dV|MpQc z62#Z_&yuhl*v|_Vw!^!`dmfFds0mhw$|rEYbUS%M(VkO#bb5w=i5rOq;`O>Qt_&)v=Z zfiIu+l%j1T%F#?qfYM?6-SQ+Q`4X1@R0h^Gy}OmAm%vc3+IK$QYMZ%o-jSz)IXli> zMy8Z@Xd+Rgjp>!>pGh?3J}Jz4IRuyJ)OBr@XE?t!n`k-Q8SN&wH^@1-+BWcfJPe20 zxM3&F-!Duk{H%kxUW;eFom=~`GNoCu_0AU^`bK9Z+HUDXx4qxgPBS81XO=iRJ{-bdYyMe}cv{w$*<8r7s zd6eXEA-&gLty`1Y8dOPflbxy;OCi%e(M8&`9&Z(L5c3vZ=FN@gAzqQrpK*c)R@;5t ztoffz;5ijfqjJ{qaV=8TC5J)tZIskrj_c1rrblj+LW!!Kw_lL_5X&}CIm6&M%4n0m zHm6Y&8VPxABRL)M%|_sas+nx}pT(TYpQR?=K^7L3pTTm9?kC>!PJRUsxM;TXuZUz= zxQo|bCGVnbOi!VrP4dcWEk08x^59iYM(x*?)J*q?4iloU7^9xnyEX~+xief3pU>tf zQpQN$dTNWn6Kjn8~m28p|^CE}y?cb;M%03(NY5CpilW3Xv zC&Cbk+c7;vFs7+c_vMDr;0@~HX;4=VCsTp$KHgtxz2~b=@)@G$uCF@Ld^O5G+h~;D zSWHpw61zXsY&fbOJsjmXITo+oMp`EJ8FBA1p_bnmA!|u(E^^327pMk$+pw0PT+P>> zzM7Wy=I;pku8dYj5UQuQ?t(}*O#Q5gu!z$0_g$#1!3;O4hTN-<{TFi;Cp(Q5nJCdB zNp@cP-AYtaMRkbMh>$9&uJe^k%`QAd$xCGQ17}zlB3#p}ynAgXv@Y=6Nl20>89~QA zl8QH^Ar)iSjNIBO_0VkWqG^!W+nN-3H@U%;-_>+-xc*KdQ*BtcR)$}OhGs+>4*80C z^oZYMN~K`L7HZ}s)m|J!@s;jp_-j1aif6P#=wZP@%_JP;wIFZn!5VLnk3f*;p1Sm4xaE0W2u?hEu9S?*D7nu2{ zl-^zZ@SCaTmQmt3p1{a~XxF^-Cpo;8Gk~C1Iy;`;ojH7eW+Wv0e8HFdeBU_3(Q)5UlK%p zdBdT>{iRovpp6Zmeps8&IrT@f`3V6wO7c6o zm4-#lx$8+qsB-kYsd<0Lj}Eh1mR>!fds1j|JlnY%d6=l!<4eU9d)0Mk^)-#iPRWk9 zh@a3p1>yTXrizY^)n~&gS5`LduXECSL!N6f!S1$yRLYr^+{0jku!cDWVebvECc|lU z*JDXvfB5K@cZY?=2{lma(Sb9bCZ9RiS*{r!5rR1SUgGEJ$~-4I5mt9uMsF&KwfC5B zo=yogKm6eRv+l>QsYnEeyfw8=Aup0E>kH_;#D#&Kk^V#qm72S$&y4>0hqb9n+JW9` z;x0sL&9g=Khu8i=NHP_ z<6$*zG3&!##T6fky1f^>EYSJ;=XYV59=j-9kx$}+p?M)OR&q(4lU0-RlHo-RV=!S^ z2md-$NH4-|0zWxkfOleT!6}Zkx@pk;xE0D z11*>5*Sk+%#Vm#PLZmu!bsJmm(uUohp{+6e{E}vmm-U9OH%o;hiy0?pL_Ua!)QvIa zL7=lX$P)M$&4^E>r~00?%r*$ zec#pDeZj63ZYD+97eL5%QanS2#5{vbuQ~VN(6Ophk+T4SESH}9Sr%9s!d{nBGr8N! zoiC(Z1aO634qnHw>%O&f=%Pin+#MzgAM8MPO|DPFTn$ z@s&Y;(21#Ev*i->;UF>xa$&=0pGTr`u#p05|WblrlbEsCwu%h7lAEeJup7oZ+Fh9y_w3_%8 zz!W)5liL18+($wDvWsxMQD;Rx?*S4a`zF^=-Q(75Gu+)JscQ#XKfB>zYh{XYH$%meisHRqlV(*|jByC0o*(l?*biv)uO3Dj~f{TwqwdUbhQvGza zxJg96KA&gv{WKb&U#Z^`8b5zqXP~ z8Yf1hOZR&-O5+Wh$o22#99bnimg=k141z}%O>MfK+|e3HpWzHm>fQO8dGO0CPWFg> z81NGy`&*FGd8Sy!nWty%icVY+ml^j*midqC_veT!nUfSNC_K2AIaNnC;ES8YdJWFf zQWKggE-RBbo8;8y%Vi|LRyIa0cf6YvwwG;`_*Nt#N&{F!S0@lovErQEUB2XtY2_Gh z89$xxhj+xKGb~1#efuPnni0AiQJrUML)CQU4WDc9#iSn;+84ZiuMhPJ;4{tWtx4Dt zIXE1s{*G9t$H)J4_SNwqO6gsCi&+}{YG2U{r#JkO^mp321aoe#Yo+Y%$oJAE0Aze! z!yDutZRNaBe(bd8mv6NBtt!z|PHnA&o1Nl8G!&Fg5)M?sBY-xWBvEy(!qY5)XSYCP z81?z4M^)LKj*87(ssYSgXW&Xoy`Ng5zFPVjUalGvsOCK=mmWl1HO$AMkobCVbp9tc zs_~=Jg~({1m(nSQ(*D>Z-Sh0`w z(nPT2LQALp+5$<$f9l!4l$ljmQz3rJH%EJc-O9Q6dvm}6>+mIhx(Hj0?$-LV$NLO; zD*#p}#NHH8>CDscpso@>`HiODX=0Np8YGZ|VS@Hg?rNdI=+#zC95|TO@}F3-FbRVS ze`q5DRw55FztW?_xW1hY&+z?VTK+?Wr(?vJBZ5YNd{SB0fA@2j-+2xkBl1)RO%MrZ zj4YhXk~KYf%N7)P2Z!u6vHy_A_^nA@0pjPWt-05G7xQ(}U3L3tB2T&32wCZd-r0PG z8IhWrdChO~h8F17&o`+_H;?g(C zI&2(-M_nIPy)6FvK>qrIr37Ip!)r+Y(4WnREgs(tUz|Yc3I>uE4@D1>=VX7nLo3B9xY%kpUs=M)pdLKO(hU$8=4Xu%O+Q zLP_ihhR1}6an~FLaDbB~iY+k7C-8^B}WYkBc?;_BQ(Pv(e&i-bji^u9Z zyeMh$>km*ewr`_Fzh&>effp)>ZM#e#E5fYwnBt_2p}z_t$;Na~B0FoVNA-=SPy>1I zB!|ave*V6Q+#gX^d5YXsa9yL`bmVP4-mI8O#WS_KehIlw-#$R9J1Z~jdR~*v-r61WCrLu-qA)+a7CzYQy#GlR!t()ZB#V_wB_je4Z^PiZca+5G zUeb}+txPw~lm-fUfzvYhhkE!$bUoAE8d2k@y{i(Qc^RqGJPdkvBh0a!gbco3PqC=q z4F&CAMUA?P?>hjo`^=A|;DSZI$F1i+DWlZ>uUviFiiMcxT|$rMQS}rm8G?o9XTKVp z%69BG_M}1y9mJpKv(09j$fZ!kI1h|x7}K$ZdsMQ0T*set8Q{nJX%RC8vEEPtY1ryD z*O}+zn4)~PkgMfk14_E(&fKq#m*U!{z-N+Bc(jp~@SzekhNK$09qYI!Eq3yaw7%;Q z-Hc#-6*usLwKY~EXCUOoc`Mr#Z>-cT$*2S*fYAYE20*r_@3+(8g?-@C=h7c+LbYIv z@I4-mIhIU5K6c%oFszVY?q}L~G*&+TO3>+s8MGt4DzI{?T=}c@1&gRdD?wVrt4>HO;NKIk*nX z8$v>8InaW}1BH6gv=_D7)0W0wI8OyQRPAz-&Z5FzmdiEFA3e&p9xmf|9-!g^bz7?x zE^EK3D|!P6eMCIq#-6A{?3#}1;rBUcEenmR?X9)!k6thW~q^=d)=ij|H@rKR{2gLOOAy9 zEQ|y(FY3Z_|B3syt_)(lWw$F#IXO&XH^5L?`!1(e- z23prR9s%;bIn_pt*MVhB0GUN%1*X!h0 zOuK40u&%+2&XrA!-JJdBDI1)>mQ$He`XnQ=$)KSP`VZ{wUm(JTpC@9<)i$Led2b6n(LBZ@&$z=m`C zH~w4?%h;8gZn?t))cG--cZYAsbMK5g@w#jjn!HLI(~QIl*6XKLt6#FOg_NG`g(qU<6?WH@sLU6= zYJQv5mA0*jM6{{EN$YWFUZQ8RE1PjV@&%m48+C(ors=Y=J8=@LN{cSizN|J6+b;p^ zD1*u1*V;KCE<~KxC!H|lLcN#sz>k>lJ^I+tTT)36HTAl&x?}dIGZca- zA2Py{yVmq&(&;7*I|%ZD!fXxqMLdx;dDEQPL8Yhv-G=rO0PJS4X1IvUH-o@^J%KGK zO_xTuFQME%5xJAruF&R3;@xvU?d}WUB9vfdj}i7y0u=iKdQ4bv5&%_PwvTvrTo^`b zWXUP5of`SkAP~XRVn0@_XP{qZ8NF#kY*QcF7X+JevpRmh8qK7vUpa917k_I00jc8h zaw7JA)TvSif%q39t31@zB9Ufx?(W-OT*_B7MW$zeCe&8Xm4#^u#D%`z+ng&i%&~tQ zLF@`tcqXXxaQGv*^gSK(yKSU^GPpj zB^U;zRK^R*_vPK>a(&kLoo@2Ex}(`~t!0b^)1KP7Kavb$%*3D$3Vigm&T(FXFYF=N z>y}EK*Q4TWOU)Qwl|w>dw`9{9;IL(>JZsE3+bZ&Xt8bnh2S~QWW|qR^&7dO+Ta?z^ zH>RqdxS2Lj%&%3+!fJRqe#d4DH66_toMjt-4vr}50{dP-;~-?X5>_!izd^J{D ztLPGTD}&#%m1l@6OR>Z_HMzVCkY;5!C<{Sbn>zWGlTwHAN-wHQtf0Fe*Kr3f_cfzS z(w=JsJc!Kay7NT7@zGb}N&;)oCLrK^McS+&cW0ez>#Gf|c7?+UPeH&ZFXN8|2-~PyOx5cKGSwyR^lKiW+}2$S`f|^m4_=t z!JI4-=Y;}xS33_V?GIlVZ|98&b$&U_@*T?o3=DaR8IjaypqFK4#xn`Stmq@%|CB67 z6?n!+D}TlliaBhcd-__>?uiD&gw1!+bZCuxFmIXS-i5u9hR7-ZdmiNP*7b~nYOF<7M z$mTCbbsQe^^OBgLI|Fe7A0e0=8%@$dNWec3EIA&4&IZd@lW zsCxhHiN|kg1F0W03&Afi-AkRzmTu!<(R5ScTTRps&O5)yq{ds6Z$QYXH5I@9GNeg$ zl=}N%zayo#H^urDymZ#l#;r?*pg#Q}6#DGXk_Lr7t<9YP<40}fTOg5?C4T~Sl;+p) zz@so)oI;H%76gX64R^j51QMo@802%qk9Fr8wk}xu_iQ?SyeGamy!a`)2p-QqMBrQ_ z8$n|vYc*ZXerfyCYw=3xdt|i;|5C#b+W)V%ua1f;YWr1`ju|?oOG3H_hLVy-T4@An z1SAKfyFpSyQW}&H>23t1JBRKVU}m`E``!1(`qur{``&f${CCcrz0TSD+56f1sozic zyC@`^m1m?UVHzI=9G819^9dCfa1Kb*Pl*ra{kYlKexwiDKx$}oEO6(2d0HBHwjM;v z)diVX|4d6dDzvdVVx8vS&H1NDKh*C1?NICKM)dI$W0~TdzB%;#tauerwATbSM!ryM zP~T5z_ECZTPydw2W~!U}+J-3dzPZ+}lqI`*RJB=IIty?l8grintH(Tt&#-66s61Z- zMwK+%if7@4kU(> zj>4qVtReTp;t|K(zSkrLw_Y_??-?^tRvCJi9XpK$ zQ09MS&d1Zf5%LKLM3a@q!g|a=|MXwD2=qf$u!g>jIa8juXiKQ4*dl=Aof=ce%UVNhi!Q-S845Gb62~g zIF*A>nFJHi<)oi5@%|FokC|w1I7nac6t~SH!NKC(ETVt*=qEJSvvJv{9PHkL&Cmnk zDV%b?VS3c~$+f(Fx&KS;nofi7P1P95)VBBHg-j7((^-E|gnzlC!*~rQPF+yWcPF)Z zB(@_r3Ec=#*68Wobq{EAH_pyjsjTp)|g*L`4CF(*cBF_%mcTxAW52uch?Z&_xnFqXot6IBsOp6qc02j)B-??Z(peM5Unkk^eC#&-CBzE>C>9NYTb z4K99c&%?rf5~$Kx zWn}_p=49oPFljXPvaXFP0?v7CRCm08yBBG(J>d|6+gwX4bApNvc4Pv^TW^X1H$xDN zN|6<>ZdBQh>?(3fbwhkkfeAwjaVg;MiQ)n(4sp|qIM=SM9t)Tz20XX?fxiL7GRnpf5SXJ-H7V7`qA=E)(3}pNc62L?UrcpTNWY) z&iP9cGzJG(R=`qu&Aop03rQET_=_U1cQXU}HzQ8dQ8F-;g*T#tcp6)Teq3rO;rksDGoB z4vaQ@8<;l96&;T#O*2`;7uT*iBp%_uG}#=x3y->%7L2b0@x}I62DQ*nIt}HuHC1TtBr2-o$h2##xGMgO zI^};pmiGd+S?xY)7Znec0X3Zsz!|>mH?Hy1K*ndEw{cU`8aX%Y z2q!wzaE6JA=iVcuJI~<_vQ&tV0JM8tPg1_y9iqI+%FOhu`>_gU?pm`SfS7Ve^2UT7O)oE>@kIi`?maH_Ll+~^ z+ozGa_P6>Mt4LH?rPF&_G~36U&Tl?MRwqN%oBjQC7=W^;x$l;!@E(r5+9tnT$nCsd z67Yn>6j%DG9&C4*Pk4;B7WIa6eLm~HYQ0j49tRa}pcc$e`>M8uXklF);qmGIosm6l zI+)$U#Dt*vSzx;2?}pKqiIVtlfCZZN&c2qgwpfVTtxX!6 zg5z9M=MmpEi1??$Z82@EQd%M_pe{*K!|zk!;@fHnMI%o;fAwxh{E!-N1QoCAPnS{$ z#CjDtjQ!@<>cdLw1Co(W#?2cJ?OU-&C`0wCpubV)l%6@!L|p^*7tIay%5Nd zoT=&YB0Rz0l9AfyJ$40u!XyU026<9+;#r(?3hT99T`wx2Ev~t4KK{I7fhSIsBXTm$ zs0Yn)Q}2zB{&U}I>~uy~R?4QAy-AzLGX-EE(yL3xrCW@6=!uA~r=YW_`gNUYK{oK_ z?R#p+P&~ZUrE~+*{g$nb=w%K658?NbSBUGo59#_Yxg~?aOXUTMHsydY-L*QUm_Hf1 zJRUXX-`|aL{L*iVgXZS??czd-{25d+f9-aCcnR8g(R?cFSy{>7hwH28FuE`~WQB6Z z)YUOIVR|L@a}b`TGmS`9XS)x&ahMS$ztZ9+AmcO$vlNK=kz>6C0HRy+3-7_OsFTw% z$@PH4984#3i>Sn9GU{&)lVm2RFloh1&!b>RMQ&g)CEGacZgx0oJww8AK`F@7=z=^tFPTkWE}vt>n>D($24_De*K4Sk>JBb* z%y^7wa>Hi)_;u$S;@iVF9^0gQ4koF)P*3E@J;z%|4&^x$Cd!aD_nnOSArVwOZ2JE8 z&Br~FC~NhM4|O-;otP09fH2d|Eaz6;9^`|atvLI-L(5&}R2*=1^2gd(g$KzEXG zVAJ8`t5yz5yW;njcwMKl(b$b&u4Et=qaxABc`Q5Lm3Ndkhc`|?e=6M}V4LyJRLtrr zkQ<$i@n`EUHK`y9ieM@n z?J?(wfPp5xBNqqIFX)Nu!Djq0qWYJ;f75ak@CRb${+MS)VkFYygUcNa$I@msNmKFsTrF9{+@fY`~op9-Q22qCIEk_d$VpA3rPKgy|~Ki}Kx? zTL;Q@Ca{F>jSQQvU`D%CwYc{;{QkqruqFd*K3f!-d*v-lwOQn{|3@c^xyvh+=o~;j z(~#XXa!hbBIn+&1|IJbrO;mX7N}%a3+vj4I-lWfh32UKk0c=Hng-ecxN93N>zW}C;LRd7F)K+U zu?A?OT?zRo!l!Y`&#hMDLi%K1x75V<=xT!^p!;g}{tV{>+qkO~)+mZMAr}|N`z+dF z<_b%DeJKmLn(p>VC>crjup8ccp#oiQVqj>~VrvRRj$vBesWtUa-FDUQt+wZCU_MUz z!46pQoATtW`tW!{R8>VQ8L?xXcIz8`z6ZFJ8-VoByS2t6W(h5=sd94Xn0?Qh&!dJV z13$ZEIgGk$+6c@ zPwwB&hsAx41w57_aDegtvCsgWS@DVahjX3B@Sue~^vE+|Y$%ds4>md>mLPX~CB2~P z3?J$?LRgUBjf99#JFVX%`iHeuax$_$I_InX4~nRAg>bJ&slUu%FW2q~;X$1Q57d51 zLF3PulT;0UQYxaCn^59=_v;gI$9?zO z6Za#b+4S~-N4eo(Xtvu&Etq-jtpLW8MpMK&-SSc6@;*em`^?V2Q<7^X7Kp}(yG|P) zAR!AXEk=$Ndhc!PM=>+hbe^!oqy#YNX2bBFOmoy22UG^CZT-?RA8=AH53JecAB^;x+!(za2neie)XI<;@fo7Y_9)KhS7Hq zV;`)uX$rTfW zO=J=HD#3d1b_s>0Hk9<&#N(p#ftjxofY}hoo>3PSb$#`2c{$egI=X2uombnsHxqZd zdq~4Bm>r#k;PdNKE+y)eNgROu`uk%s_tTgJp?s!+g0Y0m#*wf(BRf!DS$ zmOS^dLXDEKY_qK@UBf{}Lc|bb^Xa26r0CpZp<@_7Sz6L{xJA4*7Z1ALxqDA6i2@|y z35n9q?iH`QaXEN${8F%zgPRBkeETq1Rbd8{Y4e0IPmh-)XSjoo|I~3uuz60Nf=Pn4%+tWSD zgkv%e$$8_ZmK)&(5zj)onGQn%RNl7?Pf@7?ot&gjiw)d7-{LJ_at$b#PPCqdT@`U` z=j^#v^Ss%Ox+;@oy$;jE%n1P7}RbEQpuf0T0M{S+`h3$Hp_V$28cSwB6+Bk3G#27=tyhZum!U z7}GoVT2HK&sEW-KI~kIObC31+ch}e%W&=`J&lJe4io^!YV^(WjO~c90V4~}o4{&k~ zJ1f=o&@bFo$%4Rh&A!7@#4S&NEBJ05;73h?X<$T!29X4ybO7CSDe_MW07?r}(qEjIoq(O1LI9`} z@%(Q0<%t=J%E;zboIaKT6!V(2onhc6T%shN2?D7!}}+yov;iHMy-Y0U#%Y) zqFOJ{hfQn)|MDeV8h_3ukfMbd_VjK1)<{3W+ro-?5Bpw(UmFHqhIl4YB*cMft~;Gj z{-YI4oZK&K_lcnb(Gr_%N!DB`aZDdRp8&0wJp+^j!{IPJ|IMUVep;pIeNr5SHqj6v z*)K55+E2*8pp|^Sb*r*(V7>Y-a^^B0;$%KknmbwsI>5ylQi}0SE7_n@x*&MYE#DV= zD^Wk&QYh4OW$z_DeGfVv51fTuv5X%!fI(L$t<6Q2PMoLe0tg#7E%K#(gWNWKa8_Di z@@E)Xa*lfaBjX|$<%C$>u&1ILLc0urPc54_&cYda&u2o;j@_xlMf*7&_`J9vaWM9; z%`-Yj%)}gKt_XArey&sa1y4gQsNmc^TzZ7J@)&)9U%Y9Zhs^i2UeZ@h3`iPOXMvy(?lPKLZ_HwDFW#q zxUOnqSYnUk4UG}!eA`JniJu)`) zAsQd%vm&FN6Wt3B7ckd@N>f5Fr)+#kOFms(ADM=k^_mV<_0>&UKP%vJ+Ipq7^2Kdq zot%}tlv5JfL!9&bW%EqwaZ|+;OqG#)n5g$8S%3e!dvn#?YRj#_jeZt<)m9}b)kK{R zf-yZK; zl_(?q;_Ryt8pC!$M3d_#tLI=ZIl1niE{I~5)-S(#J>(w#^&g#L&^u#?io#38NkFw% zgl;r_hAi)<{Z-hI8vnV;%&z3?H8=L}H zDPHfhqBOr%GnR5L-vO7l855T{^>4-en2o&Kh7xI#cSU$m@J|e-k;9q6%Y2YX+{qV9^vyEw9iBY=S`0K>zeD?vC$QkLD_$ zqwt0vx6rW^{Ig_V5?$LdOkAGlW;cXeOS{3YHv8Hp|C+!3f1LDEfmjc;CeNj4;-`8a zKGM)s8e)QsD`34vQM#D^SiiBY;u?Fd7*r zEadaYoK(Sp;jb_0C=!d#ErwyQ^51+|`3WUMa1BHQXrje74~Q0hQI^{E(K1x?AzIlR zMp3&fjs(FT7u}PkuehDG9#gf?`PHp?u#FqxiO@1s*W?-G_5(aXuSo= ziGFqFkLyEP49ddY7A^~n4n%kNj{LD^Xpb4*YCJ4kcd<}deT)!!#^=|sFfZ_Q@qLi$ z6+ae#0A!bedc5k|%6bSN5_P||u1M?3E>~13AXS=QfpW5oP~XhL_#|L&`ZoRfh5a}B z5nc{>x0|ci#}a++A*NjvR8Yxu^B#sZ+dM&PJiOC+|C?pcQ95r8(ZgO3i{2HB5cCy6 zQ>_LwF53iF!t-qJ565qaDVrxlG*^yNcwd&5Ncek)t$MB^mbG-9o}H#Nv8D6)A#WR^ z#uGK947IYM{Nv?T&$8$~_w}o6?~F2JWUbl9@CZcHzi~j$jAxm;`#t;^HO(~bd0+ZV z+c0%unmFDK0*qIVE?6TT&Hma*o&6lIYxMyM4JbR<)BI?I6#~p&f5gZek&ZJnyVHcE zG`o1@tK$wC@xJRoLhhHPvMy;omuis=y<47J16s!h#(b8mZLR+Gadss2=VM40WJT{m6sga zi2#7ih}-e~(SstdI|lfbit2%rbwj&RmVWZmZJEBAgHOr{tkf(n_TP2kQ$`|VWg<3Z zCJckQN1u2N{AO?pS8MZru11_y_AU6}ZYp0Vb zks%dI!-5eK(E<}N$>o(_gDEwT@U-@i2R(sU+Y)=AUvHOmk162A}D zJhPsY#;LUS?-ZhRyhZUXzn!YjYa?vgTHM0icq>F#X7P#Q#;a{&=Gr8E)2}{6>tXY@ zZvKzbW_700X9>fnE*k$2uxIf0fcqPDApjTyP9Kg(d5CDEZ&}aM zi;kt09I8Kj?`oR4`h|=7v7a+*`tN~VtpdrmquW`;1sK6&3S=JhjByEct!z>^Zg8mF zr&^zt%`rka*>c{}7p;|OSAy+MdCGcuu<4S_cN)J>!)*uBY+(H}8}`Zidd{)IHPJXs z$ZZtWZ&?}LWrLeF=&PwTO@#;7z9V!YYY{h)=8Oy;lIwjfb`a0G2>iGpAp*~{`Qlxi z#b?<}?gc}E?|D?6nD}m6_hQnS!u@Wjm2qm51oy-_>H7EAgNGRJfhrV@3?H2le&x{6 z*Am5gcE-+PQkH#qOaZy8Veq(zT@%m8VfZ1+E>$ z6EV!D)Sj8$VcmSTpIkor1F!i-sIhN}{Ma8Ku!YDH>-tW@mu!S+1gIZe*jwjE6FW>q zRq@Je(!cZUNf+t%MbR}5LN3?&nSfP%Ci*&B^5J}V>oS)OZ-W!PuTKy|03%{mzW8vG z9(rrDnm7t0N zv_PP+FFi1hT4d~%nH*@{oQN2^iM1w)p(g(;@jx&Mt;z2pj{UwTUl>x{Ed?ZWN$tK= zsQ2`3#biA8rFawz2JQ`DAIAbbiGj z`sE{Ay_pC6tY2*4yQ)$rF3sM<6>o&3VVQCL=g|K;?)z;p@@p}9l+iv81O;ln zQq5wV^}{T^F-bP7r_~|67Wm$6jL`7<*u>+L@(_v=OQMAzzg#wtWbTVy;9E#!M#uDK z@PJ8o0}}!@&WV11`USoh^fk>+UyvzfI6UL)Ynv`lo=zznHN|xmcyXcWGd7i$5k2@X z#JvB<9{e{>9=JegR%_{iLh7owQQz)}&o5}h%P$?x0^`ac^rRoHQfEibi`&(kaKeqm z>&!$PLq!eBJ$l+p$#P4(zkmj=KGD0CCR@=H4Eac*l@_3ARHtJFla{V#-yV>!wcqFX zj?7j=z@QNBF|qr@uK-c`y|pJM;{pJwwr6L)72yDmAZds2^7Y)d8Kofo^bLo#H`0RP zPY1%9k5B+ephV!b(m<(96GqVU#kEdFuZ=ane$^M7==2z_Q=aXc$|2!*O>@Tg2|MAK z&>;@<)SSTLeZcn@*q@=TBZS3TG8v z_w46vx!JxL34HTGwHxZ5$@a3#;TJr~-z%t7W#W}FiAUlnZU*j8c=nZMoCdp@_X06# z-O01Ti(6l8nyVp4?w)sh0*)gFds4Snp}|(Sq01NC+}!>kO1r2}K#Y=k`7gSLycp6L zzsC}Mn$N8mgiZb?{N!i))^Q$AX^8v^b>=#(rEu>Ro7HA57c$oXU&I)jLte4|d+PC@ zNfpxL(q_BAdjecDp1&FR=X7%yL4!H320T1jNxPl1jK8vyb~~sV!`VY&tsVY7V^>4tc!J=1vSV(d2CMMKO`1k9F3E`1NcuByPS{us@TC!0JfJ5q$quZd$Eo+3$nLwGkC|zf?`kZE1^NJgNXfbH zIRKAdPS>i|D z1^buNDc=MosRQFHKItn!!FEP)=S_ z6M-~(Ev%h&Me~r|91wmg!8ie&uNA9NFV|VWYRx?E2n>E>xhn6HB3)zyYF`q|G*-`K(*R~O?DDiSR3Pl6&M`W|Hom~`HkeBS_i zp?WuL8{&-7k_~xl^Cr0T+mbi%3rQnL&7_!1)#ZT}lj1xDe-9@>Rzcx?0x78`3mj!2 zI!*6Sr$+sDWzdv=Bn+2lT6>g0$63*y8lg)ZOonkT6_-5g7X?OLQCkxN9*83ITqTp~D+nO`_zp*H+;Yi>-3S+6Yl&B@sXC74% z5wj{XBqVfoF%+DDxn3*;pxWyFPW2{OUpZ8Iw-+N;V=mlM0Y`AfOqLe(+oz+RcFy^U zNi-c61C;@XhI;TL_YuD2WiM*>=T^g?D;pg5&N$v*p*0C?XG^&ir{#r+rZ@EOfK0B@ zBnMtBkfxa)d)*Fw7h-jeJf{b+8IHfqn|Lk3BO1_e@{I$HzEJ!%hdKvhwP&t>enhe+QreJXv{Fyp0BcSp}_*6~OLmASmAkjJz; zlioCYzD;-Am6a8L${2)?uRyY;WJ!15-idZ?OtIL4W}a~1D1?@;g|&>bRbnaTHWa8i zT}PVu{Lu+XFhx)bmQ^)d1d38c)FaG&3_3ev3j}zo`*F@yX#D3XDj@7c!x1pDW1{{;A8U-|X18E|&<*Cj9yR zVU(yYuNIU0H9Zj#X5jCL5J;aa{f{pQ1&R>qh&TrAuu3uo-I}6pP7#y_6+r=(TZB@J zIJlTn7`nh=XT?gQo%j@Us%lQNV_%%^#lwoiR6b~Frr(lL3WD5G82%mzPQcQ7ChrP--W3+6(7w8c9a8w+ zajEGHv&Am27DFxMcMLcI3y_{^R!-{Kk%HBJejmBDj>Uv`?aA$Hw6rstwX9Dj;tzlN zH~D*ZDO)f`8%3nB{qg(J{O0xC%;Y4ahwP_B?Em^(|F3qz4)f?o9Kk4jog2HLC$S`= zH9zN@ydwwb^8DVb1XBxFc%pc5-0xLIfxVr@ZID>L^imwEz42tM<;Oa74p#jG!H@`% zN|}LB1rCQ01FS%lh?M(C!@;%LMsV<`ptp7D7_Xpo!AC`BdUKMz*;?iy2>YN{hqNc$R)>_bds*~=Sh z?DJRi{=XM=AS!Udm_vGu?H%GKT#aRW282A^rZ&%G4g`xc{oLM%2l@kv;4Wh7dj zK7(oq0u%_ml6mFV@ftgr$XxSj2%eJM;BeRvbDegg`$Tscb6~v;N4U}e#vU-T7m2{6 zIIH57bDZQPO-C1#N@vjkNOu^OQ1LdP}E`2S=+X& z_LY}MW#8apd;9d!;(aRl$zr2tE_8!ssadblbJgTVcUYfwVRpmF$Y=>gn&V*4r@rDp z9v_^BSzYz*TQ&5LmWFKED}Q=43mT2DYio&p{_AUob7?X)()?VA%=7%BOLRf$KKor` zTbeD94xONn1s&5q`e#PcxvJ_2mAlK@!*>bTV(!h%jD_cO(?4E&-vVak8wPCy$bm!BglB|$S(JkSX3yCW>{TgngBCaCs&E~pp27L|< zQ5u^Kik)(94Io`)w{o#cz^mu47+x*DV;eHw^Wu1=URTN+RFqPr$)VhmmCPO&)$MwH z?(*FF`RklAFJ?9^8q)A+Z-FO&pLo}`Wv2dBl!E}8@=ojT4fVKec`>acGNm-u00$(+ zW_c_=!%BSop~3#7t@TaZ_#07+V3B+OmaEu0u=(nTu(~Lrg{H}Oa2AbxKk$}jz$Bij z%`MEhv|ZG{!8AZSxO$**_tUh@&o&m1Z>uUCKjy(B+R*B|pIyu00r@s)zE=6@WCzs& zNl|(%7xC=R9o585*4d69l*Mz$F`qZS{(J%@qZ|h-k=fm}sw1l1A1H;ddg(&GbG->X zKgf;o7UoE-mD`L@H4MK8zZaDp7Ga{yg%5_%?pP+BHIBmcbMFAxx8=yKuJE>=(j+{& zW~5nS`f|VjDZb*I{sRD_Z~-`=@j=qgT*RMB4g!davsG)efw{ltOoZjDXCt9- zhZDI`Dw_RbA~%|+P*cne-9BP`b5SX73m$!w3)b%FpEd31CwKCOXwmY$N;xF2w)R7Q z&N(#8Os}5Ce3dWzdPd_Pva})95!X{O#9^uJwLiL-{qQZS9|y+#T*3t@#Uzxe+X0SL zMW~7JN5vIvxTG*M8K54uX2uPdW&9NpTJFgA)(!cx;`=GjHJyEUJK5iQqI3yb{3d zsiF&E{l}b}emDoRGw9-M7mh2FDE6*rPjZ!|)R7DgCjMiNiWPnHUw4K`Zvt+VbZ=DE zzq-FZhUelyV&Sk1J?(%mE8r1y#S5NzSKj~?{Yq4``f zrp?X<`A;j(^)=1dbG0knfP*LVn;P%h2lovA-r_3pszHqqhr(yfe++6I$+N$s|=xD>p%Q#;c%*PuqV!NJ)HQ} z*KUXIYur6VV#15T)QdE6zV4yib^s}oTcjPBE1;sy5!pa;jdKRqY;*P$)w8utN(v@p z%teO~r98#0yzrC4y%_%WfzMG?5BAcGH!}FpCSShkHzGjm)E$EWV@B)|>ZkltUA{`z HEa<-h=`tst literal 0 HcmV?d00001 diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 8ea2de271..2a2088fdf 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -3,7 +3,7 @@ "displayName": "GSD-2", "description": "VS Code integration for the GSD-2 coding agent — sidebar dashboard, @gsd chat participant, activity feed, conversation history, code lens, session forking, slash command completion, workflow controls, and 33 commands", "publisher": "FluxLabs", - "version": "0.2.0", + "version": "0.3.0", "icon": "logo.jpg", "license": "MIT", "repository": { @@ -168,6 +168,67 @@ { "command": "gsd.generateTestsSymbol", "title": "GSD: Generate Tests for Symbol" + }, + { + "command": "gsd.acceptAllChanges", + "title": "GSD: Accept All Agent Changes", + "icon": "$(check-all)" + }, + { + "command": "gsd.discardAllChanges", + "title": "GSD: Discard All Agent Changes", + "icon": "$(discard)" + }, + { + "command": "gsd.acceptFileChanges", + "title": "Accept Changes", + "icon": "$(check)" + }, + { + "command": "gsd.discardFileChanges", + "title": "Discard Changes", + "icon": "$(discard)" + }, + { + "command": "gsd.restoreCheckpoint", + "title": "GSD: Restore Checkpoint" + }, + { + "command": "gsd.fixProblemsInFile", + "title": "GSD: Fix Problems in File" + }, + { + "command": "gsd.fixAllProblems", + "title": "GSD: Fix All Problems" + }, + { + "command": "gsd.clearDiagnostics", + "title": "GSD: Clear Agent Diagnostics" + }, + { + "command": "gsd.commitAgentChanges", + "title": "GSD: Commit Agent Changes" + }, + { + "command": "gsd.createAgentBranch", + "title": "GSD: Create Branch for Agent Work" + }, + { + "command": "gsd.showAgentDiff", + "title": "GSD: Show Agent Diff" + }, + { + "command": "gsd.clearPlan", + "title": "GSD: Clear Plan View", + "icon": "$(clear-all)" + }, + { + "command": "gsd.cycleApprovalMode", + "title": "GSD: Cycle Approval Mode" + }, + { + "command": "gsd.selectApprovalMode", + "title": "GSD: Select Approval Mode" } ], "keybindings": [ @@ -240,6 +301,30 @@ "when": "view == gsd-activity", "group": "navigation" } + ], + "scm/title": [ + { + "command": "gsd.acceptAllChanges", + "group": "navigation", + "when": "scmProvider == gsd" + }, + { + "command": "gsd.discardAllChanges", + "group": "navigation", + "when": "scmProvider == gsd" + } + ], + "scm/resourceState/context": [ + { + "command": "gsd.acceptFileChanges", + "group": "inline", + "when": "scmProvider == gsd" + }, + { + "command": "gsd.discardFileChanges", + "group": "inline", + "when": "scmProvider == gsd" + } ] }, "chatParticipants": [ @@ -276,7 +361,7 @@ }, "gsd.showProgressNotifications": { "type": "boolean", - "default": true, + "default": false, "description": "Show progress notification while the agent is working" }, "gsd.activityFeedMaxItems": { @@ -297,6 +382,17 @@ "minimum": 50, "maximum": 95, "description": "Context window usage percentage that triggers a warning" + }, + "gsd.approvalMode": { + "type": "string", + "default": "auto-approve", + "enum": ["auto-approve", "ask", "plan-only"], + "enumDescriptions": [ + "Agent runs freely without prompts", + "Prompt before file changes and commands", + "Read-only mode — agent can analyze but not modify" + ], + "description": "Approval mode for agent actions" } } } diff --git a/vscode-extension/src/change-tracker.ts b/vscode-extension/src/change-tracker.ts new file mode 100644 index 000000000..f10191d65 --- /dev/null +++ b/vscode-extension/src/change-tracker.ts @@ -0,0 +1,295 @@ +import * as vscode from "vscode"; +import * as fs from "node:fs"; +import type { GsdClient, AgentEvent } from "./gsd-client.js"; + +export interface FileSnapshot { + uri: vscode.Uri; + originalContent: string; + timestamp: number; +} + +export interface Checkpoint { + id: number; + label: string; + timestamp: number; + /** Map of file path → original content at checkpoint creation time */ + snapshots: Map; +} + +/** + * Tracks file changes made by the GSD agent. Stores original file content + * before the agent modifies it, enabling diff views, SCM integration, + * and checkpoint/rollback functionality. + */ +export class GsdChangeTracker implements vscode.Disposable { + /** file path → original content (before first agent modification this session) */ + private originals = new Map(); + /** Set of file paths modified in the current agent turn */ + private currentTurnFiles = new Set(); + /** Ordered list of checkpoints */ + private _checkpoints: Checkpoint[] = []; + private nextCheckpointId = 1; + /** toolUseId → file path for in-flight tool executions */ + private pendingTools = new Map(); + /** Whether the current turn has been described in the checkpoint label */ + private turnDescribed = false; + + private readonly _onDidChange = new vscode.EventEmitter(); + /** Fires when the set of tracked files changes. Payload is array of changed file paths. */ + readonly onDidChange = this._onDidChange.event; + + private readonly _onCheckpointChange = new vscode.EventEmitter(); + readonly onCheckpointChange = this._onCheckpointChange.event; + + private disposables: vscode.Disposable[] = []; + + constructor(private readonly client: GsdClient) { + this.disposables.push(this._onDidChange, this._onCheckpointChange); + + this.disposables.push( + client.onEvent((evt) => this.handleEvent(evt)), + client.onConnectionChange((connected) => { + if (!connected) { + this.reset(); + } + }), + ); + } + + /** All file paths that have been modified by the agent */ + get modifiedFiles(): string[] { + return [...this.originals.keys()]; + } + + /** Get the original content of a file (before agent first modified it) */ + getOriginal(filePath: string): string | undefined { + return this.originals.get(filePath); + } + + /** Whether the tracker has any modifications */ + get hasChanges(): boolean { + return this.originals.size > 0; + } + + /** Current checkpoints (newest first) */ + get checkpoints(): readonly Checkpoint[] { + return this._checkpoints; + } + + /** + * Discard agent changes to a single file — restore original content. + * Returns true if the file was restored. + */ + async discardFile(filePath: string): Promise { + const original = this.originals.get(filePath); + if (original === undefined) return false; + + try { + await fs.promises.writeFile(filePath, original, "utf8"); + this.originals.delete(filePath); + this._onDidChange.fire([filePath]); + return true; + } catch { + return false; + } + } + + /** + * Discard all agent changes — restore all files to their original state. + */ + async discardAll(): Promise { + let count = 0; + const paths = [...this.originals.keys()]; + for (const filePath of paths) { + if (await this.discardFile(filePath)) { + count++; + } + } + return count; + } + + /** + * Accept changes to a file — remove from tracking (keep the current content). + */ + acceptFile(filePath: string): void { + if (this.originals.delete(filePath)) { + this._onDidChange.fire([filePath]); + } + } + + /** + * Accept all changes — clear all tracking. + */ + acceptAll(): void { + const paths = [...this.originals.keys()]; + this.originals.clear(); + if (paths.length > 0) { + this._onDidChange.fire(paths); + } + } + + /** + * Restore all files to a checkpoint state. + */ + async restoreCheckpoint(checkpointId: number): Promise { + const idx = this._checkpoints.findIndex((c) => c.id === checkpointId); + if (idx === -1) return 0; + + const checkpoint = this._checkpoints[idx]; + let count = 0; + + for (const [filePath, content] of checkpoint.snapshots) { + try { + await fs.promises.writeFile(filePath, content, "utf8"); + count++; + } catch { + // skip files that can't be restored + } + } + + // Reset originals to the checkpoint state + this.originals = new Map(checkpoint.snapshots); + + // Remove all checkpoints after this one + this._checkpoints = this._checkpoints.slice(0, idx); + + this._onDidChange.fire([...checkpoint.snapshots.keys()]); + this._onCheckpointChange.fire(); + return count; + } + + /** Clear all tracking state */ + reset(): void { + const paths = [...this.originals.keys()]; + this.originals.clear(); + this.currentTurnFiles.clear(); + this.pendingTools.clear(); + this._checkpoints = []; + this.nextCheckpointId = 1; + if (paths.length > 0) { + this._onDidChange.fire(paths); + } + this._onCheckpointChange.fire(); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } + + private handleEvent(evt: AgentEvent): void { + switch (evt.type) { + case "agent_start": + this.createCheckpoint(); + this.currentTurnFiles.clear(); + this.turnDescribed = false; + break; + + case "tool_execution_start": { + const toolName = String(evt.toolName ?? ""); + const toolInput = (evt.toolInput ?? {}) as Record; + const toolUseId = String(evt.toolUseId ?? ""); + + // Update checkpoint label with first action description + if (!this.turnDescribed) { + this.turnDescribed = true; + this.updateLatestCheckpointLabel(describeAction(toolName, toolInput)); + } + + if (toolName !== "Write" && toolName !== "Edit") break; + + const filePath = String(toolInput.file_path ?? toolInput.path ?? ""); + + if (!filePath) break; + + // Store the original content before the agent modifies it + // Only capture on FIRST modification (don't overwrite) + if (!this.originals.has(filePath)) { + try { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf8"); + this.originals.set(filePath, content); + } else { + // File doesn't exist yet — original is "empty" (new file) + this.originals.set(filePath, ""); + } + } catch { + // Can't read file, skip tracking + } + } + + if (toolUseId) { + this.pendingTools.set(toolUseId, filePath); + } + break; + } + + case "tool_execution_end": { + const toolUseId = String(evt.toolUseId ?? ""); + const filePath = this.pendingTools.get(toolUseId); + if (filePath) { + this.pendingTools.delete(toolUseId); + this.currentTurnFiles.add(filePath); + this._onDidChange.fire([filePath]); + } + break; + } + } + } + + private createCheckpoint(): void { + const now = Date.now(); + const time = new Date(now).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + const fileCount = this.originals.size; + const label = fileCount > 0 + ? `${time} (${fileCount} file${fileCount !== 1 ? "s" : ""} tracked)` + : `${time} (start)`; + + const checkpoint: Checkpoint = { + id: this.nextCheckpointId++, + label, + timestamp: now, + snapshots: new Map(this.originals), + }; + this._checkpoints.push(checkpoint); + this._onCheckpointChange.fire(); + } + + /** + * Update the label of the latest checkpoint with a description + * of the first action taken (called after first tool execution in a turn). + */ + private updateLatestCheckpointLabel(description: string): void { + if (this._checkpoints.length === 0) return; + const latest = this._checkpoints[this._checkpoints.length - 1]; + const time = new Date(latest.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + latest.label = `${time} — ${description}`; + this._onCheckpointChange.fire(); + } +} + +function describeAction(toolName: string, input: Record): string { + switch (toolName) { + case "Read": { + const p = String(input.file_path ?? input.path ?? ""); + return `Read ${p.split(/[\\/]/).pop() ?? p}`; + } + case "Write": { + const p = String(input.file_path ?? ""); + return `Write ${p.split(/[\\/]/).pop() ?? p}`; + } + case "Edit": { + const p = String(input.file_path ?? ""); + return `Edit ${p.split(/[\\/]/).pop() ?? p}`; + } + case "Bash": + return `$ ${String(input.command ?? "").slice(0, 40)}`; + case "Grep": + return `Grep: ${String(input.pattern ?? "").slice(0, 30)}`; + case "Glob": + return `Glob: ${String(input.pattern ?? "").slice(0, 30)}`; + default: + return toolName; + } +} diff --git a/vscode-extension/src/chat-participant.ts b/vscode-extension/src/chat-participant.ts index 01647e1ad..6ba3e60e2 100644 --- a/vscode-extension/src/chat-participant.ts +++ b/vscode-extension/src/chat-participant.ts @@ -39,6 +39,21 @@ export function registerChatParticipant( message = `${fileContext}\n\n${message}`; } + // Auto-include editor selection if present and not already referenced + const selectionContext = getSelectionContext(); + if (selectionContext) { + message = `${selectionContext}\n\n${message}`; + } + + // Auto-include diagnostics for the active file if the prompt mentions "fix", "error", "problem", "warning" + const fixKeywords = /\b(fix|error|problem|warning|issue|bug|lint|diagnos)/i; + if (fixKeywords.test(message)) { + const diagContext = getActiveDiagnosticsContext(); + if (diagContext) { + message = `${message}\n\n${diagContext}`; + } + } + // Track streaming state let agentDone = false; let totalInputTokens = 0; @@ -281,3 +296,42 @@ function resolveFileUri(fp: string): vscode.Uri | null { return null; } } + +/** + * Get the current editor selection as context, if any text is selected. + */ +function getSelectionContext(): string | null { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.selection.isEmpty) return null; + + const selection = editor.document.getText(editor.selection); + if (!selection.trim()) return null; + + const relativePath = vscode.workspace.asRelativePath(editor.document.uri); + const { start, end } = editor.selection; + return `Selected code in \`${relativePath}\` (lines ${start.line + 1}-${end.line + 1}):\n\`\`\`\n${selection}\n\`\`\``; +} + +/** + * Get diagnostics (errors/warnings) for the active editor file. + */ +function getActiveDiagnosticsContext(): string | null { + const editor = vscode.window.activeTextEditor; + if (!editor) return null; + + const diagnostics = vscode.languages.getDiagnostics(editor.document.uri); + const significant = diagnostics.filter( + (d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning, + ); + if (significant.length === 0) return null; + + const relativePath = vscode.workspace.asRelativePath(editor.document.uri); + const lines = [`Current diagnostics in \`${relativePath}\`:`]; + for (const d of significant) { + const sev = d.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"; + const line = d.range.start.line + 1; + const source = d.source ? ` [${d.source}]` : ""; + lines.push(`- ${sev} (line ${line}): ${d.message}${source}`); + } + return lines.join("\n"); +} diff --git a/vscode-extension/src/checkpoints.ts b/vscode-extension/src/checkpoints.ts new file mode 100644 index 000000000..584c9011c --- /dev/null +++ b/vscode-extension/src/checkpoints.ts @@ -0,0 +1,55 @@ +import * as vscode from "vscode"; +import type { GsdChangeTracker, Checkpoint } from "./change-tracker.js"; + +/** + * TreeDataProvider that shows agent checkpoints (one per agent turn). + * Each checkpoint can be restored to revert all file changes since that point. + */ +export class GsdCheckpointProvider implements vscode.TreeDataProvider, vscode.Disposable { + public static readonly viewId = "gsd-checkpoints"; + + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private disposables: vscode.Disposable[] = []; + + constructor(private readonly tracker: GsdChangeTracker) { + this.disposables.push( + this._onDidChangeTreeData, + tracker.onCheckpointChange(() => this._onDidChangeTreeData.fire()), + ); + } + + getTreeItem(checkpoint: Checkpoint): vscode.TreeItem { + const fileCount = checkpoint.snapshots.size; + const time = new Date(checkpoint.timestamp); + const timeStr = time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + + const item = new vscode.TreeItem( + checkpoint.label, + vscode.TreeItemCollapsibleState.None, + ); + item.description = `${timeStr} (${fileCount} file${fileCount !== 1 ? "s" : ""})`; + item.iconPath = new vscode.ThemeIcon("history"); + item.tooltip = `Checkpoint: ${checkpoint.label}\nTime: ${time.toLocaleString()}\nFiles tracked: ${fileCount}\n\nClick to restore to this point`; + item.contextValue = "checkpoint"; + item.command = { + command: "gsd.restoreCheckpoint", + title: "Restore Checkpoint", + arguments: [checkpoint.id], + }; + + return item; + } + + getChildren(): Checkpoint[] { + // Show newest first + return [...this.tracker.checkpoints].reverse(); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } +} diff --git a/vscode-extension/src/diagnostics.ts b/vscode-extension/src/diagnostics.ts new file mode 100644 index 000000000..cd25ccfee --- /dev/null +++ b/vscode-extension/src/diagnostics.ts @@ -0,0 +1,142 @@ +import * as vscode from "vscode"; +import type { GsdClient } from "./gsd-client.js"; + +/** + * Integrates with VS Code's diagnostic system: + * - Reads diagnostics (errors/warnings) from the Problems panel and sends them to the agent + * - Provides a DiagnosticCollection for the agent to surface its own findings + */ +export class GsdDiagnosticBridge implements vscode.Disposable { + private readonly collection: vscode.DiagnosticCollection; + private disposables: vscode.Disposable[] = []; + + constructor(private readonly client: GsdClient) { + this.collection = vscode.languages.createDiagnosticCollection("gsd"); + this.disposables.push(this.collection); + } + + /** + * Read all diagnostics for the active file and send them to the agent + * as a "fix these problems" prompt. + */ + async fixProblemsInFile(): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showWarningMessage("No active file to fix."); + return; + } + + const uri = editor.document.uri; + const diagnostics = vscode.languages.getDiagnostics(uri); + + if (diagnostics.length === 0) { + vscode.window.showInformationMessage("No problems found in this file."); + return; + } + + const fileName = vscode.workspace.asRelativePath(uri); + const problemText = formatDiagnostics(fileName, diagnostics); + + const prompt = [ + `Fix the following problems in \`${fileName}\`:`, + "", + problemText, + "", + "Fix all of these issues. Show me the changes.", + ].join("\n"); + + await this.client.sendPrompt(prompt); + } + + /** + * Read all diagnostics across the workspace (errors only) and send + * them to the agent as a "fix all errors" prompt. + */ + async fixAllProblems(): Promise { + const allDiagnostics = vscode.languages.getDiagnostics(); + const errorFiles: { fileName: string; diagnostics: vscode.Diagnostic[] }[] = []; + + for (const [uri, diagnostics] of allDiagnostics) { + // Only include errors and warnings, skip hints/info + const significant = diagnostics.filter( + (d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning, + ); + if (significant.length > 0) { + errorFiles.push({ + fileName: vscode.workspace.asRelativePath(uri), + diagnostics: significant, + }); + } + } + + if (errorFiles.length === 0) { + vscode.window.showInformationMessage("No errors or warnings found in the workspace."); + return; + } + + // Cap at 20 files to avoid overwhelming the agent + const capped = errorFiles.slice(0, 20); + const totalProblems = capped.reduce((sum, f) => sum + f.diagnostics.length, 0); + + const sections = capped.map((f) => formatDiagnostics(f.fileName, f.diagnostics)); + + const prompt = [ + `Fix the following ${totalProblems} problems across ${capped.length} file${capped.length > 1 ? "s" : ""}:`, + "", + ...sections, + "", + "Fix all of these issues.", + ].join("\n"); + + await this.client.sendPrompt(prompt); + } + + /** + * Add a GSD diagnostic (agent finding) to a file. + * Can be used to surface agent review findings in the Problems panel. + */ + addFinding( + uri: vscode.Uri, + range: vscode.Range, + message: string, + severity: vscode.DiagnosticSeverity = vscode.DiagnosticSeverity.Warning, + ): void { + const existing = this.collection.get(uri) ?? []; + const diagnostic = new vscode.Diagnostic(range, message, severity); + diagnostic.source = "GSD Agent"; + this.collection.set(uri, [...existing, diagnostic]); + } + + /** Clear all GSD diagnostics */ + clearFindings(): void { + this.collection.clear(); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } +} + +function formatDiagnostics(fileName: string, diagnostics: vscode.Diagnostic[]): string { + const lines = [`**${fileName}**`]; + for (const d of diagnostics) { + const severity = severityLabel(d.severity); + const line = d.range.start.line + 1; + const col = d.range.start.character + 1; + const source = d.source ? ` [${d.source}]` : ""; + lines.push(` - ${severity} (line ${line}:${col}): ${d.message}${source}`); + } + return lines.join("\n"); +} + +function severityLabel(severity: vscode.DiagnosticSeverity): string { + switch (severity) { + case vscode.DiagnosticSeverity.Error: return "Error"; + case vscode.DiagnosticSeverity.Warning: return "Warning"; + case vscode.DiagnosticSeverity.Information: return "Info"; + case vscode.DiagnosticSeverity.Hint: return "Hint"; + default: return "Unknown"; + } +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index d909c4e12..f5e494240 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -9,12 +9,24 @@ import { GsdConversationHistoryPanel } from "./conversation-history.js"; import { GsdSlashCompletionProvider } from "./slash-completion.js"; import { GsdCodeLensProvider } from "./code-lens.js"; import { GsdActivityFeedProvider } from "./activity-feed.js"; +import { GsdChangeTracker } from "./change-tracker.js"; +import { GsdScmProvider } from "./scm-provider.js"; +import { GsdDiagnosticBridge } from "./diagnostics.js"; +import { GsdLineDecorationManager } from "./line-decorations.js"; +import { GsdGitIntegration } from "./git-integration.js"; +import { GsdPermissionManager } from "./permissions.js"; let client: GsdClient | undefined; let sidebarProvider: GsdSidebarProvider | undefined; let fileDecorations: GsdFileDecorationProvider | undefined; let sessionTreeProvider: GsdSessionTreeProvider | undefined; let activityFeedProvider: GsdActivityFeedProvider | undefined; +let changeTracker: GsdChangeTracker | undefined; +let scmProvider: GsdScmProvider | undefined; +let diagnosticBridge: GsdDiagnosticBridge | undefined; +let lineDecorations: GsdLineDecorationManager | undefined; +let gitIntegration: GsdGitIntegration | undefined; +let permissionManager: GsdPermissionManager | undefined; function requireConnected(): boolean { if (!client?.isConnected) { @@ -128,6 +140,34 @@ export function activate(context: vscode.ExtensionContext): void { vscode.window.registerTreeDataProvider(GsdActivityFeedProvider.viewId, activityFeedProvider), ); + // -- Change tracker & SCM provider ------------------------------------- + + changeTracker = new GsdChangeTracker(client); + context.subscriptions.push(changeTracker); + + scmProvider = new GsdScmProvider(changeTracker, cwd); + context.subscriptions.push(scmProvider); + + // -- Diagnostics ------------------------------------------------------- + + diagnosticBridge = new GsdDiagnosticBridge(client); + context.subscriptions.push(diagnosticBridge); + + // -- Line-level decorations -------------------------------------------- + + lineDecorations = new GsdLineDecorationManager(changeTracker!); + context.subscriptions.push(lineDecorations); + + // -- Git integration --------------------------------------------------- + + gitIntegration = new GsdGitIntegration(changeTracker!, cwd); + context.subscriptions.push(gitIntegration); + + // -- Permissions ------------------------------------------------------- + + permissionManager = new GsdPermissionManager(client); + context.subscriptions.push(permissionManager); + // -- Progress notifications -------------------------------------------- let currentProgress: { resolve: () => void } | undefined; @@ -789,6 +829,135 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + // -- SCM commands ------------------------------------------------------- + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.acceptAllChanges", () => { + changeTracker?.acceptAll(); + vscode.window.showInformationMessage("All agent changes accepted."); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.discardAllChanges", async () => { + if (!changeTracker?.hasChanges) { + vscode.window.showInformationMessage("No agent changes to discard."); + return; + } + const confirm = await vscode.window.showWarningMessage( + `Discard all agent changes (${changeTracker.modifiedFiles.length} files)?`, + { modal: true }, + "Discard", + ); + if (confirm === "Discard") { + const count = await changeTracker.discardAll(); + vscode.window.showInformationMessage(`Reverted ${count} file${count !== 1 ? "s" : ""}.`); + } + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.discardFileChanges", async (resourceState: vscode.SourceControlResourceState) => { + if (!changeTracker || !resourceState?.resourceUri) return; + const filePath = resourceState.resourceUri.fsPath; + const success = await changeTracker.discardFile(filePath); + if (success) { + vscode.window.showInformationMessage(`Reverted ${vscode.workspace.asRelativePath(filePath)}`); + } + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.acceptFileChanges", (resourceState: vscode.SourceControlResourceState) => { + if (!changeTracker || !resourceState?.resourceUri) return; + changeTracker.acceptFile(resourceState.resourceUri.fsPath); + }), + ); + + // -- Checkpoint commands ------------------------------------------------ + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.restoreCheckpoint", async (checkpointId: number) => { + if (!changeTracker) return; + const checkpoint = changeTracker.checkpoints.find((c) => c.id === checkpointId); + if (!checkpoint) return; + + const confirm = await vscode.window.showWarningMessage( + `Restore to "${checkpoint.label}"? This will revert files to their state at ${new Date(checkpoint.timestamp).toLocaleTimeString()}.`, + { modal: true }, + "Restore", + ); + if (confirm === "Restore") { + const count = await changeTracker.restoreCheckpoint(checkpointId); + vscode.window.showInformationMessage(`Restored ${count} file${count !== 1 ? "s" : ""} to checkpoint.`); + } + }), + ); + + // -- Diagnostic commands ------------------------------------------------ + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.fixProblemsInFile", async () => { + if (!requireConnected()) return; + try { + await diagnosticBridge!.fixProblemsInFile(); + } catch (err) { + handleError(err, "Failed to fix problems"); + } + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.fixAllProblems", async () => { + if (!requireConnected()) return; + try { + await diagnosticBridge!.fixAllProblems(); + } catch (err) { + handleError(err, "Failed to fix problems"); + } + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.clearDiagnostics", () => { + diagnosticBridge?.clearFindings(); + }), + ); + + // -- Permission commands ------------------------------------------------ + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.cycleApprovalMode", () => { + permissionManager?.cycleMode(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.selectApprovalMode", () => { + permissionManager?.selectMode(); + }), + ); + + // -- Git commands ------------------------------------------------------- + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.commitAgentChanges", () => { + gitIntegration?.commitAgentChanges(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.createAgentBranch", () => { + gitIntegration?.createAgentBranch(); + }), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("gsd.showAgentDiff", () => { + gitIntegration?.showAgentDiff(); + }), + ); + // -- Auto-start --------------------------------------------------------- if (config.get("autoStart", false)) { @@ -802,9 +971,21 @@ export function deactivate(): void { fileDecorations?.dispose(); sessionTreeProvider?.dispose(); activityFeedProvider?.dispose(); + changeTracker?.dispose(); + scmProvider?.dispose(); + diagnosticBridge?.dispose(); + lineDecorations?.dispose(); + gitIntegration?.dispose(); + permissionManager?.dispose(); client = undefined; sidebarProvider = undefined; fileDecorations = undefined; sessionTreeProvider = undefined; activityFeedProvider = undefined; + changeTracker = undefined; + scmProvider = undefined; + diagnosticBridge = undefined; + lineDecorations = undefined; + gitIntegration = undefined; + permissionManager = undefined; } diff --git a/vscode-extension/src/git-integration.ts b/vscode-extension/src/git-integration.ts new file mode 100644 index 000000000..dbec79dba --- /dev/null +++ b/vscode-extension/src/git-integration.ts @@ -0,0 +1,122 @@ +import * as vscode from "vscode"; +import { exec } from "node:child_process"; +import type { GsdChangeTracker } from "./change-tracker.js"; + +/** + * Provides git integration for agent changes — commit, branch, and diff. + */ +export class GsdGitIntegration implements vscode.Disposable { + private disposables: vscode.Disposable[] = []; + + constructor( + private readonly tracker: GsdChangeTracker, + private readonly cwd: string, + ) {} + + /** + * Commit all files modified by the agent with a user-provided message. + */ + async commitAgentChanges(): Promise { + const files = this.tracker.modifiedFiles; + if (files.length === 0) { + vscode.window.showInformationMessage("No agent changes to commit."); + return; + } + + const defaultMsg = `feat: agent changes (${files.length} file${files.length !== 1 ? "s" : ""})`; + const message = await vscode.window.showInputBox({ + prompt: "Commit message for agent changes", + value: defaultMsg, + placeHolder: "feat: describe the changes", + }); + if (!message) return; + + try { + // Stage the modified files + await this.git(`add ${files.map((f) => `"${f}"`).join(" ")}`); + // Commit + await this.git(`commit -m "${message.replace(/"/g, '\\"')}"`); + + // Accept all changes (clear tracking since they're committed) + this.tracker.acceptAll(); + + vscode.window.showInformationMessage(`Committed ${files.length} file${files.length !== 1 ? "s" : ""}.`); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Git commit failed: ${msg}`); + } + } + + /** + * Create a new branch for agent work and switch to it. + */ + async createAgentBranch(): Promise { + const branchName = await vscode.window.showInputBox({ + prompt: "Branch name for agent work", + placeHolder: "feat/agent-changes", + validateInput: (value) => { + if (!value.trim()) return "Branch name is required"; + if (/\s/.test(value)) return "Branch name cannot contain spaces"; + return null; + }, + }); + if (!branchName) return; + + try { + await this.git(`checkout -b "${branchName}"`); + vscode.window.showInformationMessage(`Created and switched to branch: ${branchName}`); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Failed to create branch: ${msg}`); + } + } + + /** + * Show a git diff of all agent-modified files. + */ + async showAgentDiff(): Promise { + const files = this.tracker.modifiedFiles; + if (files.length === 0) { + vscode.window.showInformationMessage("No agent changes to diff."); + return; + } + + try { + const diff = await this.git("diff"); + if (!diff.trim()) { + // Files may be untracked — show status instead + const status = await this.git("status --short"); + const channel = vscode.window.createOutputChannel("GSD Git Diff"); + channel.appendLine("# Agent-modified files (unstaged):"); + channel.appendLine(status); + channel.show(); + } else { + const channel = vscode.window.createOutputChannel("GSD Git Diff"); + channel.clear(); + channel.appendLine(diff); + channel.show(); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Git diff failed: ${msg}`); + } + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } + + private git(args: string): Promise { + return new Promise((resolve, reject) => { + exec(`git ${args}`, { cwd: this.cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => { + if (err) { + reject(new Error(stderr.trim() || err.message)); + } else { + resolve(stdout); + } + }); + }); + } +} diff --git a/vscode-extension/src/gsd-client.ts b/vscode-extension/src/gsd-client.ts index b8ae2bc35..b2a872c5e 100644 --- a/vscode-extension/src/gsd-client.ts +++ b/vscode-extension/src/gsd-client.ts @@ -123,11 +123,10 @@ export class GsdClient implements vscode.Disposable { return; } - const proc = spawn(this.binaryPath, ["--mode", "rpc", "--no-session"], { + const proc = spawn(this.binaryPath, ["--mode", "rpc"], { cwd: this.cwd, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env }, - shell: process.platform === "win32", }); this.process = proc; @@ -580,10 +579,104 @@ export class GsdClient implements vscode.Disposable { return; } + // Extension UI request — agent needs user input + if (data.type === "extension_ui_request" && typeof data.id === "string") { + void this.handleUIRequest(data); + return; + } + // Streaming event this._onEvent.fire(data as AgentEvent); } + private async handleUIRequest(request: Record): Promise { + const id = request.id as string; + const method = request.method as string; + + try { + switch (method) { + case "select": { + const options = (request.options as string[]) ?? []; + const title = String(request.title ?? "Select"); + const allowMultiple = request.allowMultiple === true; + + if (allowMultiple) { + const picked = await vscode.window.showQuickPick(options, { + title, + canPickMany: true, + }); + if (picked) { + this.sendRaw({ type: "extension_ui_response", id, values: picked }); + } else { + this.sendRaw({ type: "extension_ui_response", id, cancelled: true }); + } + } else { + const picked = await vscode.window.showQuickPick(options, { title }); + if (picked) { + this.sendRaw({ type: "extension_ui_response", id, value: picked }); + } else { + this.sendRaw({ type: "extension_ui_response", id, cancelled: true }); + } + } + break; + } + + case "confirm": { + const title = String(request.title ?? "Confirm"); + const message = String(request.message ?? ""); + const result = await vscode.window.showInformationMessage( + `${title}: ${message}`, + { modal: true }, + "Yes", + "No", + ); + this.sendRaw({ type: "extension_ui_response", id, confirmed: result === "Yes" }); + break; + } + + case "input": { + const title = String(request.title ?? "Input"); + const placeholder = String(request.placeholder ?? ""); + const value = await vscode.window.showInputBox({ title, placeHolder: placeholder }); + if (value !== undefined) { + this.sendRaw({ type: "extension_ui_response", id, value }); + } else { + this.sendRaw({ type: "extension_ui_response", id, cancelled: true }); + } + break; + } + + case "notify": { + const message = String(request.message ?? ""); + const notifyType = String(request.notifyType ?? "info"); + if (notifyType === "error") { + vscode.window.showErrorMessage(`GSD: ${message}`); + } else if (notifyType === "warning") { + vscode.window.showWarningMessage(`GSD: ${message}`); + } else { + vscode.window.showInformationMessage(`GSD: ${message}`); + } + // Notify doesn't need a response + break; + } + + default: + // Unknown method — cancel to unblock the agent + this.sendRaw({ type: "extension_ui_response", id, cancelled: true }); + break; + } + } catch { + // On error, cancel to unblock + this.sendRaw({ type: "extension_ui_response", id, cancelled: true }); + } + } + + private sendRaw(data: Record): void { + if (this.process?.stdin) { + this.process.stdin.write(JSON.stringify(data) + "\n"); + } + } + private send(command: Record): Promise { if (!this.process?.stdin) { return Promise.reject(new Error("GSD client not started")); diff --git a/vscode-extension/src/line-decorations.ts b/vscode-extension/src/line-decorations.ts new file mode 100644 index 000000000..387986f79 --- /dev/null +++ b/vscode-extension/src/line-decorations.ts @@ -0,0 +1,130 @@ +import * as vscode from "vscode"; +import type { GsdChangeTracker } from "./change-tracker.js"; + +/** + * Provides line-level editor decorations for files modified by the GSD agent. + * Shows subtle background highlights on changed lines and gutter icons. + */ +export class GsdLineDecorationManager implements vscode.Disposable { + private readonly addedDecoration: vscode.TextEditorDecorationType; + private readonly modifiedDecoration: vscode.TextEditorDecorationType; + private readonly gutterDecoration: vscode.TextEditorDecorationType; + private disposables: vscode.Disposable[] = []; + + constructor(private readonly tracker: GsdChangeTracker) { + this.addedDecoration = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + backgroundColor: "rgba(78, 201, 176, 0.07)", + overviewRulerColor: "rgba(78, 201, 176, 0.5)", + overviewRulerLane: vscode.OverviewRulerLane.Left, + }); + + this.modifiedDecoration = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + backgroundColor: "rgba(204, 167, 0, 0.07)", + overviewRulerColor: "rgba(204, 167, 0, 0.5)", + overviewRulerLane: vscode.OverviewRulerLane.Left, + }); + + this.gutterDecoration = vscode.window.createTextEditorDecorationType({ + gutterIconPath: new vscode.ThemeIcon("hubot").id, // fallback + gutterIconSize: "contain", + // Use a colored left border as a gutter indicator (more reliable than icons) + borderWidth: "0 0 0 3px", + borderStyle: "solid", + borderColor: "rgba(78, 201, 176, 0.4)", + }); + + this.disposables.push( + this.addedDecoration, + this.modifiedDecoration, + this.gutterDecoration, + ); + + // Refresh decorations when tracked files change + this.disposables.push( + tracker.onDidChange(() => this.refreshAll()), + vscode.window.onDidChangeActiveTextEditor(() => this.refreshAll()), + vscode.workspace.onDidChangeTextDocument((e) => { + const editor = vscode.window.activeTextEditor; + if (editor && e.document === editor.document) { + this.refreshEditor(editor); + } + }), + ); + } + + private refreshAll(): void { + for (const editor of vscode.window.visibleTextEditors) { + this.refreshEditor(editor); + } + } + + private refreshEditor(editor: vscode.TextEditor): void { + const filePath = editor.document.uri.fsPath; + const original = this.tracker.getOriginal(filePath); + + if (original === undefined) { + // No tracked changes for this file — clear decorations + editor.setDecorations(this.addedDecoration, []); + editor.setDecorations(this.modifiedDecoration, []); + editor.setDecorations(this.gutterDecoration, []); + return; + } + + const currentLines = editor.document.getText().split("\n"); + const originalLines = original.split("\n"); + const { added, modified } = diffLines(originalLines, currentLines); + + const addedRanges = added.map((line) => { + const range = new vscode.Range(line, 0, line, currentLines[line]?.length ?? 0); + return { range, hoverMessage: new vscode.MarkdownString("$(hubot) *Added by GSD Agent*") }; + }); + + const modifiedRanges = modified.map((line) => { + const range = new vscode.Range(line, 0, line, currentLines[line]?.length ?? 0); + return { range, hoverMessage: new vscode.MarkdownString("$(hubot) *Modified by GSD Agent*") }; + }); + + const gutterRanges = [...added, ...modified].map((line) => ({ + range: new vscode.Range(line, 0, line, 0), + })); + + editor.setDecorations(this.addedDecoration, addedRanges); + editor.setDecorations(this.modifiedDecoration, modifiedRanges); + editor.setDecorations(this.gutterDecoration, gutterRanges); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } +} + +/** + * Simple line-level diff: compare original vs current line-by-line. + * Returns arrays of line numbers that were added or modified. + */ +function diffLines( + originalLines: string[], + currentLines: string[], +): { added: number[]; modified: number[] } { + const added: number[] = []; + const modified: number[] = []; + + const maxShared = Math.min(originalLines.length, currentLines.length); + + for (let i = 0; i < maxShared; i++) { + if (originalLines[i] !== currentLines[i]) { + modified.push(i); + } + } + + // Lines beyond original length are "added" + for (let i = originalLines.length; i < currentLines.length; i++) { + added.push(i); + } + + return { added, modified }; +} diff --git a/vscode-extension/src/permissions.ts b/vscode-extension/src/permissions.ts new file mode 100644 index 000000000..32bcc9511 --- /dev/null +++ b/vscode-extension/src/permissions.ts @@ -0,0 +1,143 @@ +import * as vscode from "vscode"; +import type { GsdClient, AgentEvent } from "./gsd-client.js"; + +type ApprovalMode = "ask" | "auto-approve" | "plan-only"; + +/** + * Permission/approval system for agent actions. + * Can be configured to prompt before file writes, command execution, etc. + */ +export class GsdPermissionManager implements vscode.Disposable { + private _mode: ApprovalMode = "auto-approve"; + private disposables: vscode.Disposable[] = []; + + private readonly _onModeChange = new vscode.EventEmitter(); + readonly onModeChange = this._onModeChange.event; + + constructor(private readonly client: GsdClient) { + // Load saved mode from configuration + this._mode = vscode.workspace.getConfiguration("gsd").get("approvalMode", "auto-approve"); + + this.disposables.push( + this._onModeChange, + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("gsd.approvalMode")) { + this._mode = vscode.workspace.getConfiguration("gsd").get("approvalMode", "auto-approve"); + this._onModeChange.fire(this._mode); + } + }), + ); + + // If mode is "ask", intercept tool executions for write operations + if (this._mode === "ask") { + this.disposables.push( + client.onEvent((evt) => this.handleEvent(evt)), + ); + } + } + + get mode(): ApprovalMode { + return this._mode; + } + + /** + * Cycle through approval modes: auto-approve -> ask -> plan-only -> auto-approve + */ + async cycleMode(): Promise { + const modes: ApprovalMode[] = ["auto-approve", "ask", "plan-only"]; + const currentIdx = modes.indexOf(this._mode); + this._mode = modes[(currentIdx + 1) % modes.length]; + + await vscode.workspace.getConfiguration("gsd").update("approvalMode", this._mode, vscode.ConfigurationTarget.Workspace); + this._onModeChange.fire(this._mode); + + const labels: Record = { + "auto-approve": "Auto-Approve (agent runs freely)", + "ask": "Ask (prompt before file changes)", + "plan-only": "Plan Only (read-only, no writes)", + }; + vscode.window.showInformationMessage(`Approval mode: ${labels[this._mode]}`); + } + + /** + * Show a QuickPick to select approval mode. + */ + async selectMode(): Promise { + const items: (vscode.QuickPickItem & { mode: ApprovalMode })[] = [ + { + label: "$(check) Auto-Approve", + description: "Agent runs freely without prompts", + detail: "Best for trusted workflows. The agent can read, write, and execute without asking.", + mode: "auto-approve", + }, + { + label: "$(shield) Ask", + description: "Prompt before file changes", + detail: "The agent will ask for approval before writing or editing files.", + mode: "ask", + }, + { + label: "$(eye) Plan Only", + description: "Read-only mode, no writes allowed", + detail: "The agent can read and analyze but cannot modify files or run commands.", + mode: "plan-only", + }, + ]; + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Current mode: ${this._mode}`, + }); + + if (selected) { + this._mode = selected.mode; + await vscode.workspace.getConfiguration("gsd").update("approvalMode", this._mode, vscode.ConfigurationTarget.Workspace); + this._onModeChange.fire(this._mode); + } + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } + + private async handleEvent(evt: AgentEvent): Promise { + if (this._mode !== "ask") return; + if (evt.type !== "tool_execution_start") return; + + const toolName = String(evt.toolName ?? ""); + if (toolName !== "Write" && toolName !== "Edit" && toolName !== "Bash") return; + + const toolInput = (evt.toolInput ?? {}) as Record; + let description = ""; + + switch (toolName) { + case "Write": + case "Edit": { + const filePath = String(toolInput.file_path ?? ""); + const shortPath = filePath.split(/[\\/]/).slice(-3).join("/"); + description = `${toolName}: ${shortPath}`; + break; + } + case "Bash": { + const cmd = String(toolInput.command ?? "").slice(0, 80); + description = `Execute: ${cmd}`; + break; + } + } + + // Note: In practice, the RPC protocol doesn't support blocking tool execution + // for approval. This notification serves as awareness — the user sees what's + // happening and can abort if needed. True blocking approval would require + // protocol changes in the RPC server. + vscode.window.showInformationMessage( + `Agent: ${description}`, + "OK", + "Abort", + ).then((choice) => { + if (choice === "Abort") { + this.client.abort().catch(() => {}); + } + }); + } +} diff --git a/vscode-extension/src/plan-viewer.ts b/vscode-extension/src/plan-viewer.ts new file mode 100644 index 000000000..a45b20978 --- /dev/null +++ b/vscode-extension/src/plan-viewer.ts @@ -0,0 +1,190 @@ +import * as vscode from "vscode"; +import type { GsdClient, AgentEvent } from "./gsd-client.js"; + +interface PlanStep { + id: number; + tool: string; + description: string; + status: "pending" | "running" | "done" | "error"; + timestamp: number; + duration?: number; +} + +/** + * TreeDataProvider that shows a plan-like view of agent tool executions. + * Displays steps as they happen, showing what the agent is doing and + * what it has completed — a live execution plan. + */ +export class GsdPlanViewerProvider implements vscode.TreeDataProvider, vscode.Disposable { + public static readonly viewId = "gsd-plan"; + + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private steps: PlanStep[] = []; + private nextId = 0; + private runningTools = new Map(); // toolUseId -> step id + private disposables: vscode.Disposable[] = []; + + constructor(private readonly client: GsdClient) { + this.disposables.push( + this._onDidChangeTreeData, + client.onEvent((evt) => this.handleEvent(evt)), + client.onConnectionChange((connected) => { + if (!connected) { + this.steps = []; + this.runningTools.clear(); + this._onDidChangeTreeData.fire(); + } + }), + ); + } + + getTreeItem(step: PlanStep): vscode.TreeItem { + const icon = stepIcon(step.status); + const item = new vscode.TreeItem(step.description, vscode.TreeItemCollapsibleState.None); + item.iconPath = icon; + item.description = step.duration !== undefined ? `${step.duration}ms` : step.status === "running" ? "running..." : ""; + + const time = new Date(step.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + item.tooltip = `${step.tool}: ${step.description}\nStatus: ${step.status}\nTime: ${time}`; + + return item; + } + + getChildren(): PlanStep[] { + return this.steps; + } + + clear(): void { + this.steps = []; + this.runningTools.clear(); + this._onDidChangeTreeData.fire(); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } + + private handleEvent(evt: AgentEvent): void { + switch (evt.type) { + case "agent_start": { + // Don't clear — keep history visible. Add a separator. + if (this.steps.length > 0) { + this.steps.push({ + id: this.nextId++, + tool: "separator", + description: "--- New Turn ---", + status: "done", + timestamp: Date.now(), + }); + } + this.steps.push({ + id: this.nextId++, + tool: "agent", + description: "Agent started", + status: "running", + timestamp: Date.now(), + }); + this._onDidChangeTreeData.fire(); + break; + } + + case "agent_end": { + // Mark the agent step as done + const agentStep = [...this.steps].reverse().find((s) => s.tool === "agent" && s.status === "running"); + if (agentStep) { + agentStep.status = "done"; + agentStep.duration = Date.now() - agentStep.timestamp; + agentStep.description = "Agent finished"; + } + this._onDidChangeTreeData.fire(); + break; + } + + case "tool_execution_start": { + const toolName = String(evt.toolName ?? ""); + const toolInput = (evt.toolInput ?? {}) as Record; + const toolUseId = String(evt.toolUseId ?? ""); + const description = describeStep(toolName, toolInput); + + const id = this.nextId++; + this.steps.push({ + id, + tool: toolName, + description, + status: "running", + timestamp: Date.now(), + }); + + if (toolUseId) { + this.runningTools.set(toolUseId, id); + } + + // Cap at 200 steps + while (this.steps.length > 200) { + this.steps.shift(); + } + + this._onDidChangeTreeData.fire(); + break; + } + + case "tool_execution_end": { + const toolUseId = String(evt.toolUseId ?? ""); + const stepId = this.runningTools.get(toolUseId); + if (stepId !== undefined) { + this.runningTools.delete(toolUseId); + const step = this.steps.find((s) => s.id === stepId); + if (step) { + const isError = evt.error === true || evt.isError === true; + step.status = isError ? "error" : "done"; + step.duration = Date.now() - step.timestamp; + this._onDidChangeTreeData.fire(); + } + } + break; + } + } + } +} + +function stepIcon(status: string): vscode.ThemeIcon { + switch (status) { + case "running": + return new vscode.ThemeIcon("sync~spin", new vscode.ThemeColor("charts.yellow")); + case "done": + return new vscode.ThemeIcon("pass", new vscode.ThemeColor("testing.iconPassed")); + case "error": + return new vscode.ThemeIcon("error", new vscode.ThemeColor("testing.iconFailed")); + default: + return new vscode.ThemeIcon("circle-outline"); + } +} + +function describeStep(toolName: string, input: Record): string { + switch (toolName) { + case "Read": { + const p = String(input.file_path ?? input.path ?? ""); + return `Read ${p.split(/[\\/]/).pop() ?? p}`; + } + case "Write": { + const p = String(input.file_path ?? ""); + return `Write ${p.split(/[\\/]/).pop() ?? p}`; + } + case "Edit": { + const p = String(input.file_path ?? ""); + return `Edit ${p.split(/[\\/]/).pop() ?? p}`; + } + case "Bash": + return `$ ${String(input.command ?? "").slice(0, 50)}`; + case "Grep": + return `Grep: ${String(input.pattern ?? "").slice(0, 40)}`; + case "Glob": + return `Glob: ${String(input.pattern ?? "").slice(0, 40)}`; + default: + return toolName; + } +} diff --git a/vscode-extension/src/scm-provider.ts b/vscode-extension/src/scm-provider.ts new file mode 100644 index 000000000..2320ab6d5 --- /dev/null +++ b/vscode-extension/src/scm-provider.ts @@ -0,0 +1,124 @@ +import * as vscode from "vscode"; +import * as path from "node:path"; +import type { GsdChangeTracker } from "./change-tracker.js"; + +const GSD_ORIGINAL_SCHEME = "gsd-original"; + +/** + * Source Control provider that shows files modified by the GSD agent + * in a dedicated "GSD Agent" section of the Source Control panel. + * Supports QuickDiff to show before/after diffs, and accept/discard per-file. + */ +export class GsdScmProvider implements vscode.Disposable { + private readonly scm: vscode.SourceControl; + private readonly changesGroup: vscode.SourceControlResourceGroup; + private readonly contentProvider: GsdOriginalContentProvider; + private disposables: vscode.Disposable[] = []; + + constructor( + private readonly tracker: GsdChangeTracker, + private readonly workspaceRoot: string, + ) { + // Register content provider for original file contents + this.contentProvider = new GsdOriginalContentProvider(tracker); + this.disposables.push( + vscode.workspace.registerTextDocumentContentProvider( + GSD_ORIGINAL_SCHEME, + this.contentProvider, + ), + ); + + // Create source control instance + this.scm = vscode.scm.createSourceControl( + "gsd", + "GSD Agent", + vscode.Uri.file(workspaceRoot), + ); + this.scm.quickDiffProvider = { + provideOriginalResource: (uri: vscode.Uri): vscode.Uri | undefined => { + const filePath = uri.fsPath; + if (this.tracker.getOriginal(filePath) !== undefined) { + return uri.with({ scheme: GSD_ORIGINAL_SCHEME }); + } + return undefined; + }, + }; + this.scm.inputBox.placeholder = "Describe changes to accept..."; + this.scm.acceptInputCommand = { + command: "gsd.acceptAllChanges", + title: "Accept All", + }; + this.scm.count = 0; + this.disposables.push(this.scm); + + // Create resource group + this.changesGroup = this.scm.createResourceGroup("changes", "Agent Changes"); + this.changesGroup.hideWhenEmpty = true; + this.disposables.push(this.changesGroup); + + // Listen for change tracker updates + this.disposables.push( + tracker.onDidChange(() => this.refresh()), + ); + + this.refresh(); + } + + private refresh(): void { + const files = this.tracker.modifiedFiles; + this.changesGroup.resourceStates = files.map((filePath) => { + const uri = vscode.Uri.file(filePath); + const fileName = path.basename(filePath); + const relativePath = path.relative(this.workspaceRoot, filePath); + + const state: vscode.SourceControlResourceState = { + resourceUri: uri, + decorations: { + strikeThrough: false, + tooltip: `Modified by GSD Agent`, + light: { iconPath: new vscode.ThemeIcon("edit") }, + dark: { iconPath: new vscode.ThemeIcon("edit") }, + }, + command: { + command: "vscode.diff", + title: "Show Changes", + arguments: [ + uri.with({ scheme: GSD_ORIGINAL_SCHEME }), + uri, + `${fileName} (GSD Agent Changes)`, + ], + }, + }; + return state; + }); + this.scm.count = files.length; + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } +} + +/** + * TextDocumentContentProvider that serves the original (pre-agent) content + * of files via the `gsd-original:` URI scheme. + */ +class GsdOriginalContentProvider implements vscode.TextDocumentContentProvider { + private readonly _onDidChange = new vscode.EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + constructor(private readonly tracker: GsdChangeTracker) { + tracker.onDidChange((paths) => { + for (const p of paths) { + this._onDidChange.fire(vscode.Uri.file(p).with({ scheme: GSD_ORIGINAL_SCHEME })); + } + }); + } + + provideTextDocumentContent(uri: vscode.Uri): string { + const filePath = uri.with({ scheme: "file" }).fsPath; + return this.tracker.getOriginal(filePath) ?? ""; + } +} diff --git a/vscode-extension/src/session-tree.ts b/vscode-extension/src/session-tree.ts index e61898e0a..a38413be4 100644 --- a/vscode-extension/src/session-tree.ts +++ b/vscode-extension/src/session-tree.ts @@ -56,18 +56,35 @@ export class GsdSessionTreeProvider implements vscode.TreeDataProvider_.jsonl - const match = file.match(/^(\d+)_(.+)\.jsonl$/); - if (!match) { + const sessionFile = path.join(sessionDir, file); + + // Try two filename formats: + // 1. ISO timestamp: 2026-03-23T17-49-05-784Z_.jsonl + // 2. Unix timestamp: _.jsonl + const isoMatch = file.match(/^(\d{4}-\d{2}-\d{2}T[\d-]+Z)_(.+)\.jsonl$/); + const unixMatch = file.match(/^(\d{10,})_(.+)\.jsonl$/); + + let timestamp: Date; + let sessionId: string; + + if (isoMatch) { + // Convert ISO-like format (dashes instead of colons) back to parseable ISO + const isoStr = isoMatch[1].replace(/(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})-(\d+)Z/, "$1:$2:$3.$4Z"); + timestamp = new Date(isoStr); + sessionId = isoMatch[2]; + } else if (unixMatch) { + timestamp = new Date(parseInt(unixMatch[1], 10)); + sessionId = unixMatch[2]; + } else { continue; } - const ts = parseInt(match[1], 10); - const sessionId = match[2]; - const sessionFile = path.join(sessionDir, file); + + if (isNaN(timestamp.getTime())) continue; + items.push({ - label: formatDate(new Date(ts)), + label: formatDate(timestamp), sessionFile, - timestamp: new Date(ts), + timestamp, sessionId, isCurrent: sessionFile === state.sessionFile, }); diff --git a/vscode-extension/src/sidebar.ts b/vscode-extension/src/sidebar.ts index 12c718633..b8bb2aee0 100644 --- a/vscode-extension/src/sidebar.ts +++ b/vscode-extension/src/sidebar.ts @@ -2,8 +2,17 @@ import * as vscode from "vscode"; import type { GsdClient, SessionStats, ThinkingLevel } from "./gsd-client.js"; /** - * WebviewViewProvider that renders a sidebar panel showing connection status, - * model info, thinking level, token usage, cost, and quick action controls. + * Send a message through VS Code's Chat panel so the user sees the response. + * Opens the Chat panel and pre-fills the @gsd participant with the message. + */ +async function sendViaChat(message: string): Promise { + await vscode.commands.executeCommand("workbench.action.chat.open", { query: message }); +} + +/** + * WebviewViewProvider that renders a compact, card-based sidebar panel. + * Designed for information density without clutter — collapsible sections, + * hidden empty data, and consolidated action buttons. */ export class GsdSidebarProvider implements vscode.WebviewViewProvider { public static readonly viewId = "gsd-sidebar"; @@ -106,22 +115,18 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { await vscode.commands.executeCommand("gsd.copyLastResponse"); break; case "autoMode": - if (this.client.isConnected) { - await this.client.sendPrompt("/gsd auto").catch(() => {}); - } + await sendViaChat("@gsd /gsd auto"); break; case "nextUnit": - if (this.client.isConnected) { - await this.client.sendPrompt("/gsd next").catch(() => {}); - } + await sendViaChat("@gsd /gsd next"); break; case "quickTask": { const quickInput = await vscode.window.showInputBox({ prompt: "Describe the quick task", placeHolder: "e.g. fix the typo in README", }); - if (quickInput && this.client.isConnected) { - await this.client.sendPrompt(`/gsd quick ${quickInput}`).catch(() => {}); + if (quickInput) { + await sendViaChat(`@gsd /gsd quick ${quickInput}`); } break; } @@ -130,15 +135,13 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { prompt: "Capture a thought", placeHolder: "e.g. we should also handle the edge case for...", }); - if (thought && this.client.isConnected) { - await this.client.sendPrompt(`/gsd capture ${thought}`).catch(() => {}); + if (thought) { + await sendViaChat(`@gsd /gsd capture ${thought}`); } break; } case "status": - if (this.client.isConnected) { - await this.client.sendPrompt("/gsd status").catch(() => {}); - } + await sendViaChat("@gsd /gsd status"); break; case "forkSession": await vscode.commands.executeCommand("gsd.forkSession"); @@ -149,6 +152,9 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { case "toggleFollowUpMode": await vscode.commands.executeCommand("gsd.toggleFollowUpMode"); break; + case "showHistory": + await vscode.commands.executeCommand("gsd.showHistory"); + break; } }); @@ -168,6 +174,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { } let modelName = "N/A"; + let modelShort = ""; let sessionId = "N/A"; let sessionName = ""; let messageCount = 0; @@ -189,6 +196,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { modelName = state.model ? `${state.model.provider}/${state.model.id}` : "Not set"; + modelShort = state.model?.id ?? ""; sessionId = state.sessionId; sessionName = state.sessionName ?? ""; messageCount = state.messageCount; @@ -216,6 +224,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { this.view.webview.html = this.getHtml({ connected, modelName, + modelShort, sessionId, sessionName, messageCount, @@ -244,6 +253,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { private getHtml(info: { connected: boolean; modelName: string; + modelShort: string; sessionId: string; sessionName: string; messageCount: number; @@ -259,57 +269,49 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { followUpMode: "all" | "one-at-a-time"; }): string { const statusColor = info.connected ? "#4ec9b0" : "#f44747"; - const statusText = info.connected - ? info.isStreaming - ? "Processing..." - : info.isCompacting - ? "Compacting..." - : "Connected" - : "Disconnected"; + const statusLabel = info.isStreaming ? "Working" : info.isCompacting ? "Compacting" : info.connected ? "Connected" : "Disconnected"; - const inputTokens = info.stats?.inputTokens?.toLocaleString() ?? "-"; - const outputTokens = info.stats?.outputTokens?.toLocaleString() ?? "-"; - const cacheRead = info.stats?.cacheReadTokens?.toLocaleString() ?? "-"; - const cacheWrite = info.stats?.cacheWriteTokens?.toLocaleString() ?? "-"; - const turnCount = info.stats?.turnCount?.toString() ?? "-"; - const duration = info.stats?.duration !== undefined - ? `${Math.round(info.stats.duration / 1000)}s` - : "-"; - const cost = info.stats?.totalCost !== undefined ? `$${info.stats.totalCost.toFixed(4)}` : "-"; + // Model short name for header + const modelDisplay = info.modelShort || "N/A"; - const thinkingBadge = info.thinkingLevel !== "off" - ? `${info.thinkingLevel}` - : `off`; + // Session display — name or truncated ID + const sessionDisplay = info.sessionName || (info.sessionId !== "N/A" ? info.sessionId.slice(0, 8) : "N/A"); - const autoCompBadge = info.autoCompaction - ? `on` - : `off`; - - const autoRetryBadge = info.autoRetry - ? `on` - : `off`; - - const streamingIndicator = info.isStreaming - ? `
Agent is working...
` + // Cost for header + const costDisplay = info.stats?.totalCost !== undefined && info.stats.totalCost > 0 + ? `$${info.stats.totalCost.toFixed(4)}` : ""; - // Context window usage + // Context window const totalTokens = (info.stats?.inputTokens ?? 0) + (info.stats?.outputTokens ?? 0); const contextPct = info.contextWindow > 0 ? Math.min(100, Math.round((totalTokens / info.contextWindow) * 100)) : 0; const contextColor = contextPct > 80 ? "#f44747" : contextPct > 50 ? "#cca700" : "#4ec9b0"; - const contextLabel = info.contextWindow > 0 - ? `${contextPct}% (${Math.round(totalTokens / 1000)}k / ${Math.round(info.contextWindow / 1000)}k)` - : "N/A"; - const steeringBadge = info.steeringMode === "one-at-a-time" - ? `1-at-a-time` - : `all`; - const followUpBadge = info.followUpMode === "one-at-a-time" - ? `1-at-a-time` - : `all`; + // Only show stats that have real data + const hasStats = info.stats && ( + (info.stats.inputTokens !== undefined && info.stats.inputTokens > 0) || + (info.stats.outputTokens !== undefined && info.stats.outputTokens > 0) + ); const nonce = getNonce(); + // Build stat rows only for non-zero values + let statRows = ""; + if (hasStats && info.stats) { + const pairs: [string, string][] = []; + if (info.stats.inputTokens) pairs.push(["In", formatNum(info.stats.inputTokens)]); + if (info.stats.outputTokens) pairs.push(["Out", formatNum(info.stats.outputTokens)]); + if (info.stats.cacheReadTokens) pairs.push(["Cache R", formatNum(info.stats.cacheReadTokens)]); + if (info.stats.cacheWriteTokens) pairs.push(["Cache W", formatNum(info.stats.cacheWriteTokens)]); + if (info.stats.turnCount) pairs.push(["Turns", String(info.stats.turnCount)]); + if (info.stats.duration) pairs.push(["Time", `${Math.round(info.stats.duration / 1000)}s`]); + if (info.stats.totalCost !== undefined && info.stats.totalCost > 0) pairs.push(["Cost", `$${info.stats.totalCost.toFixed(4)}`]); + + statRows = pairs.map(([k, v]) => + `${k}${v}` + ).join(""); + } + return /* html */ ` @@ -317,291 +319,329 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { -
-
- ${statusText} -
- - ${streamingIndicator} - -
-
Session
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Model${escapeHtml(info.modelName)}
Session - ${escapeHtml(info.sessionName || info.sessionId)} - ${info.connected ? `` : ""} -
Messages${info.messageCount}${info.pendingMessageCount > 0 ? ` +${info.pendingMessageCount} pending` : ""}
Thinking${thinkingBadge}
Auto-compact${autoCompBadge}
Auto-retry${autoRetryBadge}
Steering${info.steeringMode === "one-at-a-time" ? "1-at-a-time" : "all"}
Follow-up${info.followUpMode === "one-at-a-time" ? "1-at-a-time" : "all"}
-
- - ${info.connected && info.stats ? ` -
-
Token Usage
-
- Input - ${inputTokens} - Output - ${outputTokens} - Cache read - ${cacheRead} - Cache write - ${cacheWrite} - Turns - ${turnCount} - Duration - ${duration} - Cost - ${cost} + ${info.connected ? this.getConnectedHtml(info, { + statusLabel, + modelDisplay, + sessionDisplay, + costDisplay, + contextPct, + contextColor, + hasStats: !!hasStats, + statRows, + nonce, + }) : ` +
+
+
+ Disconnected
- - ${info.contextWindow > 0 ? ` -
-
Context Window
-
-
-
-
${contextLabel}
+
+

Agent is not running

+
- ` : ""} - ` : ""} - - ${info.connected ? ` -
-
Workflow
-
-
- - -
-
- - -
-
- - -
-
-
- ` : ""} - -
-
Controls
-
- ${info.connected - ? ` -
- - -
-
- - -
-
- - -
` - : `` - } -
-
- - ${info.connected ? ` -
-
Actions
-
-
- - -
-
- - -
-
-
- ` : ""} + `}