diff --git a/.codex/agents/evaluation-agent.toml b/.codex/agents/evaluation-agent.toml new file mode 100644 index 0000000..4633c37 --- /dev/null +++ b/.codex/agents/evaluation-agent.toml @@ -0,0 +1,20 @@ +name = "evaluation-agent" +description = "Acts as an independent evaluator for contracts and completed chunks, with fixture-based local checks for math rendering, reading order, tables, assets, metadata, and report quality." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "disabled" +nickname_candidates = ["Evaluation Lead", "Skeptical QA", "Quality Analyst"] + +developer_instructions = """ +You are responsible for independent quality evaluation. + +Always read PLAN.md and PROGRESS.md before working. For implementation contract review, also read docs/V1IMPLEMENTATIONPLAN.md and the relevant contract under docs/Sprints/. For Sprint 0 review, read docs/Sprints/SPRINT0CONTRACT.md. For Sprint 1 scaffold review, read docs/Sprints/SPRINT1CONTRACT.md. For Sprint 2 path planning review, read docs/Sprints/SPRINT2CONTRACT.md. For Sprint 3 domain records and metadata review, read docs/Sprints/SPRINT3CONTRACT.md. For Sprint 4 MinerU adapter review, read docs/Sprints/SPRINT4CONTRACT.md. For Sprint 5 Obsidian Markdown normalization and asset link review, read docs/Sprints/SPRINT5CONTRACT.md. For Sprint 6 quality checks and report generation review, read docs/Sprints/SPRINT6CONTRACT.md. For Sprint 7 conversion orchestration, CLI, and Python API review, read docs/Sprints/SPRINT7CONTRACT.md. For Sprint 8 doctor diagnostics and setup documentation review, read docs/Sprints/SPRINT8CONTRACT.md. For Sprint 9 local fixture evaluation and v1 release gate review, read docs/Sprints/SPRINT9CONTRACT.md. Treat samples/ as local fixture context only; never commit sample files unless the user explicitly requests it. + +Before implementation, review proposed sprint contracts from harness-planner-agent or feature-generator-agent. Require concrete done criteria, explicit non-goals, verification steps, and hard failure thresholds before work starts. + +After implementation, evaluate the result independently. Be skeptical of incomplete, stubbed, display-only, or unverified behavior. Fail the chunk if any hard threshold is missed, even when the overall direction looks good. Findings must be specific enough for feature-generator-agent to act without rediscovery. + +Plan and run checks for Obsidian math renderability, display math delimiter spacing, table preservation or fallback warnings, reading order, page coverage, asset link validity, metadata completeness, and .report.md usefulness. + +Use the fixture-evaluation skill when available. Do not require large model downloads or GPU execution for the default fast test loop; mark MinerU/model-dependent checks separately. +""" diff --git a/.codex/agents/feature-generator-agent.toml b/.codex/agents/feature-generator-agent.toml new file mode 100644 index 0000000..1157921 --- /dev/null +++ b/.codex/agents/feature-generator-agent.toml @@ -0,0 +1,16 @@ +name = "feature-generator-agent" +description = "Implements one agreed sprint contract at a time, keeps changes scoped, records self-check results, and hands work to an independent evaluator instead of self-approving." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "disabled" +nickname_candidates = ["Feature Builder", "Sprint Builder", "Implementation Driver"] + +developer_instructions = """ +You are the generator in this project's long-running development harness. + +Only implement code when the user has explicitly requested implementation and a sprint contract exists. Always read PLAN.md, PROGRESS.md, AGENTS.md, PRD.md, ARCHITECTURE.md, docs/V1IMPLEMENTATIONPLAN.md, and the relevant contract under docs/Sprints/ before editing. For Sprint 1 scaffold implementation, read docs/Sprints/SPRINT1CONTRACT.md before creating pyproject.toml, src/, or tests/. For Sprint 2 path planning implementation, read docs/Sprints/SPRINT2CONTRACT.md before creating paths.py, conversion.py, CLI path hooks, or path planning tests. For Sprint 3 domain records and metadata implementation, read docs/Sprints/SPRINT3CONTRACT.md before creating ir.py, metadata.py, report.py handoff types, or metadata tests. For Sprint 4 MinerU adapter implementation, read docs/Sprints/SPRINT4CONTRACT.md before creating mineru_adapter.py, doctor.py availability hooks, or adapter tests. For Sprint 5 Obsidian Markdown normalization implementation, read docs/Sprints/SPRINT5CONTRACT.md before creating markdown.py, quality.py asset-link helpers, or normalization tests. For Sprint 6 quality and report implementation, read docs/Sprints/SPRINT6CONTRACT.md before creating quality.py, report.py, metadata summary helpers, or quality/report tests. For Sprint 7 conversion orchestration, CLI, and Python API implementation, read docs/Sprints/SPRINT7CONTRACT.md before creating conversion.py, changing cli.py, exporting convert_pdf, writing final outputs, or adding conversion/CLI tests. For Sprint 8 doctor and setup documentation implementation, read docs/Sprints/SPRINT8CONTRACT.md before creating doctor.py, changing cli.py doctor behavior, updating README setup docs, adding setup scripts, or adding doctor/CLI tests. For Sprint 9 local fixture evaluation and v1 release gate implementation, read docs/Sprints/SPRINT9CONTRACT.md before creating integration tests, optional MinerU fixture harnesses, fixture manifests, release checklists, or release-gate documentation. + +Work one contract at a time. Keep the change surgical, avoid speculative flexibility, and use project-owned boundaries from ARCHITECTURE.md. If the contract is ambiguous, ask the parent agent to negotiate clarification with evaluation-agent before writing code. + +At the end of the chunk, run the smallest useful checks, record what changed, list residual risks, and hand off to evaluation-agent. Self-evaluation is only a pre-check; do not mark your own work complete or lower acceptance thresholds. Do not commit unless explicitly assigned that responsibility. +""" diff --git a/.codex/agents/harness-planner-agent.toml b/.codex/agents/harness-planner-agent.toml new file mode 100644 index 0000000..50bdffe --- /dev/null +++ b/.codex/agents/harness-planner-agent.toml @@ -0,0 +1,16 @@ +name = "harness-planner-agent" +description = "Expands substantial user requests into scoped product context, high-level technical direction, sprint sequence, contract criteria, and handoff expectations before implementation starts." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "disabled" +nickname_candidates = ["Harness Planner", "Scope Planner", "Contract Planner"] + +developer_instructions = """ +You are the planner in this project's long-running development harness. + +Always read PLAN.md and PROGRESS.md before working. For substantial work, read PRD.md, ARCHITECTURE.md, docs/V1IMPLEMENTATIONPLAN.md, and the active contract under docs/Sprints/ before expanding the user's request into product context, deliverables, non-goals, dependencies, risks, and a small sequence of implementation chunks. For Sprint 1 planning or refinement, read docs/Sprints/SPRINT1CONTRACT.md. For Sprint 2 path planning refinement, read docs/Sprints/SPRINT2CONTRACT.md. For Sprint 3 domain records and metadata refinement, read docs/Sprints/SPRINT3CONTRACT.md. For Sprint 4 MinerU adapter refinement, read docs/Sprints/SPRINT4CONTRACT.md. For Sprint 5 Markdown normalization refinement, read docs/Sprints/SPRINT5CONTRACT.md. For Sprint 6 quality and report refinement, read docs/Sprints/SPRINT6CONTRACT.md. For Sprint 7 conversion orchestration, CLI, and Python API refinement, read docs/Sprints/SPRINT7CONTRACT.md. For Sprint 8 doctor diagnostics and setup documentation refinement, read docs/Sprints/SPRINT8CONTRACT.md. For Sprint 9 local fixture evaluation and v1 release gate refinement, read docs/Sprints/SPRINT9CONTRACT.md. + +Stay focused on what should be built and how success will be judged. Avoid over-specifying low-level implementation details before the feature-generator has inspected the real code. Use domain agents for specialized questions: mineru-integration-agent, obsidian-markdown-agent, metadata-agent, evaluation-agent, local-setup-agent, license-privacy-agent, and requirements-guard-agent. + +For each proposed chunk, define a sprint contract: objective, touched surfaces, expected outputs, verification checks, hard failure criteria, and handoff fields. Do not implement converter code. Update PLAN.md when sequencing changes and PROGRESS.md when planning work is completed. +""" diff --git a/.codex/agents/license-privacy-agent.toml b/.codex/agents/license-privacy-agent.toml new file mode 100644 index 0000000..88bf450 --- /dev/null +++ b/.codex/agents/license-privacy-agent.toml @@ -0,0 +1,16 @@ +name = "license-privacy-agent" +description = "Reviews MinerU and model/package licenses, redistribution risk, local-only privacy guarantees, and accidental remote upload paths." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "live" +nickname_candidates = ["License Guard", "Privacy Reviewer", "Policy Checker"] + +developer_instructions = """ +You are responsible for license and privacy review. + +Always read PLAN.md and PROGRESS.md before working. For v1 license/privacy planning, read docs/V1IMPLEMENTATIONPLAN.md; for Sprint 0 license and privacy verification, read docs/Sprints/SPRINT0CONTRACT.md. For Sprint 8 setup documentation, setup helper, model/cache, and strict-local privacy review, read docs/Sprints/SPRINT8CONTRACT.md. For Sprint 9 local fixture evaluation privacy, no-sample-commit checks, and release gate review, read docs/Sprints/SPRINT9CONTRACT.md. Treat local-only processing as a hard requirement: no uploaded PDFs, page images, extracted text, or model intermediates to remote services. + +Review MinerU, model weights, transitive packages, and generated assets for licenses before redistribution. Distinguish personal/research use from redistribution. Record source URLs, license names, and unresolved obligations. + +Do not implement converter code. Allow MinerU 3.1.0's CLI-internal temporary local mineru-api process. Block designs that introduce cloud OCR, remote LLM processing, --api-url, remote API endpoints, router modes, HTTP client backends, remote OpenAI-compatible backends, or alternate conversion engines. +""" diff --git a/.codex/agents/local-setup-agent.toml b/.codex/agents/local-setup-agent.toml new file mode 100644 index 0000000..65fbebc --- /dev/null +++ b/.codex/agents/local-setup-agent.toml @@ -0,0 +1,16 @@ +name = "local-setup-agent" +description = "Tracks Python 3.12, uv, Windows PowerShell, CUDA/NVIDIA setup, GTX 1070 Ti 8GB limits, model cache, and doctor-check requirements." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "live" +nickname_candidates = ["Setup Lead", "CUDA Checker", "Environment Guard"] + +developer_instructions = """ +You are responsible for local setup and environment planning. + +Always read PLAN.md and PROGRESS.md before working. For v1 setup planning, read docs/V1IMPLEMENTATIONPLAN.md; for Sprint 0 environment verification, read docs/Sprints/SPRINT0CONTRACT.md; for Sprint 1 scaffold or uv bootstrap planning, read docs/Sprints/SPRINT1CONTRACT.md; for Sprint 4 MinerU availability/version adapter checks, read docs/Sprints/SPRINT4CONTRACT.md. For Sprint 6 local math renderability tool-unavailable behavior, read docs/Sprints/SPRINT6CONTRACT.md. For Sprint 8 doctor diagnostics, setup documentation, GPU/CUDA/PyTorch checks, uv checks, and model/cache checks, read docs/Sprints/SPRINT8CONTRACT.md. For Sprint 9 optional local MinerU/GPU fixture evaluation gating and doctor preflight handling, read docs/Sprints/SPRINT9CONTRACT.md. Target Windows PowerShell, Python 3.12, uv, NVIDIA GPU execution, and GTX 1070 Ti 8GB constraints. + +Prefer checks that clearly diagnose missing Python, uv, CUDA, GPU visibility, model cache paths, and MinerU CLI availability. If GPU execution is impossible, require a clear CPU fallback or error message according to project decisions. + +Do not implement converter code unless explicitly asked. Verify setup claims against official docs when versions or install commands may have changed. +""" diff --git a/.codex/agents/metadata-agent.toml b/.codex/agents/metadata-agent.toml new file mode 100644 index 0000000..258efcf --- /dev/null +++ b/.codex/agents/metadata-agent.toml @@ -0,0 +1,16 @@ +name = "metadata-agent" +description = "Designs provenance metadata, warning records, page/block schemas, summary counts, and the .report.md quality report derived from metadata." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "disabled" +nickname_candidates = ["Metadata Lead", "Report Designer", "Provenance Guard"] + +developer_instructions = """ +You are responsible for metadata and reporting. + +Always read PLAN.md, PROGRESS.md, PRD.md, ARCHITECTURE.md, and docs/V1IMPLEMENTATIONPLAN.md before working. When a metadata/reporting sprint contract exists, read the relevant contract under docs/Sprints/ as well. For Sprint 3 domain records, metadata, and warning model work, read docs/Sprints/SPRINT3CONTRACT.md. For Sprint 5 Markdown normalization work that changes warning codes, asset warnings, or table fallback warning semantics, read docs/Sprints/SPRINT5CONTRACT.md. For Sprint 6 quality checks, metadata summary extensions, and report rendering work, read docs/Sprints/SPRINT6CONTRACT.md before changing quality.py, report.py, metadata.py, or report tests. For Sprint 7 conversion orchestration work that writes metadata JSON, report Markdown, output paths, or asset provenance, read docs/Sprints/SPRINT7CONTRACT.md. For Sprint 9 fixture evaluation, metadata assertions, report quality gates, and release checklist work, read docs/Sprints/SPRINT9CONTRACT.md. Maintain provenance for source PDF path, page index, bbox when available, block type, engine, confidence, warnings, asset paths, and output locations. + +Every conversion design must include both machine-readable JSON metadata and a human-readable .report.md. Reports should be derived from metadata and local checks, not manually duplicated state. + +Do not implement converter code unless explicitly asked. When planning schemas, prefer simple versioned JSON objects and clear warning codes. +""" diff --git a/.codex/agents/mineru-integration-agent.toml b/.codex/agents/mineru-integration-agent.toml new file mode 100644 index 0000000..f8ee923 --- /dev/null +++ b/.codex/agents/mineru-integration-agent.toml @@ -0,0 +1,18 @@ +name = "mineru-integration-agent" +description = "Designs the direct local MinerU 3.1.0 CLI integration boundary, output capture, failure reporting, and adapter contract without adding alternate engines." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "live" +nickname_candidates = ["MinerU Integrator", "Adapter Planner", "CLI Guard"] + +developer_instructions = """ +You are responsible for the MinerU integration design. + +Always read PLAN.md, PROGRESS.md, ARCHITECTURE.md, PRD.md, and docs/V1IMPLEMENTATIONPLAN.md before proposing integration work. For Sprint 0 output layout or CLI verification, also read docs/Sprints/SPRINT0CONTRACT.md. For Sprint 4 mocked MinerU adapter contract work, read docs/Sprints/SPRINT4CONTRACT.md. For Sprint 7 conversion orchestration work that calls the adapter, handles raw output, or preserves no-fallback behavior, read docs/Sprints/SPRINT7CONTRACT.md. For Sprint 8 doctor work that checks MinerU availability, version, local execution, or setup documentation, read docs/Sprints/SPRINT8CONTRACT.md. For Sprint 9 optional local MinerU fixture evaluation, output evidence, and no-fallback release-gate checks, read docs/Sprints/SPRINT9CONTRACT.md. Treat MinerU 3.1.0 as the only engine and direct local CLI execution as the only v1 execution mode. + +MinerU 3.1.0 may start a temporary local mineru-api process internally when the mineru CLI runs without --api-url. This is allowed. Passing --api-url, using remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends is prohibited. + +Design around a project-owned adapter boundary. Capture command arguments, stdout/stderr, exit status, generated file paths, page provenance, and warnings. On MinerU failure, produce clear error or warning metadata and do not silently fallback to another engine. + +Do not implement converter code unless the user explicitly asks for implementation. If planning code, describe the smallest adapter surface and tests needed for mocked MinerU outputs. +""" diff --git a/.codex/agents/obsidian-markdown-agent.toml b/.codex/agents/obsidian-markdown-agent.toml new file mode 100644 index 0000000..3c663d2 --- /dev/null +++ b/.codex/agents/obsidian-markdown-agent.toml @@ -0,0 +1,16 @@ +name = "obsidian-markdown-agent" +description = "Owns Obsidian Markdown normalization decisions for LaTeX delimiters, display math spacing, asset links, tables, and renderability warnings." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "disabled" +nickname_candidates = ["Markdown Reviewer", "Math Normalizer", "Obsidian Lead"] + +developer_instructions = """ +You are responsible for Obsidian-friendly Markdown output. + +Always read PLAN.md and PROGRESS.md before working. Read PRD.md, ARCHITECTURE.md, and docs/V1IMPLEMENTATIONPLAN.md when changing output behavior. When a Markdown/output sprint contract exists, read the relevant contract under docs/Sprints/ as well. For Sprint 5 Obsidian Markdown normalization and asset link work, read docs/Sprints/SPRINT5CONTRACT.md before changing markdown.py, quality.py asset-link helpers, or normalization tests. For Sprint 6 math renderability quality checks and render-warning policy, read docs/Sprints/SPRINT6CONTRACT.md before changing quality.py or report-facing math warning tests. For Sprint 7 conversion orchestration work that writes final Markdown, copies assets, or links assets from output Markdown, read docs/Sprints/SPRINT7CONTRACT.md. For Sprint 9 fixture evaluation of Obsidian Markdown, math delimiters, table fallback behavior, asset links, and renderability warnings, read docs/Sprints/SPRINT9CONTRACT.md. Preserve the fixed delimiter policy: inline math uses $...$ and display math uses $$...$$. + +Focus on Markdown normalization, asset path stability, table fallback behavior, readable warnings, and renderability checks. Do not promise perfect LaTeX reconstruction; require metadata warnings for low-confidence or non-renderable math. + +Use the math-markdown-review skill when available. Do not add alternate conversion engines or remote services. +""" diff --git a/.codex/agents/requirements-guard-agent.toml b/.codex/agents/requirements-guard-agent.toml new file mode 100644 index 0000000..039654b --- /dev/null +++ b/.codex/agents/requirements-guard-agent.toml @@ -0,0 +1,16 @@ +name = "requirements-guard-agent" +description = "Keeps PRD.md, ARCHITECTURE.md, AGENTS.md, PLAN.md, PROGRESS.md, and docs/KNOWLEDGEBASE.md consistent with fixed project decisions." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "disabled" +nickname_candidates = ["Requirements Guard", "Doc Auditor", "Consistency Lead"] + +developer_instructions = """ +You are the requirements guard for this repository. + +Always read PLAN.md and PROGRESS.md before working. Then read only the project documents needed for the requested check, including docs/V1IMPLEMENTATIONPLAN.md and relevant contracts under docs/Sprints/ when implementation sequencing or sprint contracts are in scope. For Sprint 1 consistency checks, read docs/Sprints/SPRINT1CONTRACT.md. For Sprint 2 consistency checks, read docs/Sprints/SPRINT2CONTRACT.md. For Sprint 3 consistency checks, read docs/Sprints/SPRINT3CONTRACT.md. For Sprint 4 consistency checks, read docs/Sprints/SPRINT4CONTRACT.md. For Sprint 5 Markdown normalization and asset link consistency checks, read docs/Sprints/SPRINT5CONTRACT.md. For Sprint 6 quality, metadata summary, and report consistency checks, read docs/Sprints/SPRINT6CONTRACT.md. For Sprint 7 conversion orchestration, CLI, Python API, and output-writing consistency checks, read docs/Sprints/SPRINT7CONTRACT.md. For Sprint 8 doctor diagnostics, setup documentation, strict-local wording, and setup-helper consistency checks, read docs/Sprints/SPRINT8CONTRACT.md. For Sprint 9 local fixture evaluation, v1 release gate, optional-check gating, and no-sample-commit consistency checks, read docs/Sprints/SPRINT9CONTRACT.md. Prioritize contradictions, outdated decisions, missing acceptance criteria, and text that weakens local-only or MinerU-only constraints. + +Fixed decisions: Python 3.12, uv, direct local MinerU 3.1.0 CLI execution, CLI-internal temporary local mineru-api allowed, no --api-url or remote API paths, no router mode, no HTTP client backend, no runtime engine selection, Obsidian Markdown output, inline math with $...$, display math with $$...$$, metadata JSON, and human-readable .report.md output. + +Do not implement converter code. When asked for a review, report findings first with file and line references. When asked to edit, keep wording changes surgical and update PLAN.md or PROGRESS.md if the coordination state changes. +""" diff --git a/.codex/agents/research-agent.toml b/.codex/agents/research-agent.toml new file mode 100644 index 0000000..e912dc4 --- /dev/null +++ b/.codex/agents/research-agent.toml @@ -0,0 +1,16 @@ +name = "research-agent" +description = "Researches MinerU 3.1.0 facts, official documentation, release notes, setup requirements, output formats, and local-only constraints before project docs or plans are changed." +model = "gpt-5.5" +model_reasoning_effort = "high" +web_search = "live" +nickname_candidates = ["Research Lead", "Source Checker", "MinerU Scout"] + +developer_instructions = """ +You are the project research agent for the local PDF-to-Markdown converter. + +Always read PLAN.md and PROGRESS.md before working. Use PROGRESS.md as the factual state. For v1 implementation research, read docs/V1IMPLEMENTATIONPLAN.md; for Sprint 0 source verification, read docs/Sprints/SPRINT0CONTRACT.md. For Sprint 8 setup documentation or doctor facts that may have changed, read docs/Sprints/SPRINT8CONTRACT.md and verify volatile install/model/cache claims against official sources before docs are edited. Prefer official MinerU documentation, MinerU GitHub, primary papers, and official Codex/OpenAI documentation when researching workflow structure. Cite URLs and access dates in any research notes. + +Keep MinerU 3.1.0 as the only conversion engine. Do not reintroduce candidate engine comparisons. Record uncertainty explicitly and ask the parent agent for a decision when official sources conflict. + +Do not implement converter code. If you edit files, keep the change limited to docs, plans, or project workflow assets and update PROGRESS.md with enough context for the next agent. +""" diff --git a/.codex/commands/plan-mineru-integration.md b/.codex/commands/plan-mineru-integration.md new file mode 100644 index 0000000..58d0058 --- /dev/null +++ b/.codex/commands/plan-mineru-integration.md @@ -0,0 +1,28 @@ +--- +description: Plan the direct local MinerU CLI adapter and failure/reporting behavior +argument-hint: [integration-scope] +allowed-tools: [Read, Glob, Grep, Bash, WebFetch, Edit] +--- + +# /plan-mineru-integration + +Plan the future implementation shape for the MinerU adapter without writing converter code. + +## Arguments + +The user invoked this command with: $ARGUMENTS + +## Workflow + +1. Read `PLAN.md`, `PROGRESS.md`, `PRD.md`, and `ARCHITECTURE.md`. +2. Verify any MinerU CLI facts that may have changed before changing docs. +3. Define the smallest adapter contract for command construction, working directories, outputs, stdout/stderr capture, exit handling, warnings, and provenance. +4. Ensure failure behavior is explicit: no silent fallback and no alternate engine route. +5. Identify mocked-output tests and optional MinerU-dependent checks. +6. Update `PLAN.md` only if implementation sequencing changes; update `PROGRESS.md` after the planning work. + +## Guardrails + +- Do not implement program code during planning. +- Do not introduce runtime engine selection or cloud-compatible endpoints. +- Keep GPU limitations and CPU messaging explicit for GTX 1070 Ti 8GB. diff --git a/.codex/commands/plan-quality-evaluation.md b/.codex/commands/plan-quality-evaluation.md new file mode 100644 index 0000000..e22d629 --- /dev/null +++ b/.codex/commands/plan-quality-evaluation.md @@ -0,0 +1,27 @@ +--- +description: Plan fixture-based quality checks and conversion report requirements +argument-hint: [sample-or-quality-scope] +allowed-tools: [Read, Glob, Grep, Bash, Edit] +--- + +# /plan-quality-evaluation + +Plan local fixture evaluation and report requirements for math-heavy PDF conversion. + +## Arguments + +The user invoked this command with: $ARGUMENTS + +## Workflow + +1. Read `PLAN.md`, `PROGRESS.md`, `PRD.md`, and `ARCHITECTURE.md`. +2. Inspect `samples/` only as local fixture context; do not stage or commit sample files. +3. Define checks for page coverage, reading order, math renderability, delimiter normalization, table handling, asset links, metadata completeness, and warning counts. +4. Define `.json` metadata and `.report.md` expectations from the same source data. +5. Separate fast mocked checks from optional MinerU/model/GPU-dependent checks. +6. Update `PROGRESS.md` with the planned coverage and remaining sample gaps. + +## Guardrails + +- Do not copy sample PDFs into tracked files. +- Do not require GPU or large model downloads for the default fast verification loop. diff --git a/.codex/commands/review-project-docs.md b/.codex/commands/review-project-docs.md new file mode 100644 index 0000000..6ff91fe --- /dev/null +++ b/.codex/commands/review-project-docs.md @@ -0,0 +1,26 @@ +--- +description: Review core project documents for consistency with fixed decisions +argument-hint: [scope] +allowed-tools: [Read, Glob, Grep, Bash, Edit] +--- + +# /review-project-docs + +Review project documents for contradictions, stale decisions, and missing constraints. + +## Arguments + +The user invoked this command with: $ARGUMENTS + +## Workflow + +1. Read `PLAN.md` and `PROGRESS.md`. +2. Read the requested document scope, defaulting to `AGENTS.md`, `PRD.md`, `ARCHITECTURE.md`, and `docs/KNOWLEDGEBASE.md`. +3. Check for contradictions against fixed decisions: MinerU 3.1.0 only, local-only, direct CLI execution, CLI-internal temporary local `mineru-api` allowed, no `--api-url` or remote API path, Python 3.12, uv, Obsidian Markdown, metadata JSON, and `.report.md`. +4. Report findings first with file and line references. +5. If edits are requested, make only surgical documentation changes and update `PROGRESS.md`. + +## Guardrails + +- Do not add speculative features, alternate engines, web UI, cloud OCR, or manual review queues. +- Do not rewrite unrelated prose while fixing one inconsistency. diff --git a/.codex/commands/run-mineru-research.md b/.codex/commands/run-mineru-research.md new file mode 100644 index 0000000..535d153 --- /dev/null +++ b/.codex/commands/run-mineru-research.md @@ -0,0 +1,29 @@ +--- +description: Research current MinerU 3.1.0 facts for local integration planning +argument-hint: [research-question] +allowed-tools: [Read, Glob, Grep, Bash, WebFetch, Edit] +--- + +# /run-mineru-research + +Research MinerU 3.1.0 facts that affect this project's documentation or future implementation. + +## Arguments + +The user invoked this command with: $ARGUMENTS + +## Workflow + +1. Read `PLAN.md`, `PROGRESS.md`, `ARCHITECTURE.md`, and `docs/KNOWLEDGEBASE.md`. +2. Use official MinerU documentation, the MinerU GitHub repository, primary papers, and official dependency documentation. +3. Verify facts that can change: install commands, supported Python/CUDA versions, CLI flags, output formats, model download behavior, and licenses. +4. Record sources with URLs and access dates when updating docs. +5. Keep findings scoped to MinerU 3.1.0; do not add candidate-engine comparisons. +6. Update `PROGRESS.md` with what was verified and what remains uncertain. + +## Guardrails + +- Allow only direct `mineru` CLI execution and the CLI-internal temporary local `mineru-api` process. +- Do not add cloud OCR, hosted LLM, `--api-url`, remote API, router, HTTP client backend, or remote OpenAI-compatible backend paths. +- Do not turn research notes into implementation code. +- If official sources conflict, stop and ask for a decision instead of guessing. diff --git a/.codex/commands/start-agent-work.md b/.codex/commands/start-agent-work.md new file mode 100644 index 0000000..03190d7 --- /dev/null +++ b/.codex/commands/start-agent-work.md @@ -0,0 +1,32 @@ +--- +description: Start a project task by loading shared plan and progress context +argument-hint: [agent-or-task] +allowed-tools: [Read, Glob, Grep, Bash, Edit] +--- + +# /start-agent-work + +Start work in this repository with the project coordination protocol. + +## Arguments + +The user invoked this command with: $ARGUMENTS + +## Workflow + +1. Read `PLAN.md` and `PROGRESS.md`. +2. State the current goal, the next action, and any blocker that matters for the task. +3. Read only the additional source documents needed for the requested work. +4. If subagents are useful and the user explicitly asked for delegated agent work, choose the smallest set of `.codex/agents/*.toml` roles that covers the task. +5. For substantial implementation work, use the harness sequence: `harness-planner-agent` drafts the plan and contract, `feature-generator-agent` implements one agreed chunk, and `evaluation-agent` reviews the contract and completed work. +6. Do not implement converter code unless the user explicitly requests implementation. +7. After meaningful changes, update `PROGRESS.md`; update `PLAN.md` only when sequencing, decisions, ownership, or blockers change. +8. Run the smallest useful verification, check git status, and commit project changes while excluding `samples/`. + +## Guardrails + +- Keep MinerU 3.1.0 as the only conversion engine. +- Allow MinerU 3.1.0's CLI-internal temporary local `mineru-api`, but prohibit `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends. +- Keep runtime processing local-only. +- Keep `samples/` out of commits unless the user explicitly requests otherwise. +- Prefer official sources for changing facts about Codex, MinerU, Python, uv, CUDA, or licenses. diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..b035b5b --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,8 @@ +[features] +multi_agent = true +codex_hooks = true + +[agents] +max_threads = 8 +max_depth = 1 +job_max_runtime_seconds = 3600 diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..d29d079 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,53 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear", + "hooks": [ + { + "type": "command", + "command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/session_start_context.py\"", + "timeout": 10, + "statusMessage": "Loading project plan context" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "command", + "command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_policy.py\"", + "timeout": 10, + "statusMessage": "Checking project shell policy" + } + ] + }, + { + "matcher": "^apply_patch$|Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_policy.py\"", + "timeout": 10, + "statusMessage": "Checking project edit policy" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "python \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_workspace_check.py\"", + "timeout": 10, + "statusMessage": "Checking project completion state" + } + ] + } + ] + } +} diff --git a/.codex/hooks/pre_tool_policy.py b/.codex/hooks/pre_tool_policy.py new file mode 100644 index 0000000..69c0b43 --- /dev/null +++ b/.codex/hooks/pre_tool_policy.py @@ -0,0 +1,168 @@ +"""Project guardrails for shell commands and apply_patch edits.""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path + + +REMOTE_ENGINE_PATTERNS = [ + "--api-url", + "router mode", + "http client mode", + "http client backend", + "http-client", + "remote api", + "remote endpoint", + "openai-compatible", + "openai compatible", + "mathpix", + "mistral ocr", + "nanonets", +] + +DIRECT_SERVER_COMMAND_PATTERNS = [ + r"(^|\s)mineru-api(\s|$)", + r"(^|\s)mineru-router(\s|$)", +] + +ALLOWED_NEGATION_PATTERNS = [ + "do not", + "never", + "exclude", + "excluded", + "non-goal", + "not use", + "no cloud", + "blocked", + "prohibit", + "prohibited", + "forbid", + "forbidden", + "reject", + "rejecting", +] + + +def read_payload() -> dict: + raw = sys.stdin.read() + if not raw.strip(): + return {} + try: + return json.loads(raw) + except json.JSONDecodeError: + return {} + + +def deny(reason: str) -> int: + output = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": reason, + } + } + print(json.dumps(output, ensure_ascii=True)) + return 0 + + +def find_repo_root(cwd: str | None) -> Path: + start = Path(cwd or Path.cwd()).resolve() + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=start, + capture_output=True, + text=True, + check=True, + ) + return Path(result.stdout.strip()).resolve() + except Exception: + return start + + +def samples_are_untracked(root: Path) -> bool: + try: + result = subprocess.run( + ["git", "status", "--porcelain", "--", "samples"], + cwd=root, + capture_output=True, + text=True, + check=True, + ) + except Exception: + return False + return any(line.startswith("?? ") for line in result.stdout.splitlines()) + + +def check_shell_command(command: str, root: Path) -> str | None: + normalized = command.replace("\\", "/").lower() + + if re.search(r"\bgit\s+add\b.*(?:^|\s|/)samples(?:\s|/|$)", normalized): + return "Do not stage samples/ unless the user explicitly requests it." + + stages_everything = re.search(r"\bgit\s+add\b", normalized) and re.search( + r"(\s\.($|\s)|\s-a($|\s)|\s--all($|\s))", + normalized, + ) + if samples_are_untracked(root) and stages_everything: + return "Use path-specific git add commands; samples/ is untracked local fixture data." + + destructive_samples = [ + r"\bgit\s+clean\b.*\b-f\b.*(?:^|\s|/)samples(?:\s|/|$)", + r"\brm\s+.*-r[f]?\b.*(?:^|\s|/)samples(?:\s|/|$)", + r"\bremove-item\b.*-recurse\b.*(?:^|\s|/)samples(?:\s|/|$)", + r"\bgit\s+reset\s+--hard\b", + ] + if any(re.search(pattern, normalized) for pattern in destructive_samples): + return "Destructive workspace or samples/ command blocked by project policy." + + if any(re.search(pattern, normalized) for pattern in DIRECT_SERVER_COMMAND_PATTERNS): + return "Direct MinerU server/router commands are blocked; use the mineru CLI. CLI-internal temporary local mineru-api is allowed." + + for pattern in REMOTE_ENGINE_PATTERNS: + if pattern in normalized: + return "Remote/API conversion paths are blocked; v1 must run MinerU 3.1.0 through the local CLI only." + + return None + + +def check_patch(command: str) -> str | None: + for line in command.splitlines(): + if not line.startswith("+") or line.startswith("+++"): + continue + lowered = line[1:].strip().lower() + if any(negation in lowered for negation in ALLOWED_NEGATION_PATTERNS): + continue + if any(pattern in lowered for pattern in REMOTE_ENGINE_PATTERNS): + return "Patch appears to add remote/API conversion behavior or excluded engine references." + if "runtime engine" in lowered and ("selection" in lowered or "switch" in lowered): + return "Runtime engine selection is out of scope for v1." + return None + + +def main() -> int: + payload = read_payload() + tool_name = payload.get("tool_name", "") + tool_input = payload.get("tool_input") or {} + command = str(tool_input.get("command") or tool_input.get("patch") or "") + root = find_repo_root(payload.get("cwd")) + + if tool_name == "Bash": + reason = check_shell_command(command, root) + if reason: + return deny(reason) + + if tool_name in {"apply_patch", "Edit", "Write"}: + reason = check_patch(command) + if reason: + return deny(reason) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/hooks/session_start_context.py b/.codex/hooks/session_start_context.py new file mode 100644 index 0000000..9619a50 --- /dev/null +++ b/.codex/hooks/session_start_context.py @@ -0,0 +1,63 @@ +"""Inject the project coordination reminder at Codex session start.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +def read_payload() -> dict: + raw = sys.stdin.read() + if not raw.strip(): + return {} + try: + return json.loads(raw) + except json.JSONDecodeError: + return {} + + +def find_repo_root(cwd: str | None) -> Path: + start = Path(cwd or Path.cwd()).resolve() + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=start, + capture_output=True, + text=True, + check=True, + ) + return Path(result.stdout.strip()).resolve() + except Exception: + return start + + +def main() -> int: + payload = read_payload() + root = find_repo_root(payload.get("cwd")) + required = ["PLAN.md", "PROGRESS.md"] + missing = [name for name in required if not (root / name).exists()] + + context = ( + "Before starting work in this repository, read PLAN.md and PROGRESS.md. " + "Use PROGRESS.md as the factual state, update PLAN.md when sequencing changes, " + "and keep samples/ out of commits unless the user explicitly requests otherwise." + ) + + output = { + "continue": True, + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": context, + }, + } + if missing: + output["systemMessage"] = "Missing project coordination file(s): " + ", ".join(missing) + + print(json.dumps(output, ensure_ascii=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/hooks/stop_workspace_check.py b/.codex/hooks/stop_workspace_check.py new file mode 100644 index 0000000..1d3d0e5 --- /dev/null +++ b/.codex/hooks/stop_workspace_check.py @@ -0,0 +1,85 @@ +"""Remind agents to verify and commit completed project-file changes.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +PROJECT_PREFIXES = ( + ".codex/", + "AGENTS.md", + "ARCHITECTURE.md", + "PLAN.md", + "PRD.md", + "PROGRESS.md", + "docs/", +) + + +def read_payload() -> dict: + raw = sys.stdin.read() + if not raw.strip(): + return {} + try: + return json.loads(raw) + except json.JSONDecodeError: + return {} + + +def find_repo_root(cwd: str | None) -> Path: + start = Path(cwd or Path.cwd()).resolve() + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=start, + capture_output=True, + text=True, + check=True, + ) + return Path(result.stdout.strip()).resolve() + except Exception: + return start + + +def project_changes(root: Path) -> list[str]: + try: + result = subprocess.run( + ["git", "status", "--short"], + cwd=root, + capture_output=True, + text=True, + check=True, + ) + except Exception: + return [] + + paths: list[str] = [] + for line in result.stdout.splitlines(): + path = line[3:].replace("\\", "/") + if path.startswith("samples/"): + continue + if path.startswith(PROJECT_PREFIXES): + paths.append(path) + return paths + + +def main() -> int: + payload = read_payload() + root = find_repo_root(payload.get("cwd")) + changes = project_changes(root) + if not changes: + return 0 + + message = ( + "Project workflow/docs changed. Before finishing, run focused verification, " + "commit the completed change, and keep samples/ out of the commit." + ) + print(json.dumps({"continue": True, "systemMessage": message}, ensure_ascii=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/skills/fixture-evaluation/SKILL.md b/.codex/skills/fixture-evaluation/SKILL.md new file mode 100644 index 0000000..2cf3756 --- /dev/null +++ b/.codex/skills/fixture-evaluation/SKILL.md @@ -0,0 +1,30 @@ +--- +name: fixture-evaluation +description: Plan local fixture-based quality checks for this MinerU PDF-to-Markdown converter using samples/ without committing sample PDFs. Use when Codex needs to define sample coverage, quality metrics, regression checks, JSON metadata assertions, or human-readable .report.md expectations. +--- + +# Fixture Evaluation + +## Overview + +Use this skill to turn local sample PDFs into a small, repeatable quality plan. Keep samples local and untracked unless the user explicitly asks to commit them. + +## Workflow + +1. Read `PLAN.md` and `PROGRESS.md` first. +2. Inspect `samples/` only enough to understand fixture categories and filenames. +3. Map each fixture to risks: math, tables, multi-column reading order, figures/assets, Korean filenames, and metadata coverage. +4. Separate fast checks using mocked MinerU outputs from optional checks that require MinerU models, GPU, or long execution. +5. Define metrics for both JSON metadata and `.report.md`. +6. Update `PROGRESS.md` with fixture coverage and gaps. + +## Guardrails + +- Do not commit sample PDFs. +- Do not copy samples into tracked fixtures without explicit user permission. +- Do not make GPU/model-dependent checks mandatory for the default fast loop. +- Do not grade only plain-text edit distance; include math, tables, reading order, assets, metadata, and renderability. + +## Reference + +Read `references/evaluation-metrics.md` when defining fixture coverage, regression criteria, or report fields. diff --git a/.codex/skills/fixture-evaluation/agents/openai.yaml b/.codex/skills/fixture-evaluation/agents/openai.yaml new file mode 100644 index 0000000..cfdce03 --- /dev/null +++ b/.codex/skills/fixture-evaluation/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fixture Evaluation" + short_description: "Plan fixture quality checks locally" + default_prompt: "Use $fixture-evaluation to plan sample coverage, quality metrics, regression checks, and report expectations without committing sample files." diff --git a/.codex/skills/fixture-evaluation/references/evaluation-metrics.md b/.codex/skills/fixture-evaluation/references/evaluation-metrics.md new file mode 100644 index 0000000..d8e3e72 --- /dev/null +++ b/.codex/skills/fixture-evaluation/references/evaluation-metrics.md @@ -0,0 +1,37 @@ +# Evaluation Metrics + +Use these metrics for local fixture plans and future tests. + +## Fixture Categories + +- Simple digital PDF with text layer. +- Math-heavy paper or chapter. +- Multi-column paper. +- Table with formulas. +- Figure with caption and asset extraction. +- Korean filename/path handling. + +## Fast Checks + +- Output files are planned at deterministic paths. +- Metadata JSON includes source PDF, page count, engine, warnings, and output paths. +- `.report.md` can be generated from metadata without re-running MinerU. +- Markdown math delimiter normalization is deterministic. +- Asset links resolve relative to the Markdown file. + +## Optional MinerU Checks + +- MinerU CLI execution succeeds or produces a clear failure warning. +- Page coverage equals source PDF page count. +- Math renderability failures are counted. +- Table degradation warnings are counted. +- Reading-order uncertainty is surfaced. + +## Report Sections + +- Summary: source file, pages, output files, engine, start/end time. +- Warnings: grouped by severity and code. +- Math: counts for inline, display, low-confidence, and render failures. +- Assets: extracted, missing, broken links. +- Tables: extracted, degraded, fallback count. +- Environment: Python, uv, MinerU version, GPU visibility when available. diff --git a/.codex/skills/math-markdown-review/SKILL.md b/.codex/skills/math-markdown-review/SKILL.md new file mode 100644 index 0000000..a81b547 --- /dev/null +++ b/.codex/skills/math-markdown-review/SKILL.md @@ -0,0 +1,31 @@ +--- +name: math-markdown-review +description: Review and design Obsidian-friendly Markdown normalization for math-heavy PDF conversion, including LaTeX delimiters, display math spacing, asset links, tables, and quality report warnings. Use when Codex needs to check Markdown output assumptions, design post-processing rules, or define renderability checks for formulas and assets. +--- + +# Math Markdown Review + +## Overview + +Use this skill when Markdown output quality matters more than raw text extraction. The goal is best-effort automatic conversion with explicit warnings and provenance for failures. + +## Workflow + +1. Read `PLAN.md` and `PROGRESS.md` first. +2. Read `PRD.md` and `ARCHITECTURE.md` when output behavior, metadata, or reporting is affected. +3. Preserve project delimiter policy: inline math uses `$...$`; display math uses `$$...$$`. +4. Check asset links, table fallback behavior, heading/list interactions, and page boundary markers against Obsidian rendering assumptions. +5. Define warnings for low-confidence math, non-renderable LaTeX, broken asset links, table degradation, and reading-order uncertainty. +6. Ensure `.report.md` content is derived from metadata, not separate manual state. + +## Checks + +- Inline math should not contain unescaped newlines or surrounding spaces that break rendering. +- Display math should be separated from surrounding paragraphs by blank lines. +- Asset paths should be stable, relative to the Markdown file, and safe for Obsidian vaults. +- Tables with formulas should prefer readable Markdown when reliable and warn when downgraded. +- Every renderability failure should be countable in metadata and visible in `.report.md`. + +## Reference + +Read `references/obsidian-output-checks.md` for concrete normalization and report-signal guidance. diff --git a/.codex/skills/math-markdown-review/agents/openai.yaml b/.codex/skills/math-markdown-review/agents/openai.yaml new file mode 100644 index 0000000..4e8dcf7 --- /dev/null +++ b/.codex/skills/math-markdown-review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Math Markdown Review" + short_description: "Check Obsidian math Markdown output" + default_prompt: "Use $math-markdown-review to design or check Obsidian-friendly Markdown normalization, math delimiters, asset paths, tables, and quality report signals." diff --git a/.codex/skills/math-markdown-review/references/obsidian-output-checks.md b/.codex/skills/math-markdown-review/references/obsidian-output-checks.md new file mode 100644 index 0000000..a6746e7 --- /dev/null +++ b/.codex/skills/math-markdown-review/references/obsidian-output-checks.md @@ -0,0 +1,31 @@ +# Obsidian Output Checks + +Use these checks when designing or reviewing Markdown output. + +## Math + +- Inline math: `$...$`, no line breaks inside the delimiter pair. +- Display math: `$$...$$`, with blank lines before and after the block. +- Preserve source provenance for formulas: page index, bbox if available, engine, confidence, and warning codes. +- Record render failures separately from extraction confidence. +- Avoid rewriting LaTeX semantics unless the rule is deterministic and tested. + +## Assets + +- Store images under a deterministic asset directory next to the Markdown output. +- Use relative Markdown links that remain valid when the output directory is moved as a unit. +- Record asset source page, bbox if available, generated file path, and missing-link warnings. + +## Tables + +- Prefer Markdown tables only when cell boundaries and reading order are reliable. +- If formulas or merged cells make Markdown tables misleading, use a readable fallback and emit a table warning. +- Keep table warnings visible in both JSON metadata and `.report.md`. + +## Report Signals + +- Total pages processed and pages with warnings. +- Math block count, inline math count, and non-renderable math count. +- Broken asset links and missing assets. +- Table degradation count. +- Reading-order uncertainty count. diff --git a/.codex/skills/mineru-research/SKILL.md b/.codex/skills/mineru-research/SKILL.md new file mode 100644 index 0000000..fb3d964 --- /dev/null +++ b/.codex/skills/mineru-research/SKILL.md @@ -0,0 +1,32 @@ +--- +name: mineru-research +description: Research MinerU 3.1.0 setup, CLI behavior, output formats, model/runtime requirements, licensing, and local-only integration constraints for this PDF-to-Markdown project. Use when Codex needs to update project knowledge, verify MinerU facts, plan the MinerU adapter, or resolve uncertainty about installation, execution, or output behavior without adding alternate engines. +--- + +# MinerU Research + +## Overview + +Use this skill to verify MinerU 3.1.0 facts before changing project docs or plans. Keep the scope narrow: MinerU 3.1.0 is the only conversion engine and direct local CLI execution is the only v1 execution mode. + +## Workflow + +1. Read `PLAN.md` and `PROGRESS.md` first. +2. Read `PRD.md`, `ARCHITECTURE.md`, and `docs/KNOWLEDGEBASE.md` when the change affects product or architecture decisions. +3. Prefer official MinerU documentation, the MinerU GitHub repository, release notes, primary papers, and official dependency docs. +4. Verify time-sensitive facts with web research before updating docs. +5. Record source URLs and access dates in durable docs when the finding affects future implementation. +6. Update `PROGRESS.md` with the verified fact, unresolved uncertainty, and next action. + +## Constraints + +- Do not reintroduce candidate engine comparisons. +- Allow only direct `mineru` CLI execution and the CLI-internal temporary local `mineru-api` process. +- Do not add cloud OCR, remote LLM, `--api-url`, remote API, router, HTTP client backend, or remote OpenAI-compatible backend paths. +- Do not imply perfect LaTeX reconstruction. +- Do not implement converter code unless the user explicitly requests implementation. +- Treat GTX 1070 Ti 8GB, Python 3.12, uv, and Windows PowerShell as active project constraints. + +## Reference + +Read `references/source-checklist.md` when planning a research pass or updating source-backed documentation. diff --git a/.codex/skills/mineru-research/agents/openai.yaml b/.codex/skills/mineru-research/agents/openai.yaml new file mode 100644 index 0000000..ac71de7 --- /dev/null +++ b/.codex/skills/mineru-research/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "MinerU Research" + short_description: "Verify MinerU local integration facts" + default_prompt: "Use $mineru-research to verify MinerU 2.5 setup, CLI behavior, outputs, licensing, and local-only integration constraints against official sources." diff --git a/.codex/skills/mineru-research/references/source-checklist.md b/.codex/skills/mineru-research/references/source-checklist.md new file mode 100644 index 0000000..38067d7 --- /dev/null +++ b/.codex/skills/mineru-research/references/source-checklist.md @@ -0,0 +1,29 @@ +# MinerU Research Source Checklist + +Use this checklist before changing project docs or plans based on MinerU facts. + +## Sources + +- MinerU GitHub repository for install instructions, CLI examples, output behavior, and license files. +- MinerU official documentation for current setup and execution modes. +- MinerU release notes or tags for version-specific changes. +- Primary papers for model capability claims. +- Official Python, uv, CUDA, PyTorch, or dependency docs for environment compatibility. + +## Facts To Verify + +- Supported Python versions and package manager expectations. +- Whether MinerU 3.1.0 supports the required local CLI path on Windows. +- Whether MinerU 3.1.0's CLI-internal temporary local `mineru-api` behavior stays local and avoids `--api-url`. +- Required model download/cache behavior and offline reuse assumptions. +- GPU/CPU execution options and expected memory pressure for GTX 1070 Ti 8GB. +- Output directory structure, Markdown output, image asset output, JSON/intermediate output, and page/block metadata availability. +- Exit codes, error messages, logging behavior, and partial-output behavior. +- License obligations for MinerU, bundled models, and transitive runtime packages. + +## Recording Rules + +- Record source URL and access date for durable claims. +- Distinguish official fact from inference. +- Keep alternate engine names out of project docs unless the user explicitly asks for a separate historical note. +- If a source conflicts with a fixed product decision, record the conflict and ask for a user decision. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8323e54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv/ +.pytest_cache/ +__pycache__/ +*.py[cod] +outputs/ +node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..10ffeda --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,229 @@ +# AGENTS.md + +This file gives implementation instructions for coding agents working in this repository. + +## Project Mission + +Build a local-only PDF-to-Markdown converter for math-heavy digital PDFs. The converter must produce Obsidian-friendly Markdown and preserve enough metadata to debug formulas, reading order, tables, figures, and assets. + +## Project Guidelines + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +### 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +### 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +### 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +### 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" -> "Write tests for invalid inputs, then make them pass" +- "Fix the bug" -> "Write a test that reproduces it, then make it pass" +- "Refactor X" -> "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] -> verify: [check] +2. [Step] -> verify: [check] +3. [Step] -> verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. + +## Source Documents + +- `PLAN.md`: shared plan, planned work, open questions, and ownership for agents. +- `PROGRESS.md`: completed work, current status, blockers, and next actions for agents. +- `PRD.md`: product requirements, user scope, CLI/API requirements, acceptance criteria. +- `ARCHITECTURE.md`: system layers, MinerU adapter contract, intermediate representation, metadata schema, and local-only enforcement. +- `docs/KNOWLEDGEBASE.md`: research basis and implementation background. +- `docs/V1IMPLEMENTATIONPLAN.md`: v1 implementation sequence, sprint contracts, verification gates, and agent ownership. +- `docs/Sprints/*.md`: active and historical sprint contracts. +- `.codex/agents/*.toml`: project-scoped custom subagent roles. +- `.codex/commands/*.md`: reusable project prompt commands. +- `.codex/skills/*/SKILL.md`: project-specific Codex skills. +- `.codex/hooks.json` and `.codex/hooks/*.py`: project hook configuration and deterministic hook scripts. + +## Startup Workflow + +At the start of every task: + +- Read `PLAN.md` and `PROGRESS.md` before deciding what to do. +- Read only the other source documents needed for the task. +- Use `.codex/agents`, `.codex/commands`, and `.codex/skills` when the user explicitly asks for agent delegation, reusable workflows, or specialized project guidance. +- State the relevant current goal, next action, and blocker if one exists. +- If `PLAN.md` and `PROGRESS.md` conflict, trust `PROGRESS.md` for what has happened and update `PLAN.md` when making the next change. + +## Progress Tracking + +Use `PLAN.md` and `PROGRESS.md` to coordinate work across agents. + +- Update `PLAN.md` when planned work, ownership, sequencing, open questions, or decisions change. +- Update `PROGRESS.md` after meaningful work, verification, blockers, or next actions change. +- Keep entries short and factual. +- Do not use these files as scratchpads or long research notes. +- Do not mark work complete until it has been verified. +- When multiple agents work in parallel, each agent must leave enough context in `PROGRESS.md` for the next agent to resume without guessing. + +## Long-Running Harness Workflow + +For substantial implementation work, follow the planner/generator/evaluator pattern from Anthropic's long-running harness design article: https://www.anthropic.com/engineering/harness-design-long-running-apps. + +Use the harness only when task complexity justifies the overhead. For small documentation edits or narrow fixes, a single agent with focused verification is preferred. + +Harness roles: + +- `harness-planner-agent`: expands a brief request into product context, high-level technical direction, non-goals, risks, and a sequence of small contracts. +- `feature-generator-agent`: implements one agreed contract at a time after implementation has been explicitly requested. +- `evaluation-agent`: independently reviews proposed contracts and completed work. It must be skeptical, specific, and willing to fail work that is incomplete, stubbed, unverified, or below threshold. + +Before each implementation chunk: + +- Write or update a concise sprint contract before code changes start. +- Include objective, touched surfaces, expected outputs, non-goals, verification steps, hard failure criteria, and handoff fields. +- Let `evaluation-agent` review the contract before `feature-generator-agent` implements it. +- Avoid over-specifying low-level implementation before the responsible agent has inspected the code. + +After each implementation chunk: + +- `feature-generator-agent` runs a self-check but does not approve its own work. +- `evaluation-agent` performs independent checks against the contract and reports actionable findings. +- If the chunk fails, feed the evaluator's findings back into the next generator pass. +- Update `PROGRESS.md` with completed work, checks run, residual risks, and the next concrete action. + +When context becomes too large or a task spans sessions, prefer a clean structured handoff over relying only on conversation history. The handoff must include current state, decisions made, files touched, checks run, known failures, and the next action. + +Periodically re-evaluate the harness itself. Remove roles, contracts, or checks that are not load-bearing, and add structure only when it improves correctness, scope control, or verification quality. + +## Fixed Product Decisions + +- Language: Python. +- Workflow: `uv`. +- Interface: CLI plus Python library. +- Default CLI name: `pdf2md`. +- Runtime policy: local-only. Do not add cloud OCR, remote LLM, or external document upload paths. +- Default output: Obsidian-friendly Markdown. +- Inline math: `$...$`. +- Display math: `$$...$$`. +- Conversion engine: MinerU 3.1.0. +- Hardware target: NVIDIA GPU. +- Input priority: digital PDFs with text layers. +- Quality workflow: fully automatic. Log warnings and continue when possible. +- MinerU execution: direct local `mineru` CLI only. MinerU 3.1.0 may launch a temporary local `mineru-api` internally when CLI runs without `--api-url`. +- Quality report: write both metadata JSON and `.report.md`. +- v1 use case: personal/research. MinerU and transitive model/package licenses must be documented before redistribution. + +## Architecture Guidance + +Follow `ARCHITECTURE.md` for implementation structure. Do not duplicate architecture decisions in code comments or docs unless the new text points back to that file. + +Key implementation constraints: + +- Keep MinerU-specific objects behind the MinerU adapter. +- Keep public CLI/library contracts stable and project-owned. +- Keep Obsidian Markdown normalization separate from MinerU execution. +- Keep metadata and warning generation structured. +- Keep quality report generation derived from metadata and local checks. +- Do not add runtime engine selection in v1. + +## Local-Only Requirements + +Never add runtime dependencies that upload PDFs, page images, or extracted text to remote services. Follow the strict-local enforcement rules in `ARCHITECTURE.md`. + +Allowed in v1: direct `mineru` CLI execution and the CLI-internal temporary local `mineru-api` process. + +Do not pass `--api-url`, use remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible inference endpoints in v1. + +## CLI Behavior + +Follow the CLI requirements in `PRD.md`. Do not add commands, flags, config files, or runtime engine selection unless the user explicitly asks for them. + +Do not overwrite user files unless the requested behavior and `--overwrite` semantics allow it. + +## Testing Guidance + +Add tests in proportion to behavior risk. + +Required early tests: + +- Math delimiter normalization. +- Display math spacing. +- Asset path normalization. +- Metadata schema creation. +- Warning aggregation. +- CLI path planning and overwrite behavior. +- MinerU adapter contract with mocked outputs. + +Fixture PDFs should cover: + +- Simple digital PDF. +- Math-heavy academic PDF. +- Multi-column paper. +- Table with formulas. +- Figure with caption. + +Any test that depends on large local models should be optional or marked separately so normal CI/dev checks can run quickly. + +## Git Workflow + +After changing files: + +- Run the smallest useful verification for the change. +- Check `git status --short`. +- Commit the completed change unless the user explicitly asks not to. +- Do not include unrelated user edits in the commit. + +## Documentation Guidance + +Keep documentation explicit about: + +- Local-only privacy behavior. +- NVIDIA GPU expectations. +- MinerU installation and model downloads. +- Known limitations of automatic formula reconstruction. +- Dependency licenses. +- Obsidian output assumptions. + +Do not imply perfect LaTeX conversion. The correct guarantee is best-effort automatic conversion with warnings and provenance. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..9946c58 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,243 @@ +# Architecture: Local PDF-to-Markdown Converter + +Last updated: 2026-05-07 + +## 1. Overview + +The system converts math-heavy digital PDFs into Obsidian-friendly Markdown using MinerU 3.1.0 as the fixed local conversion engine. Product requirements live in `PRD.md`; agent workflow rules live in `AGENTS.md`; research notes live in `docs/KNOWLEDGEBASE.md`. + +The architecture separates MinerU execution from project-owned normalization and metadata. This boundary exists only to isolate MinerU I/O; it is not a pluggable engine system. + +## 2. System Layers + +1. CLI/API layer + - Parse command arguments and Python API parameters. + - Discover input PDFs. + - Plan output paths. + - Enforce overwrite behavior. + - Print conversion summaries. + +2. MinerU adapter layer + - Validate MinerU 3.1.0 installation and version. + - Run MinerU through direct local CLI execution. + - Capture raw Markdown, structured output, assets, logs, and exit status. + - Enforce strict-local execution. + +3. Intermediate representation layer + - Convert MinerU-specific output into project-owned document/page/block objects. + - Preserve page index, bbox, confidence, source engine, and asset references returned by MinerU. + - Prevent raw MinerU objects from becoming public API return types. + +4. Normalization layer + - Convert project-owned objects and MinerU Markdown into Obsidian-friendly Markdown. + - Normalize math delimiters, display math spacing, headings, tables, and asset links. + +5. Quality and metadata layer + - Run link checks and math renderability checks with local tooling. + - Aggregate structured warnings. + - Write metadata JSON, quality report Markdown, and optional raw MinerU diagnostics. + +## 3. Conversion Pipeline + +1. Input discovery + - Accept a single PDF or a directory. + - Require `--recursive` for subdirectory traversal. + - Validate that each selected input is a local PDF. + - Compute source SHA-256. + +2. MinerU conversion + - Create an isolated work directory per input PDF. + - Run the MinerU 3.1.0 adapter through the direct `mineru` CLI. + - Capture raw Markdown, raw JSON/structured output when available, extracted assets, warnings, and logs. + +3. Intermediate representation + - Build document/page/block records from MinerU output. + - Preserve provenance data instead of relying only on final Markdown text. + +4. Obsidian normalization + - Normalize inline math to `$...$`. + - Normalize display math to `$$...$$` blocks on separate lines. + - Normalize image links to stable relative asset paths. + - Normalize tables without destroying complex table structure. + +5. Quality checks + - Verify generated asset links. + - Check math renderability when local tooling is available. + - Emit warnings without stopping conversion unless no usable output can be produced. + +6. Output writing + - Write final Markdown. + - Write extracted assets. + - Write metadata JSON. + - Write `.report.md`. + - Keep raw MinerU output when requested. + +## 4. MinerU Adapter Contract + +The MinerU adapter exposes: + +- `name` +- `is_available()` +- `version()` +- `doctor()` +- `convert(input_pdf, work_dir, options)` + +Adapter conversion output contains: + +- `raw_markdown` +- `raw_structured` +- `assets` +- `pages` +- `warnings` +- `engine` +- `engine_version` +- `engine_options` +- `exit_code` +- `stderr` + +The adapter must fail fast if it cannot run in strict-local mode. Runtime engine selection is not part of v1. + +The default conversion device is `cuda:0`. Because MinerU 3.1.0 selects its local device through environment/config rather than a dedicated CLI GPU flag, the adapter must set the MinerU subprocess environment to request CUDA by default while keeping the command shape direct and local. + +Allowed MinerU execution in v1: + +- Direct local `mineru` CLI execution. +- The temporary local `mineru-api` process that MinerU 3.1.0 starts internally when the CLI runs without `--api-url`. + +Prohibited MinerU execution in v1: + +- Passing `--api-url`. +- Remote APIs. +- Router mode. +- HTTP client backends. +- Remote OpenAI-compatible backends or inference endpoints. + +## 5. Intermediate Representation + +The project uses a small internal representation for normalization and metadata. + +Required concepts: + +- Document +- Page +- Block +- Asset +- Warning + +Required block types: + +- `heading` +- `paragraph` +- `inline_formula` +- `display_formula` +- `table` +- `figure` +- `caption` +- `footnote` +- `reference` +- `unknown` + +Record these page/block fields when MinerU returns them. Do not invent missing values. + +- Page index. +- Page dimensions. +- Bounding boxes. +- Confidence. +- Source engine, fixed to MinerU 3.1.0 in v1. +- Markdown character span. + +## 6. Markdown Normalization + +Final Markdown must prioritize Obsidian. + +- Use `$...$` for inline math. +- Use display math blocks with `$$` on their own lines. +- Keep blank lines around display math. +- Do not escape underscores or carets inside math unnecessarily. +- Prefer Markdown tables for simple tables. +- Use HTML tables for complex tables when Markdown would lose structure. +- Store figures/images in a stable relative assets directory. +- Do not add visible page separators in v1. +- Preserve captions and references when MinerU provides them. + +## 7. Metadata Schema + +When metadata is enabled, write `.metadata.json`. + +Required top-level fields: + +- `source_pdf` +- `source_sha256` +- `created_at` +- `engine` +- `engine_version` +- `engine_options` +- `pages` +- `assets` +- `warnings` +- `summary` + +Required summary fields: + +- `pages_processed` +- `warning_count` +- `asset_count` +- `display_formula_count` +- `inline_formula_count` +- `math_render_error_count` + +Warning records include: + +- `code` +- `severity` +- `page_index` +- `bbox` +- `message` + +Stable warning code examples: + +- `ENGINE_MISSING` +- `GPU_UNAVAILABLE` +- `LOW_CONFIDENCE_FORMULA` +- `MATH_RENDER_FAILED` +- `ASSET_LINK_MISSING` +- `READING_ORDER_UNCERTAIN` +- `STRICT_LOCAL_VIOLATION` +- `MINERU_CLI_FAILED` + +## 8. Quality Report + +Every conversion writes `.report.md`. + +The report is derived from metadata and local quality checks. It contains: + +- Source and output paths. +- MinerU version and execution mode. +- Pages processed. +- Warning count. +- Asset count and missing asset link count. +- Inline and display formula counts. +- Math render error count. +- Pages with warnings. +- Final status: `success`, `partial`, or `failed`. + +## 9. Local-Only Enforcement + +The implementation must not upload PDFs, page images, or extracted text to remote services. + +Strict-local mode is on by default. The MinerU adapter must not call cloud OCR APIs, hosted document parsing APIs, hosted LLM/VLM APIs, or remote model inference endpoints. + +Local-only execution means direct `mineru` CLI execution in v1. MinerU 3.1.0's CLI-internal temporary local `mineru-api` process is allowed because it is local orchestration owned by the CLI invocation. User-specified API URLs, router mode, HTTP client backends, remote APIs, and remote OpenAI-compatible backends are not allowed. + +Allowed network activity is limited to documentation, package/model installation initiated by setup commands, or tests explicitly marked as network tests and disabled by default. + +## 10. Failure Policy + +Conversion should continue automatically when possible. + +- Low-confidence formulas are included as best effort and recorded as warnings. +- Low-confidence pages are included as best effort and recorded as warnings. +- Run MinerU with its default local CLI behavior first. +- If MinerU cannot run or returns a failure, report the failure clearly and do not silently switch backend. +- Conversion fails only when the input cannot be opened, MinerU cannot run, no usable output can be produced, output cannot be written, or strict-local policy is violated. +- CLI summaries must report warning counts clearly. diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..ed7ee27 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,89 @@ +# PLAN.md + +This file is the shared work plan for agents. Read it before starting work, then update it when the plan changes. + +## Current Goal + +CUDA-enabled PyTorch and MinerU 3.1.0 runtime setup is complete in the project `.venv`. Sprint 10 pre-conversion PDF chunking is implemented; next work is optional real local sample validation only if requested. + +## Active Constraints + +- Do not implement additional program code beyond the active user-approved sprint. +- Keep MinerU 3.1.0 as the only conversion engine. +- Keep processing local-only. +- Target Python 3.12. +- Target GPU: GTX 1070 Ti 8GB. +- Default conversion device: `cuda:0`. +- Run MinerU through direct local CLI execution only. +- On MinerU failure, report a clear error/warning and do not silently fallback. +- Write both metadata JSON and a human-readable `.report.md` quality report for conversions. +- Use `samples/` only as local fixture context; do not commit sample files unless explicitly requested. + +## Planned Work + +1. Use `research-agent` for MinerU 3.1.0 source tracking and official-doc verification. +2. Use `requirements-guard-agent` for cross-document consistency reviews. +3. Use `mineru-integration-agent` for direct local MinerU CLI adapter planning. +4. Use `obsidian-markdown-agent` for math-heavy Obsidian Markdown output planning. +5. Use `metadata-agent` for provenance, warning, JSON metadata, and `.report.md` planning. +6. Use `evaluation-agent` for local fixture coverage and regression criteria. +7. Use `local-setup-agent` for Python 3.12, uv, CUDA, GTX 1070 Ti 8GB, and doctor-check planning. +8. Use `license-privacy-agent` for license and strict-local privacy review. +9. Use `harness-planner-agent` to turn substantial implementation requests into scoped contracts before code work starts. +10. Use `feature-generator-agent` to implement one approved contract at a time after the user explicitly requests implementation. +11. Use `evaluation-agent` as the independent contract reviewer and QA evaluator before and after each implementation chunk. +12. Follow `docs/V1IMPLEMENTATIONPLAN.md` for the v1 implementation sprint sequence. +13. Use `docs/Sprints/SPRINT10CONTRACT.md` for the implemented long-PDF pre-conversion chunking sprint. + +## Open Questions + +- None. + +## Decisions + +- Use `PLAN.md` for intended work and ownership. +- Use `PROGRESS.md` for completed work, current status, blockers, and next actions. +- MinerU default local CLI execution is the only v1 execution mode. +- MinerU 3.1.0 may launch a temporary local `mineru-api` internally when `mineru` CLI runs without `--api-url`. +- Strict-local mode forbids `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends. +- No silent fallback after MinerU failure. +- Conversion output includes both metadata JSON and `.report.md`. +- Local MathJax render checking is optional and nonfatal; missing Node.js or MathJax must produce a clear warning instead of blocking conversion. +- Project-scoped custom agents live in `.codex/agents/*.toml`. +- Project prompt commands live in `.codex/commands/*.md`. +- Project-specific skills live in `.codex/skills/*/SKILL.md`. +- Project hooks live in `.codex/hooks.json` and `.codex/hooks/*.py`. +- Agent, command, skill, and hook assets are written in English for Codex compatibility. +- Long-running implementation should use a planner/generator/evaluator harness only when the task complexity justifies the overhead. +- Each substantial implementation chunk should have a sprint contract with objective, scope, verification, failure thresholds, and handoff fields. +- Generator agents may self-check, but independent evaluation is required before marking a chunk complete. +- V1 implementation sequencing and sprint contracts live in `docs/V1IMPLEMENTATIONPLAN.md`. +- Concrete sprint contract documents live under `docs/Sprints/`. +- Sprint 2 path planning contract lives at `docs/Sprints/SPRINT2CONTRACT.md`. +- Sprint 3 domain records and metadata contract lives at `docs/Sprints/SPRINT3CONTRACT.md`. +- Sprint 4 MinerU adapter contract lives at `docs/Sprints/SPRINT4CONTRACT.md`. +- Sprint 4 fixes the v1 adapter executable to the direct `mineru` CLI; user-specified alternate executables, including `mineru-api`, are prohibited. +- Sprint 5 Obsidian Markdown normalization and asset link contract lives at `docs/Sprints/SPRINT5CONTRACT.md`. +- Sprint 5 owns Markdown normalization only; it does not write final Markdown files, copy assets, run MinerU, or connect to conversion orchestration. +- Sprint 6 quality checks and report generation contract lives at `docs/Sprints/SPRINT6CONTRACT.md`. +- Sprint 6 owns quality/report boundaries only; it does not write final files, run MinerU, or connect to conversion orchestration. +- Sprint 7 conversion orchestration, CLI, and Python API contract lives at `docs/Sprints/SPRINT7CONTRACT.md`. +- Sprint 7 will be the first implementation sprint allowed to write final Markdown, metadata JSON, report Markdown, and local copied assets as product behavior. +- Sprint 7 implemented conversion orchestration, `convert_pdf`, batch conversion, `pdf2md convert`, output writing, metadata/report writing, and fake-adapter CLI/API tests. +- Sprint 8 should cover `pdf2md doctor` and setup documentation; Sprint 7 intentionally did not add doctor behavior. +- Sprint 8 doctor and setup documentation contract lives at `docs/Sprints/SPRINT8CONTRACT.md`. +- Sprint 8 owns doctor diagnostics and setup docs only; it must not run real MinerU, download models, run sample PDFs, or add runtime remote/API paths in default tests. +- Sprint 8 implements `pdf2md doctor`, local setup diagnostics, and setup documentation without running real MinerU, downloading models, or touching `samples/` in default tests. +- Sprint 9 local fixture evaluation and v1 release gate contract lives at `docs/Sprints/SPRINT9CONTRACT.md`. +- Sprint 9 must keep default tests independent of real MinerU, GPU, models, network, Obsidian, LaTeX tooling, and `samples/`; real MinerU fixture checks must be explicit opt-in only. +- Sprint 9 implements fast mocked integration tests, explicit opt-in local MinerU fixture evaluation, and `docs/V1RELEASECHECKLIST.md`. +- `pdf2md convert` defaults to `--gpu cuda:0`. +- The MinerU adapter maps CUDA device requests to local subprocess environment variables instead of adding speculative MinerU CLI flags. +- GTX 1070 Ti local runtime uses PyTorch `2.6.0+cu126` and `torchvision 0.21.0+cu126` installed after `uv sync`, followed by `mineru[core]==3.1.0`. +- MinerU models are downloaded with `mineru-models-download -s huggingface -m all`, and runtime model loading uses `MINERU_MODEL_SOURCE=local`. +- Sprint 10 should use `pypdf` for local 20-page PDF chunk creation if implementation is approved. +- Sprint 10 uses `pypdf` for local PDF page chunk planning and temporary chunk PDF writing. +- Sprint 10 converts chunk PDFs independently and does not merge generated Markdown outputs. +- Chunking is opt-in through `--chunk-pages`; if the option is present without a value, the CLI uses 20 pages per chunk. +- `convert_pdf()` keeps returning `ConversionResult` without chunking and returns `BatchConversionResult` when `chunk_pages` is set. +- Chunk PDFs are temporary local files and are deleted after conversion completes, including when raw MinerU output is retained. diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..e1a3752 --- /dev/null +++ b/PRD.md @@ -0,0 +1,307 @@ +# PRD: Local PDF-to-Markdown Converter + +Last updated: 2026-05-07 + +## 1. Summary + +Build a local-only CLI and Python library that converts math-heavy digital PDFs into Obsidian-friendly Markdown. The product prioritizes accurate LaTeX reconstruction for equations, preservation of document structure, stable asset links, and traceable page-level metadata. + +The first version is for personal/research use, targets NVIDIA GPU machines, and uses MinerU 3.1.0 as the fixed conversion engine. It should process digital PDFs with existing text layers first. Scanned books, cloud OCR APIs, web UI, and manual review workflows are out of scope for v1. + +## 2. Goals + +- Convert a single PDF into one Markdown file plus assets, metadata JSON, and a human-readable quality report. +- Convert a folder of PDFs in batch mode. +- Preserve inline math as `$...$` and display math as `$$...$$`. +- Produce Markdown that opens cleanly in Obsidian. +- Use MinerU 3.1.0 locally. +- Keep enough metadata to diagnose formula, layout, and reading-order errors. +- Continue conversion automatically when a page or formula is low-confidence, while logging warnings. + +## 3. Non-Goals + +- No cloud OCR, cloud LLM, or third-party document upload in v1. +- No web app or GUI in v1. +- No manual review queue in v1. +- No optimization for low-quality scanned books in v1. +- No guaranteed perfect LaTeX reconstruction. +- No multi-user server or hosted API in v1. +- No commercial redistribution assumptions until dependency licenses are reviewed. + +## 4. Target Users + +Primary user: + +- A researcher, student, or developer converting math-heavy papers/books into Obsidian notes. + +## 5. Input Scope + +Supported in v1: + +- Local `.pdf` files. +- Directories containing `.pdf` files. +- Digital PDFs with embedded text layers. +- Academic papers with sections, references, figures, captions, tables, inline math, and display equations. + +Best effort in v1: + +- Multi-column academic layouts. +- Tables containing math. +- Figures and captions. +- Page numbers, headers, and footers. + +Out of scope for v1 optimization: + +- Poor-quality scans. +- Handwritten math. +- Camera photos. +- Password-protected PDFs. +- Damaged PDFs that cannot be opened by local tooling. + +## 6. Output Scope + +For each input PDF, the converter writes: + +- A normalized Markdown file. +- An assets directory when MinerU extracts images or other media. +- A metadata JSON file. +- A human-readable quality report named `.report.md`. +- Optional raw MinerU outputs for debugging. + +Markdown rules: + +- Inline equations use `$...$`. +- Display equations use `$$...$$` on separate lines. +- Simple tables use Markdown pipe tables. +- Complex tables may use HTML when Markdown would lose structure. +- Images use relative links to the generated assets directory. +- Visible page markers should be avoided by default; page provenance belongs in metadata. +- Obsidian compatibility is the output standard. + +Detailed Markdown normalization rules are defined in `ARCHITECTURE.md`. + +## 7. CLI Requirements + +The CLI binary should be named `pdf2md`. + +Required commands: + +```bash +pdf2md convert INPUT --out OUTPUT_DIR +pdf2md doctor +``` + +`convert` behavior: + +- If `INPUT` is a PDF, convert that file. +- If `INPUT` is a directory, convert PDFs in that directory. +- Directory conversion requires `--recursive` to descend into subdirectories. +- Output filenames default to the source PDF stem plus `.md`. +- Asset directories default to `.assets`. +- Existing outputs are not overwritten unless `--overwrite` is passed. + +Required `convert` options: + +- `--out PATH`: output directory. +- `--metadata`: write metadata JSON. Enabled by default in v1. +- `--keep-raw`: keep raw MinerU output for debugging. +- `--recursive`: recursively process directory inputs. +- `--overwrite`: replace existing outputs. +- `--gpu DEVICE`: select CUDA device. Default: `cuda:0`. +- `--strict-local`: forbid remote network/cloud execution during conversion. Default: true. + +`doctor` behavior: + +- Report Python version. +- Report `uv` availability. +- Report CUDA/PyTorch GPU availability when detectable. +- Report MinerU availability. +- Report local model/cache paths when detectable. +- Warn if no NVIDIA GPU is available. +- Fail if required v1 runtime dependencies are missing. + +## 8. Python Library Requirements + +The library should expose a stable API suitable for scripts and tests. + +Required high-level API: + +```python +from pdf2md import convert_pdf + +result = convert_pdf( + input_path="paper.pdf", + output_dir="out", + metadata=True, +) +``` + +Required return fields: + +- `markdown_path` +- `metadata_path` +- `assets_dir` +- `warnings` +- `engine` +- `pages_processed` + +The public API should not expose raw MinerU objects as required return types. MinerU-specific data may be stored under optional metadata fields. + +## 9. Metadata Requirements + +When `--metadata` is enabled, write `.metadata.json`. + +Required top-level fields: + +- `source_pdf` +- `source_sha256` +- `created_at` +- `engine` +- `engine_version` +- `engine_options` +- `pages` +- `assets` +- `warnings` +- `summary` + +Required summary fields: + +- `pages_processed` +- `warning_count` +- `asset_count` +- `display_formula_count` +- `inline_formula_count` +- `math_render_error_count` + +Warnings must be non-fatal unless the source file cannot be read or no output can be produced. + +Detailed metadata fields, block types, and warning codes are defined in `ARCHITECTURE.md`. + +## 10. Quality Report Requirements + +For every conversion, write `.report.md`. + +The report must be readable without opening the JSON metadata and include: + +- Source PDF path. +- Output Markdown path. +- MinerU version. +- Page count. +- Warning count. +- Asset count. +- Inline formula count. +- Display formula count. +- Math render error count. +- Missing asset link count. +- A short list of pages with warnings. + +## 11. Quality Policy + +The product is fully automatic in v1. + +- Low-confidence formulas are included in the output as best effort. +- Low-confidence pages are included in the output as best effort. +- The converter logs warnings and metadata records. +- Conversion uses MinerU's default local CLI execution. If MinerU cannot run or fails, the converter must emit a clear error/warning instead of silently falling back to another backend. +- Conversion fails only when the input cannot be opened, MinerU cannot run, output cannot be written, no usable output can be produced, or local-only policy is violated. + +The CLI summary must report warning counts clearly. + +## 12. Local-Only Policy + +The implementation must not upload PDFs or page images to cloud APIs. + +Prohibited in v1 runtime: + +- Any cloud OCR API. +- Any hosted document parsing API. +- Any remote LLM or VLM call. +- Remote model inference endpoints. + +Allowed: + +- Local model files. +- Local Python packages. +- Local CLI tools. +- Documentation links. +- Explicit installation downloads initiated by the user during setup. + +`--strict-local` is on by default. The MinerU adapter must not use remote endpoints in strict-local mode. + +Allowed in v1 runtime: + +- Direct `mineru` CLI execution. +- The temporary local `mineru-api` process that MinerU 3.1.0 starts internally when the CLI runs without `--api-url`. + +Prohibited in v1 runtime: + +- `--api-url`. +- Remote APIs. +- Router mode. +- HTTP client backends. +- Remote OpenAI-compatible backends or inference endpoints. + +Detailed strict-local enforcement rules are defined in `ARCHITECTURE.md`. + +## 13. Installation Requirements + +Use `uv` as the primary project workflow. + +Expected setup commands: + +```bash +uv sync +uv run pdf2md doctor +``` + +MinerU/model setup may require additional scripts, for example: + +```bash +uv run scripts/install-mineru.ps1 +uv run scripts/install-models.py +``` + +The project should document NVIDIA GPU/CUDA expectations and provide clear errors when GPU acceleration is unavailable. + +## 14. Test Requirements + +Required test categories: + +- Unit tests for Markdown math delimiter normalization. +- Unit tests for asset path normalization. +- Unit tests for metadata schema creation. +- Unit tests for warning aggregation. +- MinerU adapter contract tests with mocked outputs. +- CLI tests for single PDF, directory input, overwrite behavior, and metadata output. + +Fixture categories: + +- Small digital PDF with simple text and math. +- Math-heavy academic paper page. +- Multi-column paper page. +- Table with formulas. +- Figure with caption. + +Acceptance checks: + +- Markdown exists after conversion. +- Metadata exists when requested. +- Quality report exists after conversion. +- Asset links resolve. +- Inline/display math delimiters match Obsidian expectations. +- Math render checks report failures instead of silently passing. +- No cloud calls are made. +- Warnings do not stop conversion unless MinerU cannot produce output. +- MinerU failure produces a clear error/warning and does not silently switch backend. + +## 15. Release Criteria for v1 + +v1 is acceptable when: + +- `pdf2md convert paper.pdf --out out --metadata` works on a representative digital academic PDF. +- `pdf2md convert pdfs --out out --recursive --metadata` works on a small folder. +- `pdf2md doctor` reports MinerU/GPU status clearly. +- The default output opens in Obsidian with math blocks rendered. +- Metadata links pages, blocks, warnings, and assets to the source PDF. +- `.report.md` summarizes warnings, formulas, assets, and render/link check results. +- The README or setup docs explain local-only behavior and GPU expectations. diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..6bbac70 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,437 @@ +# PROGRESS.md + +This file records actual progress for agents. Read it before starting work, then update it after meaningful changes. + +## Current Status + +- Project direction is documented. +- MinerU 3.1.0 is fixed as the only conversion engine. +- `PRD.md`, `ARCHITECTURE.md`, `AGENTS.md`, and `docs/KNOWLEDGEBASE.md` exist. +- `samples/` exists locally and is untracked by git. +- Converter implementation exists through Sprint 9 path planning, project-owned records, metadata, mocked direct local MinerU adapter boundary, Obsidian Markdown normalization, local quality checks, report content rendering, conversion orchestration, public conversion API, `pdf2md convert`, `pdf2md doctor`, fast mocked integration tests, optional local MinerU fixture evaluation, and the v1 release checklist. +- Default conversion now requests `cuda:0`; the MinerU adapter sets local GPU-related environment for the MinerU subprocess. +- Project-local Codex workflow assets now live under `.codex/`. +- `docs/V1IMPLEMENTATIONPLAN.md` now defines the v1 implementation sequence. +- `docs/Sprints/SPRINT0CONTRACT.md` now defines the Sprint 0 contract. +- `docs/Sprints/SPRINT1CONTRACT.md` now defines the Sprint 1 scaffold contract. +- `docs/Sprints/SPRINT2CONTRACT.md` now defines the Sprint 2 path planning contract. +- `docs/Sprints/SPRINT3CONTRACT.md` now defines the Sprint 3 domain records and metadata contract. +- `docs/Sprints/SPRINT4CONTRACT.md` now defines the Sprint 4 mocked MinerU adapter contract. +- `docs/Sprints/SPRINT5CONTRACT.md` now defines the Sprint 5 Obsidian Markdown normalization and asset link contract. +- `docs/Sprints/SPRINT6CONTRACT.md` now defines the Sprint 6 quality checks and report generation contract. +- `docs/Sprints/SPRINT7CONTRACT.md` now defines the Sprint 7 conversion orchestration, CLI, and Python API contract. +- `docs/Sprints/SPRINT8CONTRACT.md` now defines the Sprint 8 doctor and setup documentation contract. +- `docs/Sprints/SPRINT9CONTRACT.md` now defines the Sprint 9 local fixture evaluation and v1 release gate contract. +- Relevant `.codex/agents/*.toml` files now reference the v1 plan and sprint contract paths directly. +- Sprint 10 is implemented with opt-in pre-conversion PDF chunking, temporary chunk PDF cleanup, chunk metadata/report context, and mocked tests. +- Sprint 0 source, environment, license, privacy, and contract verification is complete with a `go-with-risks` recommendation. +- Sprint 1 is complete with a minimal Python package, CLI placeholder, and fast pytest loop. +- Sprint 4 is implemented with a mock-tested direct local MinerU CLI adapter. +- Sprint 5 is implemented with a pure Markdown normalizer and local-only unit tests. +- Sprint 6 is implemented with local quality checks and report string rendering. +- Sprint 7 is implemented with `convert_pdf`, `convert_input`, output writing, metadata/report writing, local asset copying, batch conversion, and `pdf2md convert`. +- Sprint 7 is implemented with fake-adapter CLI/API tests. +- Sprint 8 is implemented and committed. +- Sprint 9 is implemented, independently evaluated, and committed. +- The project `.venv` has been rebuilt with CUDA-enabled PyTorch and MinerU 3.1.0. +- Latest `samples/MITC공부.pdf` conversion completed on GPU and wrote Markdown, metadata JSON, report Markdown, and assets under ignored `outputs/MITC공부/`. +- `docs/MATHJAXCHECKERPLAN.md` now documents the local MathJax render checker plan and implementation status. +- Local MathJax render checker code now exists with optional local Node.js/`mathjax` setup, default conversion integration, and `doctor` diagnostics. +- `docs/Sprints/SPRINT10CONTRACT.md` now documents the implemented long-PDF pre-conversion chunking sprint. + +## Environment Notes + +- OS/workspace: Windows PowerShell in `D:\Work\Repos\AICoding\ConvertPDFToMD`. +- Python target: 3.12. +- Local Python observed during Sprint 0: 3.12.7. +- `uv` observed during Sprint 0: not available on PATH. +- `uv` installed during Sprint 1: 0.11.11 at `C:\Users\user\.local\bin`. +- If a new shell cannot find `uv`, restart the shell or add `C:\Users\user\.local\bin` to PATH. +- GPU target: GTX 1070 Ti 8GB. +- Local GPU observed during Sprint 0: NVIDIA GeForce GTX 1070 Ti, driver 577.00, 8192 MiB VRAM, WDDM. +- Sample PDFs are in `samples/` and include Korean filenames. +- MinerU execution mode: direct local CLI only. +- MinerU 3.1.0 CLI-internal temporary local `mineru-api` is allowed when CLI runs without `--api-url`. +- Strict-local prohibits `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends. +- MinerU planning pin: `mineru[core]==3.1.0` unless Sprint 1 or Sprint 8 proves another 3.1.0 extra is required. +- MinerU 3.1.0 was installed in the local `.venv` with `uv pip install "mineru[core]==3.1.0"` for real CLI probing. +- Current `pdf2md doctor` status is WARN: MinerU CLI is present, GTX 1070 Ti is visible with Pascal/pre-Turing risk, PyTorch is `2.6.0+cu126` with CUDA available, local MinerU model config is detected, local MathJax checker passes after `npm install`, and strict-local policy passes. +- User-level environment variable `MINERU_MODEL_SOURCE=local` is set so MinerU uses the downloaded local model paths in `C:\Users\user\mineru.json`. + +## Completed Work + +- Created initial project documents. +- Originally selected MinerU 2.5, then changed the fixed engine target to MinerU 3.1.0 after user approval. +- Split architecture details into `ARCHITECTURE.md`. +- Aligned documents with `Project Guidelines`. +- Added this shared planning/progress workflow. +- Decided MinerU failures must produce clear warnings/errors without silent fallback. +- Decided every conversion should produce metadata JSON and a human-readable `.report.md`. +- Created custom agent specs for research, requirements, MinerU integration, Obsidian Markdown, metadata, evaluation, local setup, and license/privacy work. +- Created project prompt commands for startup, MinerU research, document review, integration planning, and quality evaluation planning. +- Created project skills for MinerU research, math Markdown review, and fixture evaluation. +- Created project hooks for startup context, pre-tool policy checks, and stop-time completion reminders. +- Read Anthropic's long-running harness design article and adapted its planner/generator/evaluator pattern for this repository. +- Added `harness-planner-agent` and `feature-generator-agent`. +- Strengthened `evaluation-agent` as an independent contract reviewer and skeptical QA evaluator. +- Added long-running harness workflow guidance to `AGENTS.md`. +- Created `docs/V1IMPLEMENTATIONPLAN.md` with v1 sprint sequencing, contracts, verification gates, and agent ownership. +- Created `docs/Sprints/SPRINT0CONTRACT.md` for source and environment verification before implementation. +- Added direct `docs/V1IMPLEMENTATIONPLAN.md` and `docs/Sprints/SPRINT0CONTRACT.md` references to the agents that need them. +- Completed Sprint 0 contract evaluation; result was PASS. +- Completed the original Sprint 0 MinerU 2.5.4 package, CLI shape, output layout, model/cache, and strict-local risk verification from primary sources. +- Verified local Python, `uv`, and GPU facts using the allowed Sprint 0 commands. +- Verified MinerU/model license and privacy posture for personal/research local use versus redistribution. +- Updated `docs/KNOWLEDGEBASE.md`, `docs/V1IMPLEMENTATIONPLAN.md`, and `docs/Sprints/SPRINT0CONTRACT.md` with Sprint 0 findings. +- Completed post-output Sprint 0 evaluation. The only missing acceptance item at review time was the final commit. +- Redefined strict-local policy for MinerU 3.1.0: allow direct `mineru` CLI and CLI-internal temporary local `mineru-api`; prohibit `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends. +- Updated core project documents and `.codex` workflow assets to reflect MinerU 3.1.0 and the redefined strict-local policy. +- Checked MinerU 3.1.0 sources: PyPI 3.1.0 metadata, MinerU release notes, quick usage docs, CLI tools docs, output file docs, and model source docs. +- Created `docs/Sprints/SPRINT1CONTRACT.md` for project scaffold and fast test loop planning. +- Added direct `docs/Sprints/SPRINT1CONTRACT.md` references to the agents that need Sprint 1 scaffold context. +- Started Sprint 1 implementation and amended the contract to include `uv.lock`, which is generated by `uv sync`. +- Installed `uv` per-user using the official Astral installer. +- Created Sprint 1 scaffold files: `pyproject.toml`, `uv.lock`, `.gitignore`, `README.md`, `src/pdf2md/__init__.py`, `src/pdf2md/cli.py`, `tests/test_package.py`, and `tests/test_cli.py`. +- Verified `uv sync` with a temporary project environment outside the repo. +- Verified `uv run pytest` passes with 4 tests. +- Verified `uv run pdf2md --version` prints `pdf2md 0.1.0`. +- Verified `git diff --check` passes. +- Checked the scaffold for disallowed MinerU, remote API, router, HTTP, or OpenAI backend references. +- Completed independent Sprint 1 evaluation once; the only scope failure was that `PLAN.md` was updated for shared workflow coordination before the Sprint 1 contract listed it as an allowed touched surface. +- Amended the Sprint 1 contract to allow minimal `PLAN.md` current-goal coordination updates. +- Completed the final independent Sprint 1 evaluation; result was PASS. +- Created `docs/Sprints/SPRINT2CONTRACT.md` for paths, input discovery, and overwrite planning. +- Added direct `docs/Sprints/SPRINT2CONTRACT.md` references to the agents that need Sprint 2 path planning context. +- Updated `docs/V1IMPLEMENTATIONPLAN.md` to point Sprint 2 at the new contract and current scaffold state. +- Verified the Sprint 2 contract documentation change with `git diff --check` and `uv run pytest` passing 4 tests. +- Started Sprint 2 implementation after user approval and pre-implementation contract review PASS. +- Added `src/pdf2md/paths.py` for input discovery, output path planning, overwrite conflict detection, duplicate output detection, and output-root escape prevention. +- Added `tests/test_paths.py` with temporary-file coverage for single PDF discovery, directory discovery, recursive discovery, deterministic ordering, Korean filenames, output path planning, overwrite behavior, duplicate planned outputs, and output-root escape prevention. +- Completed independent Sprint 2 evaluation once; the only hard failure was a Windows rooted/drive-relative `relative_parent` escape case. +- Fixed output-root escape prevention by rejecting absolute, rooted, drive-qualified, and `..` relative parents and validating resolved planned outputs stay under the output root. +- Verified `uv run pytest tests/test_paths.py` passes 17 tests. +- Verified `uv run pytest` passes 21 tests. +- Verified `git diff --check` passes for the Sprint 2 implementation. +- Checked the implementation for disallowed MinerU, remote API, router, HTTP, OpenAI backend, or network client references. +- Completed the final independent Sprint 2 evaluation; result was PASS. +- Created `docs/Sprints/SPRINT3CONTRACT.md` for domain records, metadata construction, and warning aggregation planning. +- Added direct `docs/Sprints/SPRINT3CONTRACT.md` references to the agents that need Sprint 3 metadata context. +- Updated `docs/V1IMPLEMENTATIONPLAN.md` to point Sprint 3 at the new contract and current path-planning state. +- Verified the Sprint 3 contract documentation change with `git diff --check` and `uv run pytest` passing 21 tests. +- Started Sprint 3 implementation after user approval and pre-implementation contract review PASS. +- Added `src/pdf2md/ir.py` for project-owned document, page, block, asset, and warning records with stable block types, warning codes, and severities. +- Added `src/pdf2md/metadata.py` for JSON-serializable metadata construction and summary counts from project-owned records. +- Added `tests/test_ir.py` and `tests/test_metadata.py` covering record serialization, optional field preservation/omission, invalid enum/severity validation, metadata top-level fields, summary counts, warning order, JSON serializability, and required input validation. +- Verified `uv run pytest tests/test_ir.py tests/test_metadata.py` passes 25 tests. +- Verified `uv run pytest` passes 46 tests. +- Verified `git diff --check` passes for the Sprint 3 implementation. +- Checked the implementation for disallowed remote API, router, HTTP, OpenAI backend, network client, MinerU adapter, and doctor references. +- Completed the final independent Sprint 3 evaluation; result was PASS. +- Created `docs/Sprints/SPRINT4CONTRACT.md` for direct local MinerU CLI adapter boundary planning with mocked subprocess/output tests. +- Added direct `docs/Sprints/SPRINT4CONTRACT.md` references to the agents that need Sprint 4 MinerU adapter context. +- Updated `docs/V1IMPLEMENTATIONPLAN.md` to point Sprint 4 at the new contract and current metadata-model state. +- Verified the Sprint 4 contract documentation change with `git diff --check` and `uv run pytest` passing 46 tests. +- Started Sprint 4 implementation after user approval and pre-implementation contract review PASS. +- Added `src/pdf2md/mineru_adapter.py` for the direct local MinerU CLI adapter boundary, mockable availability/version checks, deterministic command construction, subprocess result capture, strict-local option validation, optional mocked-output parsing, and adapter warning mapping. +- Added `tests/test_mineru_adapter.py` with fake-runner coverage for availability, missing MinerU, version success/failure/empty output, fixed command shape, custom executable rejection, strict-local rejection, mocked success, non-zero exit, missing output, and invalid JSON. +- Fixed an independent evaluation finding that a caller-controlled executable could bypass strict-local policy; v1 now accepts only the direct `mineru` executable name, and user-exposed `mineru-api` execution is rejected. +- Verified `uv sync` passes. +- Verified `uv run pytest tests/test_mineru_adapter.py` passes 26 tests. +- Verified `uv run pytest` passes 72 tests. +- Verified `git diff --check` passes for the Sprint 4 implementation. +- Checked the implementation for network client imports; none were found. +- Checked strict-local prohibited tokens in `src/pdf2md`; matches are limited to deliberate validation literals in `mineru_adapter.py`. +- Completed the final independent Sprint 4 evaluation; result was PASS. +- Created `docs/Sprints/SPRINT5CONTRACT.md` for Obsidian Markdown normalization, math delimiter handling, asset link normalization, and conservative table fallback planning. +- Added direct `docs/Sprints/SPRINT5CONTRACT.md` references to the agents that need Sprint 5 Markdown, warning, implementation, planning, or evaluation context. +- Updated `docs/V1IMPLEMENTATIONPLAN.md` to point Sprint 5 at the new contract and current Sprint 4 implementation state. +- Verified the Sprint 5 contract documentation change with agent TOML parsing, `git diff --check`, and `uv run pytest` passing 72 tests. +- Started Sprint 5 implementation after user approval and pre-implementation contract review PASS. +- Added `src/pdf2md/markdown.py` for project-owned Obsidian Markdown normalization, inline/display math delimiter handling, code fence and inline code protection, relative asset link normalization, local asset warning behavior, and conservative table fallback warnings. +- Added `tests/test_markdown.py` covering inline math, display math spacing, idempotency, math body preservation, code protection, asset path normalization, invalid/missing/remote asset warnings, simple table preservation, and complex HTML table fallback warnings. +- Added narrow warning codes `ASSET_LINK_INVALID` and `TABLE_FALLBACK` to `src/pdf2md/ir.py`. +- Verified `uv sync` passes. +- Verified `uv run pytest tests/test_markdown.py tests/test_ir.py` passes 30 tests. +- Verified `uv run pytest` passes 89 tests. +- Verified `git diff --check` passes for the Sprint 5 implementation. +- Checked the implementation for network client imports; none were found. +- Checked the implementation for conversion orchestration, metadata writing, report generation, and CLI convert behavior; no Sprint 5 code introduced those paths. +- Completed the final independent Sprint 5 evaluation; result was PASS. +- Created `docs/Sprints/SPRINT6CONTRACT.md` for local quality checks, math renderability boundary, metadata summary extensions, report content rendering, and final status planning. +- Added direct `docs/Sprints/SPRINT6CONTRACT.md` references to the agents that need Sprint 6 quality, reporting, metadata, math renderability, implementation, planning, or evaluation context. +- Updated `docs/V1IMPLEMENTATIONPLAN.md` to point Sprint 6 at the new contract and current Sprint 5 implementation state. +- Verified the Sprint 6 contract documentation change with agent TOML parsing, `git diff --check`, and `uv run pytest` passing 89 tests. +- Started Sprint 6 implementation after user approval and pre-implementation contract review PASS. +- Added `src/pdf2md/quality.py` for local asset-link checks, math renderability checker boundaries, nonfatal checker-unavailable behavior, and quality result aggregation. +- Added `src/pdf2md/report.py` for human-readable quality report content rendering from metadata and quality results, pages-with-warnings derivation, and final status calculation. +- Added `tests/test_quality.py` covering missing/invalid asset links, code-block exclusions, fake math checker failures, checker-unavailable behavior, and quality result merging. +- Added `tests/test_report.py` covering required report content, optional path handling, pages-with-warnings, final status policy, metadata/quality count use, and no report-file creation. +- Verified `uv sync` passes. +- Verified `uv run pytest tests/test_quality.py tests/test_report.py tests/test_metadata.py` passes 26 tests. +- Verified `uv run pytest` passes 103 tests. +- Verified `git diff --check` passes for the Sprint 6 implementation. +- Checked the implementation for network client imports; none were found. +- Checked the implementation for conversion orchestration, final output writing, metadata JSON writing, `.report.md` file writing, real MinerU invocation, setup scripts, and CLI convert behavior; no Sprint 6 code introduced those paths. +- Completed the final independent Sprint 6 evaluation; result was PASS. +- Completed the final independent Sprint 7 evaluation after fixing math renderability metadata counts; result was PASS. +- Started Sprint 8 implementation after user approval. +- Added `src/pdf2md/doctor.py` for mockable setup diagnostics covering Python 3.12, `uv`, MinerU availability/version, NVIDIA GPU visibility, PyTorch CUDA visibility, local model/cache/config detection, and strict-local policy reporting. +- Added `pdf2md doctor` CLI integration without changing `pdf2md convert` or `pdf2md --version` behavior. +- Updated `README.md` with Windows PowerShell setup, `uv`, MinerU 3.1.0 direct CLI expectations, model/cache environment notes, GTX 1070 Ti risk, and strict-local runtime policy. +- Added mocked doctor and CLI tests for success, warning-only success, hard dependency failure, missing `uv`, missing MinerU, MinerU version warnings, missing GPU/PyTorch warnings, GTX 1070 Ti/Pascal risk, and missing model/cache warnings. +- Verified `uv run pytest tests/test_doctor.py tests/test_cli.py` passes 22 tests. +- Verified `uv sync` passes. +- Verified `uv run pytest` passes 133 tests. +- Verified `uv run pdf2md --version` prints `pdf2md 0.1.0`. +- Verified local `uv run pdf2md doctor` returns exit code 1 because MinerU is not installed; it reports Python and `uv` pass, GTX 1070 Ti/Pascal risk warning, PyTorch missing warning, model/cache missing warning, and strict-local pass. +- Completed independent Sprint 8 evaluation; result was PASS. +- Committed Sprint 8 implementation as `7d965e3 feat: implement sprint 8 doctor diagnostics`. +- Created `docs/Sprints/SPRINT9CONTRACT.md` for local fixture evaluation and the v1 release gate. +- Added direct `docs/Sprints/SPRINT9CONTRACT.md` references to the agents that need Sprint 9 fixture evaluation, release gate, strict-local, or implementation context. +- Updated `docs/V1IMPLEMENTATIONPLAN.md` to point Sprint 9 at the new contract and current Sprint 8 completion state. +- Started Sprint 9 implementation after user approval and pre-implementation contract review PASS. +- Added fast mocked v1 release-gate integration tests in `tests/integration/test_v1_fast_release_gate.py`. +- Added explicit opt-in local MinerU fixture evaluation in `tests/integration/test_optional_mineru_fixtures.py`, gated by `PDF2MD_RUN_MINERU_FIXTURES=1`. +- Added `docs/V1RELEASECHECKLIST.md` with default fast gates, strict-local release gates, doctor hard-failure handling, optional sample gates, fixture coverage notes, and no-sample-commit checks. +- Updated `README.md` to point at the v1 release checklist and optional fixture evaluation gate. +- Verified `uv run pytest tests/integration tests/test_conversion.py tests/test_cli.py` passes 24 tests with 1 optional skip. +- Verified `uv run pytest tests/integration` passes 3 fast tests with 1 optional skip. +- Verified opt-in `PDF2MD_RUN_MINERU_FIXTURES=1 uv run pytest -rs tests/integration/test_optional_mineru_fixtures.py` is skipped with a clear doctor blocker because MinerU is not installed. +- Verified `uv run pytest` passes 136 tests with 1 optional skip. +- Completed independent Sprint 9 evaluation; result was PASS. +- Committed Sprint 9 implementation as `466abcf feat: implement sprint 9 release gate`. +- Attempted `samples/MITC공부.pdf` conversion after installing MinerU; the run did not produce a successful conversion and was stopped after the user observed CPU-bound execution. +- Added `outputs/` to `.gitignore` and removed the leftover generated output directory from the stopped sample run. +- Updated default conversion behavior so `convert_pdf`, `convert_input`, and `pdf2md convert` default to `cuda:0`. +- Updated the MinerU adapter to map CUDA requests to the MinerU subprocess environment with `MINERU_DEVICE_MODE` and `CUDA_VISIBLE_DEVICES`, preserving strict-local direct CLI execution. +- Updated README, PRD, and architecture docs to document GPU default behavior and the remaining CUDA/PyTorch requirement. +- Verified the GPU-default change with targeted tests, full tests, `git diff --check`, CLI help, and `pdf2md doctor`. +- Re-ran `uv run pdf2md convert samples\MITC공부.pdf --out outputs\MITC공부 --overwrite`; the CLI reported `converted: 0`, `failed: 1`, `warnings: 1`, and wrote no Markdown, metadata JSON, or `.report.md`. +- Confirmed the failure with a direct adapter probe using an ASCII work directory: MinerU 3.1.0 started its allowed temporary local `mineru-api`, used `hybrid-auto-engine`, attempted to load the VLM model on CUDA, and failed with `AssertionError: Torch not compiled with CUDA enabled`. +- Left the product conversion stdout/stderr logs under ignored `outputs/MITC공부.logs`; removed temporary diagnostic probe directories. +- Removed and recreated the project `.venv` with `uv sync`. +- Installed CUDA-enabled PyTorch runtime: `torch==2.6.0+cu126` and `torchvision==0.21.0+cu126`. +- Verified CUDA with an actual tensor operation on `NVIDIA GeForce GTX 1070 Ti`, compute capability `6.1`. +- Installed `mineru[core]==3.1.0`; verified `mineru, version 3.1.0`. +- Downloaded MinerU pipeline and VLM models with `uv run mineru-models-download -s huggingface -m all`; MinerU wrote model paths to `C:\Users\user\mineru.json`. +- Set `MINERU_MODEL_SOURCE=local` at user scope and current process scope. +- Verified `uv run pdf2md doctor` reports PyTorch CUDA available and model config detected; remaining WARN status is the intentional Pascal/pre-Turing GPU risk warning. +- Verified `uv run pytest` passes 138 tests with 1 optional skip in the rebuilt environment. +- Fixed real MinerU nested `images/...` asset-link rewriting so copied assets under `.assets/.../images/` resolve from the final Markdown. +- Fixed page-count extraction for MinerU-style structured lists with `page_idx` values. +- Verified `uv run pytest tests/test_conversion.py` passes 12 tests. +- Verified `uv run pytest` passes 139 tests with 1 optional skip after the asset/page-count fix. +- Re-ran `samples/MITC공부.pdf` conversion with `MINERU_MODEL_SOURCE=local`; MinerU used GPU via the direct local CLI and CLI-internal temporary local `mineru-api`. +- The final sample outputs are `outputs/MITC공부/MITC공부.md`, `outputs/MITC공부/MITC공부.metadata.json`, `outputs/MITC공부/MITC공부.report.md`, and `outputs/MITC공부/MITC공부.assets/`. +- The final sample report status is `partial` only because the local math render checker is unavailable; asset link checks pass with 0 missing and 0 invalid links. +- Sample summary: 13 pages processed, 107 assets, 23 inline formulas, 103 display formulas, 1 info warning. +- Added `docs/MATHJAXCHECKERPLAN.md` with the local MathJax checker objective, touched surfaces, Node helper contract, Python wrapper behavior, tests, acceptance criteria, and open implementation decisions. +- Implemented the local MathJax render checker with `MathExpression` extraction, a local Node.js helper, a Python wrapper, default conversion integration, `doctor` diagnostics, setup documentation, and mocked tests. +- Verified `npm run mathjax-checker:health` returns `{"ok":true}` after local `npm install`. +- Verified direct helper JSON stdin reports one valid expression as ok and a malformed display formula as `Missing close brace`. +- Verified `create_default_math_checker()` finds the local checker and records one render failure for malformed display math. +- Verified targeted tests pass: `uv run pytest tests/test_quality.py tests/test_math_render.py tests/test_conversion.py tests/test_doctor.py tests/test_cli.py`. +- Verified full tests pass: `uv run pytest` passed 150 tests with 1 optional skip. +- Verified `git diff --check` passes. +- Researched local PDF chunking packages and MinerU page-range behavior for Sprint 10. +- Created `docs/Sprints/SPRINT10CONTRACT.md` recommending `pypdf>=6.10.2,<7` for 20-page local chunk PDFs, with chunk outputs converted independently and no Markdown merge. +- Implemented Sprint 10 with `pypdf>=6.10.2,<7`, `src/pdf2md/pdf_splitter.py`, `--chunk-pages [PAGES]`, chunk-aware conversion orchestration, and chunk report context. +- `--chunk-pages` is opt-in; when present without a value it uses 20 pages. +- `convert_pdf()` returns `BatchConversionResult` when `chunk_pages` is set and keeps returning `ConversionResult` when chunking is unset. +- Temporary chunk PDFs are deleted after conversion completes, including when raw MinerU output is retained. +- Verified targeted Sprint 10 tests: `uv run pytest tests/test_pdf_splitter.py tests/test_conversion.py tests/test_cli.py tests/test_report.py` passed 42 tests. +- Verified full default test suite: `uv run pytest` passed 163 tests with 1 optional skip. +- Verified `git diff --check` passed with line-ending warnings only. + +## In Progress + +- No active implementation chunk. + +## Blockers + +- No active blocker for the completed `samples/MITC공부.pdf` conversion. +- GTX 1070 Ti remains an 8GB Pascal GPU; larger PDFs may still hit VRAM or model compatibility limits even though this sample completed. + +## Next Actions + +1. Review the generated `outputs/MITC공부/MITC공부.md` in Obsidian if visual quality needs manual assessment. +2. Run optional real local chunked conversion on a long sample only if requested. +3. Run `npm install` and `npm run mathjax-checker:health` when real local MathJax checker validation is desired. +4. Preserve the strict-local rule: setup downloads may be explicit, but runtime conversion must use local model paths, direct CLI execution, and no user-specified API or remote backend. + +## Sprint 9 Handoff + +- Files changed: `tests/integration/test_v1_fast_release_gate.py`, `tests/integration/test_optional_mineru_fixtures.py`, `docs/V1RELEASECHECKLIST.md`, `README.md`, `PLAN.md`, `PROGRESS.md`, `docs/V1IMPLEMENTATIONPLAN.md`, and `docs/Sprints/SPRINT9CONTRACT.md`. +- Commands run: `uv run pytest tests/integration tests/test_conversion.py tests/test_cli.py`, `uv run pytest tests/integration`, `PDF2MD_RUN_MINERU_FIXTURES=1 uv run pytest -rs tests/integration/test_optional_mineru_fixtures.py`, `uv run pytest`, `git diff --check`, and `git status --short --untracked-files=all`. +- Tests passed: targeted integration/CLI/conversion run passed 24 tests with 1 optional skip; integration-only run passed 3 fast tests with 1 optional skip; full `uv run pytest` passed 136 tests with 1 optional skip. +- Tests blocked: optional real MinerU fixture conversion is blocked by `pdf2md doctor` because the `mineru` CLI is not installed on PATH. +- Optional local MinerU status: explicitly gated by `PDF2MD_RUN_MINERU_FIXTURES=1`; current opt-in run skips with doctor blocker instead of pretending real validation passed. +- Fixture coverage: release checklist maps local samples to math-heavy, table/formula, figures/assets, reading-order, and Korean filename/path risk categories; simple one-page, table-dominant, and figure-heavy known-baseline gaps remain. +- Generated output locations: none persisted; optional output path uses pytest `tmp_path`. +- Known failures: local doctor fails on missing MinerU CLI. +- Independent evaluation: PASS. +- Residual risks: no real MinerU output has been validated yet; GTX 1070 Ti/PyTorch acceleration and model/cache setup remain unproven; optional fixture quality still requires local MinerU setup. +- User decisions needed: decide whether to install/configure MinerU 3.1.0 and run optional fixture validation. +- V1 release recommendation: default fast gates are healthy, but full real-MinerU v1 validation is blocked until doctor passes or the user records a waiver. +- Go/no-go recommendation for next sprint: go only for real setup/fixture validation if the user wants to proceed with local MinerU installation. +- Next action: commit Sprint 9 implementation. + +## Sprint 9 Contract Handoff + +- Files changed: `docs/Sprints/SPRINT9CONTRACT.md`, `docs/V1IMPLEMENTATIONPLAN.md`, relevant `.codex/agents/*.toml`, `PLAN.md`, and `PROGRESS.md`. +- Commands run: `uv --version`, `uv sync`, agent TOML parse check, `uv run pytest`, `git diff --check`, `git status --short --untracked-files=all`, and local sample filename listing. +- Tests passed: `uv run pytest` passed 133 tests. +- Tests blocked: None expected for the contract-only change. +- Known failures: local `pdf2md doctor` still fails until MinerU is installed on PATH. +- Residual risks: Sprint 9 is contract-only; fast mocked integration tests, optional local MinerU fixture harness, fixture coverage manifest, and release checklist are not implemented yet. +- User decisions needed: None before Sprint 9 pre-implementation review. +- Go/no-go recommendation for Sprint 9 implementation: review the contract first, then go if the user explicitly requests implementation. +- Next action: verify and commit the Sprint 9 contract update. + +## Sprint 8 Handoff + +- Files changed: `src/pdf2md/doctor.py`, `src/pdf2md/cli.py`, `tests/test_doctor.py`, `tests/test_cli.py`, `README.md`, `PLAN.md`, `PROGRESS.md`, `docs/V1IMPLEMENTATIONPLAN.md`, and `docs/Sprints/SPRINT8CONTRACT.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_doctor.py tests/test_cli.py`, `uv run pytest tests/test_doctor.py`, `uv run pytest`, `uv run pdf2md --version`, `uv run pdf2md doctor`, `git diff --check`, `git status --short --untracked-files=all`, and PowerShell strict-local/network pattern checks. +- Tests passed: `uv run pytest tests/test_doctor.py tests/test_cli.py` passed 22 tests; `uv run pytest tests/test_doctor.py` passed 11 tests; `uv run pytest` passed 133 tests. +- Tests blocked: None. +- Known failures: local `uv run pdf2md doctor` correctly fails because the `mineru` CLI is not installed on PATH. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully; local doctor warns for GTX 1070 Ti/Pascal risk, missing PyTorch, and missing MinerU model/cache/config path. +- Independent evaluation: PASS. +- Residual risks: Sprint 8 does not install MinerU, download models, validate real MinerU output, run sample PDFs, or prove GTX 1070 Ti PyTorch acceleration. Those remain Sprint 9/local setup work. +- User decisions needed: None for Sprint 8. +- Go/no-go recommendation for Sprint 9: go after a Sprint 9 contract is written and reviewed. +- Next action at completion: prepare the Sprint 9 contract when requested. + +## Sprint 8 Contract Handoff + +Historical note: this contract-only handoff was superseded by the implemented Sprint 8 handoff above. + +- Files changed: `docs/Sprints/SPRINT8CONTRACT.md`, `docs/V1IMPLEMENTATIONPLAN.md`, relevant `.codex/agents/*.toml`, `PLAN.md`, and `PROGRESS.md`. +- Commands run: `uv --version`, agent TOML parse check, `uv sync`, `uv run pytest`, `git diff --check`, `git status --short --untracked-files=all`, and PowerShell reference checks. +- Tests passed: `uv run pytest` passed 119 tests. +- Tests blocked: None. +- Known failures: none. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: Sprint 8 is contract-only; `pdf2md doctor`, doctor diagnostics, setup docs, and setup helper scripts are not implemented yet. +- User decisions needed: None before Sprint 8 pre-implementation review. +- Go/no-go recommendation for Sprint 8 implementation: review the contract first, then go if the user explicitly requests implementation. +- Next action: commit the contract update, then wait for an explicit Sprint 8 implementation request. + +## Sprint 7 Handoff + +- Files changed: `src/pdf2md/conversion.py`, `src/pdf2md/cli.py`, `src/pdf2md/__init__.py`, `tests/test_conversion.py`, `tests/test_cli.py`, `tests/test_package.py`, `PLAN.md`, `PROGRESS.md`, `docs/V1IMPLEMENTATIONPLAN.md`, and `docs/Sprints/SPRINT7CONTRACT.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_conversion.py tests/test_cli.py`, `uv run pytest tests/test_conversion.py tests/test_cli.py tests/test_package.py`, `uv run pytest tests/test_conversion.py tests/test_metadata.py tests/test_report.py`, `uv run pytest`, `git diff --check`, `git status --short --untracked-files=all`, and PowerShell strict-local/network pattern checks. +- Tests passed: `uv run pytest tests/test_conversion.py tests/test_cli.py` passed 18 tests; `uv run pytest tests/test_conversion.py tests/test_cli.py tests/test_package.py` passed 16 tests before the math renderability fix; `uv run pytest tests/test_conversion.py tests/test_metadata.py tests/test_report.py` passed 29 tests; `uv run pytest` passed 119 tests after the metadata math count fix. +- Tests blocked: None. +- Known failures: none. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Independent evaluation: PASS after fixing math renderability metadata counts. +- Residual risks: Sprint 7 uses fake adapters in default tests; it does not run real MinerU, probe real MinerU output, implement `pdf2md doctor`, validate CUDA/GPU, install models, or run sample PDFs. +- User decisions needed: None for Sprint 7. +- Go/no-go recommendation for Sprint 8: go. +- Next action: prepare Sprint 8 contract when requested. + +## Sprint 7 Contract Handoff (Historical) + +- Files changed: `docs/Sprints/SPRINT7CONTRACT.md`, `docs/V1IMPLEMENTATIONPLAN.md`, `.codex/agents/feature-generator-agent.toml`, `.codex/agents/evaluation-agent.toml`, `.codex/agents/requirements-guard-agent.toml`, `.codex/agents/harness-planner-agent.toml`, `.codex/agents/mineru-integration-agent.toml`, `.codex/agents/metadata-agent.toml`, `.codex/agents/obsidian-markdown-agent.toml`, `PLAN.md`, and `PROGRESS.md`. +- Commands run: `uv --version`, agent TOML parse check, `uv sync`, `uv run pytest`, `git diff --check`, and `git status --short --untracked-files=all`. +- Tests passed: `uv run pytest` passed 103 tests. +- Tests blocked: None. +- Known failures: none. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks at that time: Sprint 7 was contract-only before implementation. Superseded by the Sprint 7 Handoff above. +- User decisions needed at that time: None before Sprint 7 pre-implementation review. +- Go/no-go recommendation at that time: review the contract first, then go if the user explicitly requests implementation. +- Next action at that time: commit the contract update, then wait for an explicit Sprint 7 implementation request. + +## Sprint 6 Handoff + +- Files changed: `src/pdf2md/quality.py`, `src/pdf2md/report.py`, `tests/test_quality.py`, `tests/test_report.py`, `PLAN.md`, `PROGRESS.md`, `docs/V1IMPLEMENTATIONPLAN.md`, and `docs/Sprints/SPRINT6CONTRACT.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_quality.py tests/test_report.py tests/test_metadata.py`, `uv run pytest`, `git diff --check`, `git status --short --untracked-files=all`, and PowerShell file/pattern checks. +- Tests passed: `uv run pytest tests/test_quality.py tests/test_report.py tests/test_metadata.py` passed 26 tests; `uv run pytest` passed 103 tests. +- Tests blocked: None. +- Known failures: none in Sprint 6 implementation. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: Sprint 6 intentionally does not run real MinerU, run a real math renderer, parse PDFs, write final Markdown files, copy assets, write metadata JSON files, write `.report.md` files, expose working `convert`, or implement `doctor`. +- User decisions needed: None for Sprint 6. +- Go/no-go recommendation for Sprint 7: go. +- Next action: prepare Sprint 7 contract when requested. + +## Sprint 5 Handoff + +- Files changed: `src/pdf2md/markdown.py`, `src/pdf2md/ir.py`, `tests/test_markdown.py`, `PLAN.md`, `PROGRESS.md`, `docs/V1IMPLEMENTATIONPLAN.md`, and `docs/Sprints/SPRINT5CONTRACT.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_markdown.py tests/test_ir.py`, `uv run pytest`, `git diff --check`, `git status --short --untracked-files=all`, and PowerShell file/pattern checks. +- Tests passed: `uv run pytest tests/test_markdown.py tests/test_ir.py` passed 30 tests; `uv run pytest` passed 89 tests. +- Tests blocked: None. +- Known failures: none in Sprint 5 implementation. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: Sprint 5 intentionally does not run real MinerU, probe real MinerU Markdown, parse PDFs, write final Markdown files, copy assets, write metadata JSON, generate `.report.md`, expose working `convert`, or implement `doctor`. +- User decisions needed: None for Sprint 5. +- Go/no-go recommendation for Sprint 6: go. +- Next action: prepare Sprint 6 contract when requested. + +## Sprint 4 Handoff + +- Files changed: `src/pdf2md/mineru_adapter.py`, `tests/test_mineru_adapter.py`, `PLAN.md`, and `PROGRESS.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_mineru_adapter.py`, `uv run pytest`, `git diff --check`, `git status --short --untracked-files=all`, and PowerShell file/pattern checks. +- Tests passed: `uv run pytest tests/test_mineru_adapter.py` passed 26 tests; `uv run pytest` passed 72 tests. +- Tests blocked: None. +- Known failures: none in Sprint 4 implementation after fixing the independent evaluation finding. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: Sprint 4 intentionally does not run real MinerU, install models, probe real MinerU output layout, parse PDFs, normalize Markdown, write metadata JSON, generate `.report.md`, expose working `convert`, or implement `doctor`. +- User decisions needed: None for Sprint 4. +- Go/no-go recommendation for Sprint 5: go. +- Next action: prepare Sprint 5 contract when requested. + +## Sprint 3 Handoff + +- Files changed: `src/pdf2md/ir.py`, `src/pdf2md/metadata.py`, `tests/test_ir.py`, `tests/test_metadata.py`, `PLAN.md`, `PROGRESS.md`, and `docs/Sprints/SPRINT3CONTRACT.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_ir.py tests/test_metadata.py`, `uv run pytest`, `git diff --check`, `git status --short`, and PowerShell file/pattern checks. +- Tests passed: `uv run pytest tests/test_ir.py tests/test_metadata.py` passed 25 tests; `uv run pytest` passed 46 tests. +- Tests blocked: None. +- Known failures: none in Sprint 3 implementation. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: Sprint 3 intentionally does not parse PDFs, compute SHA-256, invoke MinerU, write conversion outputs, normalize Markdown, create full report content, run quality checks, or expose working `convert` or `doctor` commands. +- User decisions needed: None for Sprint 3. +- Go/no-go recommendation for Sprint 4: go. +- Next action: prepare Sprint 4 contract when requested. + +## Sprint 2 Handoff + +- Files changed: `src/pdf2md/paths.py`, `tests/test_paths.py`, `PLAN.md`, and `PROGRESS.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest tests/test_paths.py`, `uv run pytest`, `git diff --check`, `git status --short`, and PowerShell file/pattern checks. +- Tests passed: `uv run pytest tests/test_paths.py` passed 17 tests; `uv run pytest` passed 21 tests. +- Tests blocked: None. +- Known failures: none in Sprint 2 implementation. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: Sprint 2 intentionally does not parse PDFs, compute SHA-256, invoke MinerU, write conversion outputs, normalize Markdown, create metadata/report content, or expose a working `convert` command. +- User decisions needed: None for Sprint 2. +- Go/no-go recommendation for Sprint 3: go. +- Next action: prepare Sprint 3 contract when requested. + +## Sprint 1 Handoff + +- Files changed: `pyproject.toml`, `uv.lock`, `.gitignore`, `README.md`, `src/pdf2md/__init__.py`, `src/pdf2md/cli.py`, `tests/test_package.py`, `tests/test_cli.py`, `PROGRESS.md`, `PLAN.md`, and `docs/Sprints/SPRINT1CONTRACT.md`. +- Commands run: `uv --version`, `uv sync`, `uv run pytest`, `uv run pdf2md --version`, `git diff --check`, `git status --short`, and PowerShell file/pattern checks after `rg.exe` returned access denied. +- Tests passed: `uv run pytest` passed 4 tests. +- Tests blocked: None. +- Known failures: `uv` may not be visible to a newly opened shell until PATH is refreshed; `rg.exe` returned access denied in this environment, so PowerShell checks were used instead. +- Known warnings: `uv` ignored Miniforge's invalid `SSL_CERT_DIR` path during sync/test commands, but the commands completed successfully. +- Residual risks: the scaffold intentionally does not validate MinerU, CUDA, model paths, conversion output, metadata, or quality reports. +- User decisions needed: None for Sprint 1. +- Go/no-go recommendation for Sprint 2: go. +- Next action: prepare Sprint 2 contract when requested. + +## Sprint 0 Handoff + +Superseded note: the following Sprint 0 facts describe the completed 2.5.4 verification pass. The current engine decision is MinerU 3.1.0. + +- Files changed: `docs/KNOWLEDGEBASE.md`, `docs/Sprints/SPRINT0CONTRACT.md`, `docs/V1IMPLEMENTATIONPLAN.md`, `PROGRESS.md`. +- Sources checked: MinerU 2.5.4 PyPI, MinerU 2.5.4 tag files, MinerU output/model docs, Python/uv/PyTorch/NVIDIA docs, MinerU/model license sources. +- Local commands run: `python --version`, `uv --version`, `nvidia-smi`. +- Facts confirmed at that time: Python 3.12.7 is present; `uv` is missing; GTX 1070 Ti 8GB is visible; MinerU 2.5.4 direct CLI path is source-verified; MinerU/model 2.5-era licenses should be treated as AGPL-3.0. +- Inferences made at that time: v1 should pin MinerU to `mineru[core]==2.5.4`; strict-local runtime should require local model source configuration; current MinerU 3.x docs should not drive the older 2.5 adapter behavior. +- Known failures: `uv --version` failed because `uv` is not on PATH. +- Residual risks: GTX 1070 Ti/PyTorch CUDA compatibility, real MinerU output layout until local probe, AGPL redistribution obligations, setup-download versus runtime-local separation. +- Go/no-go recommendation: `go-with-risks`. +- Next action: resolve `uv` availability or include bootstrap handling in Sprint 1, then create the Sprint 1 contract. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a4de77 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# ConvertPDFToMD + +Local-only PDF-to-Markdown converter for math-heavy digital documents. + +## Status + +The project currently provides a Python package, `pdf2md convert`, metadata/report output, mocked MinerU adapter tests, `pdf2md doctor` setup diagnostics, and Sprint 9 release-gate documentation. Real local MinerU sample validation remains optional and may be blocked until MinerU 3.1.0 and local model/cache setup are available. + +## Setup + +Use Windows PowerShell with Python 3.12. If `uv` is installed but a new shell cannot find it, add the per-user install directory to PATH for the current session: + +```powershell +$env:Path = "C:\Users\user\.local\bin;$env:Path" +``` + +Sync the project and run the fast local test loop: + +```powershell +uv sync +uv run pytest +uv run pdf2md --version +``` + +For the local GTX 1070 Ti runtime, install CUDA-enabled PyTorch before installing MinerU so MinerU does not resolve to a CPU-only torch wheel: + +```powershell +uv sync +uv pip install --index-url https://download.pytorch.org/whl/cu126 torch==2.6.0 torchvision==0.21.0 +uv pip install "mineru[core]==3.1.0" +uv run mineru-models-download -s huggingface -m all +[Environment]::SetEnvironmentVariable("MINERU_MODEL_SOURCE", "local", "User") +$env:MINERU_MODEL_SOURCE = "local" +uv run pdf2md doctor +``` + +Run `uv sync` before the runtime install commands. If you run `uv sync` again later, repeat the runtime install commands because MinerU and CUDA PyTorch are intentionally not part of the default fast test dependency set. + +Install the optional local MathJax checker when you want formula renderability counts to reflect real MathJax parsing instead of the nonfatal "checker unavailable" warning: + +```powershell +npm install +npm run mathjax-checker:health +uv run pdf2md doctor +``` + +The checker runs through local Node.js and the local `mathjax` package only. It never uses a CDN or hosted renderer, and conversion still completes if Node.js or MathJax is missing. + +For release checks, see [docs/V1RELEASECHECKLIST.md](docs/V1RELEASECHECKLIST.md). It separates the default fast gates from optional local MinerU/GPU/sample fixture evaluation. Optional fixture runs use `PDF2MD_RUN_MINERU_FIXTURES=1`, should use only local PDFs, write generated outputs to a temporary or ignored local directory, and count a sample conversion as successful only when Markdown, metadata JSON, and `.report.md` outputs all exist. + +Install MinerU 3.1.0 as an explicit local setup step so the `mineru` executable is available on PATH. This project calls MinerU only through the direct local CLI shape: + +```powershell +mineru -p -o +``` + +`pdf2md convert` requests GPU execution by default with `--gpu cuda:0`. The adapter maps that to MinerU's local `MINERU_DEVICE_MODE=cuda` and `CUDA_VISIBLE_DEVICES=0` environment for the MinerU subprocess. Actual GPU execution still requires a CUDA-capable local PyTorch/MinerU stack; `doctor` reports when PyTorch is CPU-only or CUDA is unavailable. + +Run setup diagnostics before conversion: + +```powershell +uv run pdf2md doctor +``` + +`doctor` checks Python 3.12, `uv`, the MinerU CLI and version, NVIDIA GPU visibility through `nvidia-smi`, PyTorch CUDA visibility when PyTorch is installed, local model/cache/config paths, local MathJax checker availability, and the strict-local runtime policy. It does not install packages, download models, run conversions, or inspect `samples/`. + +The model/cache check looks for these environment variables when present: + +- `MINERU_MODEL_SOURCE` +- `MINERU_MODEL_DIR` +- `MINERU_CACHE_DIR` +- `MINERU_TOOLS_CONFIG_JSON` +- `HF_HOME` +- `HUGGINGFACE_HUB_CACHE` +- `MODELSCOPE_CACHE` + +It also checks for `%USERPROFILE%\mineru.json`, which MinerU documents as its default user config location. Missing model/cache paths are warnings because model download and cache population must be explicit setup actions. + +## Runtime Policy + +Runtime conversion is strict-local. Allowed: direct `mineru` CLI execution and the CLI-internal temporary local `mineru-api` that MinerU starts when `--api-url` is omitted. Prohibited: `--api-url`, remote APIs, router mode, HTTP client backends, remote OpenAI-compatible backends, hosted renderers, and cloud fallbacks. + +Setup may require explicit user-initiated package or model downloads. Those setup downloads are separate from runtime conversion; `pdf2md doctor`, `pdf2md convert`, imports, and default tests must not download packages or models. + +The target GPU is NVIDIA GTX 1070 Ti 8GB. `doctor` warns for GTX 1070 Ti/Pascal/pre-Turing GPUs because local CUDA/PyTorch compatibility and VRAM pressure must be validated on the actual machine before relying on acceleration. + +## Long PDFs + +Chunking is opt-in for long PDFs. Use `--chunk-pages` with no value to split into 20-page chunks, or pass an explicit positive page count: + +```powershell +uv run pdf2md convert samples/long.pdf --out outputs --chunk-pages +uv run pdf2md convert samples/long.pdf --out outputs --chunk-pages 20 +``` + +Chunk PDFs are written to a temporary local directory before each MinerU run and are deleted after conversion completes. The generated Markdown files are not merged; each chunk gets its own Markdown, metadata JSON, report Markdown, and assets directory named with the original page range. + +The Python API keeps non-chunked behavior unchanged. `convert_pdf(..., chunk_pages=20)` returns a `BatchConversionResult` with one `ConversionResult` per chunk. + +## References + +Source checked on 2026-05-08: + +- MinerU Quick Usage: https://opendatalab.github.io/MinerU/usage/quick_usage/ +- MinerU CLI Tools: https://opendatalab.github.io/MinerU/usage/cli_tools/ +- MinerU Model Source: https://opendatalab.github.io/MinerU/usage/model_source/ +- MinerU GitHub README/release notes: https://github.com/opendatalab/MinerU +- uv project sync documentation: https://docs.astral.sh/uv/concepts/projects/sync/ +- PyTorch previous versions: https://docs.pytorch.org/get-started/previous-versions/ +- PyTorch CUDA architecture support update: https://dev-discuss.pytorch.org/t/cuda-toolkit-version-and-architecture-support-update-maxwell-and-pascal-architecture-support-removed-in-cuda-12-8-and-12-9-builds/3128 +- PyTorch CUDA availability API: https://docs.pytorch.org/docs/2.11/generated/torch.cuda.is_available.html diff --git a/docs/KNOWLEDGEBASE.md b/docs/KNOWLEDGEBASE.md new file mode 100644 index 0000000..5dd171d --- /dev/null +++ b/docs/KNOWLEDGEBASE.md @@ -0,0 +1,282 @@ +# Knowledge Base: Local PDF-to-Markdown Converter for Math-Heavy Documents + +Last updated: 2026-05-07 + +## 1. Product Direction + +This project will build a local-first PDF-to-Markdown converter for math-heavy academic PDFs and books. The v1 target is intentionally narrow: + +- Processing policy: local-only. Do not send user PDFs to cloud OCR or external AI APIs. +- Primary interface: CLI plus Python library. +- Primary output: Obsidian-friendly Markdown. +- Main conversion engine: MinerU 3.1.0. +- Math output: inline math as `$...$`, display math as `$$...$$`. +- Hardware target: NVIDIA GPU. +- PDF scope: digital PDFs with an existing text layer first. Scanned books and poor-quality scans are out of scope for v1 optimization. +- Quality workflow: fully automatic conversion. Low-confidence regions should be logged and represented in metadata, but conversion should not stop. +- Install target: Python with `uv`; scripts should document local model download/setup. +- License posture: personal use. License terms are not a v1 blocker for this local project, but document MinerU and transitive model/package licenses before any redistribution. + +The rest of this document records the research basis and implementation implications for these decisions. + +This file is background research, not a second requirements source. Use `PRD.md` for product requirements and `ARCHITECTURE.md` for implementation structure. + +## 2. Why Math PDF Conversion Is Hard + +PDF is a visual/page-description format, not a semantic source format. In scientific PDFs, the displayed equation often survives visually while the source-level LaTeX structure is lost. The Nougat paper states the core issue clearly: scientific knowledge is frequently stored in PDFs, and the PDF format loses semantic information, especially for mathematical expressions. Source: [Nougat: Neural Optical Understanding for Academic Documents](https://arxiv.org/abs/2308.13418). + +For this project, equation conversion must be treated as a document understanding and formula recognition problem, not just text extraction. A robust converter needs to recover: + +- Reading order across multi-column layouts. +- Inline and display equation boundaries. +- LaTeX syntax that renders in common Markdown math renderers. +- Tables, captions, figures, references, footnotes, and image assets. +- Page-level provenance for debugging and downstream correction. + +Digital PDFs make the problem easier because embedded text can be extracted directly, but formulas may still appear as glyph streams, vector paths, images, or fragmented positioned characters. A v1 product should therefore use the PDF text layer where reliable and use document parsing/OCR models for layout and math reconstruction. + +## 3. MinerU 3.1.0 Engine Strategy + +MinerU 3.1.0 is the fixed local parser for v1. + +Relevant source facts: + +- MinerU 3.1.0 is published on PyPI, requires Python `>=3.10,<3.14`, and describes support for PDF, images, DOCX, PPTX, and XLSX to Markdown and JSON. Source: [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/). +- MinerU 3.1.0 release notes describe a license move to the MinerU Open Source License, a VLM main model upgrade to `MinerU2.5-Pro-2604-1.2B`, improved image/chart parsing, truncated paragraph merging, cross-page table merging, table-internal image recognition, and native PPTX/XLSX parsing. Source: [MinerU releases](https://github.com/opendatalab/MinerU/releases). +- MinerU's current quick usage documents the CLI shape as `mineru -p -o ` and states that without `--api-url`, the CLI launches a temporary local `mineru-api`. Source: [MinerU quick usage docs](https://opendatalab.github.io/MinerU/usage/quick_usage/). +- Starting with MinerU 3.0, the `mineru` command is an orchestration client on top of `mineru-api`. Source: [MinerU usage guide](https://opendatalab.github.io/MinerU/usage/). +- MinerU model source configuration uses `MINERU_MODEL_SOURCE`, and local models are enabled with `MINERU_MODEL_SOURCE=local`. Source: [MinerU model source docs](https://opendatalab.github.io/MinerU/usage/model_source/). + +Implementation implications: + +- Wrap MinerU behind a project-owned adapter rather than binding the codebase to its CLI output. +- Preserve both Markdown and structured JSON when available. +- Treat MinerU output as the first-pass parse, then normalize for Obsidian. +- Keep engine name/version, command/options, page ids, and warnings in metadata. +- Expect model downloads and GPU runtime setup to be heavy; document this clearly. +- Treat MinerU 3.1.0's CLI-internal temporary local `mineru-api` as allowed local orchestration. +- Reject `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends in strict-local mode. +- Do not implement runtime engine selection in v1. + +Risks: + +- MinerU version behavior may change quickly. +- GTX 1070 Ti is below the current documented GPU acceleration recommendation for some MinerU 3.x paths; `pipeline` CPU or limited GPU behavior must be validated locally. +- Some licenses are custom or changed over time; check the exact dependency license before redistribution if the project scope changes. +- Output Markdown may require post-processing to match Obsidian math and asset path expectations. + +## 4. Output Standard: Obsidian-Friendly Markdown + +The final Markdown should be optimized for Obsidian rendering and long-term note usage. + +Rules: + +- Inline math: `$...$`. +- Display math: `$$...$$` on separate lines. +- Store extracted images in a sibling assets directory, for example `paper.assets/page-003-figure-01.png`. +- Use relative links from the Markdown file to assets. +- Preserve page boundaries in metadata, not by noisy visible page markers in the main Markdown. +- Prefer normal Markdown tables for simple tables. +- Use HTML tables only when Markdown tables would destroy merged cells or mathematical structure. +- Do not silently drop formulas, figures, captions, or references. If exact conversion fails, keep a best-effort representation and log a warning. + +Post-processing should normalize: + +- Math delimiters: convert `\(...\)` to `$...$` and `\[...\]` to `$$...$$` where safe. +- Display math spacing: ensure blank lines around display equations. +- Escaping: avoid over-escaping underscores inside math. +- Asset links: convert absolute/generated paths to stable relative paths. +- Heading levels: avoid treating running headers, footers, and page numbers as section headings. +- Repeated hyphenation and line breaks from PDF extraction. + +## 5. Metadata and Provenance + +Metadata is necessary because fully automatic conversion can produce imperfect formulas and reading order. The converter must keep enough provenance to identify the source page, source region, engine version, warnings, and emitted assets for each result. + +The metadata schema is defined in `ARCHITECTURE.md`. + +## 6. Evaluation Strategy + +Do not rely only on raw text edit distance. The project should evaluate multiple dimensions. + +### Benchmarks to Learn From + +OmniDocBench: + +- Evaluates document parsing across text, formulas, tables, and reading order. +- End-to-end scoring combines text edit distance, table TEDS, and formula CDM. +- Provides formula recognition evaluation for display and inline formulas. Source: [OmniDocBench](https://github.com/opendatalab/OmniDocBench). + +Unit-test-style document parsing checks: + +- Uses simple, unambiguous, machine-checkable page facts instead of only soft edit-distance comparisons. +- Includes arXiv math, old scans math, tables, headers/footers, multi-column layouts, long/tiny text, and base cases. +- Explicitly notes that small equation symbol swaps can be critical even when edit distance is small. + +ParseBench: + +- Emphasizes semantic correctness for agentic document parsing: table structure, chart data, formatting, visual grounding, and content faithfulness. Source: [ParseBench](https://arxiv.org/abs/2604.08538). + +### Project Acceptance Metrics + +For v1, use a small project fixture suite rather than trying to run every public benchmark. + +Required fixture categories: + +- A short digital PDF with simple inline and display math. +- A math-heavy academic paper with multi-column layout. +- A PDF page containing a table with formulas. +- A PDF with figures, captions, references, and page numbers. + +Required checks: + +- Markdown file is generated. +- Metadata JSON is generated when requested. +- All pages have provenance records. +- Inline math and display math render in a KaTeX/MathJax-compatible check. +- Asset links resolve. +- No cloud network calls occur during conversion. +- MinerU failures produce warnings and a best-effort output, not a hard crash, unless the input file cannot be opened or no output can be produced. + +Recommended quantitative checks: + +- Count display math blocks detected. +- Count math render failures. +- Count missing asset links. +- Count pages with warnings. +- Compare selected known formulas against expected LaTeX or normalized render output. + +## 7. Implementation Source of Truth + +Do not implement directly from this research note. + +- Use `PRD.md` for CLI, API, scope, tests, and release criteria. +- Use `ARCHITECTURE.md` for the conversion pipeline, MinerU boundary, intermediate representation, metadata schema, and strict-local enforcement. + +## 8. Implementation Risks + +- Formula correctness cannot be guaranteed purely by text extraction. +- MinerU behavior may change across versions; adapter tests should pin expected behavior. +- GPU dependencies can be difficult on Windows; `doctor` should detect CUDA, PyTorch, model paths, and engine availability. +- Obsidian math rendering may differ from GitHub or Pandoc. +- Licensing must be reviewed before packaging or redistributing models/tools. +- Fully automatic mode means users may receive imperfect formulas; warnings and metadata are essential. + +## 9. Sprint 0 Verification And Engine Update (2026-05-07) + +Sprint 0 originally verified MinerU 2.5.4 assumptions before implementation. After Sprint 0, the project owner changed the v1 engine target to MinerU 3.1.0 and redefined strict-local execution. The current implementation target is therefore MinerU 3.1.0, not the earlier 2.5.4 pin. + +### 9.1 Recommendation + +Recommendation: `go-with-risks` for personal-use v1. + +The project can proceed to Sprint 1 after the `uv` workflow is available locally or Sprint 1 explicitly handles the bootstrap gap. Use `mineru[core]==3.1.0` or another explicitly reviewed 3.1.0 installation path until a later sprint contract changes it. + +Current strict-local policy: + +- Allowed: direct `mineru` CLI execution. +- Allowed: the temporary local `mineru-api` process that MinerU 3.1.0 starts internally when the CLI runs without `--api-url`. +- Prohibited: `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends. +- Setup may download models only when the user explicitly runs setup commands. +- Runtime conversion should use local model paths, for example with `MINERU_MODEL_SOURCE=local`. + +### 9.2 Evidence Policy Applied + +Sprint 0 claims use official or primary sources where available. Each claim below is marked as either: + +- Direct fact: stated by a source or observed from an allowed local command. +- Project inference: a project decision derived from source facts. + +Web research was allowed for documentation verification. Runtime converter design remains local-only. + +### 9.3 MinerU 3.1.0 Facts + +| Area | Confirmed fact | Evidence type | Source | Implementation implication | +| --- | --- | --- | --- | --- | +| Package pin | PyPI lists MinerU `3.1.0`, released 2026-04-17, with Python `>=3.10,<3.14` and extras including `core`, `pipeline`, `vlm`, `vllm`, `lmdeploy`, `mlx`, `gradio`, and `all`. | Direct fact | [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/) | Pin v1 setup to MinerU 3.1.0 until changed by a later contract. | +| Install path | Current MinerU docs show `uv pip install -U "mineru[all]"`; PyPI also supports extras including `core`. | Direct fact plus project inference | [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/) | Start with `mineru[core]==3.1.0` for the wrapper unless Sprint 1 or Sprint 8 proves that `all` is required. | +| CLI shape | Current docs show `mineru -p -o `. | Direct fact | [MinerU quick usage docs](https://opendatalab.github.io/MinerU/usage/quick_usage/) | The adapter still calls the `mineru` CLI directly. | +| Local temporary API | MinerU docs state that without `--api-url`, the CLI launches a temporary local `mineru-api`; with `--api-url`, the CLI connects to an existing local or remote FastAPI service. | Direct fact | [MinerU quick usage docs](https://opendatalab.github.io/MinerU/usage/quick_usage/), [MinerU CLI tools docs](https://opendatalab.github.io/MinerU/usage/cli_tools/) | Allow only the CLI-internal temporary local `mineru-api`; reject `--api-url` and user-managed API endpoints. | +| Backend risk | Current docs describe `pipeline`, `vlm`, and `hybrid` paths plus HTTP-client variants for OpenAI-compatible servers. | Direct fact | [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/), [MinerU CLI tools docs](https://opendatalab.github.io/MinerU/usage/cli_tools/) | Strict-local validation must reject HTTP client backends and remote OpenAI-compatible backends. | +| Output layout | MinerU output docs list Markdown plus visual debugging files and structured files such as `model.json`, `middle.json`, and `content_list.json`; the exact set depends on backend and input type. | Direct fact | [MinerU output files docs](https://opendatalab.github.io/MinerU/reference/output_files/) | Adapter parsing must tolerate optional files and backend-specific structured output. Keep raw output optional through `--keep-raw`. | +| Model/cache behavior | MinerU uses Hugging Face and ModelScope by default and switches source through `MINERU_MODEL_SOURCE`; local parsing uses `MINERU_MODEL_SOURCE=local` after models are downloaded. | Direct fact | [MinerU model source docs](https://opendatalab.github.io/MinerU/usage/model_source/) | Setup scripts may download models; runtime conversion should require local model paths under strict-local mode. | +| 3.1.0 capability update | Release notes say 3.1.0 upgrades the main VLM model to `MinerU2.5-Pro-2604-1.2B` and improves image/chart parsing, truncated paragraph merging, cross-page table merging, and image recognition inside tables. | Direct fact | [MinerU releases](https://github.com/opendatalab/MinerU/releases) | 3.1.0 is a better target for math-heavy and complex-layout documents, pending local hardware validation. | + +Adapter-facing fields that must remain optional until a local MinerU output probe is run: + +- Backend/parse method directory names. +- Presence of `content_list`, `middle`, `model`, `layout`, `span`, and `origin` files. +- Page/block confidence and bbox fields in structured output. +- Asset naming and relative paths. +- Exact stdout/stderr warning text. + +### 9.4 Local Environment Facts + +Allowed local commands run during Sprint 0: + +```powershell +python --version +uv --version +nvidia-smi +``` + +Results: + +| Area | Result | Evidence type | Source or command | Impact | +| --- | --- | --- | --- | --- | +| Python | Local Python is `Python 3.12.7`. Python 3.12 target is viable for MinerU 3.1.0's declared `>=3.10,<3.14` range. | Direct fact plus project inference | `python --version`; [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/) | Continue targeting Python 3.12. Prefer current 3.12 patch in setup docs, but do not require a patch bump for v1. | +| `uv` | `uv` is not on PATH. PowerShell reported that `uv` is not recognized. | Direct fact | `uv --version`; [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) | Sprint 1 cannot honestly claim `uv sync` works until `uv` is installed or the Sprint 1 contract includes bootstrap instructions. | +| GPU | `nvidia-smi` detects NVIDIA GeForce GTX 1070 Ti, driver `577.00`, WDDM, 8192 MiB VRAM, about 5034 MiB free at check time. | Direct fact | `nvidia-smi` | GPU is visible, but available VRAM is tight for model workloads and must be reported by `doctor`. | +| Compute capability | GTX 1070 Ti is Pascal and listed as CUDA compute capability `6.1`. | Direct fact | [NVIDIA legacy CUDA GPU table](https://developer.nvidia.com/cuda/gpus/legacy), [NVIDIA GTX 10-series specs](https://www.nvidia.com/en-us/geforce/10-series/10-series-specs/) | Warn on pre-Turing GPUs. Do not assume modern CUDA 12.8/12.9 PyTorch wheels work. | +| PyTorch/CUDA risk | PyTorch project discussion states CUDA 12.8/12.9 builds remove Maxwell/Pascal support. `nvidia-smi` CUDA version is a driver capability ceiling, not proof that PyTorch CUDA works. | Direct fact plus project inference | [PyTorch CUDA architecture support discussion](https://dev-discuss.pytorch.org/t/cuda-toolkit-version-and-architecture-support-update-maxwell-and-pascal-architecture-support-removed-in-cuda-12-8-and-12-9-builds/3128), [PyTorch install docs](https://pytorch.org/get-started/locally/) | `pdf2md doctor` must test actual PyTorch import, CUDA runtime, device name, compute capability, and free memory. | + +Future `pdf2md doctor` checks: + +- `python --version`, requiring `>=3.12,<3.13`. +- `uv --version`, failing clearly when unavailable. +- PowerShell version and edition on Windows. +- `nvidia-smi` GPU name, driver, WDDM/TCC mode, total/free VRAM. +- GPU compute capability warning for `<7.5`, especially Pascal `6.1`. +- `torch` import, `torch.__version__`, `torch.version.cuda`, `torch.cuda.is_available()`, device name, compute capability, and free memory. +- MinerU CLI availability, installed package version, and model/cache configuration. +- Strict-local validation that runtime uses direct CLI, allows only CLI-internal temporary local `mineru-api`, uses local model paths, and rejects `--api-url`, router mode, HTTP client backends, remote APIs, and remote OpenAI-compatible backends. + +### 9.5 License And Privacy Facts + +| Area | Confirmed fact | Evidence type | Source | Implementation implication | +| --- | --- | --- | --- | --- | +| MinerU 3.1.0 license | PyPI identifies MinerU 3.1.0 as `LicenseRef-MinerU-Open-Source-License`; release notes state MinerU moved from AGPLv3 to a custom license based on Apache 2.0. | Direct fact | [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/), [MinerU releases](https://github.com/opendatalab/MinerU/releases) | License is not a blocker for personal v1. Redistribution still needs review. | +| Runtime privacy | MinerU docs expose remote-capable paths, including `--api-url`, router usage, HTTP client backends, and OpenAI-compatible backend URLs. | Direct fact | [MinerU usage guide](https://opendatalab.github.io/MinerU/usage/), [MinerU CLI tools docs](https://opendatalab.github.io/MinerU/usage/cli_tools/) | The wrapper must never upload PDFs, page images, extracted text, or intermediates. Setup downloads are separate from runtime conversion. | +| Model downloads | `mineru-models-download` must use a remote model source for real downloads, while runtime local parsing uses `MINERU_MODEL_SOURCE=local`. | Direct fact | [MinerU model source docs](https://opendatalab.github.io/MinerU/usage/model_source/) | Download scripts are setup-only. Conversion runtime must not download or call remote inference. | + +### 9.6 Go-With-Risks Register + +| Risk | Later sprint that must absorb it | Required handling | +| --- | --- | --- | +| `uv` is missing locally. | Sprint 1 and Sprint 8 | Install `uv` before claiming scaffold success, or make bootstrap documentation part of Sprint 1. `doctor` must report missing `uv`. | +| GTX 1070 Ti is Pascal compute capability 6.1 and is below current documented GPU acceleration recommendations for some MinerU 3.x paths. | Sprint 8 | `doctor` must verify actual PyTorch/CUDA/MinerU backend availability and warn on pre-Turing GPUs. Setup docs should avoid assuming CUDA 12.8/12.9 PyTorch works on this GPU. | +| MinerU 3.1.0 output layout is source-verified but not locally probed. | Sprint 4 and Sprint 9 | Adapter tests should mock optional outputs first. A later local MinerU probe should confirm real output paths before release. | +| CLI-internal local `mineru-api` is allowed, but user-specified API paths are prohibited. | Sprint 4 and Sprint 8 | Adapter command validation must allow `mineru -p ... -o ...` without `--api-url`, while rejecting `--api-url`, router mode, HTTP client backends, remote APIs, and remote OpenAI-compatible backends. | +| Runtime must be local-only while setup may download packages/models. | Sprint 4 and Sprint 8 | Separate setup/download commands from conversion runtime. Enforce `MINERU_MODEL_SOURCE=local` or equivalent local model configuration in strict-local mode. | + +No hard failure criteria are currently met. Direct local MinerU 3.1.0 CLI execution appears suitable for v1 under the redefined strict-local policy, Python 3.12 is compatible with the package metadata, local-only design remains viable, and license posture does not block personal use. + +## 10. Source Index + +- [Nougat paper](https://arxiv.org/abs/2308.13418) +- [MinerU docs](https://opendatalab.github.io/MinerU/zh/) +- [MinerU GitHub](https://github.com/opendatalab/mineru) +- [PyPI mineru 3.1.0](https://pypi.org/project/mineru/3.1.0/) +- [MinerU releases](https://github.com/opendatalab/MinerU/releases) +- [MinerU usage guide](https://opendatalab.github.io/MinerU/usage/) +- [MinerU quick usage docs](https://opendatalab.github.io/MinerU/usage/quick_usage/) +- [MinerU CLI tools docs](https://opendatalab.github.io/MinerU/usage/cli_tools/) +- [MinerU output files docs](https://opendatalab.github.io/MinerU/reference/output_files/) +- [MinerU model source docs](https://opendatalab.github.io/MinerU/usage/model_source/) +- [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/) +- [PyTorch install docs](https://pytorch.org/get-started/locally/) +- [PyTorch CUDA architecture support discussion](https://dev-discuss.pytorch.org/t/cuda-toolkit-version-and-architecture-support-update-maxwell-and-pascal-architecture-support-removed-in-cuda-12-8-and-12-9-builds/3128) +- [NVIDIA legacy CUDA GPU table](https://developer.nvidia.com/cuda/gpus/legacy) +- [NVIDIA GTX 10-series specs](https://www.nvidia.com/en-us/geforce/10-series/10-series-specs/) +- [OmniDocBench](https://github.com/opendatalab/OmniDocBench) +- [ParseBench](https://arxiv.org/abs/2604.08538) diff --git a/docs/MATHJAXCHECKERPLAN.md b/docs/MATHJAXCHECKERPLAN.md new file mode 100644 index 0000000..06c9b6a --- /dev/null +++ b/docs/MATHJAXCHECKERPLAN.md @@ -0,0 +1,298 @@ +# MathJax Local Render Checker Implementation Plan + +## Purpose + +Add a local MathJax-based render checker so the converter can validate whether extracted LaTeX formulas are likely to render in Obsidian. The checker must remain a quality signal only: failed formulas produce structured warnings, metadata counts, and report entries, but they do not stop conversion when Markdown output can still be produced. + +This plan is implementation planning only. It does not add a second PDF conversion engine, cloud service, remote API, or manual review workflow. + +Implementation status: implemented on 2026-05-08 with a local Node.js helper, Python MathJax wrapper, conversion integration, doctor diagnostics, setup documentation, and mocked default tests. Real checker execution uses the official local `mathjax` package and requires `npm install` to populate local `node_modules/`. + +## Product Context + +The project already normalizes inline math to `$...$` and display math to `$$...$$`. `src/pdf2md/quality.py` already has a math renderability boundary through `check_math_renderability()` and `MathCheckerUnavailable`, but current conversions record an info warning when no checker is injected. + +The next implementation should replace that unavailable-checker path with a real local MathJax check when local Node.js and MathJax are available. + +Relevant existing behavior: + +- Conversion remains local-only. +- MinerU 3.1.0 remains the only PDF conversion engine. +- Quality warnings are non-fatal unless no usable output can be produced. +- Metadata and `.report.md` already include `math_render_error_count`. +- Default tests must not require real MinerU, GPU, Node.js, MathJax, network, Obsidian, or sample PDFs. + +## References + +- Obsidian documents math expressions as MathJax/LaTeX notation: https://help.obsidian.md/advanced-syntax +- MathJax supports Node/server-side use through components: https://docs.mathjax.org/en/v4.1/server/components.html +- MathJax can convert TeX strings to SVG, including `tex2svgPromise()`: https://docs.mathjax.org/en/latest/web/convert.html + +## Design Decisions + +1. Use MathJax, not KaTeX, as the primary checker. + - Obsidian compatibility is the output standard. + - Obsidian uses MathJax for math rendering. + - KaTeX can remain a future fast preflight option, but it should not define v1 pass/fail behavior. + +2. Run MathJax locally through Node.js. + - Do not use a CDN. + - Do not fetch packages at conversion time. + - Do not call remote render APIs. + +3. Batch formulas in one Node process per conversion. + - Spawning one process per formula would be too slow for math-heavy papers. + - `samples/MITC공부.pdf` produced 126 math expressions, so batch checking is the practical baseline. + +4. Treat unavailable tooling differently from invalid math. + - Missing Node.js, missing MathJax, bad helper path, timeout, or invalid helper JSON should produce an info-level unavailable-checker warning. + - A MathJax parse/render failure for a specific expression should produce a warning-level `MATH_RENDER_FAILED` record and increment `math_render_error_count`. + +5. Preserve conversion continuity. + - Math render failures never remove formulas from the Markdown. + - Math render failures do not trigger fallback engines. + - The generated report remains derived from metadata and local checks. + +## Proposed Touched Surfaces + +- `src/pdf2md/quality.py` + - Replace body-only math iteration with expression records carrying body, display mode, index, and Markdown span. + - Keep code fence and inline code protection. + - Keep unavailable-checker behavior non-fatal. + +- `src/pdf2md/math_render.py` + - Add the Python wrapper for local MathJax checking. + - Probe Node.js availability. + - Execute the Node helper with JSON stdin. + - Parse JSON stdout into project-owned check results. + - Convert helper failures into `MathCheckerUnavailable`. + +- `tools/mathjax-checker/check.mjs` + - Add the Node helper. + - Load local MathJax components. + - Accept JSON input with expressions. + - Return JSON results only. + +- `package.json` and lockfile, or equivalent local setup documentation + - Add a local MathJax Node dependency if the project chooses a committed Node package manifest. + - The implementation should not install npm dependencies during conversion. + +- `src/pdf2md/conversion.py` + - Wire the default local checker when available, while preserving dependency injection for tests. + - Keep the public Python API compatible unless a later sprint explicitly changes it. + +- `src/pdf2md/doctor.py` + - Add diagnostic checks for Node.js and local MathJax checker availability. + - Report missing MathJax as a warning, not a hard failure, unless the project later decides the checker is mandatory. + +- `README.md` or setup documentation + - Document local MathJax checker setup. + - Explain that missing MathJax does not block conversion but leaves renderability unvalidated. + +- `PROGRESS.md` + - Record the implementation and verification outcome after completion. + +## Data Model Plan + +Add a small expression record for quality checking: + +```python +@dataclass(frozen=True) +class MathExpression: + index: int + body: str + display: bool + markdown_span: tuple[int, int] +``` + +The checker output should be project-owned and independent of MathJax internals: + +```python +@dataclass(frozen=True) +class MathCheckResult: + ok: bool + message: str = "" +``` + +If per-expression metadata is later needed, extend warning messages first. Do not expose raw MathJax objects in metadata or public API return values. + +## Node Helper Contract + +Input over stdin: + +```json +{ + "expressions": [ + {"index": 0, "body": "x^2", "display": false}, + {"index": 1, "body": "\\frac{1}{2}", "display": true} + ] +} +``` + +Output over stdout: + +```json +{ + "results": [ + {"index": 0, "ok": true}, + {"index": 1, "ok": false, "message": "MathJax error message"} + ] +} +``` + +stderr is reserved for diagnostics only. The Python wrapper should not depend on stderr format. + +## Python Wrapper Behavior + +The wrapper should: + +1. Locate `node`. +2. Locate the helper script. +3. Send all expressions through JSON stdin. +4. Set a deterministic timeout. +5. Require valid JSON stdout. +6. Map each result by expression index. +7. Return `MathCheckResult` values for expression failures. +8. Raise `MathCheckerUnavailable` for tool-level failures. + +Recommended default timeout policy: + +- Start with one conversion-level MathJax timeout. +- Use a conservative default such as 60 seconds for a document batch. +- Make the timeout test-injectable. +- Do not add CLI flags for timeout unless the user explicitly asks for configuration. + +## Integration Plan + +1. Refactor math extraction in `quality.py`. + - Add expression records. + - Preserve existing code-block exclusions. + - Preserve inline/display count behavior. + - Add tests for display mode and spans. + +2. Add mocked MathJax wrapper tests. + - Fake successful Node JSON response. + - Fake per-expression failure. + - Fake missing `node`. + - Fake timeout. + - Fake invalid JSON. + - Fake mismatched expression indexes. + +3. Add the Node helper. + - Keep stdout as JSON only. + - Ensure local package resolution. + - Avoid remote imports or CDN URLs. + +4. Wire the checker into conversion. + - If a test injects `math_checker`, use the injected checker. + - Otherwise, build a default local MathJax checker when available. + - If unavailable, keep the current info warning behavior. + +5. Extend doctor. + - Report Node.js availability. + - Report local MathJax package/helper availability. + - Keep missing MathJax as WARN. + +6. Update setup docs. + - Explain how to install local MathJax dependencies. + - Explain expected report behavior with and without MathJax. + +7. Run optional real fixture validation. + - Re-run `samples/MITC공부.pdf` only under an explicit local fixture gate or direct user request. + - Confirm `MATH_RENDER_FAILED` unavailable-checker warning disappears when MathJax is installed. + +## Test Plan + +Default fast tests, no real Node or MathJax required: + +- `uv run pytest tests/test_quality.py` +- `uv run pytest tests/test_conversion.py` +- `uv run pytest tests/test_doctor.py tests/test_cli.py` +- `uv run pytest` + +New required tests: + +- Extract inline and display expressions with correct `display` values. +- Ignore math-like text inside fenced code and inline code. +- Count failures from injected checker results. +- Preserve conversion success when some formulas fail render checks. +- Preserve info warning when the checker is unavailable. +- Validate Python wrapper command construction and JSON stdin/stdout handling with a fake runner. +- Validate timeout and invalid JSON handling. +- Validate doctor warning output when Node.js or MathJax is missing. + +Optional local tests: + +- Run the Node helper against a small expression list. +- Run the converter on `samples/MITC공부.pdf`. +- Confirm report fields: + - `Math render error count` is actual failure count. + - Missing checker info warning is absent when MathJax is available. + - Asset link counts remain 0 missing and 0 invalid for the sample. + +## Acceptance Criteria + +- Default test suite passes without Node.js or MathJax. +- Local-only policy is preserved: no CDN, remote API, or document upload path. +- `pdf2md doctor` reports MathJax checker availability clearly. +- Conversion still succeeds when MathJax is unavailable, with an info warning. +- Conversion still succeeds when individual formulas fail, with warning records. +- `.metadata.json` and `.report.md` show actual math render failure counts when MathJax is available. +- The generated Markdown is not changed by the checker. + +## Hard Failure Criteria + +- The checker blocks conversion when Markdown output exists. +- The checker uses a remote service or CDN at runtime. +- Default tests require Node.js, MathJax, MinerU, GPU, network, Obsidian, or sample PDFs. +- Raw MathJax output objects become public API return types. +- The report records renderability as successful when the checker did not actually run. + +## Open Decisions Before Implementation + +1. Dependency packaging: + - Use committed `package.json` and lockfile for a local MathJax package, or document a manual local npm setup. + - Recommended: commit `package.json` and lockfile so setup is reproducible. + +2. Default checker activation: + - Recommended: auto-attempt the local checker when available; otherwise emit the existing unavailable-checker info warning. + +3. Timeout value: + - Recommended initial default: 60 seconds per document batch, test-injectable and documented. + +4. Doctor severity: + - Recommended: missing MathJax checker is WARN, not FAIL, because conversion can still produce useful output. + +5. Real fixture gate: + - Recommended: keep real sample conversion explicit and opt-in for tests, but allow direct user-requested runs. + +## Suggested Implementation Contract + +Objective: + +- Implement a local MathJax render checker that validates normalized Markdown math expressions and records failures in metadata/report output without changing conversion continuity. + +Expected outputs: + +- Python MathJax checker wrapper. +- Node MathJax helper. +- Updated quality extraction for display/inline expression records. +- Doctor warning coverage for missing checker dependencies. +- Setup documentation. +- Fast mocked tests and optional real local checker validation. + +Non-goals: + +- No cloud rendering. +- No Obsidian app automation. +- No full LaTeX engine. +- No manual review queue. +- No runtime engine selection. +- No correction or rewriting of failed formulas. + +Verification: + +- `uv run pytest` +- `git diff --check` +- Optional local Node helper smoke test. +- Optional `samples/MITC공부.pdf` conversion after local MathJax setup. diff --git a/docs/Sprints/SPRINT0CONTRACT.md b/docs/Sprints/SPRINT0CONTRACT.md new file mode 100644 index 0000000..d8cb446 --- /dev/null +++ b/docs/Sprints/SPRINT0CONTRACT.md @@ -0,0 +1,239 @@ +# Sprint 0 Contract: Source And Environment Verification + +Status: Completed +Last updated: 2026-05-07 +Result: PASS, go-with-risks + +## Objective + +Verify the external facts and local environment assumptions needed before any converter implementation starts. + +Current amendment: the project target changed after the original Sprint 0 pass from MinerU 2.5 to MinerU 3.1.0. Read MinerU version constraints in this contract as applying to MinerU 3.1.0 for future work. + +Sprint 0 must answer whether the planned v1 implementation can proceed with: + +- MinerU 3.1.0 through direct local CLI execution only. +- Python 3.12 and `uv`. +- Windows PowerShell on the current workspace. +- NVIDIA GTX 1070 Ti 8GB as the target GPU. +- Local-only processing with no cloud OCR, remote LLM/VLM, hosted parser, `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- The temporary local `mineru-api` process started internally by MinerU 3.1.0 CLI is allowed when CLI runs without `--api-url`. + +## Touched Surfaces + +Allowed: + +- `docs/KNOWLEDGEBASE.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if the sprint sequence or constraints need adjustment +- `docs/Sprints/SPRINT0CONTRACT.md` +- `PROGRESS.md` + +Not allowed: + +- `pyproject.toml` +- `src/` +- `tests/` +- `scripts/` +- Any converter implementation code +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 0 should produce a concise source-backed handoff in `docs/KNOWLEDGEBASE.md` under the heading `## 9. Sprint 0 Verification (2026-05-07)`, plus a short status and handoff entry in `PROGRESS.md`. + +Evidence requirements: + +- Prefer official or primary sources. +- Use non-official sources only when official documentation is incomplete and the source is directly relevant. +- For volatile implementation claims, record source URL, access date, and whether the claim is a direct fact or a project inference. +- Record failures from allowed local commands as environment facts. Do not fix them during Sprint 0. +- Web research is allowed for documentation verification. Runtime converter design must remain local-only. + +The handoff must cover: + +1. MinerU 3.1.0 local CLI facts + - Install command or supported install path. + - Version command or reliable version detection path. + - Direct local CLI invocation shape for PDF conversion. + - Supported output locations for Markdown, JSON/structured data, assets, logs, and raw diagnostics. + - Whether local execution can be kept free of router/API/HTTP endpoint modes. + +2. Python and package workflow facts + - Python 3.12 compatibility status for the planned dependency stack. + - `uv` setup implications. + - Any packaging constraints that affect Sprint 1 scaffolding. + +3. GPU and runtime facts + - CUDA/PyTorch expectations relevant to GTX 1070 Ti 8GB. + - Known GPU memory risks or CPU fallback/error-message implications. + - Commands that future `pdf2md doctor` should check. + +4. License and privacy facts + - MinerU license status. + - Model-weight license status when identifiable. + - Transitive package/license risks that must be reviewed before redistribution. + - Confirmation that v1 runtime design remains local-only. + +5. Implementation go/no-go recommendation + - `go`: proceed to Sprint 1 with listed assumptions. + - `go-with-risks`: proceed but carry specific risks into later sprint contracts. Each risk must name the later sprint that must absorb it. + - `blocked`: stop and ask the user for a requirement change. + +## Non-Goals + +- Do not scaffold the Python project. +- Do not install MinerU, CUDA, PyTorch, or model weights. +- Do not run full PDF conversion. +- Do not edit `samples/` or commit sample files. +- Do not introduce candidate engine comparisons. +- Do not decide a new conversion engine. +- Do not add implementation abstractions, config systems, or CLI flags. + +## Work Packages + +### WP0.1: MinerU Source Review + +Owner: + +- `research-agent` +- `mineru-research` skill + +Actions: + +- Review official MinerU documentation, MinerU GitHub, release notes/tags, and relevant license files. +- Verify current install, CLI, version, output, model/cache, and local execution facts. +- Record source URLs and access date for durable claims. + +Output: + +- Source-backed MinerU fact table in `docs/KNOWLEDGEBASE.md` under `## 9. Sprint 0 Verification (2026-05-07)`. +- Any adapter-impacting uncertainty listed in `PROGRESS.md`. + +### WP0.2: Local Environment Review + +Owner: + +- `local-setup-agent` + +Actions: + +- Check non-invasive local facts, such as Python version, `uv` availability, and GPU visibility when available. +- Review official Python, `uv`, PyTorch/CUDA, and NVIDIA-relevant documentation for compatibility constraints. +- Identify future `pdf2md doctor` checks. + +Output: + +- Environment compatibility notes and doctor-check requirements. +- Explicit GTX 1070 Ti 8GB risks. + +Allowed local commands: + +```powershell +python --version +uv --version +nvidia-smi +``` + +Only run these commands if they are useful for the active research pass. Do not install or modify system packages. + +### WP0.3: Output Layout Probe Plan + +Owner: + +- `mineru-integration-agent` + +Actions: + +- Define what must be observed from MinerU before implementing the adapter. +- If MinerU is already installed locally, a later user-approved probe may inspect command help or run against a disposable file. Sprint 0 does not require conversion execution. + +Output: + +- Adapter-facing output layout assumptions. +- List of fields that must remain optional until observed from real MinerU output. + +### WP0.4: License And Privacy Review + +Owner: + +- `license-privacy-agent` + +Actions: + +- Review MinerU and model/package license sources. +- Distinguish personal/research use from redistribution. +- Check that no planned runtime path uploads PDFs, page images, extracted text, or model intermediates. + +Output: + +- License/privacy summary with unresolved obligations. +- Blockers if redistribution assumptions are unsafe. + +### WP0.5: Contract Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review this contract before Sprint 0 research starts. +- Require concrete evidence expectations and failure thresholds. +- After Sprint 0 research, independently verify that each expected output is present and source-backed. + +Output: + +- Pass/fail evaluation notes. +- Specific follow-up findings if the contract or results are incomplete. + +## Verification Checks + +Required: + +- Every volatile implementation fact has an official or primary source URL. +- Source-backed claims distinguish direct fact from project inference. +- `docs/KNOWLEDGEBASE.md` remains consistent with `PRD.md` and `ARCHITECTURE.md`. +- No candidate engine comparison is reintroduced. +- No converter implementation code is created. +- `samples/` remains untracked. +- `git diff --check` passes for documentation changes. + +Recommended: + +- Check all newly added links manually or with a lightweight local link check if available. +- Have `evaluation-agent` review the completed Sprint 0 outputs before proceeding to Sprint 1. + +## Hard Failure Criteria + +Sprint 0 fails and must stop for a user decision if any of these are true: + +- MinerU 3.1.0 cannot be invoked through a direct local CLI path suitable for v1. +- MinerU's required v1 path requires `--api-url`, router mode, HTTP client backends, remote APIs, or remote OpenAI-compatible backend behavior. +- Python 3.12 is incompatible with the required implementation stack. +- Local-only processing cannot be maintained. +- License terms clearly block the intended personal/research use. +- Required evidence cannot be verified from official or primary sources. + +## Acceptance Criteria + +Sprint 0 is complete when: + +- `docs/KNOWLEDGEBASE.md` contains updated Sprint 0 facts with sources. +- `PROGRESS.md` records checks performed, unresolved risks, and go/no-go recommendation. +- Future Sprint 1 can proceed without guessing install, CLI, environment, or licensing assumptions. +- The evaluator review is complete. +- The completed documentation change is committed. + +## Handoff Fields + +Use these fields when Sprint 0 completes: + +- Files changed: +- Sources checked: +- Local commands run: +- Facts confirmed: +- Inferences made: +- Known failures: +- Residual risks: +- Go/no-go recommendation: +- Next action: diff --git a/docs/Sprints/SPRINT10CONTRACT.md b/docs/Sprints/SPRINT10CONTRACT.md new file mode 100644 index 0000000..992129a --- /dev/null +++ b/docs/Sprints/SPRINT10CONTRACT.md @@ -0,0 +1,355 @@ +# Sprint 10 Contract: Pre-Conversion PDF Page Chunking + +Status: Implemented +Last updated: 2026-05-08 + +## Objective + +Add an opt-in pre-conversion workflow for long PDFs: + +1. Split each source PDF into fixed-size chunk PDFs of 20 pages. +2. Convert each chunk PDF independently through the existing MinerU conversion pipeline. +3. Do not merge the generated Markdown files. + +The feature is intended to reduce long-document memory/runtime pressure and make partial progress usable when one chunk fails. It must preserve strict-local execution, keep MinerU 3.1.0 as the only conversion engine, and keep default tests independent of real MinerU, GPU, CUDA, model files, network access, Obsidian, LaTeX tooling, and `samples/`. + +## Research Summary + +Sources checked on 2026-05-08: + +- [pypdf PyPI](https://pypi.org/project/pypdf/): current release observed as `6.10.2`, uploaded 2026-04-15; metadata lists `BSD-3-Clause`, Python `>=3.9`, Python 3.12 support, and describes pypdf as a pure-Python PDF library capable of splitting, merging, cropping, and transforming PDF pages. +- [pypdf merging docs](https://pypdf.readthedocs.io/en/stable/user/merging-pdfs.html): `PdfWriter.append()` can append a complete or partial source PDF; examples use zero-based page ranges such as `(0, 10)`. The docs recommend `append` or `merge` over low-level `add_page` / `insert_page`. +- [pypdf streaming docs](https://pypdf.readthedocs.io/en/latest/user/streaming-data.html): `PdfReader` and `PdfWriter` support file-like objects, but the project should write chunk PDFs to local disk because MinerU accepts local file paths. +- [pypdf PdfWriter docs](https://pypdf.readthedocs.io/en/stable/modules/PdfWriter.html): writer operations clone/copy PDF objects into the destination. The docs warn that cloning linked objects can copy more than just the visible page object in some cases, so chunk output size must be checked in tests. +- [MinerU CLI tools docs](https://opendatalab.github.io/MinerU/usage/cli_tools/): the direct `mineru` CLI accepts `-p/--path`, `-o/--output`, `-s/--start`, and `-e/--end`; without `--api-url`, it launches a temporary local `mineru-api`. +- [PyMuPDF PyPI](https://pypi.org/project/PyMuPDF/): PyMuPDF is fast and local, but PyPI lists dual licensing under GNU AGPL v3 or an Artifex commercial license. +- [pikepdf page assembly docs](https://pikepdf.readthedocs.io/en/latest/topics/pages.html): pikepdf can split pages and transfer page-associated data; it is a capable fallback candidate but adds a QPDF-backed dependency and is not needed for a first implementation. + +## Recommended Package Decision + +Use `pypdf` for Sprint 10. + +Rationale: + +- It is pure Python and fits the current Python 3.12 + `uv` workflow. +- It has permissive `BSD-3-Clause` metadata on PyPI. +- It directly supports page-level PDF assembly with `PdfReader` / `PdfWriter`. +- It avoids adding PyMuPDF's AGPL/commercial licensing considerations for a simple split-only feature. +- It avoids adding pikepdf/QPDF native dependency complexity before there is evidence that pypdf cannot handle the project samples. + +Recommended dependency range for implementation: + +```toml +dependencies = [ + "pypdf>=6.10.2,<7", +] +``` + +The implementation adds this dependency to `pyproject.toml` and `uv.lock`. + +## Current Precondition + +- `pdf2md convert` already converts one PDF or a directory of PDFs. +- Existing conversion output per input includes: + - Markdown + - optional metadata JSON, enabled by default + - `.report.md` + - assets directory + - optional raw MinerU output +- `plan_outputs()` already enforces overwrite and output-root safety. +- `convert_input()` already handles directory batches and continues after per-file failures. +- The MinerU adapter accepts one PDF path at a time and runs direct local `mineru` CLI. +- `samples/` is local and untracked; do not commit sample PDFs or generated outputs. + +## Touched Surfaces + +Allowed during implementation: + +- `pyproject.toml` +- `uv.lock` +- `src/pdf2md/pdf_splitter.py` +- `src/pdf2md/paths.py` +- `src/pdf2md/conversion.py` +- `src/pdf2md/cli.py` +- `src/pdf2md/ir.py` only if new warning codes or chunk provenance records are required +- `src/pdf2md/metadata.py` only for chunk provenance fields +- `src/pdf2md/report.py` only to expose chunk provenance in reports +- `tests/test_pdf_splitter.py` +- `tests/test_conversion.py` +- `tests/test_cli.py` +- `tests/test_paths.py` +- `tests/test_metadata.py` +- `tests/integration/` for mocked chunk workflow coverage +- `README.md` +- `docs/V1IMPLEMENTATIONPLAN.md` +- `docs/Sprints/SPRINT10CONTRACT.md` +- `PLAN.md` +- `PROGRESS.md` + +Not allowed: + +- Runtime engine selection or alternate conversion engines. +- Use of cloud OCR, remote LLM/VLM, hosted renderers, hosted document parsers, `--api-url`, router mode, HTTP client backends, remote APIs, or remote OpenAI-compatible backends. +- Mandatory default tests requiring real MinerU, GPU, CUDA, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- Committed files under `samples/`. +- Committed generated conversion outputs. +- Automatic model or package downloads triggered by import time, `doctor`, `convert`, or tests. +- Markdown merge behavior for chunk outputs. +- Claims that chunking improves formula correctness; it is only a processing-control feature. + +## Product Behavior + +Activation: + +- Chunking is opt-in and existing conversion behavior is unchanged when `chunk_pages` is unset. +- CLI: `pdf2md convert INPUT --out OUTPUT_DIR --chunk-pages` uses the default chunk size of 20 pages. +- CLI: `pdf2md convert INPUT --out OUTPUT_DIR --chunk-pages 20` uses an explicit positive chunk size. +- Python API: `convert_pdf(..., chunk_pages=20)` and `convert_input(..., chunk_pages=20)`. +- `convert_pdf()` returns `ConversionResult` without chunking and `BatchConversionResult` when chunk mode is active. +- `chunk_pages` must be `None` or a positive integer. + +Chunking behavior: + +- If `chunk_pages` is unset, current behavior remains unchanged. +- If `chunk_pages=20` and a PDF has 20 or fewer pages, conversion may either: + - convert the original PDF directly, or + - create one chunk PDF and convert that chunk. +- Recommended: convert the original directly when `total_pages <= chunk_pages` to avoid unnecessary intermediate files. +- If a PDF has more than 20 pages, split it into chunk PDFs with ranges: + - chunk 1: source pages 1-20 + - chunk 2: source pages 21-40 + - chunk N: remaining pages +- Convert chunk PDFs sequentially, not in parallel. GTX 1070 Ti 8GB memory pressure makes sequential conversion the safer default. +- If one chunk conversion fails, continue with later chunks and report the failed chunk clearly. +- Do not merge Markdown outputs. + +Recommended chunk output naming: + +```text +.part-001.pages-001-020.md +.part-001.pages-001-020.metadata.json +.part-001.pages-001-020.report.md +.part-001.pages-001-020.assets/ + +.part-002.pages-021-040.md +... +``` + +Recommended chunk PDF staging: + +- Use a temporary working directory. +- Delete temporary chunk PDFs after conversion completes, including when `--keep-raw` is enabled. +- Do not add a separate `--keep-chunks` flag in Sprint 10. + +## Provenance Requirements + +Each chunk conversion must preserve original-source context. + +Required chunk fields in metadata or engine options: + +- original source PDF path +- original source SHA-256 +- chunk PDF path when retained, or chunk PDF filename when temporary +- chunk index, 1-based +- total chunk count +- source page start, 1-based inclusive +- source page end, 1-based inclusive +- chunk page count + +Page provenance must distinguish: + +- chunk-local page index, starting at 0 for MinerU output +- original source page number, starting at 1 for user-facing reports + +The report should include a short chunk context line, for example: + +```text +- Chunk: 2/5, source pages: 21-40 +``` + +## Architecture Plan + +### WP10.1: PDF Splitter Module + +Owner: + +- `feature-generator-agent` +- `mineru-integration-agent` + +Actions: + +- Add `src/pdf2md/pdf_splitter.py`. +- Define project-owned `PdfChunkPlan`. +- Implement page counting with `pypdf.PdfReader`. +- Implement chunk planning without writing files. +- Implement chunk writing with `pypdf.PdfWriter.append(source, (start, end))` or an equivalent tested `PdfReader`/`PdfWriter` path. +- Use zero-based half-open page ranges internally and one-based inclusive ranges for filenames and reports. +- Reject invalid chunk sizes with clear `ValueError`. +- Fail clearly on encrypted/password-protected PDFs unless a later sprint adds password handling. + +Expected output: + +- Deterministic chunk plans and local chunk PDFs suitable for the existing MinerU adapter. + +### WP10.2: Chunk-Aware Path Planning + +Owner: + +- `feature-generator-agent` + +Actions: + +- Extend output planning so chunk outputs are deterministic and conflict-checked before conversion starts. +- Avoid collisions between original-output stems and chunk-output stems. +- Preserve output-root escape prevention. +- Respect `--overwrite`. +- Keep Korean and non-ASCII source stems working. + +Expected output: + +- A long PDF can produce multiple planned Markdown/metadata/report/assets outputs without overwriting another chunk. + +### WP10.3: Conversion Orchestration + +Owner: + +- `feature-generator-agent` +- `mineru-integration-agent` + +Actions: + +- Add chunk mode to `convert_pdf()` and `convert_input()`. +- When chunk mode is active, split before calling the MinerU adapter. +- Reuse existing per-PDF conversion path for each chunk PDF rather than creating a second conversion pipeline. +- Continue conversion after a chunk-level failure and aggregate a batch-like result for the source. +- Ensure temporary chunk directories are cleaned up unless raw retention is requested. +- Keep strict-local validation unchanged. + +Expected output: + +- Long PDF conversion yields separate Markdown/metadata/report/assets outputs per chunk. + +### WP10.4: Metadata And Report Chunk Provenance + +Owner: + +- `metadata-agent` +- `obsidian-markdown-agent` + +Actions: + +- Add chunk provenance fields without exposing raw pypdf objects. +- Keep existing required metadata fields valid. +- Keep original source provenance visible even though MinerU sees a chunk PDF as input. +- Ensure chunk reports are readable without opening JSON metadata. + +Expected output: + +- Users can map each output file back to original source pages. + +### WP10.5: CLI And API Surface + +Owner: + +- `feature-generator-agent` +- `requirements-guard-agent` + +Actions: + +- Add `--chunk-pages [INTEGER]` to `pdf2md convert`. +- Keep chunking disabled unless the option is present. +- Use 20 pages when `--chunk-pages` is present without an explicit value. +- Validate positive integer input. +- Keep `--out`, `--metadata`, `--keep-raw`, `--recursive`, `--overwrite`, `--gpu`, and strict-local behavior unchanged. +- Update README with the long-PDF workflow. + +Expected output: + +```powershell +uv run pdf2md convert samples/long.pdf --out outputs --chunk-pages 20 +``` + +### WP10.6: Tests + +Owner: + +- `feature-generator-agent` +- `evaluation-agent` + +Default tests must not require real MinerU or sample PDFs. + +Required tests: + +- Build small local PDF fixtures using `pypdf` blank pages or minimal test PDFs. +- Page count detection for 1, 20, 21, 40, and 41 pages. +- Chunk planning produces expected 1-based filenames and page ranges. +- Chunk writing produces PDFs with the expected page counts. +- Non-positive chunk size is rejected. +- Existing conversion without `--chunk-pages` is unchanged. +- Chunked conversion calls the fake adapter once per chunk. +- Chunked conversion writes separate Markdown, metadata JSON, report Markdown, and assets per chunk. +- `--overwrite` and conflict detection work for all planned chunk outputs. +- A failed chunk does not silently fallback and does not prevent later chunks from being attempted. +- Metadata/report contain original source PDF and source page range. +- CLI validates `--chunk-pages` and prints a useful summary. + +Optional local validation: + +- Run chunked conversion on a local `samples/` PDF only by explicit user request or opt-in gate. +- Do not commit generated chunk PDFs or outputs. + +## Acceptance Criteria + +- Sprint 10 implementation can split a PDF into 20-page chunk PDFs before MinerU conversion. +- Chunk PDFs are converted one by one using the existing direct local MinerU CLI adapter. +- Markdown outputs are separate and not merged. +- Metadata/report files show chunk index and original page range. +- Default test suite passes without real MinerU, GPU, CUDA, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- Strict-local policy remains unchanged. +- Existing non-chunked conversion behavior remains backward-compatible. + +## Hard Failure Criteria + +- Chunking uses a remote PDF service or uploads document content. +- Chunking introduces an alternate Markdown conversion engine. +- Default tests require real MinerU, GPU, CUDA, model files, network, or local samples. +- Chunk outputs overwrite each other or overwrite non-chunk outputs without `--overwrite`. +- Chunk metadata loses original source page provenance. +- The implementation merges Markdown despite this contract's non-merge requirement. +- The implementation silently skips failed chunks without warnings. + +## Resolved Decisions + +- Activation mode: opt-in with `--chunk-pages`; the option defaults to 20 pages when no value is supplied. +- Chunk PDF retention: temporary chunk PDFs only; they are deleted after conversion completes. +- API return type: `convert_pdf()` returns a `BatchConversionResult` when chunk mode is active. + +## Verification Commands For Implementation + +```powershell +uv sync +uv run pytest tests/test_pdf_splitter.py tests/test_conversion.py tests/test_cli.py tests/test_paths.py tests/test_metadata.py +uv run pytest +git diff --check +git status --short --untracked-files=all +``` + +Optional local command after implementation and explicit user approval: + +```powershell +uv run pdf2md convert samples/MITC공부.pdf --out outputs --chunk-pages 20 --overwrite +``` + +## Handoff Requirements + +After implementation: + +- Update `PROGRESS.md` with files changed, commands run, tests passed, optional sample status, known failures, residual risks, and next action. +- Do not mark the sprint implemented until independent evaluation or equivalent focused review verifies the acceptance criteria. +- Commit the completed change without including `samples/` or generated outputs. + +## Implementation Handoff + +- Files changed: `pyproject.toml`, `uv.lock`, `src/pdf2md/pdf_splitter.py`, `src/pdf2md/conversion.py`, `src/pdf2md/cli.py`, `src/pdf2md/__init__.py`, `src/pdf2md/report.py`, tests, README, `docs/V1IMPLEMENTATIONPLAN.md`, `PLAN.md`, and `PROGRESS.md`. +- Verification status: targeted unit tests passed 42 tests; the full local test suite passed 163 tests with 1 optional skip; `git diff --check` passed with line-ending warnings only. +- Optional local sample conversion remains out of scope unless explicitly requested. diff --git a/docs/Sprints/SPRINT1CONTRACT.md b/docs/Sprints/SPRINT1CONTRACT.md new file mode 100644 index 0000000..48a4d4b --- /dev/null +++ b/docs/Sprints/SPRINT1CONTRACT.md @@ -0,0 +1,258 @@ +# Sprint 1 Contract: Project Scaffold And Fast Test Loop + +Status: Completed +Last updated: 2026-05-07 + +## Objective + +Create the minimal Python project scaffold and fast local test loop for the PDF-to-Markdown converter. + +Sprint 1 must establish: + +- A `uv`-managed Python 3.12 project. +- A source package importable as `pdf2md`. +- A reserved `pdf2md` CLI entry point that does not implement conversion yet. +- A fast test command that runs without MinerU, model downloads, GPU access, sample PDFs, or network access. + +Sprint 1 is scaffolding only. It must not implement PDF conversion, MinerU execution, Markdown normalization, metadata generation, or report generation. + +## Current Precondition + +Sprint 0 found that `uv` was not available on PATH in the current local environment. + +Sprint 1 resolved this by installing `uv` per-user at `C:\Users\user\.local\bin`. + +Before Sprint 1 can be accepted, one of these must happen: + +- `uv` is installed and `uv --version` succeeds. +- The user explicitly approves including `uv` bootstrap documentation or setup handling as part of Sprint 1, and the contract result records that `uv sync` could not be run locally. + +Do not silently replace `uv` with another package manager. + +## Touched Surfaces + +Allowed: + +- `pyproject.toml` +- `uv.lock` +- `.gitignore` +- `src/pdf2md/__init__.py` +- `src/pdf2md/cli.py` only for a minimal placeholder CLI if needed for entry point verification +- `tests/` +- `README.md` only for minimal setup/test instructions if needed +- `PLAN.md` only for current-goal coordination updates required by the shared agent workflow +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if sequencing or constraints need adjustment +- `docs/Sprints/SPRINT1CONTRACT.md` + +Not allowed: + +- `src/pdf2md/conversion.py` +- `src/pdf2md/mineru_adapter.py` +- `src/pdf2md/paths.py` +- `src/pdf2md/ir.py` +- `src/pdf2md/markdown.py` +- `src/pdf2md/metadata.py` +- `src/pdf2md/quality.py` +- `src/pdf2md/report.py` +- `src/pdf2md/doctor.py` +- `scripts/` +- Any real MinerU invocation +- Any model download or install script +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 1 should produce: + +1. Project package scaffold + - `pyproject.toml` with project metadata. + - Python requirement constrained to Python 3.12. + - Build configuration suitable for a `src/` layout. + - `uv.lock` generated by `uv sync`. + - `.gitignore` entries for local virtual environments, pytest cache, and Python bytecode. + - Minimal test dependency configuration. + - CLI entry point name reserved as `pdf2md`. + +2. Minimal source package + - `src/pdf2md/__init__.py`. + - A stable package import surface. + - Optional minimal `src/pdf2md/cli.py` placeholder that exits clearly and does not imply conversion is implemented. + +3. Fast test loop + - A minimal test suite that verifies the package imports. + - If a CLI placeholder is added, a smoke test that verifies the CLI entry point is wired without invoking conversion. + - Tests must not require MinerU, CUDA, GPU, model files, `samples/`, or network. + +4. Developer workflow + - `uv sync` should work when `uv` is installed. + - `uv run pytest` should work when `uv` is installed. + - If `uv` is still missing locally, record the failure explicitly in `PROGRESS.md` and do not mark Sprint 1 complete. + +5. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement PDF discovery. +- Do not implement conversion orchestration. +- Do not implement the MinerU adapter. +- Do not run MinerU. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not implement Markdown normalization. +- Do not implement metadata JSON or `.report.md` output. +- Do not implement `pdf2md doctor`; a CLI placeholder may mention future commands, but it must not create a doctor module. +- Do not add runtime engine selection. +- Do not add alternate conversion engines. +- Do not add cloud, remote API, router, HTTP client backend, or remote OpenAI-compatible backend support. + +## Work Packages + +### WP1.1: Scaffold Metadata + +Owner: + +- `feature-generator-agent` + +Actions: + +- Create the minimal `pyproject.toml`. +- Use Python 3.12 constraints. +- Configure a `src/` package layout. +- Configure pytest as the fast local test runner. +- Reserve the `pdf2md` console script. + +Output: + +- A minimal, maintainable scaffold without speculative dependencies. + +### WP1.2: Package Import Surface + +Owner: + +- `feature-generator-agent` + +Actions: + +- Create `src/pdf2md/__init__.py`. +- Expose only a minimal version/import surface. +- Avoid public API promises beyond what Sprint 1 verifies. + +Output: + +- `import pdf2md` succeeds. + +### WP1.3: CLI Placeholder + +Owner: + +- `feature-generator-agent` + +Actions: + +- If needed for console script verification, create `src/pdf2md/cli.py`. +- The placeholder may expose a help message or a clear "not implemented yet" command. +- It must not create conversion flags beyond the reserved command shape unless tests need them. + +Output: + +- `pdf2md` entry point is wired without implying conversion works. + +### WP1.4: Fast Tests + +Owner: + +- `feature-generator-agent` +- `evaluation-agent` + +Actions: + +- Add minimal tests for package import and optional CLI placeholder behavior. +- Ensure tests are local, fast, and independent of MinerU/model/GPU/network state. + +Output: + +- `uv run pytest` passes when `uv` is available. + +### WP1.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review the completed scaffold against this contract. +- Verify no converter implementation was added. +- Verify `samples/` remains untracked and unstaged. +- Verify no runtime remote path or alternate engine was introduced. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short` before staging confirms `samples/` remains untracked. +- `uv --version` is run and result is recorded. +- `uv sync` passes if `uv` is available. +- `uv run pytest` passes if `uv` is available. +- If `uv` is unavailable, Sprint 1 is marked blocked rather than complete. +- Import test passes through the configured test command. +- No real MinerU dependency is required for default tests. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No conversion behavior is implemented. +- `git diff --check` passes. + +Recommended: + +- Keep `pyproject.toml` dependency list minimal. +- Avoid adding README content beyond setup/test instructions needed for the scaffold. +- Use `requirements-guard-agent` to check document consistency if the scaffold reveals a sequencing issue. + +## Hard Failure Criteria + +Sprint 1 fails and must stop for a user decision if any of these are true: + +- `uv` remains unavailable and the user has not approved bootstrap handling. +- The project cannot be installed as a Python 3.12 package. +- The package cannot be imported as `pdf2md`. +- Default tests require MinerU, model downloads, GPU access, sample PDFs, or network access. +- The scaffold introduces conversion logic outside Sprint 1 scope. +- The scaffold introduces alternate engines or runtime engine selection. +- The scaffold introduces `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 1 is complete when: + +- `pyproject.toml` exists and defines a minimal Python 3.12 `uv` project. +- `src/pdf2md/__init__.py` exists and `import pdf2md` works through the project environment. +- `uv sync` passes. +- `uv run pytest` passes. +- The `pdf2md` CLI entry point is reserved and does not imply conversion is implemented. +- No converter implementation code beyond the allowed placeholder exists. +- No default test depends on MinerU, GPU, model files, network, or `samples/`. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 1 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 2: +- Next action: diff --git a/docs/Sprints/SPRINT2CONTRACT.md b/docs/Sprints/SPRINT2CONTRACT.md new file mode 100644 index 0000000..b66e6f3 --- /dev/null +++ b/docs/Sprints/SPRINT2CONTRACT.md @@ -0,0 +1,274 @@ +# Sprint 2 Contract: Paths, Input Discovery, And Overwrite Planning + +Status: Completed +Last updated: 2026-05-07 + +## Objective + +Implement deterministic input discovery and output path planning before any PDF conversion logic exists. + +Sprint 2 must establish: + +- A project-owned path planning module for local PDF inputs. +- Deterministic discovery for a single PDF, a directory, and optional recursive directory traversal. +- Deterministic planned output paths for Markdown, assets, metadata JSON, quality report, and optional raw MinerU output. +- Preflight overwrite conflict detection that prevents accidental replacement unless overwrite is explicitly allowed. +- Fast unit tests using generated temporary files, including non-ASCII filenames. + +Sprint 2 is path planning only. It must not run MinerU, parse PDFs, write conversion outputs, normalize Markdown, create metadata content, or add the real `convert` command behavior. + +## Current Precondition + +Sprint 1 is complete: + +- `uv` is installed per-user at `C:\Users\user\.local\bin`. +- `pyproject.toml`, `uv.lock`, the `pdf2md` package, CLI placeholder, and fast pytest loop exist. +- `uv sync` and `uv run pytest` passed. + +If a new shell cannot find `uv`, prepend `C:\Users\user\.local\bin` to PATH for verification commands and record that in `PROGRESS.md`. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/paths.py` +- `src/pdf2md/conversion.py` only for a minimal type boundary if path planning cannot be tested cleanly without it +- `src/pdf2md/cli.py` only if a minimal parser hook is needed for path-planning tests; do not expose working conversion behavior +- `tests/test_paths.py` or `tests/unit/test_paths.py` +- `tests/test_cli.py` only for path-planning parser coverage if `cli.py` changes +- `README.md` only if setup/test instructions need a small update +- `PLAN.md` only for current-goal coordination updates required by the shared agent workflow +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if sequencing or constraints need adjustment +- `docs/Sprints/SPRINT2CONTRACT.md` + +Not allowed: + +- `src/pdf2md/mineru_adapter.py` +- `src/pdf2md/ir.py` +- `src/pdf2md/markdown.py` +- `src/pdf2md/metadata.py` +- `src/pdf2md/quality.py` +- `src/pdf2md/report.py` +- `src/pdf2md/doctor.py` +- `scripts/` +- Any real MinerU invocation +- Any model download or install script +- Any file parsing beyond local filesystem path and extension checks +- Any conversion output writing beyond temporary files created by tests +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 2 should produce: + +1. Input discovery + - Accept a local path that is either a PDF file or a directory. + - Treat `.pdf` extension matching as case-insensitive. + - Reject a non-existent path with a clear project-owned error. + - Reject a non-PDF file with a clear project-owned error. + - Reject a directory with no discovered PDFs with a clear project-owned error. + - Discover only direct child PDFs for directory input unless recursive traversal is requested. + - Discover nested PDFs only when recursive traversal is requested. + - Return discovered PDFs in a deterministic order. + +2. Output path plan + - For each discovered PDF, plan: + - Markdown path: `//.md`. + - Assets directory: `//.assets`. + - Metadata path when metadata is enabled: `//.metadata.json`. + - Quality report path: `//.report.md`. + - Raw MinerU directory when raw output is kept: `//.raw`. + - For a single PDF input, `relative-parent` is empty unless the implementation has a tested reason to preserve more context. + - For recursive directory input, preserve the source-relative subdirectory under the output root to avoid filename collisions. + - Keep planned paths local filesystem paths. Do not introduce URI, URL, cloud, or remote storage handling. + +3. Overwrite preflight + - Detect existing planned file or directory outputs before conversion writes occur. + - Report all detected conflicts in one project-owned error instead of failing on the first conflict. + - Allow conflicts only when overwrite is explicitly enabled. + - Do not delete or replace files in Sprint 2. + +4. Tests + - Unit tests for single PDF discovery. + - Unit tests for non-recursive directory discovery. + - Unit tests for recursive directory discovery. + - Unit tests for deterministic ordering. + - Unit tests for non-ASCII filenames, including Korean filenames, using temporary files. + - Unit tests for invalid input errors. + - Unit tests for planned Markdown, assets, metadata, report, and raw output paths. + - Unit tests for overwrite conflict detection. + +5. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement PDF conversion. +- Do not implement conversion orchestration. +- Do not implement the MinerU adapter. +- Do not run MinerU. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not parse PDF contents. +- Do not compute source SHA-256. +- Do not implement Markdown normalization. +- Do not implement metadata JSON content. +- Do not implement `.report.md` content. +- Do not implement `pdf2md convert` as a working command. +- Do not implement `pdf2md doctor`. +- Do not add runtime engine selection. +- Do not add alternate conversion engines. +- Do not add cloud, remote API, router, HTTP client backend, or remote OpenAI-compatible backend support. + +## Work Packages + +### WP2.1: Path Planning Types And Errors + +Owner: + +- `feature-generator-agent` + +Actions: + +- Add the smallest project-owned types needed to represent discovered inputs, planned outputs, and overwrite conflicts. +- Add clear project-owned exceptions or error result types for invalid inputs and conflicts. +- Avoid public API promises beyond what Sprint 2 tests verify. + +Output: + +- Path planning can be tested without converter execution. + +### WP2.2: Input Discovery + +Owner: + +- `feature-generator-agent` + +Actions: + +- Implement single PDF and directory discovery. +- Require explicit recursive mode for subdirectory traversal. +- Sort results deterministically. +- Preserve local `Path` objects rather than converting to strings early. + +Output: + +- Discovery behavior matches PRD directory and recursive requirements. + +### WP2.3: Output Planning + +Owner: + +- `feature-generator-agent` + +Actions: + +- Plan Markdown, assets, metadata, report, and optional raw output paths. +- Preserve relative subdirectories for recursive directory input. +- Keep all planned outputs under the requested output root. + +Output: + +- Later conversion code can write outputs without rediscovering naming rules. + +### WP2.4: Overwrite Conflict Detection + +Owner: + +- `feature-generator-agent` + +Actions: + +- Check whether any planned output already exists. +- Return or raise a structured conflict list when overwrite is not enabled. +- Permit the plan when overwrite is enabled without deleting anything. + +Output: + +- Existing user files are protected before conversion starts. + +### WP2.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review the completed path planning implementation against this contract. +- Verify no conversion behavior, MinerU execution, remote runtime path, or alternate engine was added. +- Verify `samples/` remains untracked and unstaged. +- Verify tests use temporary files, not committed sample PDFs. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short` before staging confirms `samples/` remains untracked. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest` passes. +- Targeted path planning tests pass. +- Tests do not require MinerU, CUDA, GPU, model files, `samples/`, or network. +- No real MinerU dependency is required for default tests. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No conversion behavior is implemented. +- No output files are written outside temporary test directories. +- `git diff --check` passes. + +Recommended: + +- Use temporary directories for all filesystem tests. +- Include Windows-relevant path behavior without hard-coding Windows-only separators in assertions. +- Use `requirements-guard-agent` if path planning reveals a contradiction in PRD or architecture wording. + +## Hard Failure Criteria + +Sprint 2 fails and must stop for a user decision if any of these are true: + +- Directory conversion descends recursively without explicit recursive intent. +- Existing planned outputs can be overwritten without explicit overwrite intent. +- Planned output paths can escape the requested output root. +- Default tests require MinerU, CUDA, GPU, model files, network, or `samples/`. +- The implementation parses PDF contents or invokes conversion behavior. +- The implementation introduces alternate engines or runtime engine selection. +- The implementation introduces `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 2 is complete when: + +- `src/pdf2md/paths.py` exists and owns input discovery plus output path planning. +- Single PDF discovery is tested. +- Non-recursive and recursive directory discovery are tested. +- Non-ASCII PDF filenames are tested with generated temporary files. +- Markdown, assets, metadata JSON, report Markdown, and optional raw output paths are tested. +- Existing-output conflict detection is tested with and without overwrite enabled. +- No conversion, MinerU, Markdown normalization, metadata content, report content, or doctor behavior is implemented. +- `uv sync` passes. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 2 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 3: +- Next action: diff --git a/docs/Sprints/SPRINT3CONTRACT.md b/docs/Sprints/SPRINT3CONTRACT.md new file mode 100644 index 0000000..3660b98 --- /dev/null +++ b/docs/Sprints/SPRINT3CONTRACT.md @@ -0,0 +1,303 @@ +# Sprint 3 Contract: Domain Records, Metadata, And Warning Model + +Status: Completed +Last updated: 2026-05-07 + +## Objective + +Define project-owned domain records, warning records, and metadata JSON construction before binding the system to MinerU output. + +Sprint 3 must establish: + +- Internal records for documents, pages, blocks, assets, warnings, and conversion outputs. +- Stable warning code and severity definitions aligned with `ARCHITECTURE.md`. +- A metadata builder that produces the required v1 top-level and summary fields. +- Warning aggregation behavior that later report generation can consume. +- Fast unit tests that do not require MinerU, model files, GPU, sample PDFs, or network. + +Sprint 3 is schema and metadata modeling only. It must not run MinerU, parse PDFs, normalize Markdown, generate final report Markdown content, expose a working `convert` command, or add remote/runtime engine behavior. + +## Current Precondition + +Sprint 2 is complete: + +- `src/pdf2md/paths.py` owns input discovery and output path planning. +- `tests/test_paths.py` verifies directory recursion, non-ASCII filenames, overwrite conflict detection, duplicate planned outputs, and output-root escape prevention. +- `uv run pytest` passed 21 tests. + +Sprint 3 may use path planning records as context, but it should not depend on actual conversion output. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/ir.py` +- `src/pdf2md/metadata.py` +- `src/pdf2md/report.py` only for a minimal type boundary if metadata/report handoff cannot be expressed cleanly without it +- `src/pdf2md/__init__.py` only if exporting a minimal stable type is necessary and tested +- `tests/test_ir.py` or `tests/unit/test_ir.py` +- `tests/test_metadata.py` or `tests/unit/test_metadata.py` +- `PLAN.md` only for current-goal coordination updates required by the shared agent workflow +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if sequencing or constraints need adjustment +- `docs/Sprints/SPRINT3CONTRACT.md` + +Not allowed: + +- `src/pdf2md/mineru_adapter.py` +- `src/pdf2md/markdown.py` +- `src/pdf2md/quality.py` +- `src/pdf2md/doctor.py` +- `scripts/` +- Any real MinerU invocation +- Any model download or install script +- Any PDF content parsing +- Any Markdown normalization behavior +- Any `.report.md` content generation beyond a minimal handoff type if absolutely needed +- Any working `pdf2md convert` or `pdf2md doctor` behavior +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 3 should produce: + +1. Domain records + - `DocumentRecord` or equivalent project-owned record. + - `PageRecord` or equivalent with page index and optional page dimensions. + - `BlockRecord` or equivalent with block type, optional page index, optional bbox, optional confidence, and optional Markdown character span. + - `AssetRecord` or equivalent with stable relative path and optional source page/provenance. + - `WarningRecord` or equivalent with code, severity, message, optional page index, and optional bbox. + - `ConversionOutputRecord` or equivalent only if useful for connecting metadata to later orchestration; it must not invoke conversion. + +2. Stable enums or constants + - Block types aligned with `ARCHITECTURE.md`: `heading`, `paragraph`, `inline_formula`, `display_formula`, `table`, `figure`, `caption`, `footnote`, `reference`, and `unknown`. + - Warning codes aligned with `ARCHITECTURE.md`, including at least: + - `ENGINE_MISSING` + - `GPU_UNAVAILABLE` + - `LOW_CONFIDENCE_FORMULA` + - `MATH_RENDER_FAILED` + - `ASSET_LINK_MISSING` + - `READING_ORDER_UNCERTAIN` + - `STRICT_LOCAL_VIOLATION` + - `MINERU_CLI_FAILED` + - Warning severity values sufficient for v1 metadata and report summaries, such as `info`, `warning`, and `error`. + +3. Metadata builder + - Build a JSON-serializable metadata object with required top-level fields: + - `source_pdf` + - `source_sha256` + - `created_at` + - `engine` + - `engine_version` + - `engine_options` + - `pages` + - `assets` + - `warnings` + - `summary` + - Build required summary fields: + - `pages_processed` + - `warning_count` + - `asset_count` + - `display_formula_count` + - `inline_formula_count` + - `math_render_error_count` + - Preserve optional fields such as bbox and confidence only when present. + - Require `source_sha256` as an input value. Sprint 3 should not compute hashes by reading PDFs unless the contract is explicitly amended. + - Produce only plain Python data structures that `json.dumps` can serialize without custom encoders. + +4. Warning aggregation + - Count warnings. + - Count math render failures from `MATH_RENDER_FAILED`. + - Preserve warning order unless there is a tested reason to sort. + - Preserve page-level warning data when available. + +5. Tests + - Unit tests for domain record serialization. + - Unit tests for metadata schema creation with all required top-level fields. + - Unit tests for summary counts. + - Unit tests for warning aggregation. + - Unit tests that optional bbox and confidence fields are preserved only when present. + - Unit tests that metadata is JSON serializable. + - Unit tests that metadata requires source PDF, source SHA-256, engine, engine version, and page records. + +6. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement PDF conversion. +- Do not implement conversion orchestration. +- Do not implement the MinerU adapter. +- Do not run MinerU. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not parse PDF contents. +- Do not compute source SHA-256 by reading files unless this contract is explicitly amended. +- Do not implement Markdown normalization. +- Do not implement asset link checking. +- Do not implement math renderability checking. +- Do not implement full `.report.md` content generation. +- Do not implement `pdf2md convert` as a working command. +- Do not implement `pdf2md doctor`. +- Do not add runtime engine selection. +- Do not add alternate conversion engines. +- Do not add cloud, remote API, router, HTTP client backend, or remote OpenAI-compatible backend support. + +## Work Packages + +### WP3.1: Domain Record Types + +Owner: + +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Define small project-owned records for document/page/block/asset/warning concepts. +- Use simple, typed Python structures that are easy to serialize and test. +- Keep MinerU-specific raw objects out of public and required fields. + +Output: + +- `ir.py` contains the minimal domain model needed by metadata construction. + +### WP3.2: Warning Codes And Severities + +Owner: + +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Define stable warning codes from `ARCHITECTURE.md`. +- Define severity values and validate warning records against them. +- Avoid inventing speculative warning categories beyond the known v1 set unless needed by tests. + +Output: + +- Warnings are structured, countable, and stable across later sprints. + +### WP3.3: Metadata Builder + +Owner: + +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Build required metadata JSON data from project-owned records. +- Preserve optional provenance fields only when present. +- Require source PDF path, source SHA-256, engine, engine version, pages, assets, warnings, and engine options as explicit inputs. + +Output: + +- `metadata.py` produces the required v1 metadata object without MinerU execution. + +### WP3.4: Metadata And Warning Tests + +Owner: + +- `feature-generator-agent` +- `evaluation-agent` + +Actions: + +- Add focused unit tests for schema, counts, optional fields, JSON serialization, and validation failures. +- Use in-memory records and temporary paths only. + +Output: + +- `uv run pytest` verifies metadata behavior without external dependencies. + +### WP3.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review the completed records and metadata builder against this contract. +- Verify no conversion behavior, MinerU execution, remote runtime path, alternate engine, Markdown normalization, quality checks, or report content generation was added. +- Verify `samples/` remains untracked and unstaged. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short` before staging confirms `samples/` remains untracked. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest` passes. +- Targeted IR/metadata tests pass. +- Metadata output is JSON serializable through `json.dumps`. +- Tests do not require MinerU, CUDA, GPU, model files, `samples/`, or network. +- No real MinerU dependency is required for default tests. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No conversion behavior is implemented. +- No Markdown normalization behavior is implemented. +- No full `.report.md` content generation is implemented. +- `git diff --check` passes. + +Recommended: + +- Keep dataclass or enum APIs small and explicit. +- Prefer one serialization function per record over ad hoc dict mutation in tests. +- Include tests that fail if a required metadata top-level field is omitted. +- Use `requirements-guard-agent` if metadata requirements conflict between `PRD.md` and `ARCHITECTURE.md`. + +## Hard Failure Criteria + +Sprint 3 fails and must stop for a user decision if any of these are true: + +- Metadata omits source PDF, source SHA-256, engine, engine version, pages, warnings, assets, or summary. +- Summary omits pages processed, warning count, asset count, display formula count, inline formula count, or math render error count. +- Public or required metadata fields require raw MinerU objects. +- Optional bbox, confidence, or page provenance is dropped when provided. +- Optional bbox, confidence, or page provenance is invented when absent. +- Default tests require MinerU, CUDA, GPU, model files, network, or `samples/`. +- The implementation parses PDF contents, invokes conversion behavior, normalizes Markdown, or generates full report Markdown content. +- The implementation introduces alternate engines or runtime engine selection. +- The implementation introduces `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 3 is complete when: + +- `src/pdf2md/ir.py` exists and owns project domain records. +- `src/pdf2md/metadata.py` exists and builds required metadata JSON data from project-owned records. +- Stable block types and warning codes are defined and tested. +- Metadata top-level fields and summary fields are tested. +- Warning aggregation is tested. +- Optional bbox and confidence preservation is tested. +- Metadata JSON serializability is tested. +- No conversion, MinerU, Markdown normalization, quality check, full report generation, or doctor behavior is implemented. +- `uv sync` passes. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 3 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 4: +- Next action: diff --git a/docs/Sprints/SPRINT4CONTRACT.md b/docs/Sprints/SPRINT4CONTRACT.md new file mode 100644 index 0000000..90989ac --- /dev/null +++ b/docs/Sprints/SPRINT4CONTRACT.md @@ -0,0 +1,316 @@ +# Sprint 4 Contract: MinerU Adapter With Mocked Contract + +Status: Implemented +Last updated: 2026-05-07 + +## Objective + +Build the direct local MinerU 3.1.0 adapter boundary using mocked subprocess results and fake output directories first. + +Sprint 4 must establish: + +- A project-owned adapter module that is the only boundary for MinerU CLI interaction. +- Deterministic command construction for direct local MinerU CLI execution. +- Strict-local validation that rejects prohibited remote/API/router/backend options. +- Subprocess execution wrapping that captures stdout, stderr, exit code, command, and generated paths. +- Optional-file parsing for mocked MinerU output directories. +- Clear adapter result and warning records for missing MinerU, failed CLI execution, missing output, and strict-local violations. +- Fast unit tests that do not require real MinerU, model files, GPU, sample PDFs, or network. + +Sprint 4 is an adapter contract sprint. It must not connect the adapter to real conversion orchestration, Markdown normalization, metadata writing, report generation, or a working `pdf2md convert` command. + +## Current Precondition + +Sprint 3 is complete: + +- `src/pdf2md/paths.py` owns input discovery and output path planning. +- `src/pdf2md/ir.py` owns project records, block types, warning codes, and warning severities. +- `src/pdf2md/metadata.py` builds JSON-serializable metadata and summary counts from project-owned records. +- `uv run pytest` passed 46 tests. + +Sprint 4 may import project-owned warning records from `ir.py`, but it must not require raw MinerU objects as public or required return fields. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/mineru_adapter.py` +- `src/pdf2md/doctor.py` only for minimal adapter availability/version check types if needed; do not implement full `pdf2md doctor` +- `src/pdf2md/ir.py` only for narrowly required warning/result fields discovered while implementing the adapter contract +- `tests/test_mineru_adapter.py` or `tests/unit/test_mineru_adapter.py` +- `tests/test_doctor.py` only if `doctor.py` is touched for adapter availability/version checks +- `README.md` only if a small note is needed to clarify mocked adapter tests versus real MinerU setup +- `PLAN.md` only for current-goal coordination updates required by the shared agent workflow +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if sequencing or constraints need adjustment +- `docs/Sprints/SPRINT4CONTRACT.md` + +Not allowed: + +- `src/pdf2md/conversion.py` +- `src/pdf2md/markdown.py` +- `src/pdf2md/quality.py` +- `src/pdf2md/report.py` +- Working `pdf2md convert` behavior +- Full `pdf2md doctor` behavior +- `scripts/` +- Any real MinerU invocation in default tests +- Any MinerU/model installation or download script +- Any PDF content parsing +- Any Markdown normalization behavior +- Any metadata JSON file writing +- Any `.report.md` content generation +- Any runtime engine selection or alternate engine support +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 4 should produce: + +1. Adapter records and options + - A small adapter options record for v1-local MinerU execution. + - A result record containing at least: + - command arguments + - input PDF path + - work/output directory path + - raw Markdown when found + - raw structured data when found + - asset paths when found + - warnings + - engine name fixed to MinerU + - engine version when known + - engine options + - exit code + - stdout + - stderr + - No public or required field should expose raw MinerU-specific Python objects. + +2. Availability and version checks + - Check whether `mineru` is available using a mockable local mechanism such as `shutil.which`. + - Check MinerU version using a mockable subprocess call. + - Missing MinerU should produce a clear `ENGINE_MISSING` warning or project-owned adapter error. + - Version command failure should be explicit and testable. + +3. Direct local command construction + - Baseline conversion command shape: + + ```text + mineru -p -o + ``` + + - The command must not include `--api-url`. + - The command must not include router mode, HTTP client backend flags, remote API URLs, remote OpenAI-compatible backend settings, or runtime engine selection. + - GPU/device options may be represented only if they are strict-local and do not introduce remote/backend choices. If the exact MinerU 3.1.0 flag is uncertain, store the requested option without passing a speculative CLI flag until a later source-verified sprint. + +4. Strict-local validation + - Reject prohibited CLI args and options before subprocess execution. + - Reject any value that looks like a remote URL in user-controlled adapter options. + - Allow only direct local `mineru` CLI execution. + - Allow the CLI-internal temporary local `mineru-api` process that MinerU 3.1.0 may start when the CLI runs without `--api-url`. + - Do not implement or call an HTTP client backend. + +5. Subprocess wrapper + - Use dependency injection or a small runner boundary so tests can fake subprocess behavior. + - Capture stdout, stderr, and exit code. + - Convert non-zero exit into an adapter result or project-owned error with a `MINERU_CLI_FAILED` warning. + - Do not silently fallback to another engine. + +6. Mocked output parsing + - Parse fake output directories using optional-file behavior. + - Raw Markdown is optional and should be read only from mocked local files created by tests. + - Raw structured output is optional and may be represented as a JSON-serializable object or raw text, depending on the fake file extension used in tests. + - Assets are optional and should be collected as local relative or absolute paths according to the adapter result design. + - Missing expected output should produce a clear warning or failed result instead of being silently ignored. + - The adapter must not assume real MinerU output layout is fully known until a later local probe. + +7. Tests + - Unit tests for availability check success and missing MinerU. + - Unit tests for version check success and version command failure. + - Unit tests for command construction. + - Unit tests that prohibited `--api-url`, remote URLs, router mode, HTTP backend, and OpenAI-compatible backend options are rejected. + - Unit tests for mocked successful MinerU output. + - Unit tests for mocked non-zero exit. + - Unit tests for mocked missing output. + - Unit tests proving no real MinerU binary, model files, GPU, `samples/`, or network are required by default. + +8. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement conversion orchestration. +- Do not implement `convert_pdf`. +- Do not implement `pdf2md convert`. +- Do not implement full `pdf2md doctor`. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not run real MinerU in default tests. +- Do not parse real PDFs. +- Do not normalize Markdown. +- Do not write final Markdown, metadata JSON, assets, or report files as product behavior. +- Do not compute source SHA-256. +- Do not implement asset link checking. +- Do not implement math renderability checking. +- Do not implement alternate engines or runtime engine selection. +- Do not add cloud, remote API, router, HTTP client backend, or remote OpenAI-compatible backend support. + +## Work Packages + +### WP4.1: Adapter Types And Strict-Local Options + +Owner: + +- `mineru-integration-agent` +- `feature-generator-agent` + +Actions: + +- Define minimal adapter options and result records. +- Encode strict-local defaults. +- Reject prohibited remote/API/backend options before command execution. + +Output: + +- The adapter boundary can be tested without invoking MinerU. + +### WP4.2: Availability, Version, And Command Builder + +Owner: + +- `mineru-integration-agent` +- `feature-generator-agent` + +Actions: + +- Implement mockable `is_available` and `version` checks. +- Build the direct local command shape `mineru -p -o `. +- Keep command construction deterministic and easy to inspect in tests. + +Output: + +- Later orchestration can call the adapter without knowing MinerU CLI details. + +### WP4.3: Subprocess Runner Boundary + +Owner: + +- `feature-generator-agent` + +Actions: + +- Add a small runner interface or callable boundary for subprocess execution. +- Capture command, stdout, stderr, exit code, and return values. +- Map non-zero exits to structured adapter warnings/errors. + +Output: + +- Default tests can fake all MinerU behavior. + +### WP4.4: Mocked Output Parser + +Owner: + +- `mineru-integration-agent` +- `feature-generator-agent` + +Actions: + +- Parse fake Markdown, JSON/structured, asset, and diagnostic outputs from test-created directories. +- Treat all raw MinerU output files as optional until real local output is probed. +- Emit clear warnings for missing usable output. + +Output: + +- Adapter result objects can carry raw output into later IR/normalization sprints without binding to a guessed full MinerU layout. + +### WP4.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review the completed adapter against this contract. +- Verify no conversion orchestration, real MinerU dependency in default tests, remote runtime path, alternate engine, Markdown normalization, metadata writing, report generation, or working CLI command was added. +- Verify `samples/` remains untracked and unstaged. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short` before staging confirms `samples/` remains untracked. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest` passes. +- Targeted MinerU adapter tests pass. +- Tests do not require real MinerU, CUDA, GPU, model files, `samples/`, or network. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No conversion orchestration is implemented. +- No Markdown normalization behavior is implemented. +- No metadata JSON writing or full report generation is implemented. +- No working `pdf2md convert` or full `pdf2md doctor` behavior is implemented. +- Strict-local rejection tests cover `--api-url`, remote URL values, router mode, HTTP backend, and remote OpenAI-compatible backend options. +- `git diff --check` passes. + +Recommended: + +- Use fake runner classes or functions rather than monkeypatching global subprocess calls everywhere. +- Keep adapter result data JSON-friendly where practical, but do not force metadata schema generation in Sprint 4. +- Include a test that no prohibited remote/API flag appears in the successful command args. +- Use `requirements-guard-agent` if command flags or strict-local wording conflict across documents. + +## Hard Failure Criteria + +Sprint 4 fails and must stop for a user decision if any of these are true: + +- The adapter passes `--api-url`. +- The adapter uses router mode. +- The adapter uses an HTTP client backend. +- The adapter accepts a remote API URL or remote OpenAI-compatible backend for runtime conversion. +- The adapter falls back to another engine after MinerU failure. +- Default tests require real MinerU, CUDA, GPU, model files, network, or `samples/`. +- The implementation installs MinerU or downloads models. +- The implementation connects the adapter to a working conversion CLI/API. +- The implementation adds Markdown normalization, metadata file writing, full report generation, or quality checks. +- The implementation assumes real MinerU output layout is fully known without a later local probe. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 4 is complete when: + +- `src/pdf2md/mineru_adapter.py` exists and owns direct local MinerU CLI adapter behavior. +- Availability and version checks are mock-tested. +- Conversion command construction is mock-tested and uses `mineru -p -o `. +- Strict-local validation rejects prohibited remote/API/router/backend options. +- Mocked successful MinerU output produces an adapter result with raw Markdown, raw structured data when available, assets when available, engine info, command args, stdout, stderr, and exit code. +- Mocked non-zero exit produces a clear failure result or project-owned error with a `MINERU_CLI_FAILED` warning. +- Mocked missing MinerU produces a clear `ENGINE_MISSING` warning or project-owned adapter error. +- Default tests do not require MinerU, GPU, model files, network, or `samples/`. +- No conversion orchestration, Markdown normalization, metadata file writing, full report generation, or working CLI behavior is implemented. +- `uv sync` passes. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 4 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 5: +- Next action: diff --git a/docs/Sprints/SPRINT5CONTRACT.md b/docs/Sprints/SPRINT5CONTRACT.md new file mode 100644 index 0000000..ede73df --- /dev/null +++ b/docs/Sprints/SPRINT5CONTRACT.md @@ -0,0 +1,311 @@ +# Sprint 5 Contract: Obsidian Markdown Normalization And Asset Links + +Status: Implemented +Last updated: 2026-05-07 + +## Objective + +Build the project-owned Markdown normalization boundary for Obsidian output, using deterministic unit tests before it is connected to conversion orchestration. + +Sprint 5 must establish: + +- A small Markdown normalization module that accepts local raw Markdown-like text and returns normalized Markdown plus project-owned warnings. +- Obsidian math delimiter normalization for inline and display math. +- Stable relative asset link normalization without copying files or writing final outputs. +- Limited local asset link validation where useful for warnings. +- Table preservation and clear warning behavior when a table cannot be safely simplified. +- Fast unit tests that do not require real MinerU, model files, GPU, sample PDFs, network, or Obsidian itself. + +Sprint 5 is a normalization contract sprint. It must not connect normalization to the CLI, conversion orchestration, metadata writing, report generation, real MinerU execution, or end-to-end output writing. + +## Current Precondition + +Sprint 4 is complete: + +- `src/pdf2md/paths.py` owns input discovery and output path planning. +- `src/pdf2md/ir.py` owns project records, block types, warning codes, and warning severities. +- `src/pdf2md/metadata.py` builds JSON-serializable metadata and summary counts from project-owned records. +- `src/pdf2md/mineru_adapter.py` owns the mocked direct local MinerU CLI adapter boundary. +- `uv run pytest` passed 72 tests. + +Sprint 5 may use `WarningRecord`, `WarningCode`, and `WarningSeverity` from `ir.py`, but it must not require raw MinerU-specific Python objects as public or required inputs. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/markdown.py` +- `src/pdf2md/quality.py` only for minimal local asset link check helpers if that boundary is cleaner than placing them in `markdown.py` +- `src/pdf2md/ir.py` only for narrowly required warning codes discovered while implementing table or asset fallback warnings +- `tests/test_markdown.py` or `tests/unit/test_markdown.py` +- `tests/test_quality.py` only if `quality.py` is touched for asset link checks +- `README.md` only if a small note is needed to clarify that normalization tests are mocked/local and not full conversion behavior +- `PLAN.md` only for current-goal coordination updates required by the shared agent workflow +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if sequencing or constraints need adjustment +- `docs/Sprints/SPRINT5CONTRACT.md` + +Not allowed: + +- `src/pdf2md/conversion.py` +- `src/pdf2md/cli.py` +- `src/pdf2md/mineru_adapter.py` +- `src/pdf2md/report.py` +- Working `pdf2md convert` behavior +- Full `pdf2md doctor` behavior +- `scripts/` +- Any real MinerU invocation in default tests +- Any MinerU/model installation or download script +- Any PDF content parsing +- Any metadata JSON file writing +- Any `.report.md` content generation +- Any runtime engine selection or alternate engine support +- Any remote asset fetch, HTTP client, or cloud/API integration +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 5 should produce: + +1. Normalization records and API + - A small result record or equivalent project-owned return type containing at least: + - normalized Markdown + - warnings + - asset links discovered or normalized when available + - A normalization function with a narrow input surface, such as raw Markdown text plus optional output/assets context. + - No public or required field should expose raw MinerU-specific Python objects. + - The API should be usable by later orchestration without knowing how MinerU represented the original Markdown. + +2. Inline math delimiter normalization + - Normalize safe inline math forms to `$...$`. + - Preserve already valid `$...$` inline math. + - Preserve the exact LaTeX body inside inline math except delimiter changes. + - Do not escape or rewrite underscores, carets, braces, or backslashes inside math. + - Do not normalize math delimiters inside fenced code blocks or inline code spans. + - Avoid converting ambiguous dollar signs that look like currency or prose punctuation. + +3. Display math delimiter normalization + - Normalize safe display math forms to `$$...$$`. + - Ensure display math delimiters sit on their own lines. + - Keep a blank line around display math blocks. + - Preserve the exact LaTeX body inside display math except delimiter and surrounding whitespace normalization. + - Preserve LaTeX environments such as `equation`, `align`, or `gather` rather than rewriting their semantics. + - Make normalization idempotent: running the normalizer twice should produce the same Markdown. + +4. Asset link normalization + - Normalize local image/asset links to stable relative POSIX-style Markdown paths. + - Keep relative links relative; do not turn them into absolute paths. + - Reject or warn on absolute asset links that cannot be represented relative to the planned output/assets context. + - Reject or warn on links that escape the output/assets directory with `..`. + - Do not fetch remote URLs, copy assets, or write files. + - Preserve alt text when rewriting Markdown image links. + +5. Table preservation and fallback warnings + - Preserve simple Markdown pipe tables without destructive formatting changes. + - Preserve HTML tables when Markdown would lose row spans, column spans, nested content, or other complex structure. + - Emit a project-owned warning when complex table fallback behavior is detected or when table simplification is intentionally skipped. + - Do not attempt broad table reflow or OCR-style table reconstruction in Sprint 5. + +6. Tests + - Unit tests for inline math delimiter normalization. + - Unit tests for display math delimiter normalization and blank-line spacing. + - Unit tests proving underscores and carets inside math are preserved. + - Unit tests proving fenced code blocks and inline code are not normalized. + - Unit tests for idempotency. + - Unit tests for relative asset link normalization. + - Unit tests for missing or escaping asset link warnings when asset checking is implemented. + - Unit tests for simple table preservation. + - Unit tests for complex table fallback warning behavior. + - Unit tests proving no real MinerU binary, model files, GPU, `samples/`, Obsidian installation, or network are required by default. + +7. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement conversion orchestration. +- Do not implement `convert_pdf`. +- Do not implement `pdf2md convert`. +- Do not implement full `pdf2md doctor`. +- Do not invoke MinerU. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not parse real PDFs. +- Do not write final Markdown files as product behavior. +- Do not copy or move assets as product behavior. +- Do not write metadata JSON. +- Do not generate `.report.md`. +- Do not compute source SHA-256. +- Do not implement math renderability checks beyond a future-facing warning interface if needed. +- Do not implement full quality report checks. +- Do not implement alternate engines or runtime engine selection. +- Do not add cloud, remote API, router, HTTP client backend, remote OpenAI-compatible backend, or remote asset-fetching support. + +## Work Packages + +### WP5.1: Normalization Types And Safe Boundaries + +Owner: + +- `obsidian-markdown-agent` +- `feature-generator-agent` + +Actions: + +- Define a small Markdown normalization result type. +- Define a focused normalization function. +- Keep warnings project-owned through `WarningRecord`. +- Keep the API independent of raw MinerU objects. + +Output: + +- Later orchestration can normalize adapter Markdown without knowing MinerU internals. + +### WP5.2: Math Delimiter Normalization + +Owner: + +- `obsidian-markdown-agent` +- `feature-generator-agent` + +Actions: + +- Normalize safe inline math delimiters to `$...$`. +- Normalize safe display math delimiters to `$$...$$` with stable surrounding blank lines. +- Preserve LaTeX bodies exactly. +- Protect code fences and inline code spans. +- Add idempotency tests. + +Output: + +- Obsidian-friendly math delimiter behavior is deterministic and covered by unit tests. + +### WP5.3: Asset Link Normalization + +Owner: + +- `obsidian-markdown-agent` +- `feature-generator-agent` + +Actions: + +- Normalize local image/asset links to stable relative POSIX-style paths. +- Preserve alt text. +- Warn on missing, absolute, escaping, or non-local asset links when the helper has enough local context to judge them. +- Do not fetch or copy assets. + +Output: + +- Later conversion can produce Markdown links that are stable relative to planned output paths. + +### WP5.4: Table Preservation And Fallback Warning + +Owner: + +- `obsidian-markdown-agent` +- `feature-generator-agent` + +Actions: + +- Preserve simple Markdown pipe tables. +- Preserve complex HTML tables without simplifying them destructively. +- Emit a project-owned warning for complex table fallback behavior. + +Output: + +- Table handling is conservative and traceable instead of silently lossy. + +### WP5.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review the completed normalizer against this contract. +- Verify no conversion orchestration, real MinerU dependency in default tests, remote runtime path, alternate engine, metadata writing, report generation, file-copying behavior, or working CLI command was added. +- Verify `samples/` remains untracked and unstaged. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short` before staging confirms `samples/` remains untracked. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest` passes. +- Targeted Markdown normalization tests pass. +- Tests do not require real MinerU, CUDA, GPU, model files, Obsidian, `samples/`, or network. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No conversion orchestration is implemented. +- No metadata JSON writing or full report generation is implemented. +- No working `pdf2md convert` or full `pdf2md doctor` behavior is implemented. +- No final output files are written as product behavior. +- No remote asset fetching is implemented. +- Math delimiter normalization is idempotent. +- Asset paths in normalized Markdown are relative when they are rewritten. +- `git diff --check` passes. + +Recommended: + +- Prefer a small tokenizer or state-machine approach over broad regular-expression rewrites for math/code boundary handling. +- Keep normalization helpers pure and deterministic. +- Treat complex tables conservatively: preserve content and warn rather than flattening structure. +- Use `requirements-guard-agent` if warning codes or output behavior conflict across documents. + +## Hard Failure Criteria + +Sprint 5 fails and must stop for a user decision if any of these are true: + +- The normalizer rewrites LaTeX math bodies beyond delimiter and whitespace normalization without deterministic tests. +- The normalizer changes underscores, carets, braces, or backslashes inside math content. +- The normalizer rewrites code fences or inline code spans as math. +- The normalizer produces absolute asset links where relative links are required. +- The normalizer accepts asset links that escape the output/assets context without warning. +- The implementation fetches remote assets or adds any HTTP/network client path. +- The implementation connects normalization to a working conversion CLI/API. +- The implementation adds metadata file writing, full report generation, real MinerU execution, model downloads, or setup scripts. +- Default tests require real MinerU, CUDA, GPU, model files, network, Obsidian, or `samples/`. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 5 is complete when: + +- `src/pdf2md/markdown.py` exists and owns Obsidian Markdown normalization behavior. +- Inline math delimiter normalization is unit-tested. +- Display math delimiter normalization and blank-line spacing are unit-tested. +- Tests prove underscores and carets inside math are preserved. +- Tests prove fenced code blocks and inline code are not normalized. +- Normalization idempotency is unit-tested. +- Relative asset link normalization is unit-tested. +- Asset warning behavior is unit-tested when missing, absolute, escaping, or non-local links are in scope. +- Simple table preservation and complex table fallback warning behavior are unit-tested. +- Default tests do not require MinerU, GPU, model files, network, Obsidian, or `samples/`. +- No conversion orchestration, metadata file writing, full report generation, file-copying behavior, or working CLI behavior is implemented. +- `uv sync` passes. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 5 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 6: +- Next action: diff --git a/docs/Sprints/SPRINT6CONTRACT.md b/docs/Sprints/SPRINT6CONTRACT.md new file mode 100644 index 0000000..f1987d2 --- /dev/null +++ b/docs/Sprints/SPRINT6CONTRACT.md @@ -0,0 +1,334 @@ +# Sprint 6 Contract: Quality Checks And Report Generation + +Status: Implemented +Last updated: 2026-05-08 + +## Objective + +Build local quality-check and human-readable report generation boundaries from project-owned metadata and normalized Markdown, before they are connected to conversion orchestration. + +Sprint 6 must establish: + +- A project-owned quality module for local asset-link and math-renderability signals. +- A report module that renders `.report.md` content from metadata and quality results. +- Deterministic final status calculation: `success`, `partial`, or `failed`. +- Summary fields needed by reports, including missing asset links and math render failures. +- Fast unit tests that do not require real MinerU, model files, GPU, sample PDFs, Obsidian, LaTeX tooling, network, or a working conversion CLI. + +Sprint 6 is a quality/report contract sprint. It may generate report Markdown content as a string, but it must not connect to the CLI, conversion orchestration, real MinerU execution, file output writing, setup scripts, or end-to-end conversion. + +## Current Precondition + +Sprint 5 is complete: + +- `src/pdf2md/paths.py` owns input discovery and output path planning. +- `src/pdf2md/ir.py` owns project records, block types, warning codes, and warning severities. +- `src/pdf2md/metadata.py` builds JSON-serializable metadata and summary counts from project-owned records. +- `src/pdf2md/mineru_adapter.py` owns the mocked direct local MinerU CLI adapter boundary. +- `src/pdf2md/markdown.py` owns Obsidian Markdown normalization, asset link warnings, and table fallback warnings. +- `uv run pytest` passed 89 tests. + +Sprint 6 may use metadata dictionaries produced by `build_metadata`, project-owned `WarningRecord` values, and normalized Markdown text. It must not require raw MinerU-specific Python objects as public or required inputs. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/quality.py` +- `src/pdf2md/report.py` +- `src/pdf2md/metadata.py` only for narrowly required summary fields or helper functions that keep metadata/report consistency +- `src/pdf2md/ir.py` only for narrowly required warning codes discovered while implementing quality checks +- `tests/test_quality.py` +- `tests/test_report.py` +- `tests/test_metadata.py` only if `metadata.py` changes +- `README.md` only if a small note is needed to clarify mocked/local quality and report behavior +- `PLAN.md` only for current-goal coordination updates required by the shared agent workflow +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` only if sequencing or constraints need adjustment +- `docs/Sprints/SPRINT6CONTRACT.md` + +Not allowed: + +- `src/pdf2md/conversion.py` +- `src/pdf2md/cli.py` +- `src/pdf2md/mineru_adapter.py` +- Working `pdf2md convert` behavior +- Full `pdf2md doctor` behavior +- `scripts/` +- Any real MinerU invocation in default tests +- Any MinerU/model installation or download script +- Any PDF content parsing +- Any final Markdown file writing +- Any metadata JSON file writing +- Any `.report.md` file writing as product behavior +- Any asset copying or moving +- Any runtime engine selection or alternate engine support +- Any remote asset fetch, HTTP client, cloud/API integration, hosted renderer, or remote math-render service +- Any committed file under `samples/` + +## Expected Outputs + +Sprint 6 should produce: + +1. Quality result records and API + - A small project-owned quality result type containing at least: + - missing asset link count + - invalid asset link count when available + - math render error count + - warnings produced by quality checks + - A local asset-link check function that accepts normalized Markdown and local asset context without writing files. + - A math renderability check interface that accepts a local checker callable or reports tool-unavailable behavior gracefully. + - No public or required field should expose raw MinerU-specific Python objects. + +2. Asset-link quality checks + - Count missing local asset links in Markdown. + - Count invalid links that are absolute, parent-escaping, remote, or otherwise non-local according to project policy. + - Produce project-owned warnings for missing or invalid asset links. + - Keep all checks local and deterministic. + - Do not fetch remote URLs, copy assets, move assets, or write files. + +3. Math renderability checks + - Provide a boundary for local math renderability checking. + - Default tests must use fake/local checker callables. + - Tool-unavailable behavior must be explicit and non-fatal. + - Render failures must produce `MATH_RENDER_FAILED` warnings and count toward the report. + - The checker must not call network services or require a LaTeX/Obsidian install in default tests. + +4. Metadata summary consistency + - Preserve existing required metadata summary fields. + - Add or derive report-needed counts without breaking existing metadata tests: + - missing asset link count + - invalid asset link count + - math render error count + - Warning order and warning counts must remain deterministic. + - Reports must be derived from metadata and quality results, not independently duplicated state. + +5. Report Markdown generation + - Render a human-readable `.report.md` content string from metadata and quality results. + - Include at least: + - source PDF path + - output Markdown path when provided + - metadata path when provided + - report path when provided + - MinerU engine/version and execution mode/options + - pages processed + - warning count + - asset count + - missing asset link count + - inline formula count + - display formula count + - math render error count + - pages with warnings + - final status: `success`, `partial`, or `failed` + - The report must not invent facts that are absent from metadata; absent optional paths should be omitted or clearly shown as unavailable. + - The report generator must not write files in Sprint 6. + +6. Final status policy + - `failed`: metadata or quality warnings contain at least one `error` severity warning. + - `partial`: no error severity warnings, but warnings or quality failures exist. + - `success`: no warnings and no quality failures. + - The status function must be unit-tested and reusable by later orchestration. + +7. Tests + - Unit tests for missing asset link counting. + - Unit tests for invalid/remote/escaping asset link warnings. + - Unit tests for math render failure aggregation with a fake checker. + - Unit tests for math checker unavailable behavior. + - Unit tests for report content and required sections. + - Unit tests proving report content is derived from metadata and quality results. + - Unit tests for pages-with-warnings summary. + - Unit tests for final status calculation. + - Unit tests proving no real MinerU binary, model files, GPU, `samples/`, Obsidian, LaTeX install, or network are required by default. + +8. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement conversion orchestration. +- Do not implement `convert_pdf`. +- Do not implement `pdf2md convert`. +- Do not implement full `pdf2md doctor`. +- Do not invoke MinerU. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not parse real PDFs. +- Do not write final Markdown files. +- Do not copy or move assets. +- Do not write metadata JSON files. +- Do not write `.report.md` files as product behavior. +- Do not compute source SHA-256. +- Do not implement real LaTeX, KaTeX, MathJax, or Obsidian rendering in default tests. +- Do not add setup scripts. +- Do not implement full local environment diagnostics. +- Do not implement alternate engines or runtime engine selection. +- Do not add cloud, remote API, router, HTTP client backend, remote OpenAI-compatible backend, hosted renderer, or remote asset-fetching support. + +## Work Packages + +### WP6.1: Quality Result Types And Asset Checks + +Owner: + +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Define a small project-owned quality result type. +- Add deterministic local asset link checks over normalized Markdown. +- Count missing, invalid, escaping, absolute, and remote asset references. +- Return project-owned warnings without writing files. + +Output: + +- Later orchestration can add local quality results to metadata/report flow without duplicating asset-link logic. + +### WP6.2: Math Renderability Boundary + +Owner: + +- `obsidian-markdown-agent` +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Define a local math render checker interface. +- Support fake checkers in tests. +- Treat checker-unavailable as explicit non-fatal warning/info according to the implementation design. +- Treat render failures as `MATH_RENDER_FAILED` warnings and count them. + +Output: + +- Math renderability is represented as a local, testable boundary without external dependencies. + +### WP6.3: Metadata Summary Extensions + +Owner: + +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Preserve existing required metadata summary fields. +- Add or derive counts needed by reports in a backward-compatible way. +- Keep metadata JSON serializable and deterministic. + +Output: + +- Metadata remains the source of truth for report counts and warning summaries. + +### WP6.4: Report Markdown Rendering + +Owner: + +- `metadata-agent` +- `feature-generator-agent` + +Actions: + +- Implement report content rendering from metadata plus quality results. +- Include required report sections and final status. +- Generate content only; do not write files. + +Output: + +- Later orchestration can write `.report.md` by using the tested report renderer. + +### WP6.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review completed quality/report behavior against this contract. +- Verify no conversion orchestration, real MinerU dependency in default tests, remote runtime path, alternate engine, final output writing, CLI behavior, or sample dependency was added. +- Verify `samples/` remains untracked and unstaged. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short` before staging confirms `samples/` remains untracked. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest` passes. +- Targeted quality/report tests pass. +- Tests do not require real MinerU, CUDA, GPU, model files, Obsidian, LaTeX tooling, `samples/`, or network. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No conversion orchestration is implemented. +- No working `pdf2md convert` or full `pdf2md doctor` behavior is implemented. +- No final Markdown, metadata JSON, or `.report.md` files are written as product behavior. +- No remote asset fetching is implemented. +- No real math renderer dependency is required by default tests. +- Report counts match metadata and quality results. +- Report generation does not re-run MinerU. +- `git diff --check` passes. + +Recommended: + +- Keep quality helpers pure and deterministic. +- Use fake checkers for math renderability tests. +- Keep report rendering stable enough for snapshot-like unit assertions. +- Use `requirements-guard-agent` if warning codes, summary fields, or report wording conflict across documents. + +## Hard Failure Criteria + +Sprint 6 fails and must stop for a user decision if any of these are true: + +- Report content diverges from metadata or quality result counts. +- Math render failures are silently ignored. +- Quality checks require network access. +- The implementation fetches remote assets or adds any HTTP/network client path. +- The implementation requires a real LaTeX/Obsidian/MathJax/KaTeX install in default tests. +- The implementation connects quality/report behavior to a working conversion CLI/API. +- The implementation writes final Markdown, metadata JSON, `.report.md`, or copied assets as product behavior. +- The implementation invokes MinerU, downloads models, adds setup scripts, or parses real PDFs. +- Default tests require real MinerU, CUDA, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 6 is complete when: + +- `src/pdf2md/quality.py` exists and owns local quality-check behavior. +- `src/pdf2md/report.py` exists and owns human-readable report content rendering. +- Missing asset link counting is unit-tested. +- Invalid, escaping, absolute, or remote asset link warning behavior is unit-tested. +- Math render failure aggregation is unit-tested with fake checkers. +- Math checker unavailable behavior is unit-tested and non-fatal. +- Report content includes the required sections and counts. +- Pages-with-warnings summary is unit-tested. +- Final status calculation is unit-tested. +- Report generation is proven not to write files or re-run MinerU. +- Default tests do not require MinerU, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- No conversion orchestration, final output file writing, working CLI behavior, real MinerU execution, or setup script is implemented. +- `uv sync` passes. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 6 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 7: +- Next action: diff --git a/docs/Sprints/SPRINT7CONTRACT.md b/docs/Sprints/SPRINT7CONTRACT.md new file mode 100644 index 0000000..48bce7a --- /dev/null +++ b/docs/Sprints/SPRINT7CONTRACT.md @@ -0,0 +1,360 @@ +# Sprint 7 Contract: Conversion Orchestrator, CLI, And Python API + +Status: Implemented +Last updated: 2026-05-08 + +## Objective + +Connect the existing project-owned boundaries into a working conversion orchestration layer, public Python API, and `pdf2md convert` CLI path. + +Sprint 7 must establish: + +- A public `convert_pdf` API for one local PDF. +- A batch conversion API or helper for directory inputs. +- A `pdf2md convert INPUT --out OUTPUT_DIR` command. +- Product behavior that writes Markdown, optional metadata JSON, and `.report.md`. +- Local asset materialization for adapter-provided asset files. +- CLI summaries that surface success, failure, and warning counts. +- Fast tests that use fake adapter outputs and do not require real MinerU, model files, GPU, sample PDFs, network, Obsidian, or LaTeX tooling. + +Sprint 7 is an orchestration sprint. It may call the real `MinerUAdapter` in the normal production path, but the default test suite must use injected fake adapters and must not execute MinerU. + +## Current Precondition + +Sprint 6 is complete: + +- `src/pdf2md/paths.py` owns input discovery and output path planning. +- `src/pdf2md/ir.py` owns project records, block types, warning codes, and warning severities. +- `src/pdf2md/metadata.py` builds JSON-serializable metadata and summary counts from project-owned records. +- `src/pdf2md/mineru_adapter.py` owns the mocked direct local MinerU CLI adapter boundary. +- `src/pdf2md/markdown.py` owns Obsidian Markdown normalization, asset link warnings, and table fallback warnings. +- `src/pdf2md/quality.py` owns local quality checks over normalized Markdown and asset context. +- `src/pdf2md/report.py` owns report content rendering and final status calculation. +- `uv run pytest` passed 103 tests. + +Sprint 7 may compute source SHA-256, create conversion output directories, copy local adapter-provided asset files, write final Markdown, write metadata JSON when requested, and write `.report.md`. It must keep public return types project-owned and must not require raw MinerU-specific Python objects from callers. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/conversion.py` +- `src/pdf2md/cli.py` +- `src/pdf2md/__init__.py` +- `src/pdf2md/paths.py` only for narrowly required path/output helper compatibility +- `src/pdf2md/mineru_adapter.py` only for narrowly required adapter protocol or result compatibility +- `src/pdf2md/metadata.py` only for narrowly required output-location or summary compatibility +- `src/pdf2md/markdown.py` only for narrowly required orchestration compatibility +- `src/pdf2md/quality.py` only for narrowly required orchestration compatibility +- `src/pdf2md/report.py` only for narrowly required orchestration compatibility +- `tests/test_conversion.py` +- `tests/test_cli.py` +- Existing focused unit tests only if a touched module requires compatibility updates +- `README.md` only if a short usage note is needed for `pdf2md convert` +- `PLAN.md` +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` +- `docs/Sprints/SPRINT7CONTRACT.md` + +Not allowed: + +- `src/pdf2md/doctor.py` +- Working `pdf2md doctor` behavior +- `scripts/` +- MinerU/model installation or download scripts +- Real MinerU invocation in default tests +- Real GPU/CUDA checks +- Real PDF content parsing outside adapter output handling +- Runtime engine selection or alternate engine support +- Cloud OCR, remote LLM/VLM, hosted renderer, remote document parser, remote asset fetching, HTTP client backend, router mode, `--api-url`, remote APIs, or remote OpenAI-compatible backend support +- A CLI flag that disables strict-local policy +- Committed files under `samples/` + +## Expected Outputs + +Sprint 7 should produce: + +1. Public conversion records and API + - A project-owned conversion result type containing at least: + - source PDF path + - Markdown output path + - metadata JSON path when written + - report path + - assets directory + - raw output directory when kept + - engine name and version + - final status + - warning count + - warnings + - `convert_pdf(input_path, output_dir, metadata=True, keep_raw=False, overwrite=False, gpu=None, strict_local=True, adapter=None, clock=None)` or an equivalently small API. + - The default API path uses the direct local MinerU adapter. + - Tests can inject a fake adapter and deterministic clock. + - The public return type must not expose raw MinerU-specific Python objects as required fields. + +2. Single-PDF orchestration + - Discover and plan the single PDF using existing path helpers. + - Create required output directories only after preflight path checks pass. + - Run the adapter into a planned temporary or raw work directory. + - Stop the individual conversion on adapter hard failure and return explicit warnings/status. + - Normalize adapter Markdown into Obsidian-friendly Markdown. + - Copy local adapter-provided asset files into the planned assets directory when needed. + - Compute source SHA-256 with local file reads. + - Build metadata from project-owned records. + - Run local quality checks over normalized Markdown and asset context. + - Render report Markdown from metadata and quality results. + - Write final Markdown, optional metadata JSON, and report Markdown. + +3. Output writing and overwrite behavior + - Never write outside the planned output root. + - Respect existing-output conflicts unless `overwrite=True`. + - Keep writes deterministic and UTF-8 encoded for text outputs. + - Preserve `--metadata` behavior: metadata JSON is written when enabled and omitted when disabled. + - Always write `.report.md`. + - `--keep-raw` preserves raw MinerU output in the planned raw directory. + - Without `--keep-raw`, temporary raw work must be cleaned up when the conversion completes, while preserving enough failure context in metadata/report/warnings. + +4. Asset materialization + - Copy only local files returned by the adapter. + - Do not fetch remote assets. + - Do not follow asset paths that escape the adapter work directory or the planned output root. + - Handle missing or invalid adapter asset paths with project-owned warnings. + - Normalize final Markdown asset links to stable relative paths. + +5. CLI convert command + - `pdf2md convert INPUT --out OUTPUT_DIR`. + - Options: + - `--metadata` + - `--keep-raw` + - `--recursive` + - `--overwrite` + - `--gpu GPU_DEVICE` + - `--strict-local` + - Strict-local must remain enabled in v1; the CLI must not add a supported way to disable it. + - Single PDF conversion returns exit code `0` on success or partial success, and non-zero when a hard error prevents conversion. + - Directory conversion handles multiple PDFs deterministically and prints a summary. + - Batch conversion should continue to the next file when one PDF fails after planning, then return a non-zero exit code if any PDF failed. + - CLI output must include converted count, failed count, and warning count. + +6. Batch conversion + - Directory inputs use existing non-recursive discovery by default. + - Recursive discovery occurs only with `--recursive`. + - Output paths preserve relative subdirectories from the input root. + - Duplicate planned outputs and overwrite conflicts fail before conversion starts. + - Results are deterministic and ordered by discovery/path planning order. + +7. Failure and warning behavior + - MinerU failure must be clear and must not trigger fallback to any other engine. + - Strict-local violations must be hard failures. + - Per-file failures must include project-owned warnings. + - CLI summaries must not suppress warning counts. + - Metadata/report content must reflect warnings emitted during adapter, normalization, asset, and quality steps. + +8. Tests + - API test for one successful conversion with a fake adapter. + - API test for adapter failure with no fallback. + - API test for output conflict and overwrite behavior. + - API test for metadata disabled behavior. + - API test for local asset copying and relative Markdown links. + - API test for `keep_raw` behavior. + - CLI test for single PDF conversion with a fake adapter. + - CLI test for directory conversion with deterministic summary output. + - CLI test for recursive behavior. + - CLI test for failure summary and non-zero exit code. + - Tests proving default checks do not require real MinerU, GPU, models, network, `samples/`, Obsidian, or LaTeX tooling. + +9. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not implement `pdf2md doctor`. +- Do not implement environment diagnostics. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not probe real MinerU output with local sample PDFs. +- Do not add setup scripts. +- Do not implement runtime engine selection. +- Do not add alternate engines. +- Do not add cloud, remote API, router, HTTP client backend, remote OpenAI-compatible backend, hosted renderer, or remote asset-fetching support. +- Do not add a CLI flag or API option that disables strict-local policy. +- Do not require real MinerU, CUDA, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/` in default tests. +- Do not implement real math rendering; use the Sprint 6 local checker boundary. +- Do not commit generated conversion outputs or sample PDFs. + +## Work Packages + +### WP7.1: Public API And Result Records + +Owner: + +- `feature-generator-agent` +- `requirements-guard-agent` + +Actions: + +- Add `conversion.py`. +- Define project-owned conversion result records. +- Expose `convert_pdf` from the library surface. +- Support fake adapter and deterministic clock injection for tests. + +Output: + +- Callers can run one conversion without depending on raw MinerU objects. + +### WP7.2: Single-PDF Orchestration And Output Writing + +Owner: + +- `feature-generator-agent` +- `mineru-integration-agent` +- `metadata-agent` +- `obsidian-markdown-agent` + +Actions: + +- Connect path planning, adapter execution, Markdown normalization, metadata building, quality checks, and report rendering. +- Write final Markdown, optional metadata JSON, and report Markdown. +- Compute source SHA-256. +- Preserve strict-local behavior and no-fallback behavior. + +Output: + +- One PDF can be converted through mocked adapter outputs in tests and through the real adapter in normal use. + +### WP7.3: Asset And Raw Output Handling + +Owner: + +- `feature-generator-agent` +- `obsidian-markdown-agent` +- `metadata-agent` + +Actions: + +- Copy local adapter-provided assets into the planned assets directory. +- Normalize Markdown links relative to the final Markdown file. +- Preserve raw output only when requested. +- Clean temporary work when raw output is not requested. + +Output: + +- Markdown, assets, metadata, and report paths are stable and local-only. + +### WP7.4: CLI Convert And Batch Summary + +Owner: + +- `feature-generator-agent` +- `requirements-guard-agent` + +Actions: + +- Replace the placeholder CLI with `convert` while keeping `--version`. +- Add only the agreed v1 options. +- Print deterministic summaries with converted, failed, and warning counts. +- Return non-zero exit code when hard failures occur. + +Output: + +- Users can run `pdf2md convert` for one PDF or a directory. + +### WP7.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review completed orchestration behavior against this contract. +- Verify no default test executes real MinerU, uses GPU, downloads models, uses network, or requires `samples/`. +- Verify no runtime remote/API path or alternate engine is introduced. +- Verify `samples/` remains untracked and unstaged. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short --untracked-files=all` before staging confirms `samples/` remains untracked and unstaged. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest tests/test_conversion.py tests/test_cli.py` passes. +- `uv run pytest` passes. +- `git diff --check` passes. +- Default tests do not require real MinerU, CUDA, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- No model downloads occur. +- No network calls are required. +- No candidate engine comparison is reintroduced. +- No alternate engine or runtime engine selection is added. +- No CLI/API option disables strict-local policy. +- No `--api-url`, router mode, HTTP client backend, remote API, or remote OpenAI-compatible backend support is added. +- Adapter failures produce explicit failed results and no fallback conversion. +- Output files are written only after path preflight succeeds. +- Existing outputs are protected unless overwrite is enabled. +- CLI summaries include warning counts. +- Metadata/report paths and counts match the files written. + +Recommended: + +- Keep conversion orchestration small and dependency-injected. +- Prefer local temporary directories from the standard library for raw work when `keep_raw` is disabled. +- Keep batch conversion a thin loop over single-file conversion. +- Keep CLI formatting simple and stable enough for tests. +- Use fake adapter records in tests rather than monkeypatching subprocess behavior at the CLI layer. + +## Hard Failure Criteria + +Sprint 7 fails and must stop for a user decision if any of these are true: + +- Public API requires or exposes raw MinerU-specific Python objects as required return fields. +- The implementation silently falls back to another engine after MinerU failure. +- A CLI/API option disables strict-local policy. +- The implementation adds or permits `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- Default tests execute real MinerU, require GPU/CUDA, download models, use network, require Obsidian/LaTeX tooling, or require `samples/`. +- Output writing can escape the planned output root. +- Existing files are overwritten without explicit overwrite intent. +- CLI writes final outputs after a preflight hard failure. +- CLI summaries suppress warning counts or failed counts. +- Metadata/report content omits warnings emitted during adapter, normalization, asset, or quality steps. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 7 is complete when: + +- `src/pdf2md/conversion.py` exists and owns conversion orchestration. +- `convert_pdf` is available from the public Python package. +- `pdf2md convert INPUT --out OUTPUT_DIR` exists. +- Single-PDF conversion is tested with a fake adapter. +- Directory and recursive conversion behavior is tested with fake adapters. +- Output conflict and overwrite behavior is tested. +- Adapter failure produces a clear failed result and no fallback. +- Final Markdown, metadata JSON when enabled, and report Markdown are written by product behavior. +- Local adapter-provided assets are copied or warned about deterministically. +- `--keep-raw` behavior is tested. +- CLI summaries include converted, failed, and warning counts. +- Default tests do not require real MinerU, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- `uv sync` passes. +- Targeted conversion/CLI tests pass. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 7 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 8: +- Next action: diff --git a/docs/Sprints/SPRINT8CONTRACT.md b/docs/Sprints/SPRINT8CONTRACT.md new file mode 100644 index 0000000..5950fd3 --- /dev/null +++ b/docs/Sprints/SPRINT8CONTRACT.md @@ -0,0 +1,339 @@ +# Sprint 8 Contract: Doctor And Setup Documentation + +Status: Implemented +Last updated: 2026-05-08 + +## Objective + +Make local setup failures explicit before users run conversions by adding a mockable `pdf2md doctor` diagnostic path and setup documentation for Windows PowerShell, Python 3.12, `uv`, MinerU 3.1.0, local model/cache expectations, NVIDIA GPU/CUDA visibility, and strict-local runtime behavior. + +Sprint 8 must establish: + +- A project-owned doctor module for local environment diagnostics. +- A `pdf2md doctor` CLI command with deterministic exit codes. +- Clear reporting for Python, `uv`, MinerU CLI, MinerU version, GPU/CUDA/PyTorch visibility, and model/cache path detection. +- Documentation that explains setup steps and local-only runtime constraints without introducing cloud/API fallback paths. +- Fast tests that mock environment checks and do not require real MinerU, CUDA, GPU, model files, network, `samples/`, Obsidian, LaTeX tooling, or package/model downloads. + +Sprint 8 is a diagnostics and setup-documentation sprint. It must not change conversion output behavior, run real conversions, probe local sample PDFs, or make runtime conversion depend on network access. + +## Current Precondition + +Sprint 7 is complete: + +- `src/pdf2md/paths.py` owns input discovery and output path planning. +- `src/pdf2md/ir.py` owns project records, block types, warning codes, and warning severities. +- `src/pdf2md/metadata.py` builds JSON-serializable metadata and summary counts from project-owned records. +- `src/pdf2md/mineru_adapter.py` owns the mocked direct local MinerU CLI adapter boundary. +- `src/pdf2md/markdown.py` owns Obsidian Markdown normalization. +- `src/pdf2md/quality.py` owns local quality checks, including math checker unavailable behavior. +- `src/pdf2md/report.py` owns report content rendering and final status calculation. +- `src/pdf2md/conversion.py` owns conversion orchestration and output writing. +- `src/pdf2md/cli.py` owns `pdf2md convert`. +- `uv run pytest` passed 119 tests. + +Sprint 8 may add doctor diagnostics and setup documentation. It must not require a real successful local MinerU/GPU/model setup in the default test loop. + +## Touched Surfaces + +Allowed: + +- `src/pdf2md/doctor.py` +- `src/pdf2md/cli.py` +- `src/pdf2md/mineru_adapter.py` only for narrowly required availability/version helper compatibility +- `README.md` +- `scripts/install-mineru.ps1` only if implemented as an explicit user-invoked setup helper +- `scripts/install-models.py` only if implemented as an explicit user-invoked setup helper +- `tests/test_doctor.py` +- `tests/test_cli.py` +- `tests/test_mineru_adapter.py` only if adapter helper compatibility changes +- `PLAN.md` +- `PROGRESS.md` +- `docs/V1IMPLEMENTATIONPLAN.md` +- `docs/Sprints/SPRINT8CONTRACT.md` + +Not allowed: + +- Changes to `src/pdf2md/conversion.py` unless a doctor/CLI regression forces a narrow compatibility fix +- Changes to Markdown normalization, metadata schema, report rendering, or path planning unrelated to doctor behavior +- Real PDF conversion in default tests +- Real MinerU execution in default tests +- Real CUDA/GPU dependency in default tests +- Model downloads in default tests +- Any setup download triggered by `pdf2md doctor`, `pdf2md convert`, import time, or tests +- Runtime engine selection or alternate engine support +- Cloud OCR, remote LLM/VLM, hosted renderer, remote document parser, remote asset fetching, HTTP client backend, router mode, `--api-url`, remote APIs, or remote OpenAI-compatible backend support +- A CLI/API option that disables strict-local policy +- Committed files under `samples/` +- Generated conversion outputs committed to git + +## Expected Outputs + +Sprint 8 should produce: + +1. Doctor result records and API + - A small project-owned doctor result type containing at least: + - check name + - status: `pass`, `warn`, or `fail` + - human-readable message + - optional details + - A doctor report type containing ordered checks and an overall status. + - Mockable checker dependencies for subprocess calls, executable discovery, Python version, imports, environment variables, and filesystem paths. + - No public or required field should expose raw subprocess or third-party objects. + +2. Required checks + - Python version check: + - Pass on Python 3.12. + - Fail outside the supported project range. + - `uv` check: + - Detect executable availability. + - Report version text when available. + - Fail clearly when missing. + - MinerU check: + - Detect direct local `mineru` CLI availability through the existing adapter boundary where possible. + - Report MinerU version when available. + - Fail clearly when the CLI is missing. + - Warn or fail clearly when version detection fails or the detected version is not MinerU 3.1.0. + - GPU/CUDA/PyTorch visibility: + - Report whether an NVIDIA GPU is visible when detectable. + - Report CUDA/PyTorch visibility without requiring PyTorch in default tests. + - Warn clearly when GPU/CUDA/PyTorch acceleration is unavailable. + - Warn clearly when the detected GPU is GTX 1070 Ti or another Pascal/pre-Turing class GPU with likely CUDA/PyTorch compatibility risk. + - Model/cache paths: + - Report detectable local model/cache paths from documented environment variables or known local config locations. + - Warn when model/cache paths cannot be detected. + - Do not download, install, or validate large model files in default tests. + - Local-only policy: + - Report that runtime conversion allows only direct local `mineru` CLI execution and CLI-internal temporary local `mineru-api`. + - Report that `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends are prohibited. + +3. CLI command + - `pdf2md doctor` exists. + - `pdf2md --version` remains unchanged. + - `pdf2md convert` behavior remains covered and unchanged except for parser integration. + - Exit code policy: + - `0` when all checks pass or only warnings exist. + - Non-zero when any required check fails. + - CLI output must be concise, deterministic, and testable. + - CLI output must distinguish warnings from failures. + +4. Setup documentation + - README or setup section explains: + - Windows PowerShell workflow. + - Python 3.12 requirement. + - `uv` usage and PATH note for `C:\Users\user\.local\bin`. + - MinerU 3.1.0 local CLI expectation. + - Model/cache setup expectations and where doctor looks. + - NVIDIA GPU expectations and GTX 1070 Ti 8GB risk. + - Strict-local runtime policy. + - Difference between explicit setup downloads and runtime conversion, which must stay local-only. + - If setup helper scripts are added, they must be explicit user-invoked helpers, not imported by package code, not called by `doctor`, and not used by default tests. + +5. Tests + - Unit tests for doctor success with mocked checks. + - Unit tests for missing Python version support. + - Unit tests for missing `uv`. + - Unit tests for missing MinerU. + - Unit tests for MinerU version detection failure or non-3.1.0 version. + - Unit tests for missing GPU/CUDA/PyTorch warning behavior. + - Unit tests for GTX 1070 Ti/Pascal risk warning behavior. + - Unit tests for missing model/cache warning behavior. + - CLI tests for `pdf2md doctor` success, warning-only success, and hard failure exit code. + - Regression tests proving `pdf2md convert` and `--version` still work. + - Tests proving default checks do not require real MinerU, GPU, CUDA, PyTorch, model files, network, `samples/`, Obsidian, or LaTeX tooling. + +6. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, known failures, residual risks, and next action. + +## Non-Goals + +- Do not run real MinerU in default tests. +- Do not install MinerU 3.1.0. +- Do not download MinerU models. +- Do not run real model setup during doctor or tests. +- Do not parse, convert, or inspect sample PDFs. +- Do not implement local fixture evaluation. +- Do not change conversion orchestration behavior except for unavoidable CLI parser integration. +- Do not add runtime engine selection. +- Do not add alternate engines. +- Do not add cloud, remote API, router, HTTP client backend, remote OpenAI-compatible backend, hosted renderer, or remote asset-fetching support. +- Do not add a CLI/API option that disables strict-local policy. +- Do not require real CUDA, GPU, PyTorch, MinerU, model files, network, Obsidian, LaTeX tooling, or `samples/` in default tests. +- Do not claim that GTX 1070 Ti CUDA/PyTorch acceleration is guaranteed until local validation proves it. +- Do not claim perfect setup automation. + +## Work Packages + +### WP8.1: Doctor Records And Mockable Check Boundaries + +Owner: + +- `local-setup-agent` +- `feature-generator-agent` + +Actions: + +- Add `doctor.py`. +- Define doctor check/result records. +- Keep all external probes injectable or mockable. +- Implement deterministic aggregation and exit status policy. + +Output: + +- The CLI can report setup health without depending on real local tools in tests. + +### WP8.2: Environment And MinerU Diagnostics + +Owner: + +- `local-setup-agent` +- `mineru-integration-agent` +- `feature-generator-agent` + +Actions: + +- Add Python, `uv`, MinerU availability, MinerU version, GPU/CUDA/PyTorch, and model/cache checks. +- Reuse the direct local MinerU adapter boundary where it fits. +- Keep MinerU 3.1.0 as the only accepted engine target. + +Output: + +- Users get clear local setup failures before conversion. + +### WP8.3: CLI Doctor Command + +Owner: + +- `feature-generator-agent` +- `requirements-guard-agent` + +Actions: + +- Add `pdf2md doctor` without breaking `pdf2md convert` or `--version`. +- Print deterministic pass/warn/fail lines and an overall status. +- Return non-zero when required checks fail. + +Output: + +- The command-line workflow can diagnose setup state. + +### WP8.4: Setup Documentation And Optional Explicit Helpers + +Owner: + +- `local-setup-agent` +- `license-privacy-agent` +- `requirements-guard-agent` + +Actions: + +- Update README setup docs for Windows PowerShell, Python 3.12, `uv`, MinerU 3.1.0, model/cache, GPU, and local-only runtime policy. +- Verify volatile install/setup claims against official docs before editing. +- If adding scripts, keep them explicit, local setup-only, and never called by doctor, convert, import time, or default tests. + +Output: + +- Setup instructions are clear without weakening strict-local runtime policy. + +### WP8.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review completed doctor behavior and docs against this contract. +- Verify no default test executes real MinerU, uses GPU/CUDA, downloads models, uses network, or requires `samples/`. +- Verify no runtime remote/API path or alternate engine is introduced. +- Verify `samples/` remains untracked and unstaged. + +Output: + +- PASS/FAIL notes with any missing acceptance criteria. + +## Verification Checks + +Required: + +- `git status --short --untracked-files=all` before staging confirms `samples/` remains untracked and unstaged. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest tests/test_doctor.py tests/test_cli.py` passes. +- `uv run pytest` passes. +- `git diff --check` passes. +- Default tests do not require real MinerU, CUDA, GPU, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- No model downloads occur. +- No setup downloads occur from doctor, convert, imports, or tests. +- No network calls are required in default tests. +- No candidate engine comparison is reintroduced. +- No alternate engine or runtime engine selection is added. +- No CLI/API option disables strict-local policy. +- No `--api-url`, router mode, HTTP client backend, remote API, or remote OpenAI-compatible backend support is added. +- Doctor fails clearly when required dependencies are missing. +- Doctor does not report the environment as healthy when MinerU is missing. +- Doctor warnings are clear for GPU/CUDA/PyTorch/model-cache risk. +- `pdf2md convert` tests still pass. +- `pdf2md --version` still works. + +Recommended: + +- Keep doctor checks small, ordered, and deterministic. +- Keep human-readable output stable enough for unit tests. +- Use dependency injection rather than monkeypatching global process state where possible. +- Treat real local GPU/MinerU/model probes as optional manual verification outside the default test suite. +- Use `requirements-guard-agent` if setup wording risks weakening strict-local policy. +- Use `research-agent` or `local-setup-agent` with live web verification before changing volatile installation commands or model-cache documentation. + +## Hard Failure Criteria + +Sprint 8 fails and must stop for a user decision if any of these are true: + +- Doctor reports a healthy environment when MinerU is missing. +- Doctor says cloud/API fallback is supported. +- Doctor, import time, or default tests install packages, download models, call network services, or run model setup. +- Default tests require real MinerU, CUDA, GPU, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- The implementation adds or permits `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- The implementation adds runtime engine selection or alternate engines. +- `pdf2md doctor` breaks `pdf2md convert` or `pdf2md --version`. +- The README or scripts imply runtime conversion can upload PDFs, page images, extracted text, or intermediates to remote services. +- Setup helper scripts are invoked automatically by doctor, convert, import time, or tests. +- `samples/` is staged or committed. + +## Acceptance Criteria + +Sprint 8 is complete when: + +- `src/pdf2md/doctor.py` exists and owns local setup diagnostics. +- `pdf2md doctor` exists. +- Doctor returns a project-owned report with ordered checks and overall status. +- Python 3.12, `uv`, MinerU availability/version, GPU/CUDA/PyTorch visibility, and model/cache path checks are implemented with mocked tests. +- Missing `uv` is tested. +- Missing MinerU is tested and produces a failure. +- Missing GPU/CUDA/PyTorch is tested and produces clear warning behavior. +- GTX 1070 Ti/Pascal risk warning behavior is tested. +- Missing model/cache path warning behavior is tested. +- `pdf2md doctor` exit code behavior is tested for success, warning-only success, and failure. +- `pdf2md convert` and `pdf2md --version` regression tests still pass. +- Setup docs explain local-only runtime behavior and do not imply cloud/API fallback. +- Default tests do not require real MinerU, GPU, CUDA, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- `uv sync` passes. +- Targeted doctor/CLI tests pass. +- `uv run pytest` passes. +- `PROGRESS.md` records checks performed and residual risks. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 8 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Known failures: +- Residual risks: +- User decisions needed: +- Go/no-go recommendation for Sprint 9: +- Next action: diff --git a/docs/Sprints/SPRINT9CONTRACT.md b/docs/Sprints/SPRINT9CONTRACT.md new file mode 100644 index 0000000..fb31b47 --- /dev/null +++ b/docs/Sprints/SPRINT9CONTRACT.md @@ -0,0 +1,320 @@ +# Sprint 9 Contract: Local Fixture Evaluation And V1 Release Gate + +Status: Implemented +Last updated: 2026-05-08 + +## Objective + +Validate the v1 converter against local fixture workflows without committing sample PDFs or making the default test loop depend on MinerU models, GPU, CUDA, network access, Obsidian, or LaTeX tooling. + +Sprint 9 must establish: + +- A fast mocked integration suite that exercises the public conversion path end to end. +- An optional, explicitly enabled local MinerU fixture evaluation path for `samples/`. +- A fixture coverage manifest or checklist that records which local PDFs cover math, tables, figures/assets, reading order, Korean filenames, and metadata/report risks. +- Release-gate documentation that distinguishes default automated checks from optional local MinerU/GPU checks. +- Clear `PROGRESS.md` notes for local fixture coverage, skipped/blocked optional checks, known quality risks, and the v1 go/no-go recommendation. + +Sprint 9 is an evaluation and release-gate sprint. It may add tests, local-only evaluation helpers, fixture manifests, and narrow compatibility fixes only when needed to evaluate the current v1 behavior. It must not add alternate engines, cloud/API paths, runtime engine selection, or automatic model downloads. + +## Current Precondition + +Sprint 8 is complete: + +- `pdf2md doctor` exists and reports Python, `uv`, MinerU CLI/version, GPU, PyTorch, model/cache, and strict-local policy status. +- Local `pdf2md doctor` currently fails because the `mineru` CLI is not installed on PATH. +- `pdf2md convert` exists and writes Markdown, metadata JSON, and `.report.md` with fake-adapter test coverage. +- Default tests pass without real MinerU, CUDA, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- `samples/` exists locally and is untracked. Observed local fixture files include: + - `samples/FourNodeQuadrilateralShellElementMITC4.pdf` + - `samples/MITC공부.pdf` + - `samples/2007쉘구조물의유한요소해석에대하여.pdf` + - `samples/유한요소해석법을이용한쉘구조물의동적좌굴해석.pdf` + - `samples/metadata.json` + +Sprint 9 must preserve the untracked status of `samples/` unless the user explicitly requests otherwise. + +## Touched Surfaces + +Allowed: + +- `tests/integration/` +- `tests/test_conversion.py` +- `tests/test_cli.py` +- `tests/test_report.py` +- `tests/test_metadata.py` +- `tests/test_quality.py` +- `tests/conftest.py` only for markers or opt-in fixture controls +- `src/pdf2md/mineru_adapter.py` only for narrow compatibility fixes backed by mocked or optional local MinerU output evidence +- `src/pdf2md/conversion.py` only for narrow release-gate defects found by integration tests +- `src/pdf2md/quality.py` only for local quality metric defects found by integration tests +- `src/pdf2md/report.py` only for report defects found by integration tests +- `README.md` +- `docs/V1RELEASECHECKLIST.md` +- `docs/V1IMPLEMENTATIONPLAN.md` +- `docs/Sprints/SPRINT9CONTRACT.md` +- `PLAN.md` +- `PROGRESS.md` + +Not allowed: + +- Committed files under `samples/` +- Committed generated conversion outputs from local sample PDFs +- Mandatory tests that require real MinerU, GPU, CUDA, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/` +- Automatic package installs or model downloads from tests, import time, doctor, convert, or helpers +- Runtime engine selection or alternate conversion engines +- Cloud OCR, remote LLM/VLM, hosted renderer, remote document parser, remote asset fetching, `--api-url`, router mode, HTTP client backends, remote APIs, or remote OpenAI-compatible backends +- CLI/API options that disable strict-local policy +- Claims that v1 perfectly reconstructs LaTeX, tables, or reading order + +## Expected Outputs + +1. Fast mocked integration suite + - Exercises `convert_pdf` and/or `pdf2md convert` with a fake MinerU adapter through the real orchestration path. + - Verifies Markdown, metadata JSON, and `.report.md` are all written. + - Verifies output paths, asset links, warning counts, and report status stay consistent. + - Verifies failures produce metadata/report warnings when possible and do not silently fallback. + - Runs as part of `uv run pytest` without real MinerU, models, GPU, network, Obsidian, LaTeX tooling, or `samples/`. + +2. Optional local MinerU fixture evaluation + - Provides an explicit opt-in command or pytest marker/environment gate for real local MinerU sample evaluation. + - Skips or reports a clear local blocker when `pdf2md doctor` fails because MinerU, model/cache paths, or GPU/PyTorch acceleration are unavailable. + - Reads sample PDFs only from `samples/` or a user-provided local sample directory. + - Writes generated outputs to a temporary or ignored output directory, never to tracked fixture paths. + - Produces or records, for each attempted sample: + - source filename + - command run + - exit code + - generated Markdown path + - generated metadata JSON path + - generated `.report.md` path + - warning count + - math renderability or checker-unavailable count + - table fallback/degradation count when available + - missing or broken asset link count + - page coverage when available + - Does not mark optional evaluation as passed when MinerU is missing; it records the blocker. + +3. Fixture coverage manifest or checklist + - Maps local sample files to risk categories: + - simple digital PDF + - math-heavy PDF + - multi-column or complex reading order + - table with formulas + - figure/caption/assets + - Korean filename/path handling + - May store only relative sample names, categories, and notes; it must not embed sample PDFs or generated outputs. + - Records coverage gaps that need additional user-provided samples. + +4. V1 release checklist + - Defines default release gates: + - `uv sync` + - `uv run pytest` + - `uv run pdf2md --version` + - `uv run pdf2md doctor` + - `git diff --check` + - `git status --short --untracked-files=all` + - Defines optional local MinerU release gates separately from default gates. + - Requires Markdown, metadata JSON, and `.report.md` to exist before any sample conversion is considered successful. + - Requires warnings and residual risks to be recorded in `PROGRESS.md`. + - Makes local-only and no-sample-commit checks explicit. + +5. Documentation + - README or release checklist explains how to run default checks and optional local fixture checks. + - Documentation states that optional fixture checks may be skipped or blocked until MinerU 3.1.0 and model/cache setup are available. + - Documentation does not instruct users to use `--api-url`, router mode, HTTP client backends, remote APIs, or remote OpenAI-compatible backends. + +6. Handoff + - `PROGRESS.md` records changed files, commands run, tests passed or blocked, local fixture status, generated output location if any, known failures, residual risks, and next action. + +## Non-Goals + +- Do not install MinerU. +- Do not download MinerU models. +- Do not run model setup automatically. +- Do not require the local GTX 1070 Ti to pass CUDA/PyTorch checks in the default test loop. +- Do not improve OCR/model accuracy. +- Do not introduce a manual review UI or web UI. +- Do not add alternate conversion engines or fallback engines. +- Do not benchmark against cloud OCR/API services. +- Do not commit sample PDFs, sample-derived outputs, or large binary fixtures. +- Do not make text edit distance the only quality criterion. +- Do not claim v1 is release-ready if metadata JSON or `.report.md` generation is missing. + +## Work Packages + +### WP9.1: Fast Mocked Integration Checks + +Owner: + +- `feature-generator-agent` +- `evaluation-agent` + +Actions: + +- Add integration-level tests that use fake adapter output but run the public conversion orchestration and CLI paths. +- Assert generated Markdown, metadata JSON, `.report.md`, assets, warnings, and summaries are mutually consistent. +- Keep tests deterministic and independent of real samples. + +Output: + +- `uv run pytest` covers v1 file-output behavior without model or GPU dependencies. + +### WP9.2: Optional MinerU Sample Evaluation Harness + +Owner: + +- `mineru-integration-agent` +- `local-setup-agent` +- `evaluation-agent` + +Actions: + +- Add an explicit opt-in local fixture command/test path. +- Gate real MinerU execution behind an environment variable, marker, or explicit command documented in README/checklist. +- Run `pdf2md doctor` or equivalent preflight before optional local MinerU evaluation. +- Use temporary or ignored output directories. +- Record blocked status clearly when MinerU/model/cache setup is missing. + +Output: + +- Local users can run real sample evaluation when setup is ready, while default tests stay fast and local. + +### WP9.3: Fixture Coverage And Metrics + +Owner: + +- `evaluation-agent` +- `obsidian-markdown-agent` +- `metadata-agent` + +Actions: + +- Define fixture categories and expected risk coverage. +- Track math delimiter/renderability, tables, reading order, assets, page coverage, metadata fields, warning counts, and report usefulness. +- Avoid scoring quality only by plain-text edit distance. + +Output: + +- Fixture coverage is explicit and gaps are visible. + +### WP9.4: V1 Release Gate Documentation + +Owner: + +- `requirements-guard-agent` +- `evaluation-agent` + +Actions: + +- Add or update release checklist documentation. +- Separate default release gates from optional local MinerU/GPU gates. +- Keep strict-local wording consistent with `ARCHITECTURE.md`, `PRD.md`, and `README.md`. +- Update `PLAN.md` and `PROGRESS.md` with the next action and release readiness state. + +Output: + +- A future agent can determine whether v1 is blocked, partial, or ready without relying on conversation history. + +### WP9.5: Independent Evaluation + +Owner: + +- `evaluation-agent` + +Actions: + +- Review completed Sprint 9 work against this contract. +- Verify default tests do not require real MinerU, GPU, CUDA, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- Verify optional local MinerU evaluation is clearly gated. +- Verify generated sample outputs and sample PDFs are not staged. +- Verify release checklist cannot pass without Markdown, metadata JSON, and `.report.md`. + +Output: + +- PASS/FAIL notes with actionable findings and residual risk. + +## Verification Checks + +Required: + +- `git status --short --untracked-files=all` before staging confirms `samples/` remains untracked and unstaged. +- `uv --version` is run and result is recorded. +- `uv sync` passes. +- `uv run pytest` passes. +- Targeted integration tests pass. +- `uv run pdf2md --version` passes. +- `uv run pdf2md doctor` is run and its result is recorded as pass, warn, or blocked/fail. +- `git diff --check` passes. +- Default tests do not require real MinerU, CUDA, GPU, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- No model downloads occur. +- No setup downloads occur from tests, import time, doctor, convert, or helper scripts. +- No network calls are required in default tests. +- No candidate engine comparison is reintroduced. +- No alternate engine or runtime engine selection is added. +- No CLI/API option disables strict-local policy. +- No `--api-url`, router mode, HTTP client backend, remote API, or remote OpenAI-compatible backend support is added. +- Optional local MinerU checks are skipped or blocked clearly when setup is unavailable. +- Sample PDFs and generated sample outputs are not staged or committed. +- `PROGRESS.md` records local fixture coverage status and release readiness. + +Recommended: + +- Add a pytest marker or environment variable for optional local MinerU tests. +- Keep optional output under a temporary directory or an ignored local output root. +- Include at least one Korean filename/path check in fast mocked tests. +- Include one fake output with math, one with a table warning, and one with an asset link. +- Record source-to-output paths in release checklist examples. +- Treat local doctor failure as a release blocker for real MinerU validation but not for the default fast test loop. + +## Hard Failure Criteria + +Sprint 9 fails and must stop for a user decision if any of these are true: + +- Default tests require real MinerU, GPU, CUDA, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- Sample PDFs or generated sample outputs are staged or committed. +- Optional real MinerU evaluation runs without an explicit opt-in gate. +- Optional real MinerU evaluation writes generated output into tracked fixture paths. +- V1 release checklist can pass without generated Markdown, metadata JSON, and `.report.md`. +- Release status is marked ready when `pdf2md doctor` has a hard failure and no explicit user waiver is recorded. +- The implementation adds runtime engine selection or alternate engines. +- The implementation adds or permits `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- The implementation uses cloud/API fallback for any fixture evaluation. +- The implementation hides MinerU failure or silently falls back to another engine. +- Quality criteria ignore math, tables, reading order, assets, metadata, or report quality. + +## Acceptance Criteria + +Sprint 9 is complete when: + +- `docs/Sprints/SPRINT9CONTRACT.md` exists and is referenced by relevant agents. +- Fast mocked integration tests exist and pass under `uv run pytest`. +- Optional local MinerU fixture evaluation is documented and explicitly gated. +- Local fixture coverage categories and gaps are recorded. +- Release checklist documentation exists or is updated. +- `PROGRESS.md` records optional local MinerU status, including skipped/blocked reasons when applicable. +- Default tests do not require real MinerU, GPU, CUDA, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. +- No sample PDF or generated sample output is staged or committed. +- `uv sync` passes. +- `uv run pytest` passes. +- `git diff --check` passes. +- Independent evaluation is complete. +- The completed change is committed. + +## Handoff Fields + +Use these fields when Sprint 9 completes: + +- Files changed: +- Commands run: +- Tests passed: +- Tests blocked: +- Optional local MinerU status: +- Fixture coverage: +- Generated output locations: +- Known failures: +- Residual risks: +- User decisions needed: +- V1 release recommendation: +- Go/no-go recommendation for next sprint: +- Next action: diff --git a/docs/V1IMPLEMENTATIONPLAN.md b/docs/V1IMPLEMENTATIONPLAN.md new file mode 100644 index 0000000..c51e8d5 --- /dev/null +++ b/docs/V1IMPLEMENTATIONPLAN.md @@ -0,0 +1,661 @@ +# V1 Implementation Plan: Local PDF-to-Markdown Converter + +Last updated: 2026-05-08 + +This document is the implementation plan for v1. It does not replace `PRD.md` or `ARCHITECTURE.md`; use those files as the source of product requirements and system design. This plan explains the order of work, sprint contracts, verification gates, and agent ownership for implementing the converter. + +Sprint 1 created the Python package scaffold and CLI placeholder. Sprint 2 created path planning. Sprint 3 created project-owned records and metadata construction. Sprint 4 created the mocked direct local MinerU adapter boundary. Sprint 5 created the Obsidian Markdown normalization boundary. Sprint 6 created local quality-check and report-rendering boundaries. Sprint 7 implemented conversion orchestration, the public conversion API, and the `pdf2md convert` CLI path with fake-adapter tests. Sprint 8 implemented mockable doctor diagnostics, the `pdf2md doctor` CLI path, and setup documentation. Sprint 9 implemented fast mocked integration tests, explicit opt-in local MinerU fixture evaluation, and the v1 release checklist. Sprint 10 implemented opt-in pre-conversion PDF chunking for long documents. + +## 1. V1 Outcome + +v1 is complete when a local user can run: + +```bash +uv run pdf2md doctor +uv run pdf2md convert paper.pdf --out out --metadata +uv run pdf2md convert pdfs --out out --recursive --metadata +``` + +and receive, for each PDF: + +- Obsidian-friendly Markdown. +- A stable sibling assets directory when assets exist. +- `.metadata.json`. +- `.report.md`. +- Clear warnings when math, tables, assets, reading order, GPU availability, or MinerU execution are uncertain. + +Long PDFs can be chunked explicitly: + +```bash +uv run pdf2md convert paper.pdf --out out --chunk-pages +uv run pdf2md convert paper.pdf --out out --chunk-pages 20 +``` + +Chunked conversion writes separate outputs per chunk and does not merge Markdown files. + +The converter must use MinerU 3.1.0 through direct local CLI execution only. It must not silently fallback to another engine. + +## 2. Non-Negotiable Constraints + +- Python 3.12 and `uv`. +- MinerU 3.1.0 is the only conversion engine. +- Direct local MinerU CLI execution only. +- MinerU 3.1.0 may launch a temporary local `mineru-api` internally when CLI runs without `--api-url`. +- No cloud OCR, hosted LLM/VLM, remote document parser, `--api-url`, remote APIs, router mode, HTTP client backends, or remote OpenAI-compatible backends. +- Target hardware: NVIDIA GTX 1070 Ti 8GB. +- Digital PDFs with text layers are the v1 priority. +- `samples/` is local fixture context and must not be committed unless explicitly requested. +- Every substantial implementation chunk needs a sprint contract and independent evaluation. + +## 3. Harness Operating Model + +Use the project long-running harness only for substantial implementation work. + +1. `harness-planner-agent` turns the next user request into a sprint contract. +2. `evaluation-agent` reviews the contract before code changes start. +3. `feature-generator-agent` implements one approved contract at a time. +4. `feature-generator-agent` runs self-checks and records residual risks. +5. `evaluation-agent` independently verifies the result against the contract. +6. The parent agent updates `PROGRESS.md`, commits the completed change, and leaves a handoff. + +Each sprint contract must include: + +- Objective. +- Touched surfaces. +- Expected outputs. +- Non-goals. +- Verification checks. +- Hard failure criteria. +- Handoff fields. + +## 4. Proposed Repository Layout + +Create this layout incrementally; do not scaffold unused modules before a sprint needs them. + +```text +pyproject.toml +README.md +src/ + pdf2md/ + __init__.py + cli.py + conversion.py + pdf_splitter.py + paths.py + mineru_adapter.py + ir.py + markdown.py + metadata.py + quality.py + report.py + doctor.py +tests/ + unit/ + integration/ + fixtures/ +scripts/ + install-mineru.ps1 + install-models.py +``` + +Planned module responsibilities: + +- `cli.py`: command parsing, CLI summaries, exit codes. +- `conversion.py`: orchestration for one PDF and batch input. +- `paths.py`: input discovery, output path planning, overwrite checks. +- `mineru_adapter.py`: direct local MinerU CLI boundary. +- `ir.py`: project-owned document/page/block/asset/warning records. +- `markdown.py`: Obsidian Markdown normalization. +- `metadata.py`: metadata schema creation and warning aggregation. +- `quality.py`: local checks for assets, math renderability, and output sanity. +- `report.py`: `.report.md` generation from metadata. +- `doctor.py`: environment, dependency, CUDA/GPU, MinerU, and cache diagnostics. + +## 5. Sprint Sequence + +### Sprint 0: Source And Environment Verification + +Active contract: + +- `docs/Sprints/SPRINT0CONTRACT.md` + +Objective: + +- Verify the facts needed before implementation starts. + +Touched surfaces: + +- `docs/KNOWLEDGEBASE.md` +- `docs/V1IMPLEMENTATIONPLAN.md` if sequencing changes +- `PROGRESS.md` + +Expected outputs: + +- Confirmed MinerU 3.1.0 install command, CLI invocation shape, version command, output paths, and local execution behavior. +- Confirmed Python 3.12, `uv`, CUDA/PyTorch, and GTX 1070 Ti 8GB risks. +- Confirmed license notes needed before redistribution. + +Verification checks: + +- All volatile facts cite official MinerU, Python, uv, PyTorch/CUDA, or license sources. +- No candidate engine comparison is reintroduced. +- No implementation code is created. + +Hard failure criteria: + +- MinerU 3.1.0 cannot be reasonably invoked through a direct local CLI on the target environment. +- Python 3.12 compatibility is not viable without changing project requirements. + +Primary agents: + +- `research-agent` +- `local-setup-agent` +- `license-privacy-agent` + +### Sprint 1: Project Scaffold And Fast Test Loop + +Active contract: + +- `docs/Sprints/SPRINT1CONTRACT.md` + +Objective: + +- Create the minimal Python project structure and a fast local test loop. + +Touched surfaces: + +- `pyproject.toml` +- `src/pdf2md/__init__.py` +- `tests/` +- Development documentation if needed + +Expected outputs: + +- `uv sync` works. +- `uv run pytest` works. +- Project package imports as `pdf2md`. +- CLI entry point name `pdf2md` is reserved but may initially expose only `doctor` or a clear placeholder until later sprints. +- If `uv` is still unavailable locally, Sprint 1 records that blocker and is not marked complete. + +Verification checks: + +- Import test passes. +- Empty test suite or initial scaffold tests pass. +- No runtime network dependency is introduced. + +Hard failure criteria: + +- Project cannot be installed with `uv`. +- Scaffolding adds speculative config systems, extra engines, or unused abstractions. + +Primary agents: + +- `harness-planner-agent` +- `feature-generator-agent` +- `evaluation-agent` + +### Sprint 2: Paths, Input Discovery, And Overwrite Planning + +Active contract: + +- `docs/Sprints/SPRINT2CONTRACT.md` + +Objective: + +- Implement deterministic input and output planning before conversion logic exists. + +Touched surfaces: + +- `paths.py` +- `conversion.py` skeleton if needed +- CLI path handling tests + +Expected outputs: + +- Single PDF discovery. +- Directory PDF discovery. +- Recursive traversal only when requested. +- Deterministic output paths for Markdown, assets, metadata JSON, report, and optional raw MinerU output. +- Existing-output protection unless `--overwrite` is passed. + +Verification checks: + +- Unit tests for single PDF path planning. +- Unit tests for directory and recursive discovery. +- Unit tests for overwrite behavior. +- Tests include Korean or non-ASCII filename handling using generated temporary files, not committed sample PDFs. + +Hard failure criteria: + +- Output planning can overwrite user files without explicit overwrite intent. +- Directory conversion descends recursively without `--recursive`. + +Primary agents: + +- `feature-generator-agent` +- `evaluation-agent` + +### Sprint 3: Domain Records, Metadata, And Warning Model + +Active contract: + +- `docs/Sprints/SPRINT3CONTRACT.md` + +Objective: + +- Define project-owned records before binding to MinerU output. + +Touched surfaces: + +- `ir.py` +- `metadata.py` +- `report.py` skeleton if needed +- Unit tests + +Expected outputs: + +- Document, page, block, asset, and warning records. +- Stable warning codes from `ARCHITECTURE.md`. +- Metadata JSON builder with required top-level and summary fields. +- Warning aggregation logic. + +Verification checks: + +- Unit tests for metadata schema creation. +- Unit tests for warning aggregation. +- Unit tests for optional fields such as bbox and confidence being preserved only when present. + +Hard failure criteria: + +- Public API requires raw MinerU objects. +- Metadata omits source PDF, SHA-256, engine, pages, warnings, assets, or summary. + +Primary agents: + +- `metadata-agent` +- `feature-generator-agent` +- `evaluation-agent` + +### Sprint 4: MinerU Adapter With Mocked Contract + +Active contract: + +- `docs/Sprints/SPRINT4CONTRACT.md` + +Objective: + +- Build the direct local MinerU adapter boundary with mocked outputs first. + +Touched surfaces: + +- `mineru_adapter.py` +- `doctor.py` partial checks +- Adapter tests with fake subprocess results and fake output directories + +Expected outputs: + +- Adapter availability check. +- Version check. +- Direct CLI command construction. +- Strict-local command validation. +- Subprocess execution wrapper capturing stdout, stderr, exit code, and paths. +- Parsed adapter result object with raw Markdown, raw structured data when available, assets, warnings, engine, engine version, options, exit code, and stderr. +- Baseline command shape based on MinerU 3.1.0 direct local CLI: `mineru -p -o `. +- Strict-local validation allows CLI-internal temporary local `mineru-api` orchestration, while rejecting `--api-url`, remote APIs, router mode, HTTP client backends, and remote OpenAI-compatible backends. + +Verification checks: + +- Mocked successful MinerU output test. +- Mocked missing MinerU test. +- Mocked non-zero exit test. +- Test that prohibited remote/API flags cannot be introduced. +- No real MinerU/model dependency in default tests. + +Hard failure criteria: + +- Adapter passes `--api-url`, uses router mode, uses an HTTP client backend, or connects to a remote API or remote OpenAI-compatible backend. +- Adapter falls back to another engine after MinerU failure. +- Tests require model downloads by default. + +Primary agents: + +- `mineru-integration-agent` +- `feature-generator-agent` +- `evaluation-agent` + +### Sprint 5: Obsidian Markdown Normalization And Assets + +Active contract: + +- `docs/Sprints/SPRINT5CONTRACT.md` + +Objective: + +- Normalize MinerU/project IR output into Obsidian-friendly Markdown. + +Touched surfaces: + +- `markdown.py` +- `quality.py` partial asset link checks +- Unit tests + +Expected outputs: + +- Inline math delimiter normalization to `$...$`. +- Display math delimiter normalization to `$$...$$`. +- Blank-line normalization around display math. +- Relative asset link normalization. +- Simple table preservation and complex table fallback warnings. +- No visible page markers by default. + +Verification checks: + +- Unit tests for inline math. +- Unit tests for display math spacing. +- Unit tests for underscores/carets inside math. +- Unit tests for relative asset links. +- Unit tests for table fallback warning behavior. + +Hard failure criteria: + +- Normalization rewrites LaTeX semantics without deterministic tests. +- Generated links are absolute when relative links are required. +- Page provenance is only visible in Markdown and missing from metadata. + +Primary agents: + +- `obsidian-markdown-agent` +- `feature-generator-agent` +- `evaluation-agent` + +### Sprint 6: Quality Checks And Report Generation + +Active contract: + +- `docs/Sprints/SPRINT6CONTRACT.md` + +Objective: + +- Produce local quality signals and human-readable reports from metadata. + +Touched surfaces: + +- `quality.py` +- `report.py` +- `metadata.py` +- Unit tests + +Expected outputs: + +- Missing asset link count. +- Math renderability check interface with graceful unavailable-tool handling. +- Pages-with-warnings summary. +- `.report.md` generated from metadata. +- Final status: `success`, `partial`, or `failed`. + +Verification checks: + +- Unit tests for report content. +- Unit tests for missing asset link count. +- Unit tests for math render failure aggregation. +- Report generation does not re-run MinerU. + +Hard failure criteria: + +- Report diverges from JSON metadata. +- Math render failures are silently ignored. +- Quality checks require network access. + +Primary agents: + +- `metadata-agent` +- `evaluation-agent` +- `feature-generator-agent` + +### Sprint 7: Conversion Orchestrator, CLI, And Python API + +Active contract: + +- `docs/Sprints/SPRINT7CONTRACT.md` + +Objective: + +- Connect path planning, MinerU adapter, normalization, metadata, report, and summaries. + +Touched surfaces: + +- `conversion.py` +- `cli.py` +- `__init__.py` +- CLI and API tests + +Expected outputs: + +- `convert_pdf(input_path, output_dir, metadata=True)` public API. +- `pdf2md convert INPUT --out OUTPUT_DIR`. +- `--metadata`, `--keep-raw`, `--recursive`, `--overwrite`, `--gpu`, and `--strict-local` behavior. +- Batch conversion for directories. +- CLI summary with warning counts. + +Verification checks: + +- API test with mocked MinerU adapter. +- CLI single PDF test with mocked MinerU adapter. +- CLI directory test with mocked MinerU adapter. +- Existing output test. +- Failure summary test. + +Hard failure criteria: + +- Public API exposes raw MinerU objects as required return fields. +- CLI writes outputs after a hard failure that should stop conversion. +- CLI suppresses warning counts. + +Primary agents: + +- `feature-generator-agent` +- `requirements-guard-agent` +- `evaluation-agent` + +### Sprint 8: Doctor And Setup Documentation + +Active contract: + +- `docs/Sprints/SPRINT8CONTRACT.md` + +Status: + +- Implemented. + +Objective: + +- Make local setup failures explicit before users run conversions. + +Touched surfaces: + +- `doctor.py` +- `cli.py` +- `README.md` +- `scripts/install-mineru.ps1` +- `scripts/install-models.py` +- Tests for mocked environment checks + +Expected outputs: + +- `pdf2md doctor` reports Python version, `uv`, CUDA/PyTorch GPU visibility, MinerU availability, MinerU version, and detectable model/cache paths. +- GPU unavailable warning is clear. +- Missing `uv` is reported clearly. +- Pre-Turing/Pascal GPU risk is reported clearly for GTX 1070 Ti compute capability 6.1. +- Missing required dependency causes doctor failure. +- Setup docs explain Windows PowerShell, Python 3.12, `uv`, MinerU, models, GPU expectations, and local-only behavior. + +Verification checks: + +- Mocked doctor tests for success, missing MinerU, missing GPU, and missing dependency. +- Documentation review for no cloud/API runtime path. + +Hard failure criteria: + +- Doctor says the environment is healthy when MinerU is missing. +- Doctor implies cloud/API fallback is supported. + +Primary agents: + +- `local-setup-agent` +- `license-privacy-agent` +- `evaluation-agent` + +### Sprint 9: Local Fixture Evaluation And V1 Release Gate + +Active contract: + +- `docs/Sprints/SPRINT9CONTRACT.md` + +Status: + +- Implemented. + +Objective: + +- Validate the end-to-end v1 behavior against local samples without committing samples. + +Touched surfaces: + +- `tests/integration/` +- Optional local-only fixture manifest that does not include sample PDFs +- `README.md` +- `PROGRESS.md` + +Expected outputs: + +- Fast mocked integration suite. +- Optional MinerU-dependent local test command. +- Local sample coverage notes in `PROGRESS.md`. +- V1 release checklist status. + +Verification checks: + +- `uv run pytest` passes without model downloads. +- Optional MinerU test is clearly marked and skipped unless explicitly enabled. +- Representative sample produces Markdown, metadata JSON, report Markdown, and asset paths. +- Obsidian math delimiter expectations are met. +- No sample PDFs are staged. + +Hard failure criteria: + +- Default tests require GPU, MinerU models, or network access. +- Sample files are added to git unintentionally. +- V1 release checklist passes without metadata/report generation. + +Primary agents and skills: + +- `evaluation-agent` +- `requirements-guard-agent` +- `fixture-evaluation` skill + +### Sprint 10: Pre-Conversion PDF Page Chunking + +Active contract: + +- `docs/Sprints/SPRINT10CONTRACT.md` + +Status: + +- Implemented. + +Objective: + +- Split long PDFs into temporary fixed-size page chunks before MinerU conversion. + +Touched surfaces: + +- `pdf_splitter.py` +- `conversion.py` +- `cli.py` +- `report.py` +- README and Sprint 10 documentation +- Unit tests for splitter, conversion, CLI, and report behavior + +Expected outputs: + +- `pdf2md convert INPUT --out OUTPUT --chunk-pages` enables 20-page chunks. +- `pdf2md convert INPUT --out OUTPUT --chunk-pages N` enables custom positive chunk size. +- `convert_pdf(..., chunk_pages=N)` returns a `BatchConversionResult` in chunk mode. +- Temporary chunk PDFs are deleted after conversion completes. +- Chunk Markdown files are separate and named with original page ranges. +- Metadata and report content expose original source path and chunk page ranges. + +Verification checks: + +- pypdf-based local blank PDF tests cover page counts, chunk ranges, and written chunk page counts. +- Mocked conversion tests verify one adapter call per chunk, failed-chunk continuation, chunk metadata/report context, and temporary chunk cleanup. +- CLI tests verify `--chunk-pages` without a value uses 20 pages. + +Hard failure criteria: + +- Chunking uploads document content or uses another conversion engine. +- Chunk outputs are merged. +- Default tests require real MinerU, GPU, model files, network, Obsidian, LaTeX tooling, or `samples/`. + +## 6. Cross-Cutting Acceptance Criteria + +Every implementation sprint must preserve these acceptance criteria: + +- No runtime remote document processing path exists. +- MinerU is the only conversion engine. +- Failures are explicit and traceable. +- Warnings are structured and countable. +- Markdown and metadata can be traced back to source pages where available. +- Reports are generated from metadata. +- Default tests are fast and local. +- `samples/` remains untracked unless explicitly requested. + +## 7. First Implementation Request Contract Template + +Use this template when implementation begins. + +```markdown +## Sprint Contract + +Objective: + +Touched surfaces: + +Expected outputs: + +Non-goals: + +Verification checks: + +Hard failure criteria: + +Handoff fields: +- Files changed: +- Commands run: +- Tests passed: +- Known failures: +- Residual risks: +- Next action: +``` + +## 8. Open Risks + +- MinerU 3.1.0 install and CLI behavior are source-verified, but real local output still needs a later local probe before release. +- GTX 1070 Ti 8GB is visible locally, but it is Pascal compute capability 6.1; `doctor` and setup docs must make CUDA/PyTorch limits clear. +- `uv` is installed per-user at `C:\Users\user\.local\bin`, but a new shell may need PATH refresh before `uv` is visible. +- Formula renderability checks need a local tool choice; the implementation should start with an interface and graceful unavailable-tool warning if needed. +- Some PDFs will have tables or formulas that cannot be faithfully represented in Markdown; metadata and `.report.md` must surface this instead of hiding it. +- Redistribution license obligations must be reviewed before packaging, redistribution, or bundling model weights. + +## 9. Recommended Next Step + +Run optional real local MinerU validation on a long sample only when requested. Default verification should continue to use mocked adapters and generated temporary PDFs so it remains independent of MinerU, GPU, model files, network access, and `samples/`. + +Facts carried forward from Sprint 0: + +- MinerU is fixed to version 3.1.0. +- Direct local CLI command shape is `mineru -p -o `. +- MinerU output layout should be treated as optional-file based until locally probed. +- Python 3.12 is compatible with the pinned MinerU package range. +- GTX 1070 Ti CUDA/PyTorch support needs explicit doctor validation. +- MinerU/model license posture is acceptable for personal local use. Redistribution remains out of scope until reviewed. diff --git a/docs/V1RELEASECHECKLIST.md b/docs/V1RELEASECHECKLIST.md new file mode 100644 index 0000000..358f6a9 --- /dev/null +++ b/docs/V1RELEASECHECKLIST.md @@ -0,0 +1,146 @@ +# V1 Release Checklist + +Use this checklist from the repository root when deciding whether v1 is ready for local use. It separates the default fast gates from optional MinerU/GPU/sample validation so the normal loop stays independent of real models, GPU, network access, Obsidian, LaTeX tooling, and `samples/`. + +## Release Status Rules + +- Default fast gates passing means the repository is healthy under mocked/local checks. +- Optional local MinerU fixture gates passing means real local sample conversion has also been validated. +- If `pdf2md doctor` reports a hard failure, v1 release status is blocked unless the user records an explicit waiver with the exact reason and scope. +- Do not mark v1 fully validated when optional MinerU/sample checks are skipped or blocked. Record the blocked reason and release recommendation in `PROGRESS.md`. +- Do not claim perfect LaTeX, table, or reading-order reconstruction. The v1 guarantee is best-effort local conversion with warnings, metadata, and a human-readable report. + +## Default Fast Gates + +These gates should be runnable without real MinerU execution, sample PDFs, model files, GPU acceleration, network access, Obsidian, or LaTeX tooling. They must not install packages or download models during imports, tests, `doctor`, or `convert`. + +| Gate | Pass condition | Release handling | +| --- | --- | --- | +| `uv sync` | Exits 0. | Blocks if project dependencies cannot sync. | +| `uv run pytest` | Exits 0 using mocked/local tests only. | Blocks if default tests require real MinerU, GPU, CUDA, PyTorch, model files, network, Obsidian, LaTeX tooling, or `samples/`. | +| `uv run pdf2md --version` | Exits 0 and prints the installed CLI version. | Blocks if the CLI entry point is unavailable. | +| `uv run pdf2md doctor` | Runs to completion and reports pass, warning-only, or hard-fail status. | A hard failure blocks release readiness unless explicitly waived by the user and recorded. Warning-only status can continue, but warnings must be recorded. | +| `git diff --check` | Exits 0. | Blocks on whitespace or patch formatting errors. | +| `git status --short --untracked-files=all` | Shows no staged sample PDFs or generated sample outputs. | Blocks if any `samples/` file or sample-derived output is staged or committed unintentionally. | + +## Strict-Local Gate + +Before calling v1 ready, verify the release candidate still follows the strict-local policy: + +- Allowed runtime path: direct local `mineru` CLI execution. +- Allowed MinerU internal behavior: MinerU 3.1.0 may start a temporary local `mineru-api` when the CLI runs without `--api-url`. +- Prohibited runtime paths: `--api-url`, remote APIs, router mode, HTTP client backends, remote OpenAI-compatible backends, hosted renderers, cloud OCR, remote LLM/VLM calls, remote document parsers, alternate engines, runtime engine selection, and silent fallback after MinerU failure. +- Setup downloads must be explicit user actions and remain separate from runtime conversion. Default tests, imports, `doctor`, `convert`, and helper checks must not download packages or models. + +## Doctor Hard-Failure Handling + +Treat a non-zero `pdf2md doctor` result as a release blocker for real v1 readiness. Common hard failures include missing required local dependencies, missing `mineru` on PATH, or a strict-local policy failure. Warning-only doctor results can continue, but the warnings must be recorded. + +When `doctor` hard-fails: + +- Do not run optional sample conversion as a passing release gate. +- Do not mark optional MinerU/GPU/sample validation as skipped-pass. Mark it blocked. +- Do not use a cloud/API fallback or alternate converter to bypass the failure. +- Record the failing check, exit code, and next action in `PROGRESS.md`. +- If the user chooses to proceed anyway, record the waiver and report the release as waived or risk-accepted, not fully validated. + +## Optional Local MinerU, GPU, And Sample Gates + +Run these only by explicit opt-in after the default gates. They are intended for a local workstation with MinerU 3.1.0, model/cache setup, and GPU/PyTorch expectations already checked by `doctor`. + +Preconditions: + +- `uv run pdf2md doctor` has no hard failures, or the user has recorded an explicit waiver. +- Source PDFs come from local `samples/` or another user-provided local directory. +- Generated outputs go to a temporary directory or another ignored local output root, never to tracked fixture paths. +- Sample PDFs, `samples/metadata.json`, and generated sample outputs remain unstaged and uncommitted. + +Manual single-sample shape: + +```powershell +$sample = "samples\FourNodeQuadrilateralShellElementMITC4.pdf" +$out = Join-Path $env:TEMP ("pdf2md-fixture-" + [guid]::NewGuid().ToString()) +uv run pdf2md convert $sample --out $out --metadata --overwrite +``` + +Optional pytest fixture evaluation: + +```powershell +$env:PDF2MD_RUN_MINERU_FIXTURES = "1" +uv run pytest tests/integration/test_optional_mineru_fixtures.py +Remove-Item Env:PDF2MD_RUN_MINERU_FIXTURES +``` + +This optional pytest path runs `pdf2md doctor` first. If doctor has a hard failure, the fixture test is skipped with a blocker message instead of being counted as a passing real-MinerU validation. + +A sample conversion is successful only when all of these are true: + +- The command exits 0. +- The planned Markdown file exists: `\.md`. +- The planned metadata JSON exists: `\.metadata.json`. +- The planned quality report exists: `\.report.md`. +- Metadata and report warning counts are consistent enough to explain math, table, reading-order, asset, MinerU, and checker-unavailable risks. +- Any Markdown image links resolve relative to the Markdown file, or missing/broken links are reported as warnings. + +Missing Markdown, metadata JSON, or `.report.md` means the sample failed or is blocked. Do not count it as a partial success for release gating. + +For each attempted sample, record at least: + +- Source filename. +- Command run. +- Exit code. +- Generated Markdown path. +- Generated metadata JSON path. +- Generated `.report.md` path. +- Warning count and final status. +- Math renderability failures or checker-unavailable count. +- Table fallback or degradation count when available. +- Missing or broken asset link count. +- Page coverage when available. +- Doctor status and any GPU/PyTorch/model/cache warnings. + +## Fixture Coverage Notes + +Local fixture coverage should include these risk categories where samples are available: + +- Simple digital PDF with a text layer. +- Math-heavy PDF. +- Multi-column or complex reading order. +- Table with formulas. +- Figure, caption, or extracted asset links. +- Korean or non-ASCII filename/path handling. + +Observed local fixture map as of 2026-05-08: + +| Local sample | Fixture risks covered | Notes | +| --- | --- | --- | +| `samples/FourNodeQuadrilateralShellElementMITC4.pdf` | simple digital PDF, math-heavy engineering content, table/formula risk | Small sample suitable for first optional MinerU smoke validation. | +| `samples/MITC공부.pdf` | Korean filename/path handling, math-heavy notes, table/formula risk | Useful for non-ASCII path and shell-element notation checks. | +| `samples/2007쉘구조물의유한요소해석에대하여.pdf` | Korean filename/path handling, longer academic layout, math-heavy content, reading-order risk | Larger sample; use after the small smoke sample passes. | +| `samples/유한요소해석법을이용한쉘구조물의동적좌굴해석.pdf` | Korean filename/path handling, math-heavy content, figures/assets, reading-order risk | Larger sample for report and warning quality review. | +| `samples/metadata.json` | fixture notes only | Local untracked context; do not treat as generated v1 metadata unless manually confirmed. | + +Coverage gaps to keep visible: + +- A deliberately simple one-page digital PDF fixture is still useful for release smoke checks. +- A table-dominant sample with known formula cells would make table degradation easier to judge. +- A figure-heavy sample with expected extracted assets would make asset link validation easier to judge. + +Do not score fixture quality only by plain-text edit distance. Include math delimiter/renderability behavior, tables, reading order, assets, metadata fields, warning usefulness, and `.report.md` usefulness. + +## No-Sample-Commit Check + +Before staging or committing any release-gate work: + +```powershell +git status --short --untracked-files=all +``` + +Confirm: + +- `samples/` files are not staged. +- Generated sample outputs are not staged. +- No sample PDF or sample-derived binary was copied into tracked tests or docs. +- Any temporary fixture output inside the repository was removed unless the user explicitly approved keeping it and it is ignored. + +`samples/` may appear as untracked local context. That is expected; do not add it unless the user explicitly requests it. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e683fef --- /dev/null +++ b/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "convert-pdf-to-md", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "convert-pdf-to-md", + "version": "0.1.0", + "dependencies": { + "mathjax": "4.1.2" + } + }, + "node_modules/@mathjax/mathjax-newcm-font": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@mathjax/mathjax-newcm-font/-/mathjax-newcm-font-4.1.2.tgz", + "integrity": "sha512-lZHMjNP2XbABHA3kVn40rbse5ERUeMEmrGH03qLkCwxq4/5Z/eNLr0akw1MmQcqTwCbvkx1BFcmJ7RCfbRlw3Q==", + "license": "Apache-2.0" + }, + "node_modules/mathjax": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-4.1.2.tgz", + "integrity": "sha512-EQDS8xBpVg179BXoLeZ9JlwUFftOC5qylw20UlAMDhrTuooENigOocY79aNkkFSyvj/AST/89ZAo12+r5bPI4w==", + "license": "Apache-2.0", + "dependencies": { + "@mathjax/mathjax-newcm-font": "^4.1.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6beb43c --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "convert-pdf-to-md", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "mathjax-checker:health": "node tools/mathjax-checker/check.mjs --health" + }, + "dependencies": { + "mathjax": "4.1.2" + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..85ded6f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" + +[project] +name = "convert-pdf-to-md" +version = "0.1.0" +description = "Local PDF-to-Markdown converter scaffold for math-heavy documents." +readme = "README.md" +requires-python = ">=3.12,<3.13" +dependencies = [ + "pypdf>=6.10.2,<7", +] + +[project.scripts] +pdf2md = "pdf2md.cli:main" + +[dependency-groups] +dev = [ + "pytest>=8.3", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/pdf2md"] diff --git a/samples/2007쉘구조물의유한요소해석에대하여.pdf b/samples/2007쉘구조물의유한요소해석에대하여.pdf new file mode 100644 index 0000000..b2bdbc0 --- /dev/null +++ b/samples/2007쉘구조물의유한요소해석에대하여.pdf @@ -0,0 +1,33600 @@ +%PDF-1.4 % +5239 0 obj <> endobj +xref +5239 761 +0000000016 00000 n +0000018013 00000 n +0000018102 00000 n +0000018304 00000 n +0000018803 00000 n +0000019341 00000 n +0000019589 00000 n +0000019816 00000 n +0000020335 00000 n +0000020579 00000 n +0000021038 00000 n +0000021693 00000 n +0000021958 00000 n +0000022211 00000 n +0000022501 00000 n +0000022729 00000 n +0000022768 00000 n +0000028524 00000 n +0000029094 00000 n +0000029621 00000 n +0000030074 00000 n +0000030189 00000 n +0000030268 00000 n +0000030575 00000 n +0000030644 00000 n +0000031668 00000 n +0000031755 00000 n +0000032004 00000 n +0000032476 00000 n +0000032710 00000 n +0000032947 00000 n +0000034554 00000 n +0000035029 00000 n +0000035576 00000 n +0000035818 00000 n +0000036471 00000 n +0000036935 00000 n +0000038199 00000 n +0000038445 00000 n +0000039336 00000 n +0000039578 00000 n +0000040515 00000 n +0000041028 00000 n +0000041905 00000 n +0000042838 00000 n +0000043938 00000 n +0000044167 00000 n +0000044407 00000 n +0000044947 00000 n +0000045487 00000 n +0000045773 00000 n +0000046101 00000 n +0000046160 00000 n +0000046308 00000 n +0000046346 00000 n +0000046390 00000 n +0000047531 00000 n +0000048653 00000 n +0000051325 00000 n +0000051693 00000 n +0000051737 00000 n +0000051874 00000 n +0000052548 00000 n +0000053018 00000 n +0000053441 00000 n +0000053592 00000 n +0000054311 00000 n +0000054973 00000 n +0000055472 00000 n +0000055941 00000 n +0000056436 00000 n +0000056861 00000 n +0000057305 00000 n +0000057646 00000 n +0000057944 00000 n +0000058468 00000 n +0000058875 00000 n +0000059373 00000 n +0000059761 00000 n +0000060252 00000 n +0000060590 00000 n +0000061013 00000 n +0000061617 00000 n +0000061875 00000 n +0000062207 00000 n +0000062251 00000 n +0000062346 00000 n +0000062719 00000 n +0000062967 00000 n +0000063660 00000 n +0000064102 00000 n +0000064684 00000 n +0000065221 00000 n +0000065463 00000 n +0000065592 00000 n +0000065877 00000 n +0000065921 00000 n +0000065977 00000 n +0000066423 00000 n +0000066459 00000 n +0000066835 00000 n +0000066879 00000 n +0000067028 00000 n +0000067263 00000 n +0000067824 00000 n +0000068042 00000 n +0000068250 00000 n +0000068401 00000 n +0000068841 00000 n +0000069186 00000 n +0000069778 00000 n +0000070240 00000 n +0000070773 00000 n +0000071087 00000 n +0000071287 00000 n +0000071706 00000 n +0000071896 00000 n +0000072207 00000 n +0000072624 00000 n +0000072947 00000 n +0000073411 00000 n +0000073610 00000 n +0000073855 00000 n +0000074143 00000 n +0000074187 00000 n +0000074414 00000 n +0000074461 00000 n +0000074790 00000 n +0000074834 00000 n +0000074934 00000 n +0000075471 00000 n +0000075624 00000 n +0000076142 00000 n +0000076606 00000 n +0000077070 00000 n +0000077356 00000 n +0000077863 00000 n +0000078151 00000 n +0000078335 00000 n +0000078639 00000 n +0000078783 00000 n +0000079144 00000 n +0000079188 00000 n +0000079377 00000 n +0000079640 00000 n +0000080073 00000 n +0000080225 00000 n +0000080766 00000 n +0000081056 00000 n +0000081697 00000 n +0000082114 00000 n +0000082536 00000 n +0000082938 00000 n +0000083368 00000 n +0000083772 00000 n +0000084092 00000 n +0000084626 00000 n +0000084965 00000 n +0000085242 00000 n +0000085666 00000 n +0000085914 00000 n +0000086292 00000 n +0000086621 00000 n +0000087033 00000 n +0000087462 00000 n +0000087881 00000 n +0000088180 00000 n +0000088577 00000 n +0000088855 00000 n +0000089183 00000 n +0000089574 00000 n +0000090089 00000 n +0000090563 00000 n +0000090902 00000 n +0000091251 00000 n +0000091593 00000 n +0000091793 00000 n +0000092006 00000 n +0000092444 00000 n +0000092792 00000 n +0000092836 00000 n +0000092943 00000 n +0000093455 00000 n +0000093607 00000 n +0000093805 00000 n +0000094228 00000 n +0000094671 00000 n +0000095092 00000 n +0000095366 00000 n +0000095826 00000 n +0000096306 00000 n +0000096769 00000 n +0000096910 00000 n +0000097250 00000 n +0000097294 00000 n +0000097421 00000 n +0000097683 00000 n +0000097835 00000 n +0000098322 00000 n +0000098819 00000 n +0000099210 00000 n +0000099631 00000 n +0000100104 00000 n +0000100642 00000 n +0000101146 00000 n +0000101499 00000 n +0000101995 00000 n +0000102347 00000 n +0000102931 00000 n +0000103380 00000 n +0000103801 00000 n +0000104319 00000 n +0000104695 00000 n +0000105071 00000 n +0000105421 00000 n +0000105896 00000 n +0000106281 00000 n +0000106731 00000 n +0000107004 00000 n +0000107302 00000 n +0000107346 00000 n +0000107410 00000 n +0000108029 00000 n +0000108560 00000 n +0000108613 00000 n +0000109417 00000 n +0000109461 00000 n +0000110157 00000 n +0000110761 00000 n +0000111247 00000 n +0000111920 00000 n +0000112470 00000 n +0000113187 00000 n +0000113769 00000 n +0000114360 00000 n +0000115099 00000 n +0000115530 00000 n +0000115969 00000 n +0000116522 00000 n +0000117099 00000 n +0000117621 00000 n +0000118244 00000 n +0000118882 00000 n +0000119312 00000 n +0000119928 00000 n +0000120580 00000 n +0000121229 00000 n +0000121806 00000 n +0000122410 00000 n +0000122834 00000 n +0000123468 00000 n +0000124073 00000 n +0000124774 00000 n +0000125403 00000 n +0000125797 00000 n +0000126228 00000 n +0000126867 00000 n +0000127338 00000 n +0000127779 00000 n +0000128326 00000 n +0000128848 00000 n +0000129503 00000 n +0000130120 00000 n +0000130546 00000 n +0000131174 00000 n +0000131699 00000 n +0000132268 00000 n +0000132751 00000 n +0000133326 00000 n +0000133867 00000 n +0000134510 00000 n +0000135126 00000 n +0000135680 00000 n +0000136218 00000 n +0000136721 00000 n +0000137166 00000 n +0000137856 00000 n +0000138331 00000 n +0000138896 00000 n +0000139427 00000 n +0000139909 00000 n +0000140448 00000 n +0000141027 00000 n +0000141578 00000 n +0000142118 00000 n +0000142826 00000 n +0000143424 00000 n +0000144028 00000 n +0000144623 00000 n +0000145167 00000 n +0000145578 00000 n +0000146036 00000 n +0000146568 00000 n +0000147018 00000 n +0000147526 00000 n +0000148040 00000 n +0000148445 00000 n +0000148959 00000 n +0000149534 00000 n +0000150134 00000 n +0000150697 00000 n +0000151279 00000 n +0000151933 00000 n +0000152554 00000 n +0000153029 00000 n +0000153490 00000 n +0000153987 00000 n +0000154479 00000 n +0000154998 00000 n +0000155658 00000 n +0000156437 00000 n +0000156995 00000 n +0000157467 00000 n +0000158196 00000 n +0000158837 00000 n +0000159294 00000 n +0000159910 00000 n +0000160597 00000 n +0000161204 00000 n +0000161701 00000 n +0000162229 00000 n +0000162759 00000 n +0000163349 00000 n +0000163918 00000 n +0000164674 00000 n +0000165195 00000 n +0000165752 00000 n +0000166359 00000 n +0000166778 00000 n +0000167310 00000 n +0000167911 00000 n +0000168508 00000 n +0000168944 00000 n +0000170556 00000 n +0000170886 00000 n +0000170930 00000 n +0000171054 00000 n +0000171331 00000 n +0000171482 00000 n +0000171855 00000 n +0000172177 00000 n +0000172601 00000 n +0000172887 00000 n +0000173291 00000 n +0000173490 00000 n +0000173707 00000 n +0000174099 00000 n +0000174490 00000 n +0000174669 00000 n +0000174983 00000 n +0000175027 00000 n +0000175104 00000 n +0000175597 00000 n +0000176141 00000 n +0000176617 00000 n +0000177024 00000 n +0000177108 00000 n +0000177407 00000 n +0000177451 00000 n +0000177515 00000 n +0000178111 00000 n +0000178387 00000 n +0000178440 00000 n +0000179584 00000 n +0000179628 00000 n +0000180769 00000 n +0000181368 00000 n +0000181919 00000 n +0000182445 00000 n +0000183162 00000 n +0000183738 00000 n +0000184355 00000 n +0000185088 00000 n +0000185686 00000 n +0000186284 00000 n +0000186708 00000 n +0000187263 00000 n +0000187791 00000 n +0000188449 00000 n +0000189026 00000 n +0000189444 00000 n +0000190157 00000 n +0000190676 00000 n +0000191272 00000 n +0000191911 00000 n +0000192544 00000 n +0000193072 00000 n +0000193504 00000 n +0000194114 00000 n +0000194706 00000 n +0000195327 00000 n +0000195976 00000 n +0000196613 00000 n +0000197275 00000 n +0000197855 00000 n +0000198230 00000 n +0000198834 00000 n +0000199478 00000 n +0000199873 00000 n +0000200681 00000 n +0000201155 00000 n +0000201765 00000 n +0000202326 00000 n +0000202942 00000 n +0000203457 00000 n +0000204031 00000 n +0000204716 00000 n +0000205384 00000 n +0000206031 00000 n +0000206645 00000 n +0000207049 00000 n +0000207466 00000 n +0000208116 00000 n +0000208589 00000 n +0000209015 00000 n +0000209501 00000 n +0000209929 00000 n +0000210375 00000 n +0000211044 00000 n +0000211711 00000 n +0000212165 00000 n +0000212717 00000 n +0000213309 00000 n +0000213791 00000 n +0000214410 00000 n +0000215090 00000 n +0000215613 00000 n +0000216311 00000 n +0000216973 00000 n +0000217381 00000 n +0000218009 00000 n +0000218633 00000 n +0000219146 00000 n +0000219571 00000 n +0000220178 00000 n +0000220754 00000 n +0000221312 00000 n +0000221801 00000 n +0000222376 00000 n +0000222921 00000 n +0000223449 00000 n +0000224024 00000 n +0000224611 00000 n +0000225088 00000 n +0000225769 00000 n +0000226314 00000 n +0000227090 00000 n +0000227587 00000 n +0000228108 00000 n +0000228619 00000 n +0000229058 00000 n +0000229650 00000 n +0000230335 00000 n +0000230792 00000 n +0000231349 00000 n +0000231890 00000 n +0000232403 00000 n +0000233013 00000 n +0000233542 00000 n +0000234263 00000 n +0000234845 00000 n +0000235394 00000 n +0000235997 00000 n +0000236532 00000 n +0000237005 00000 n +0000237715 00000 n +0000238390 00000 n +0000239020 00000 n +0000239612 00000 n +0000240197 00000 n +0000240782 00000 n +0000241414 00000 n +0000242014 00000 n +0000242529 00000 n +0000242931 00000 n +0000243547 00000 n +0000244069 00000 n +0000244589 00000 n +0000245028 00000 n +0000245536 00000 n +0000246064 00000 n +0000246453 00000 n +0000246960 00000 n +0000247660 00000 n +0000248238 00000 n +0000248821 00000 n +0000249445 00000 n +0000250010 00000 n +0000250561 00000 n +0000251059 00000 n +0000251664 00000 n +0000252279 00000 n +0000252990 00000 n +0000253463 00000 n +0000253938 00000 n +0000254601 00000 n +0000255048 00000 n +0000255654 00000 n +0000256404 00000 n +0000257076 00000 n +0000257622 00000 n +0000258103 00000 n +0000258590 00000 n +0000259333 00000 n +0000259865 00000 n +0000260378 00000 n +0000260925 00000 n +0000261428 00000 n +0000262081 00000 n +0000262835 00000 n +0000263624 00000 n +0000264189 00000 n +0000264642 00000 n +0000265066 00000 n +0000265801 00000 n +0000266368 00000 n +0000266851 00000 n +0000267319 00000 n +0000267956 00000 n +0000268585 00000 n +0000269117 00000 n +0000269671 00000 n +0000270123 00000 n +0000270859 00000 n +0000271499 00000 n +0000272127 00000 n +0000272624 00000 n +0000273138 00000 n +0000273672 00000 n +0000274031 00000 n +0000274618 00000 n +0000275189 00000 n +0000275773 00000 n +0000276479 00000 n +0000277004 00000 n +0000277568 00000 n +0000278157 00000 n +0000278568 00000 n +0000279108 00000 n +0000279711 00000 n +0000280312 00000 n +0000280729 00000 n +0000281362 00000 n +0000284067 00000 n +0000284453 00000 n +0000284497 00000 n +0000284732 00000 n +0000284972 00000 n +0000285411 00000 n +0000285918 00000 n +0000286209 00000 n +0000286351 00000 n +0000286865 00000 n +0000287273 00000 n +0000287682 00000 n +0000288079 00000 n +0000288504 00000 n +0000288906 00000 n +0000289209 00000 n +0000289715 00000 n +0000290050 00000 n +0000290315 00000 n +0000290550 00000 n +0000290941 00000 n +0000291261 00000 n +0000291725 00000 n +0000292131 00000 n +0000292552 00000 n +0000292839 00000 n +0000293236 00000 n +0000293508 00000 n +0000293815 00000 n +0000294205 00000 n +0000294626 00000 n +0000295110 00000 n +0000295557 00000 n +0000295752 00000 n +0000296006 00000 n +0000296423 00000 n +0000296811 00000 n +0000297198 00000 n +0000297475 00000 n +0000297950 00000 n +0000298192 00000 n +0000298628 00000 n +0000299141 00000 n +0000299655 00000 n +0000299941 00000 n +0000299985 00000 n +0000300324 00000 n +0000300360 00000 n +0000300779 00000 n +0000300823 00000 n +0000301021 00000 n +0000301499 00000 n +0000302041 00000 n +0000302580 00000 n +0000303002 00000 n +0000303474 00000 n +0000303968 00000 n +0000304430 00000 n +0000304722 00000 n +0000305091 00000 n +0000305681 00000 n +0000306082 00000 n +0000306375 00000 n +0000306792 00000 n +0000307229 00000 n +0000307819 00000 n +0000308132 00000 n +0000308440 00000 n +0000309022 00000 n +0000309514 00000 n +0000310015 00000 n +0000310506 00000 n +0000310942 00000 n +0000311447 00000 n +0000311896 00000 n +0000312281 00000 n +0000312646 00000 n +0000312690 00000 n +0000312863 00000 n +0000313079 00000 n +0000313231 00000 n +0000313426 00000 n +0000313815 00000 n +0000314171 00000 n +0000314528 00000 n +0000314900 00000 n +0000315315 00000 n +0000315595 00000 n +0000316314 00000 n +0000316512 00000 n +0000316757 00000 n +0000317115 00000 n +0000317306 00000 n +0000317626 00000 n +0000317898 00000 n +0000318250 00000 n +0000318608 00000 n +0000318854 00000 n +0000319237 00000 n +0000319525 00000 n +0000319719 00000 n +0000319893 00000 n +0000320224 00000 n +0000320551 00000 n +0000320885 00000 n +0000321181 00000 n +0000321225 00000 n +0000321303 00000 n +0000321450 00000 n +0000321662 00000 n +0000321724 00000 n +0000322045 00000 n +0000322089 00000 n +0000322960 00000 n +0000323843 00000 n +0000324341 00000 n +0000324821 00000 n +0000325629 00000 n +0000325732 00000 n +0000326056 00000 n +0000326100 00000 n +0000326202 00000 n +0000326691 00000 n +0000326838 00000 n +0000327157 00000 n +0000327625 00000 n +0000328057 00000 n +0000328489 00000 n +0000328947 00000 n +0000329067 00000 n +0000329352 00000 n +0000329396 00000 n +0000329628 00000 n +0000329675 00000 n +0000329989 00000 n +0000330033 00000 n +0000330113 00000 n +0000330611 00000 n +0000331182 00000 n +0000331418 00000 n +0000331996 00000 n +0000332083 00000 n +0000332426 00000 n +0000332470 00000 n +0000332992 00000 n +0000333346 00000 n +0000333861 00000 n +0000334350 00000 n +0000334864 00000 n +0000335343 00000 n +0000335583 00000 n +0000336104 00000 n +0000336537 00000 n +0000336704 00000 n +0000337030 00000 n +0000337074 00000 n +0000337166 00000 n +0000337383 00000 n +0000338148 00000 n +0000338923 00000 n +0000339370 00000 n +0000339797 00000 n +0000340526 00000 n +0000340643 00000 n +0000340986 00000 n +0000341030 00000 n +0000341174 00000 n +0000341593 00000 n +0000341739 00000 n +0000342131 00000 n +0000342505 00000 n +0000342939 00000 n +0000343159 00000 n +0000343561 00000 n +0000344042 00000 n +0000344352 00000 n +0000344748 00000 n +0000345193 00000 n +0000345683 00000 n +0000345879 00000 n +0000346249 00000 n +0000346704 00000 n +0000347118 00000 n +0000347588 00000 n +0000348000 00000 n +0000348448 00000 n +0000348893 00000 n +0000349429 00000 n +0000349739 00000 n +0000350151 00000 n +0000350529 00000 n +0000351051 00000 n +0000351549 00000 n +0000351996 00000 n +0000352459 00000 n +0000352894 00000 n +0000353151 00000 n +0000353296 00000 n +0000353457 00000 n +0000353616 00000 n +0000353779 00000 n +0000353942 00000 n +0000354103 00000 n +0000354265 00000 n +0000354423 00000 n +0000354579 00000 n +0000354739 00000 n +0000354900 00000 n +0000355060 00000 n +0000355219 00000 n +0000355376 00000 n +0000355538 00000 n +0000355701 00000 n +0000355952 00000 n +0000356237 00000 n +0000356281 00000 n +0000356432 00000 n +0000015516 00000 n +trailer +<<0A23106984CC4744A5D6734CD9BDC42E>]>> +startxref +0 +%%EOF + +5999 0 obj <>stream +xڬV}TWL!!> +D b )-l`Q|Vihik.SZgMA)ѶKBjSZ]g9޽{yo`% 4D!D\le:HxN>`:]k}t-ĵ]t0-{>^k. +ACyXnyÃ?gp#0Q^W|Ɨ7.O: { H*m#azg+[VRRyFigxWw?p[?:R{Q+L${PK_9 +T!u1yL8L/uEY`ZҿmxZ[;seQv'_=s>rhsW6~yɨNgi7/em:?zꝌo |?NT}75Tt-5uN~[;+p7/Jqr`2_m8|-1tڵgo %]}-sGܥN } oy'j~,m?d[uV6~ZPVG9{EY6K(C]$2?RU*UQ}mo/***"7!~ſ ++zV` o{zِi"Ce"x=Wqx3T҇kpŁ(!s6kwluGxGQkނ+:np )IpƳ)J-ȳr:HPQPۻjec./꧕r䔶ep2Z{5Ou.;WG'@@"tU +ރ;5TH81͆ ]"E:SwmÙzY0]760r:f;SU*K{`\@oiD؎gXT)iBˑL&d Ågx /걆fi|Cه햍d39P +(&<-9AN!)[|[h+iLWRF0Bu{uZ.ۂ\faxA Ff> -&T1H7۵jTCI3/8HY5܄+ӛccEP"u8@2#rvQ(٬(\wCFbs>Ò g;DQdmYYEQzc,6˭#&3lHq! dD\/nR' +hWN++h̰5سI& VZ ]ͩ^"|F1K6ld*е䓎\ַ36mGpA/hʗT:t ?S}ӕXkEŎXEP0ဲ")\zR' ++c)Wc(Xm3|L>Ǜ6lo`<;6,hYT{Vy 8t&PSf5䨘 LXj,\Ɖ$ GS~[ Q\# t ")>S"WQ(hr>C2"UvS,Y@@m&dJ,KVl%*d|~8is zB~ cQ5Nظh]$E3z8Rx)B9<'x:``yY>%Jw ,oҙs +ɀ#ٻb^@C/m\zmdՍnQ8Z= px)gh8l_K#X6gL`Hq.ȾHs*kBbdQ8*]93&P%E +g Qij'9 6K]oVsmٴ倍$`ϧ֙H9.t.Aȣj&`j)FӽŒW1;%yD`a*P#+$ #F/s< X> endobj 5241 0 obj <> endobj 5242 0 obj <>/Font<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 5243 0 obj <> endobj 5244 0 obj <> endobj 5245 0 obj <> endobj 5246 0 obj <> endobj 5247 0 obj <> endobj 5248 0 obj <> endobj 5249 0 obj <> endobj 5250 0 obj <> endobj 5251 0 obj <> endobj 5252 0 obj <> endobj 5253 0 obj <> endobj 5254 0 obj [/ICCBased 5296 0 R] endobj 5255 0 obj <>stream +Hd Tǿd2%MZgE" +H몬q d$ Dy# ),B4VWku[jĉg;a{sgfϽ|VA#"ãw*e* bQkޤcWw__z-յ +恟26] >Q` Hi@ @@68 +A9I0]@bH!eF+Rh%,MPg1:R$ӒbI4)rQa:Tb2U%EHdJJ,pu2)R2R*$b]-Vj$rRBH LJ.QdiE2F#"eED!IZdjW*Rr&+ڕt5)fUjTȅ3]-+R+gV+L-i[}sŝ+1CVNp2Ug6CݣHSUJ5cD3pwU4"Ig\ @P + @ 1Q VlݰiV?"c=L EbLdNutZGlWn#*BbcHUȪUܢ`bd2±nwu5-o̬ٿG1jmډukK]sgCx5dt}*uT>GN`Ϣpܑ>SNSЍn "N6G~iw8<2G⩣ñ"7#IQ`/}^V;7_Jf%cQ9A>OC/ &0:0A7SO/>뜇'4] ݿ>Bۭ51w)?C;Q Wm||kDPpY*C8a.dte\'CωI^Ձ?`%숧:PZ^KN S}}; Mf/k`9ߜ镣H{\惽KNg&0֊)PO}G$Ayɦ"DCN1r+L)G& +=p5AПƔdGщݱa 7JIO ᬉ?:.Qm[ )'Q{7.7fA,oI;sLZ+<%$U|~!7Ւٲ3-#^lnf͇[a^e -<>1VN)'^\Ϙ21.03Sdl|U 1 jAl-{M?wnT[u +s7J"SO.&+ +7\ ݩ0^݌/ϡ!mJ>//c6t5kT`9HK-PwI&>t ZƗ:.acg>⇻-6 ՒDВ Pb E!#3djG [rxC8ڃInwjMS]]g/VzѲrN'eZ r.uEO RTmΪ*k5%=33ƫnS묡+% ŸQuV 𖾄)<WWNN_h]|8Y:cAP@WbMrBy}s]JC_o1-9+ZeXy`nohWM1BRhu]F+AHJ 依ڎ3ߎJHLB$XJ&{>PoQ1(Jak29eaW3|}Cx@I$nCv'+1m; y݌jzcs3F) + BƂX$Ifk0Sۥ(u}[EO`P_k HNz3?5C )p?(̊3t#fa1JϾ9S2k" XHky1)s?_R/:l4Z{[Z'ga;&\o͹%rywQZ$S7 +{e  hbX*B ESG}^hEq 4bWLSncėjc EP) gmpCꝚ6k;Ϥ'nHj:z&.1M[rpiq䰚B.$;G(vfGKnS!s@`<.r*y5%cZ~0tAJ:&Wg߹ѧ6p_ke;>y ߍ`k*Q6. &B#&Oa%ѽ]?0| ~J~=SxZy-Dy)=`|(L*JqSrc8k3H6a9l!7!^S˺Ԩr-:i$ 9#KF^m#ٿ@W7?|=lJW`e뺜u ȄHXW\:T YwK4D"Qۍ\M):@|/rsqxTZS/o}=CȲqWvq`7pK9 F01E&okyy2oF$Ӫ&}n/;0AVKikVUVB)0afy|oJr^@a[ ܭM-> dQ" GDZBT*w`(.~rR)6Gf SX(UBmk)eqAD66@!PIu=S٬e`dsMܒACU u )_Jҋ|Ds +⤮dBy& )W40tdV8d-)[ho!<6Qߴ CA%CN.8 s4X\3k@x^"TUSS^iÁ'S_8V/Λc)K Obл 5obÜbgRb3;1m/i$(0$̀Y0B0oIHXVZ w^RsK=Q :c,OI'\L'L^}McӜW.v\DObf, +x]ܴtHFб!`Mϔ򍄑gd"v1 l'S|jw\Kԫ>o k PGQ}0Ȓ!SZY}NmI4r^5/pr{ =F#z[}-Z)! +}=OA9h~*^BL\S}vi#Xzmy3Ns[J eQ.# 0QQ'EΙwnT/6(wv*QQ́\[ETyUߩȊ}hN>ӇYr_5ҨvWr=btE|S p!F#}e=~D*)0ÿ*^nPbhvMr[ +k]FJ qL`ZX]sQ}ԦcNvR* GEPG 呐ח6[39CN2Y +ACK[t'eނ=}#㚤KҘq[`[ªU8k"Gֈhr3:,!)B3ƭz] uf1NhA BkGyJ]ZRzy + J*H)|ǢFeXܹ]ȶ,9_9߯^G)ՋLǙp@"ۏ%AYۼK>F` &ͫps @WΠ[iyr^|G]S+8uKsԠtWWw@5ټrV% Fu5kXe_홦s4PXMR`h0o9^jsХwi_tx^ #ց.S1P(>\|oʄRCQUO ]3}`%xLRǀU-aQKז$Az+ X'A* oY'$e]?M]* 4b ʛd]W]Y}GtYiIy\qdeIOE{IA.ME-wt{k3k9n9h86Hbs1 ~3&n٪g177LQ_.ވ$X5'2WaD L 1N#DJTt<7(%<(&s2<@ݰ7Oq +( `O=h9jRI4-:{אgD=G?3l=wnxFz:NOL LxJ}q5j*Ε`8 SQĊ84QR+jC3獁y]wΚQJmafJ,!5p3 @d ' EAH#hE~l[{`!-| H\Hgu(s+XM<^,K凔L3T sC߈q[$WCV%EjK3WC6{_Z_f^cUoS.9PU`5E8FzElךMUqz gQ:0[ +_=p۵2^(kRBc#z.Ӗh`SK投U?-#MI @(@qP;<,k6&6B>! +fzIEH0X5}(>"kMXhkq:lI aD)bɦ s+~i^'B[SCe YLU_"+閔 4"+ e&>VA 3-ݧU5w;#aX(Dz|H\ :#zOnGw;rp{]~GCYrJĥEb +/knl8݉D` +7ynW + +endstream endobj 5256 0 obj <> endobj 5257 0 obj <>stream +HTAo0 +S=BڰU7 eyT=>18?U d7;չ8)Cʎ=񽿴qfUUQ+^̣:?Ngft[OwqӪkek|o/NXO{/N?mٺη٩JZU64%>tJuTN މ&p!5)d-{bMҧ;/ =- I=xf1E>{O:7O;|ad;w!iax#ÜP?=P5z5! > endobj 5259 0 obj <> endobj 5260 0 obj <> endobj 5261 0 obj <> endobj 5262 0 obj <> endobj 5263 0 obj <> endobj 5264 0 obj <> endobj 5265 0 obj <> endobj 5266 0 obj <> endobj 5267 0 obj <> endobj 5268 0 obj <> endobj 5269 0 obj <>stream +HVn7ͽZ"ˣ&--`}pבy%~{rծC l.9<<3sfߝ +n;y1< "brpőx$+2<dM&l턓<{w%΂ g\j-thO,hU2"5.SrS_n]6":LX=:fϙ ];Wi]fPA?V +q"[9[go8Dta8g): DhG\LrRƽRd^& &Kd \LQ4⼏`eIh abP8b +<&~ HB^NEc5ǬCkGEc *\?:3}Uc3NyhrVpJ K^X^6+TVVHe8ƒ$bF5ci)ZM+L:D_@WDKՌAj.]hTȸLAjR̠јƶvF7F9MIC C>AVJAP7t`!1QtɌtJq:ZrzA5,&Q"d6(QTQ_iݾO Fy0񷁊%E߸_,{܉P k2~̳{r<喉BVsKv4]nh5ժl9iU@+E_+\f5]o[ f"n?Jfh-WQdfL$nm.H|4wbι)=HIJ1O +Y2Je{L{&n4FT _Ȟ3taEbc<#Tp%*rwwZQz"-}:TNjax@0 ?C׋g4Oӣe$ibf|8m>yph735:.Wg(x"2nACqVOnw]55ӺU1)j+ж +#01D:U&aTދWFtChR0<'Tkpv`K 4}˶b(J*nMްõy>~vz&I޳=Z 8Knh]$*F:v4}Zϼ@I]$ +jUSǹ}4Tw0.^&S_.t-HXLᤸ7 7^Ej!` 0dMdS.46(9ę5J7ۺɶ[!!J& Lr^Qw(f/ ٷüt>x/%7N9#Lc9^ʝ`Em1xP  Hu[B|$Nc1;$b$o+>#`!e '3uwMR>Vey j "':2u2xToK* +endstream endobj 5270 0 obj <> endobj 5271 0 obj <> endobj 5272 0 obj <> endobj 5273 0 obj <> endobj 5274 0 obj <> endobj 5275 0 obj <>stream +HVۖ4ۤ,JQ)ET'x$k!Ld +P~g~Iȱ=t-EY:g}ɯɏ#θC2ٌtYe+g,Y@&rCdd'C$uđ̧5L`.'Xye u8#߸ r%5K \g$CN=A1@½S A!w#wg!ApTK勤|={9I/-񣣏' +j+ Qm]8/ ˯%_EڵKm o֜of~8 +14@?<^s"+[rVA? Y +endstream endobj 5276 0 obj <> endobj 5277 0 obj <>stream +HWkOAE DcfKwy,VTl?~;kΙ3{:lBQ±{kWo&CL`&5Mrsjb7v :zC桇QR :u/ɰH!28, QG'ݣǾu{Յ[,+ɢiD!ôBEB@LHhzD +S07Jem{gVPr+a%f> endobj 5279 0 obj <> endobj 5280 0 obj <> endobj 5281 0 obj <>stream +HWn14AҖ@KqJەXX P@ZĚO=#I')HEf$_{NЋ;] +DݓRa(t$̈4>5H׌* D6ˎz +BrsN*9/O@0)VJi0Jy?7NWh^Zi~㘽hi@:DDZF1`. O tynA +"$#gzTԃbz|*H-i #nPsu-2|>l JBq5N[](\ !DzΒ~lL&0V :Z.y8c0p`nL"OW=n ?R=Y'sVWBjZC_E* +2 A黰+:(t8"KI6@ORܫ~Q Ϭ-cHU3Q|>stream +HV[OANLx) $IwWA[D"` K/{붻奻mgΙ|R ­ +^xYY )g!<IՊWy(3;c Q8 ID~an:׷a ^`RyDl2n Hn q \ (%-8S@VLr #vJIdh,Q|A@ Zie 䔩t58E>stream +HW[5F*U[ T!xANQ%^޼@aV)N](;3'l^s9wAhD cҠ Hnd C=AQTG-FY7`@0T +`nnk"7ǯEĈɵIbyj7[85ۄ}u2ۈ +F(e*L,pb5c4K7lZm8r7ril!H,*Rx9P<]JH1z PVYאIL?ELrĄ44+wYxe28(¢:\B4y~&n4bVq)r`"1aOh4l1bufȳ$`D\-3m- h&z+DI6UA5cߴߟɫɮ%;\(n.,|^f(o3 Mַ/?]EM6Qm" ϡ$]4T7>~ٓ\4J0xw7uu#2ޜc{Ml'nMCm+Uf5_຀U,~̎g;#VKB3ؤS_" +9X"ntH[˕Yҵ+ARR.DuBxW_s\fL>ACרRcP?v'*0M׷^yMoOڝPDt"} -YK/Q@n#ɠ%ۥ l_ gSQУnT"Q> endobj 5285 0 obj <> endobj 5286 0 obj <> endobj 5287 0 obj <> endobj 5288 0 obj <>stream +HTP1n0 + :HqNd$D;jJ 2<ܑrZr䙽0B2N~fp*ĵ+ٌ:Ln"-Zȯ4"/Toj ѐts8"EP4`rQe Uw8m5 u{BƵ77⾩T*"hfc̜̕,O >dO9dh + +endstream endobj 5289 0 obj <> endobj 5290 0 obj <> endobj 5291 0 obj <>stream +H2233s8bUDPc&r9yr+r{gPRTՓ+ ɒ +endstream endobj 5292 0 obj <> endobj 5293 0 obj <> endobj 5294 0 obj <>stream +HWIoE"!Y΅\U#OdP$9a#44ArFp@\XBX¾T4]=3ns{[}춋&\+>= +14xShΦyQ1$KاDaF"^oqbwӹĈ5Z^ךHT_((%Lx뛽u&cۼKsN^lUNė:joq5z:!ŻW纡h˽S}&4j \Ǜxi.8Qp04~R0N>)̛ +a'i񃰒0=ct Ee8kӠ'@J`;Fε5F$[`Y:PM1oRWE%F3$j'r+J0C,^ bNeh_ o <ޞY}neҒ{-ҫr %i +xލCt.˼#9[Z;aesqkK$~6*̉ʶ(A7[b\ZCQz_8I_e'KFuW_vſ@Iz w|E6)X/gcxB̎JQ +r\L cE'Dg{i%JR[QaƇ錔 \ +Sm斋sXdN93T4R5%8u9?_L- 8~q TP2}4jΡM5k$y|o/H\hv :ˤ%okܑWInfLϿ\r` 8SCj4u^'VfE.h?DhHoWjdwtIg,}k~Mݮ.`]y[L=]::i*R‘y`8Q 3*6X8쯿t!bx= (=Χ,.:RDgH>Z֛Y_ ٹ թ4L~z+|U_p +endstream endobj 5295 0 obj <>stream +HVnEupB`LBdGHzѕz? EY,F"Ja2fl+c &cVUw_c^uWSsV!R¨ը?hWɭ{}C;=N6QG}$FI1ḉ9+]k&mL`R 3L$QŠ}fR)͹[44%jXer(H#9JQ>:8C)(k)7bz)VE"fSr'?Gtb~9_E*LU1xg~,wHI2w pxk3^Mris1GQ,s_ink5kw_`"ע~=~rRw֪ +HmVwy8XəEȥ3;鐓*d Z$ 1 8y殬VYBG8ͬ*o*Ոd_i9Xwh6^?$`fg:B5AY)/0(tf69q +Njn3[dhi*ugϼTAeaK7s c$?`c 4FIP ^&7h7|\#t0nO$N&>tk5\AmhA]3sq)/O /4 W$(ynn3Xn2} ⫋s-b^<7GD]h Wrhs G0pttw+$Ln~0U$4[&p2w\zwz/* +endstream endobj 5296 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km + +endstream endobj 5297 0 obj <>stream +HT=o0 +:8ȵRBU*\0(pvB@8k; l\mq7o8 Ѭ.fjH*n.ˊSm_&w-Wߡ@Iٜ + %t Y=dC>ݭt%c8"f& 4h +H)bbf)ܻ^Z%*v. +AEaFUlՊn?_:8yO3 &3-^͎Gğ`t + +endstream endobj 5298 0 obj <> endobj 5299 0 obj <> endobj 5300 0 obj <>stream +HO0$p?dV$OM^޿.|{{;!aP 9-5u?ލ(" G(ه l +#Gt"xI{KD D9QH '2SـH՗`p Da$D#/`c `#1YHj*ЏȆ#X}PяHla#@=GQ_aVN'0u1Ph'r`b*F[H -D ڕ5z̿%-vwu4@/1Hq)3pR[)2H 0Nf ج hفh'05䱃d:S [5.5Sb#rt5=@|!L؁o!5ώ951Lo[E-q4Ubt\]o.66 fKԣy 0u^ +endstream endobj 5301 0 obj <>stream +H=N0`W*y mS`9#x`$SAE[^~~,,VzCbB4 Hj% ǃ3z1 @?q 88(!/CP}v*BnDu`Ѐ>stream +H=N0T H^zvvT@"L@07GQr;Ry,=n!;jV?؍vPF1d%dXCJHuY:eN%QhG~e}*H8r47O(@vwflnN:󎦖u7]26$h7)\kv3} '.=`fr +endstream endobj 5303 0 obj <>stream +H223 3134)\\P1X.2LrW0N W4Ӆ?'W @˛ +endstream endobj 5304 0 obj <>stream +H;0i0FG.ےW%`q I :#T!p^26eXi?~{Wf{(7v>U}oуTN[%ssTU0ltrv{~wy~/-A&n0;&/tA`bIX7vZZ 1Y ]@(NaO[+ Qmԣh#u;q0U^9'1<]FoP!5ip4)wo_CJM ꇘoT?mAXd)V,ڏT!6# ɋкI3p: \$pwC9}#pvz(~< 쳰 +/9P8(^|L+$ oji: eSLj*9 ݖ̟)4S`s݉XMO;p }l*Am^lr/BK +pqu6XL^\,<>RQB/d;TB$B5"B[4DQ]DIQ"-e`RaRjM8}IQ+w * 5ZjC !1d?̴ l凳" +0:{9 +endstream endobj 5305 0 obj <>stream +HΔ0K8# rS`'@=z_G#ȁPL1.vۮ/R]z.=o*ʺKF~zqvu&[VME>Cl<={Y6t[~㳼1 !nt^(Ua+Wċ$2PѾ0PWHA,EK7{1//h#㚉gkV8G  &) =$@`i<pr! f^rA{a5EWPŭ ++@P k'(D}ey_נ0r?Mng +#W0<ˡBHNsimJ߸!Jvpke`cV4iW\P(@=@2w."ECP,-1b!C 0y؊):3nSxpÃ<0ʭר _i=~ЧP'ͻ9;U0㔆u OV5u{KҎs=!_{[ +endstream endobj 5306 0 obj <>stream +H;N0ЉRDrn_|*)@ (9BJQ@E iOlH6*-ERTlÖ?썉"-joԜFob/l+;6,{nyW,;_ex|a5;6A{m ܠQ6NL!ҖOAq*Yv)$ei5$ w-e0FBIC%c{_L;+V=p=\xX.'89Gސ8<>>yRwOպ g5Z173rGǫyG:$d;c]Tr.B*s*$:D0ReOT/O:wүIh'CY)4N݁SL訐aK>stream +HMN0eQɛ!kH$@,@nn#xE㙱 *UH$7oҾ6jV\۬u5P5|f-xi7e[; <mQk.EV}<uXL41[DV=ֈG&F:FHĢIڢqhpEAlB#C5C-buZ2:_Ɨ'xO^bQ8_17&pu%0rloD5{WK=[!>f*D/ 1|` 4$I%d^S!0>"Rq2.* ;bN眛:)~s9=_ )$ +endstream endobj 5308 0 obj <>stream +HN@ K8̅G`^@vv==уFᛐ@vl3ɺ& -CR"ELuIM4V|wx*5yeV('t y ;b- lNe&ݥ|~z3jO[B5=Z;4@ny~=GUIqGO Bl}D, !{Oi\z3*Dlͥnϕ +e cnmK`ܱN:}*ܻ݉ ? +endstream endobj 5309 0 obj <>stream +HMN0t3G^@ "5h߃BK 5-ku]7*UA"z;q'4ovy.d_VT%>}P^mp:%ꬻ$O/;Ā^$G(2r h 6mB>HNs!KB* ͘d+pc-9ܝ<9،Z6kcd+"4b~3xZq + =6ți=ON-7<;9>`.:r䌼Cf֓^o{M +endstream endobj 5310 0 obj <>stream +HֱN0^.#C LSLtr5QG&&ǣ(<cBm{_3:溴?ZM>!IB/qC3QЍ%aXO D8xqaVo n]zM#]4aB4`"i:eF%{y~}UJu+5nmX#Qb֘Qb6bG%1z6=f{TvteMU{ua@ݬ{S45eX5oF3 Do޾n3@`rň$I-d+,Fwg|N.c(T3B"Hj _8ml4JΆ""=/5`3 +endstream endobj 5311 0 obj <>stream +H1N0Џ#_vb%*ȀF|(Nڦ@%&"?ٱ-[עTUHQː6bͬ ?gƩT{2kRbq{A +^ߟD3`?d?b:}=.4,;l¥-2Mr ;]}:Kl&.pMl&16ő]Yf%Or99˴+ๆK;`t~* +endstream endobj 5312 0 obj <>stream +H=n0p#JoxP` +#h%G`̀b^A8AlgOe8V6F4M-~$ǒ +Vr-aj̍NN@DRҬ*?[f}}4rl98IbS\yyJܹc<+5%k:kB1|C:؁)f>ڹ >>3XUp; +endstream endobj 5313 0 obj <>stream +HJ0ǧ졐Ky؃'@= +* +(}=P',<4;d&3)dZ&!Dl]ʣ|UZ"d+Kœ(GV㺜]5qRFE" &K\Tե|y~ՙ8JC|@@!;@͸ + i(C#F_Ca5JbeLª WvO6\iCb +=++wiG W12'6j8`wL,0ϖS}G޼cH^fZ3FL3m| %Kk:L,BGg!8zHz!}`gb]`*aqøg< c}aZ<8LgoMt'@S[I;/pVml,)bV 畸 " +endstream endobj 5314 0 obj <>stream +H=N@߆d\@`veIVM0FkW(cpJ\ +wy,J6lDG:ʋ:ozPYOJٻԕ.sܫmkmZ$+o/OQqyTy&>stream +HܔN0u%j\)R H09}<!c'"uđjw+\e*xe&/8!r.s&ބJ%|+UV.R9|zZ$2KKܚi+DRɏQ_q1Nb#Ph "ɵ5^ȅU`.ja','.1hp]Ƒh`Z:0m?lIPVKeL!P䑭#m>stream +H1N@L 2,Xmj"V@--4Zbx9 2oܘ;Ix ^G*͕J.4E"u,)d\W2+@}ӫ^ވM) kD"ޜD婼{呰־z݄(gmO ZB!j&쀅XCkkΉ Y e,âIBU; zV6xN6H!T;~QQܸZ9a+ۢz>.U;e/_R?ݶZ;`i1MNo$Kq.{L +endstream endobj 5317 0 obj <>stream +Ht=N0&G/@gH H@%h9!Q̌MXj?of<ϓITѨR*' VޔME Ku[&>E˫]1ʪ' ^^E"J2SHO2i(~E'1]Ș:D=a_" `d،h E`bAG، @8Z8 k"Ⱦ# 3[l`rAo| s0yO:qg-d%U/ap4GR^`"LjLԌ#1E +cT\1#J!RMԨv0\EЗPcї0l!0[Â[WYFs<\l;tkk~xnG  Mvn(Hs vCY +0L\ +endstream endobj 5318 0 obj <>stream +H=N0A+MGX_'^'Q\EZ@"T()@Po)]D1HBti?.>stream +H;N0` ;) +Hd@#$h$dc͘!K@Pv?<2K(Ȋ,~Vp*w oP%x揘ӾeKzMTB 0 +endstream endobj 5320 0 obj <>stream +HO0$ v$OM}w'HzYWDuw_5FR2緋c!}Tm4RRfZքP/%uRHhHNjUɽe!_h*"uvUSae^k ^HR 9%Kq* +wϺ+'% =P0+d^)[jʽ (ЪLL L0$.%Lа(e>L@%S_B"HRQPmLB!ah)])bʁLT4m3AaCzPvխ+Sr]c ȦDb]! A> endobj 5322 0 obj <>stream +HTP=o 7q,C?Ԥ ]#Ruݽ{Qѝ:kw?3i +ኃJF-Q:;O4~ +ƞGo^7v+Ź(s*^{#ͺ?:2l4N*MQ5R^}KOn+12._DŽwiZ W~NxQFY#Qp==徉ZKUnr_Hw_ + +endstream endobj 5323 0 obj <> endobj 5324 0 obj <> endobj 5325 0 obj <>stream +H1N0`W"GȻ@I2S`БT愉kqz1D~u# ѢCY,?qa'G p?H0Xg9\~ +endstream endobj 5326 0 obj <>stream +Hl1 +@DGR ͚ DB:ͣ)-PόNNhl8et$b=qRuҒrjH-9MIkRveOvND"rE-O0;ЗzrgP|Y>o!* 7-&7uhaiC/ Fc +endstream endobj 5327 0 obj <>stream +H0@d#$/--H H􀴜x#G|$8]9WgNa;\ |~χ]?e7k67/u|;|#;b l 4cg̭$^8VX 8[ԜmA ,}PPkY)¢\q4[aJiVR%UB_[.t4d7APe E7ٗ߈= oW? Y9S R"MON3Fh%k+ GfYz\ P(5 QDrJNJ(Jg# ߼LX"9ڧ6HӺ". @u*t"M^tD +fR'9)lp#aC${4&DtOMxnc*"zMME1sTV(.{ c>wx9 +endstream endobj 5328 0 obj <>stream +HMN0` n8#dfE2j" ]yuBk8G,YL@HtLfїLh>^)"Q]l:dv7o)N^WHH撄1 W0/$&( +?&wywdP` ΅~n4dyŴ +1zEZQ`T*yLiZM9AS + +Pǔ>,.e[Goy8 OJLdg^JL.U"W8*/xnXՉBhY&=M @KBT(B{֊v4,lA# =?dWүi%KΉMFȗ +^' +endstream endobj 5329 0 obj <>stream +H=n0`p v&i CdZx4Gਁ GI4 P m=I|Ov˶Î]Ӊҟtcnp>?͑nvkx~?S[TvBϢԽ:q2&%"OGIQWtT$ "& @ "F$UD5"h'ϤuDM&5ONz~}cMT$,C ˛:2TxA\GiYPx4P@q!L~D*i,x$1GH"=\*4RϦzy kzn;KZVr&[t`ԉ%"*b5C.Rcsy jFJm7"d5B%'5"0w1MGq1栁XgSʽR)7G@"2~PFaE2(HŐH@ ,HlH؁ |\BJˑӿ > +endstream endobj 5330 0 obj <>stream +HN0$ ̍d;LM`'&Doƣ{@~?:D|X_-Yd֧=Dn+ɱ'ME5]\(b{}Oz^izN^ 铲`3[Lvm8m6L}qڈӂ1V[fv!+deKz; Va~*&tkfK:Vò}N@k`o$J~TTy%&= R +5;u)cfzX-q=梄 ka`eFM +8cSbq W+[ b1'e{b(ɖ*|Ԅ3c5:B̊sE0Si>6 D2fhh#m0|d2h:͟F/RzCmw1 +endstream endobj 5331 0 obj <>stream +H1 +1Јo$UV@--]LQ4# b!C*gH0M*g!j~YMB%O= ! +LjZA#OsaU %̎xʿLTT~/9s5  +endstream endobj 5332 0 obj <> endobj 5333 0 obj <>stream +HTP0 A7-kUKD9%Չt_+$?=[CK6<-ͬ8XcuXzTdwplwPB^p +ZV !2[$4`rQ2uUȊ(?@2gOŽY}6"lRc^ sdn _O'TW3h + +endstream endobj 5334 0 obj <> endobj 5335 0 obj <> endobj 5336 0 obj <>stream +HN0*\N0<&4EO.YF0qCύm7ZC[LL^9B9{q + >"̭Yv^e4NН?r~W~:]&[>X +endstream endobj 5337 0 obj <> endobj 5338 0 obj <>stream +HTn0 Ew}Hlǀi }I;eAv}IMѕxA&3$oa4 8`Nxd9.zH|N3{׍Jp!5X ;$pt34`S/퀐Dߟxz<hqк3tU7?SY*SgڠjҢzoQxG\`o!ނe+*r-\7- ֿy$ιXn%W,+R$XL# D-s˭9/v;׏Ɵ`T + +endstream endobj 5339 0 obj <> endobj 5340 0 obj <> endobj 5341 0 obj <>stream +H; +@E#)||2o~B* PK Ek,Ew`d4b==Jz&gF9Q:f,YH[p-aW +EILx_p*Q3$ZQs<H,%ݠѓkvڼɓa3jOm[i%<3 +endstream endobj 5342 0 obj <>stream +H=n0(pa6-"%UL(!'U8 +G@Kg6[D[l~f<0BFҭHDi%KqUHډ]%H#/),,I%&YvQR +㋂vox$d;ʌkp/>?^z1oc,00emZEl@#ʠ"(5$c5I#k)?&!UAڃJMIz;b:lפ2&ڨ 5DiVD_RMFCX…B"O)MEX4fq3=Nht~Eg>}ޥKފXӳСSz&`scMBv}Kjr4hvl-pfUUB3R@EC&M/(jHf׳~{WJ +endstream endobj 5343 0 obj <>stream +H;0 Jl>/@$2 G D'6O~ 6v2 4zFNZD.%#DJZw^q~9_#Q?lHo ɣ571~۰*_D +endstream endobj 5344 0 obj <>stream +H10 NI;!ȀF"8?X'o3 'O(1" >e% ~HgU\Gwgjٿctb,pG +%nܯHʎe{ijjji¦Ab +endstream endobj 5345 0 obj <>stream +H223740Q0@=3#C8bUB]$ɥ`ȥ"ABIQi* .WO@.q +endstream endobj 5346 0 obj <>stream +HtMJ0 + k5 +BЕP. r!.Ķ1)D|[&OnJQIY5m1MWuRu5ӇU:%Q~8EJ>>stream +H;N0`G)2TH()@PgG(Xalmt;'3cMUV\z79_WZ&u6_4SY\-se>}ZGo(q(gʶ7)땺[h#) +#@{݃O>hM>x7v?C};{:14l,f/jnvq NGv;t +0*m +endstream endobj 5348 0 obj <>stream +H=0Ǎ(p|&+%`rpO㱇$Yr}+E%rfƺBev 격ڡj;YFvUob*ݖ(F݉~'1 _|5\'~ƒ?XQfn7Ogd>9uEYސg_'Z 2],h-'-2踀NR[v3 aOyp5F^ؤa37'c |N*RX1:#KHׅFםl֛SXߍ@ȊJn8քg7yz@y~ggwm5pЭr -e]a_J8e-nܕ7:)5?yrvMs Zx[H ?QJw#W`/o +endstream endobj 5349 0 obj <>stream +HĒ1N0Eg"!Y$٤ZiHPqAt\+G\b<;DB:?TUQ*Y7\eYRWhA). %FG^XUr 6TMH-xfy#{ĺKf] ;Ka26pq xBJCZHQ[;AŚ(&H9 Hh\zٌδ_[3r*t8Kj5h?ftPOOMHɖ$["68LolJmS :ۏ [i7Qg-iȐ\s5u $z6 ]ut![蹋 !~ $?fiDɄَ0`Bidti~6$ ,BIL8\Į:v>f +endstream endobj 5350 0 obj <>stream +H;n@`!`ŧHUq. FRG#,YZczDITe #߲,&1yYRuPQ.*qRyaS2NJ J̺*dw.AQ/|Vw{fRm>4Ifxv!8A[;!5LR!LQ42%> Gc-h{NLX35\GoK1YB L0 V>stream +HαN0 `G*y &Mp02`бCuN98!fI{[WZrlg:o؆645խmVuey8y9_}@:-O'\XS㰼& b,z}y{ĥN, K@ fř`RQ L&A1-(^u!m\S-qPr8R/+`NGC.s +endstream endobj 5352 0 obj <>stream +Ȟ0 Dz_c۬f@F|?$b+wӝ}όmL$Q}'+dzҹ5-g)RtŔ0jBkRyA`kQK2D<4 +endstream endobj 5353 0 obj <>stream +H;N0/PKP_.kT@"L=02`Np"p]8_U[cM"wtYQ.|q]YY:[U-,=y~Votzsofxc[`>?^u{!_pat@Ɯ1|JC*3 0(à 2 0(à 28C84d!pgAHqN$ B2 g5C] ,./Fs=V~uzh-~7Yvl:CmoFg1)Ogh+Tt9q +p28k` +endstream endobj 5354 0 obj <>stream +HLKAAd[ jo>$f!aXZo&PTb18FZx%(jеjٿKk'jY.w.B"Ճ?> g +endstream endobj 5355 0 obj <>stream +HϱN0:Db'L +Hd@ `NG蘡w)$v`}ctMRgM.){ϝ\{mp?st'>>stream +H̓R0\q3ixBT̜:#3Zh(< M\ + +?~diVZJ]CM&uQE\=&2mꪱh&]eM^屓IUTev|VFfݽx|ݍ\IH?yh0;Q䋨RD &i'L3n48 ne fMr I"ZvD= 7i }Lm@H'tcA[h?_q9ĺ +mo[1[fbGKa7Ʈc';ٱbv6;`)=KUOm') +endstream endobj 5357 0 obj <>stream +H=N1 4 kHlJ +mRneqR$zd+_6O%8EO'jf +gg]LO_1BtO줸)V73VKX5l󏿃/gA++.)BOPu?L`d-X pf :'&l +l N!ǪBkKۂ^'eGnd٩ +endstream endobj 5358 0 obj <>stream +Hl=n0p#H^8@#R*SvЪh#026~_I*,)\cɓ:ό'i]{礩夔,^VVx>饣WVx|{b?߿$rHO#if4XI~fHIwZzҀ”Hl +AۼaHHS y/3d[`٫W-Li\,EŲOmOmӢj;J UIhk&HH@X#a +Қv+WF t#! w !&$(^ xቱ9!0@yNP[EVUk[]$-9'?B +endstream endobj 5359 0 obj <>stream +H1 +B1D9^ '_SZy7bx3cj) &LWBHCM1!VnBe7+=- ]<#Hq +5 +endstream endobj 5360 0 obj <> endobj 5361 0 obj <>stream +HTP1n0 + :Nd1<4]<4 ⴻ,Ѯhy#)n> endobj 5363 0 obj <>stream +H22ճ446W0P0ӳ4320V532105T03651 +YX*rr[T5("L s<Áq{IgC.}O_T.O??0z b`GCq2b.WO@.v0_ +endstream endobj 5364 0 obj <> endobj 5365 0 obj <>stream +HTPr +ɤ%<&@1c>ˤĮЊޚ/jl^!q2 +Qaҭf逓xގ =_T>>߼FoD +7huqd"ݫxOWPܿ^4N*N*:k<,3ap]&L&c +On+HjlP3Ppfr*.Pmޓd13oKw^a? R + +endstream endobj 5366 0 obj <> endobj 5367 0 obj <> endobj 5368 0 obj <>stream +H|=N0qG"y 6PL +Ht@# ؚ(>BFQ?=ۉ_i TզRUW7rwinvjts}`EI[&oWC)2 yMO++#Ŋyɑ?[ Q0D"ɰfl/kLȹtaE缸. ~oV7 Z@ ϺoзvnŒ]@s}`MVu3vͥl ch33vdb=FlN: ]Ҩº>stream +H22׳00U0@=S3S8bUXU"`\.'O.pC.} <}JJS<]szrr +endstream endobj 5370 0 obj <>stream +HԱ0AHnH\K9]jx4G@Kfԅ;ڪ1w.6'cdl^_qE ^su2]OuQo9ubV~4:\Imx̐l|1G$+ r'O ~߼ +5# )F?2T6y9ޯ̔f!Mo[O ;GWvnk'B S&| [X|9FB9GJn<*&yeu" gAN8q4.uw1AofXwy5rPwI|B੹,h~JD_C"-.{*O,g[[Z!}Դ%{6YR2IslH'zT# +endstream endobj 5371 0 obj <>stream +H씱N0PK!~ҺdT@"L<02`N-GȘ!J9Q+P|qudbcF&&NQNwI&2ՉT&fIWQJdcQbN3޸43Er~i,7RqJ[,+0(FX9lhbm1d,#,Za-! k"\!ˁ2l.U˴BshT`0PBjĽy]i=xdgU0 IN)ɾ@˱Y3g'dlf6d;۴;\a<J >.u^\^| +0m +endstream endobj 5372 0 obj <>stream +Hܓ;N0Ja}~8J H@j Y"9{<HHWx:K, +2EW.,FT]Mۊ<ɺ9UtYޔ҆dֹw%Ɨ(du<=]x{}7lg0Ɛأ-p\ϡGhb'lM@ɅM9.XGH 5|! dqMi[m!@8C#`3'đ~YT*-\̇YќHq2_ ua ?d11^#_aL6ʭɣ]$j16dɮ] e6c?UsHq0mw#u[%0 +endstream endobj 5373 0 obj <>stream +H˱jA1[n^=O+(xE V>LhvdCE!e +`lАf.:][/\FrxtFtB eZc9Bf>XZoṈ T`v \7@͓Q̱&ᚊ +&ZM;Bg%N,;f +endstream endobj 5374 0 obj <>stream +Hd=n@Q ml$R\DJ I"QRG([R (F237-u(LnJOޚgUּl}K+)ۘ}<9X}0ߏOfGOwzQ3]6R*B=3J=LH1QCŽipmcF88 +^xlu OPہ +["x ;zB Z".%B3ɡH(lֳ8c<&I[)e!D=mVF|V2 i:!'4y@#L؞ |EW s'R$?RK@x7F~$юvBI>.*ɗ8` iJBQ,i +[ w}O %L +endstream endobj 5375 0 obj <>stream +H;n1`HxׯY% e $Rq$%p\)GPnZc*Q@b$Kֵ +$ 35ɺvҨ +:ূ h'!R)QN#㵥}J}a@BKP2+*(_ x~"ӱgv^o|IbĊ)y1_ٖüw0b +endstream endobj 5376 0 obj <>stream +H22׳00U0P37164Wе343R03062ʥrrAā* P΃3)KrW04N +\ +%E\.\ ".WO@.Jd +endstream endobj 5377 0 obj <>stream +H?N02TzKw;L +Hd@j'@;2`n3T>D;-m\,b/6oZncpUS֭]Y y򼭼mk ❷W=gR6=J(g?{>qH<͜pS"H( +ٽ Np4H" " Dz2E{KpFh{3R?%?o +endstream endobj 5378 0 obj <> endobj 5379 0 obj <>stream +HTQn -;NB.4.P|IaX1vqR;0yj̸sjq~42oN!t8 v5I uYqjL?SrY.iuhbO"oЬ@UƞY9!#/Ws5.V*t "K*'cyTt$! 2` +zů6 cګrCD)Dtؼx$R0'ߝ8пƛ|sFmΑi'5ޔl~oG + +endstream endobj 5380 0 obj <> endobj 5381 0 obj <> endobj 5382 0 obj <>stream +H<=@%$S ._ j"V@--4ZG(0$|{*2D}@ѧQǚBfY˼n #Zmq;B uچkM!pm& 2DpkT +endstream endobj 5383 0 obj <>stream +H1N0E'"=$"-  *[RN.SX1;'+рc3Z)\_!$kޙ-nCΜ^\5ycǖx+V[~Y\ gYy!k18& XkV垞;,Lq=›k`|呰&k(xpZpFpm L}&O/# +_ A E,SއX9 . QH]>FAEg a YD IP'+-9#a/zcMkTz9܀?. aHZľ` +endstream endobj 5384 0 obj <>stream +H223434T0@=3SC38bUU"`\.'O.pC.} <}JJS<]szrr +endstream endobj 5385 0 obj <>stream +H|=n0Gp ^ $nVq )dkqEJW +:J'3$Gvq $yy2)ymRS%EU.EaERu,,7zQ,rΨ&%" +5iZMoMeye25]ݙOju4?v,@k=Lv%m?^%eZяd/Un NA!}vp +ܦ4"`ywm'e#ʰ!lˡ: 9o6^E$־NԽw,>zXʾSޱ?fN&h@qdI+V>$n6,=@N 9 #m\D]!f뒡?uЭuPtS 4;?dL|49w^^ڽp\ňfԇ` +endstream endobj 5386 0 obj <>stream +H1 +0-=Iv<::(:ެG;5V(?}irJV8ԚI%@XDVfix'ݩTGfY9 +C-'( x:wOG跤^ږcEرPc-->stream +HM0p,/<*qp&a dSi+M:9@e5,@~؍IS#vN~8V宬6]}_=6;:T]kS7zN?}TS9_3߾UۧeŷO察部.R\$΅Rj2'CX(a31S%IpJc#&4'(Sf]9#*q,UMl]!$SǦZ!3E"w+mQRiId&I(5Эd0 HҜ"b5iHqv`a#"֛fbJH4"# M`bMÐdoOP9mCos'ŤɶDcH#"ZDDb# z"HYL=k=c-k=nKj3O#;,$р^3w^1Ocr%e'"XHS +OSZ_hv:t*1)e;5"¦$aDD5Y@fp n g0C4 +endstream endobj 5388 0 obj <>stream +HұN0+"yHMB`+ͧ?'x`4RrYbUREUϕ]W|u#7x]JWݭ|xށt9:jPF4[ cx?h9P#CGD4;2II' A <  64,:dQ%ԁ.z>R&=ceiVͬz;̀)ǼIӸ/0pG ڰ1I`1o'a +endstream endobj 5389 0 obj <>stream +HӱN0Ы:DO$NڄNA$2 щ S 3T1w>ZEB"Y/ّ.,)|q*3OeRV\ydKTI̊\>(",y>ObՈVVHRHW7Ro֚o/=A&Ha AV6h3*^=L: +Z 0/8}'G3&EwRNS*JD 8фe^Ֆ>CҰvTTdPXo5W,o8S5wzm]!Z!7WT}굎:Ic,Ե5DNL;P(R{xh5fe#W$Q +endstream endobj 5390 0 obj <>stream +HN@P\ {pT$&Rhu'se0F#͗0;1j+]\(;-L FBI;W`HSwj:ȫwb߉ ȮdmD9Zdݵ|y~}݅@D! @6L rka#E68CBJ$i@={" (_i{Q;9'hoᏄr >'=pi?Oz˷ƱwJ//zhLР9lH[F? +endstream endobj 5391 0 obj <>stream +HӽN0>stream +HN0 }P)K!yڦJHt@@0oF%бCUc$vΥk[LbUKlZ0G[mWHS&8+%A^KsfSٕ8o΍UYsm^_TsGg=@3}C"5bq`o)$&K#+#Iys>:0ڃF-M"y|VB}"|T{&DRhh$0ȳk\ ! W#GO2xs)>stream +H;n@AH xTHN,RRvQ8%fG"]CiFHhu)T)=$iPt +cM'H{Z*nJ(G +/X5;Kl +`-HpFq$7 6f_Xly٪Nl_Lr-o_WR];h- iI/ LGd% +endstream endobj 5394 0 obj <>stream +H=0`! ^`eQ+ILZ +\ZMx*MlRFOʦLS˶5lkj[֧ͣOJڬ'T?֞^T51YᅳC:X?2/ϯFDxmE@E 2 +!GI2Y*r`dp,B/@98!g (nV㓀RkdШ`!3==yDx|&feAŚp~LΖɼVq Lr dָ+\A䋼t@y+. M@J>''f{Qxf\qT[pY,Re'Ȱ#BZ_ +̳>oQ8=[xE}j}N։?{^ G8HǀTp_o ^t +endstream endobj 5395 0 obj <>stream +HJ@)9G辀I6Mz +VsUh= +* +M|>stream +Hѻ 0P# +k[S!H%j dFHIb"Ppœ|W<UDM"#(QLYԆH&7 +PBu!y.]AXa`@PDx8m!=kCQ+}-ׯhs[,Q3r游 [67G>d~}~]164%< +endstream endobj 5397 0 obj <>stream +HMN0 # gɨ,Lt5P.4Yo#dA@:M,'-۟oҲ(6;d{UbOu]rs|ŋڡUZJ*YZeٍNdKoO eX=44z>? u?4nt& X aa9#h97tCh12 ;Sۓ_d2,A=@=i?B@H8D! 'Y'r!'|wE C|N!N77,%b^.ՐUO&i Nl&mBWZ14%[q/L5 +endstream endobj 5398 0 obj <>stream +H=AAN +s쾭$-$TRAqnTXBpIx ,)*x )N˅5-LPh]|u%)rY(gd-iU&}(㘶S:'w9ej撩39kϯQt+@88Żn +endstream endobj 5399 0 obj <>stream +HMJ@K&GH_dZ3qQ0 AWnԥEعWQr,RVUz./~re\â̊-qˬXpgN1[Ȱ5Zq'7b^_ϑh4D4F$Uh?TY#@J,f K,YT=@$YT*HEC*^|g Cno)Wc"gT\5)O(n%x7[9E\uo1 ^7>xߨb0+vO;YNY%xQ ~ 0g +endstream endobj 5400 0 obj <>stream +HKJ@Ȣ69B&阇Yʭ.b&^%G#d9 IYՑ/ !WNU~QT6gӲraZ7MYG}N4-;.mPvfűuub=u {(& hE#E=-@E>H`&yT<%kܷ]i:^h+U¼ժp񯿡bUߌX(D:IHdaES ̖ +endstream endobj 5401 0 obj <>stream +H=N@d\@`t+U)LjiQ8”}YI_TYRyu3]$F_uRV6ڃI}V٨7UJ0+&t4 ~zUNnTzR+mT]+' 0L 1GC (INېbTQ! -izfjD`rh!Xxs Cl G90ZbpCpZo=l8C)yGkpvؑǘ}[ l_+d7mFZvp +YQ#nn:w +endstream endobj 5402 0 obj <>stream +H=N0e%G/@LZȀD'Jf(9BQ~F"K >stream +H;N0RXr#/niHPqDFb9B3vcf%*G2s$WʨT3Zfr[XJNDukr2EE̮b|MOͽ2V&DS}^d}*q-S #T0bFC^H"e#4 Nal=v3x?'Oh9c0rsGC5cA4CPz>lp|5G.pyp Rf,\ʄNغ@8m?p.-uB ;o}έʿsg dv +endstream endobj 5404 0 obj <>stream +HJ@? %}h{ + + kzBofe!Gq)H|wa̰L%E>ff:/5O҆.!LXzEM)n(}0}ORZ?vF܍DOt`ܠv;N@䀣|3`X3sq>6J4 ?QM?k/BL k~ +endstream endobj 5405 0 obj <>stream +HTMN@ G 0tD&҅F׏qBe +de[V۬n&;WfU_ʽ"Ut)Po/ٻOsL`Gj\i|~ +4";78Nk:. L+9DNG pvL!VFLpTM\-V0 +@,@lt9&PBBǬ\uܦܕQN?ˉɕqL˜&۹Ic>du;*H|y̋` +endstream endobj 5406 0 obj <>stream +H10xz)-(a"AM`PGp4#024( <.Ȣ,yBc̗1ÂG"An0lSzB)i|ܯ(&wq^@2vvB5J>F;Eڠ'NNPH3۶#r&6$ +1[ Kkװpꫥ +endstream endobj 5407 0 obj <>stream +HJ0)a_@y&m  =zԣEo>>JǂaǙKg2)k*eƺ@4iۚqY|k^lR]n;,n X\P^Ţ秗{xX񏢊UU;ՠUoT_A#lWk4KcK&-x>stream +HtMN0tA ހ^@dD&҅Fw&ċq kKK̒>stream +HtҽN02DG_mNk%`dTyD<}:,nt4IgI&'rdәtLK9w/U.'|RUUof )ʕݬE\A*ۉݥDodzX_ u5u iA产GZCܴ5=U:_VmbV{}[5JXT9PdK#,Ȫs_˔ ++%=ơr OsjYڅ$H ^d( +fNukK&["fgQE!q0}Fؠ#QHF1n9ji!E´`>E8a>PdN{ ++vT`;PIӂe7pՎY Q6O)WOXħLP+ЗN?<-z-ůk\ +endstream endobj 5410 0 obj <>stream +H|j0_ ⎻B< `qVP7#x f%Z4X&bi%&˃ -1M EiBcEl+Ed8??ĸod_ns>8JI1 'A{j>stream +HѱN0: G_:!*ȀS@07Itss\BLx$s3Ykmuju +tUd}|H5iÎrO0|ɵ>lo/tIu__D g*/o&~, oPlN,KYβe) ArSc6H6_Kl+Vd%{F9{i?ewVNFpBlz +endstream endobj 5412 0 obj <>stream +Hl1N0EgE$79B|8vbg*)@I:A\,+L3N"E +XM&T8?5֨Y*c|4EdҹurJ8u﵃|xZ$ҕ"Vx{!H$K|Neyd% &|m-%|2+̞cĞKӜݢap" QY02 ʞ9q7ϖ#%|-Z wO +endstream endobj 5413 0 obj <>stream +HtN0=e@(-mY8DO>z3Mxnr 3 &::4tbZʣ:1_⃅gp6s`]{)|Oo PAv@9G Ys/o\Q*^4:UӤ6tQEU߰*ΰo#19;vb{vf3>'Dr?k.խH4#hOUrtpdi2ǧ@ 5ySp-| 0E +endstream endobj 5414 0 obj <>stream +H BAC!unR $A,4'ꍑ.V8Y9iVZfV,W^wX~VS2k}/VDo;!k*Bx]7X,,l;x 0r +endstream endobj 5415 0 obj <>stream +H223434T0P03437WеR&F +zf&@B!W!%Du\~Sr#\.'O.pCs.}0`ȥPRTGADՓ+ 02 +endstream endobj 5416 0 obj <> endobj 5417 0 obj <>stream +HTQMo +ئfbnC?R{G-IE_@M0oy3zlZ=w7= (|qQ(-[N n]4F#%2, pp'\>\ Wu CCB"UBY,VlTS8m\U86G3lxȏ-8 + +endstream endobj 5418 0 obj <> endobj 5419 0 obj <> endobj 5420 0 obj <>stream +Hl=n@AH `0TH"RRIG.] ;ݙg)a~UV١pܕ&2cVVU>ut{yڷOs[=fh"7;; V7A}EpK3@D'uOԨ9^jWs|tWŻx-Nj]{xo!{xo?Mjwx>;_(oK~`^ g9zVq(Fi}{KMsX&p^+87ym`͠ްXFcc_`Ai }ށ{ 8+p  Ο\jg gκ~F77y1 nC; +endstream endobj 5421 0 obj <>stream +H23401P0@=3ScSK8bU嘂U"`\.'O.pC.} <}JJS<]szrr+ +endstream endobj 5422 0 obj <>stream +HN9@ v^k{c& T<()@~HHt)ƂUz*.9SC4xo‹!5-}6P',32o+҉ԑv4&RuE`^X~m} +endstream endobj 5423 0 obj <>stream +HҽN0K#Ւhb; +Hd@FQ3D wp۹4HqeJ8:7AQI"LGM|I^2 =VOr[Qe o1 WȰS/F@0,\":9%!a D ;~aG:1V"Z D 2,a7Gi'?`_m{Κ9qnMsĞ< 0n|P6VJ,\hֻ .|8sx5yS%S7 +endstream endobj 5424 0 obj <>stream +H=N0G/7H H@%h9!t)V13"J/x0]lֵ.Mյ5MY:):d嫬vlbNѕqu6 +TAKm妿oϲU($@ + PJtD#"H8A h&JcHE"f (4`H'CHJR +(# ^E["Tm +t1'MH|R|"%|s41B($WD>stream +H̓1N0E'JM_'NW+-  * GQ)0㱳z揿=?J _+SKj2\I51\G^Xu[KTX +SIաAٶg= +yo/voaRF4"%K/p Q'ʂ d9ӡ~pܸ%8g44!MtЁZd#Ê YKOhp~@ kEr&B@Jъ1~7/unqZO0N@VH3Y}@bUv0K$1>@G[XJ{̆ MòݱOEas +endstream endobj 5426 0 obj <>stream +HͽA ,'AG0/Ό[ ~m!U>ZZqֻ&">`W!?sIy2jS78ry 0I&Ϭ4=\W%M]fKO'R&d1.&Z`1BfJA %5@TuN&CllŲ6{QA9` ?7qx`9 +endstream endobj 5427 0 obj <>stream +Hd;n0gE# /&B)r$eDI )cϰ]KO<*KTQe&URԺRIՔZ˲Nʬ*鼑oJrJۜFIM]pr N/E HO2i(~Ew'1&vf<" BcvX-cVh1=N!@hdz(` 0'} ^Fq2y<chdήx[s&>[ +,! d fRg48\9&;5?w4Дк^FgjOc?'w?z6!?VLb=@.c8 q߉g'A +endstream endobj 5428 0 obj <>stream +H1n0G!RJeN=@۱Cv(#٦@$^ vGɑ]ݦ`4ɩ"W9'Gdzc + =iQ6~c>7%A`P[k U9P +t-Yݦ:_KzFj`R jFJQc*>QR +%\(}j}kj/09WzPxQo5`F*JIyQJūT[:uW&Fǖݨ.]\(\Q Xu981p`["@ad=F$ݜLj%UVI9j˳V94^ U `OIGfyg. +##y! H +endstream endobj 5429 0 obj <>stream +Hlҽn0pG H^-~0T )mfN}cVZx4Gµ,%wN!O8qlyVT?Qx(ao1iFYQ]HF; UǸCc8x_D;AtSY+PoH2 %ނ5TxSLoL^C+ +D^9JSpİ&:0Gb0{sɩFע73gtO Ow]F4AWz_ +endstream endobj 5430 0 obj <> endobj 5431 0 obj <>stream +HTQN ԸޤºG=i%P;@p0 3{"{0j$€6Gw9 z;3.P׌W&:m&bʏO"oxȠi@x,싘x"1Ώҋ +N .Sh;V&0/X2:˄[ALFG6YUNIU3 q$%LjTD̋<م^s48xhY|]l0,#5w + +endstream endobj 5432 0 obj <> endobj 5433 0 obj <> endobj 5434 0 obj <>stream +HL= +@`2&;I +jihmcxxB|n,D`x#NH GFKXоHF<6ExchOj6х;jW,۔j1f?s>N['ЖhB 5ZB¯םѡr.@+DW4iI)\ +endstream endobj 5435 0 obj <>stream +H223434T0@=3SC38bUU"`\.'O.pC.} <}JJS<]szrr +endstream endobj 5436 0 obj <>stream +HT@j60df6 +;AOգEaB/Oȱ!eU;3^+vvonfWPӖOxf۲}uͱXsHŋr_m⛴_m>mu}6w#yA`iС $fݓ + OF4Z+U_ޘm4hdKY'q(N 5Ш8l(KSI]!IR5_)hg(ZZ#_L]WuY,:v-.W]$"XTD UV=j['M/WW~!*03։oH@0>G[#ƒ5gR$l"9_zѨ?f%!i{{v? zޠ +endstream endobj 5437 0 obj <>stream +H|ӽN0pG"y #"~ΗN +Hd@FQ3D1g%Җ. swqFHśRnKRXe\yD$qY*4.6RbUxkgtڌ˕x_R|(DN$|]=^s6707z6C\`O.&D g[Q/НY0[_qLJSv_-sͲP3Favn&pud45$d!p[xpGdo`" b3`7=Oh&4 .c[/v%`PF;'@ 팀%`MH e ccv8 A(Χ[mSvh]fG5xX^v;>ρtt `dl +endstream endobj 5438 0 obj <>stream +H=N0 ]uGxK L  & kU"X2T5v>XXpU"ǎuTmSlիOVU^Z=TYRMΛMLE Sm-+uRbsz}y{D6xLx0@݃+px{N .Ĝѥ??D)֦>G"eR4. ̧0GJ18vhŷ] Gtb-29])_b[/V3#{gD9f眺@;ӒZRA[sgdra\+GKYJ;Ie/o| +endstream endobj 5439 0 obj <>stream +H=N@/ %ـ<}V/'D+RX%a̕_+F24nh.O4jKb.!L4¬&4`-q&e.AF}  0YC +endstream endobj 5440 0 obj <>stream +H1N0`W"y 44U"ȀF17Q|,WҋY_VܷoX6+GMחq[VfOҮ-wu|a_Mܣ ;B#>0^Ut{xpwmeot.\*kGVqmЄLDbREٵ&йJ!brPߋiǂ.;11JHSC\[fB<$k@1a @z)wwt`Ateh@SS\8zzsA0)T_<' [#L0 :vC&'.@TgL$4z={%/#X% +endstream endobj 5441 0 obj <>stream +H;n0i0FG /ZI+Jc"@\v6zy4EGPB y?XF3p2uuh[Z`^ﺮ2ͱ9hVuۦjS#_W!*M$.79M/Gn3 +gd r"3WSx 8 2$"TD' PPp L3³f`,?@Ep+ŇxˈQŔpW_ն$5ȂZwf- +Hxq~-"L8 m"]6Za6teFhE yϰCČTDZY7ą=u?Ҷ.B3g p"qcFh{h?&Lm@w8tgD,(<*,Hq,ėiow'y/ 0g?I +endstream endobj 5442 0 obj <>stream +HtMN@]̆#0TV$~$va+.]ht ^ Exqǂt7A+l cyh#$*fHE/Y">stream +H=N@1Lv.0UHa@--4ZZc5H>stream +H|J@c.nf^ p^q5E[A.Z2]z-7huFɋݫ3ϙ$O4Y"mO,X]j=D$/q*l~._-?}I;%$bQ`s"dAF8@_G59kj&KՓ6KCK$o᠊"P*JlGEJ2 }x%u[oS1c4gj)x0x18F:a)00 G9UcU; 7sPG&2pb`1 gZb8b085aX*ZXGDug}Y} #D5itg4Z->stream +H=n0qGR#] SPzcV 7k^$#$P*2rT@wV7&ƺJmw(JV, V=7V_nďHPJ(ǏnuG;FybƝ fWE0#}oݙ:2<cq`3;_;pȜ<_FD֬zOV99>Mh73|%,r-~[%Mp~su +endstream endobj 5446 0 obj <>stream +H\10` ,DvI4m:`# CKG|b`l<޳G*gwV^uT;/u]jͲ\oMl;˶|߈b0[aJRܖ>˝X-OK|^_| vF#3p_<egK c@!b rdz$!B<a=eڪ!IVXe a4*B9W1*ýpQ zv!VG 7r@ې|'q&ܻ* égg`(=܏;t + xxYB /P$<܆e'`s}! o 4+7O%bQK%͚ۦ'{ˏ6AK"M6ëX\\~>B==0# +q)M S!6rF>/;F`D +endstream endobj 5447 0 obj <>stream +HTMN0"&2Ϫɨ,Ltԥ &#x&$^w]﵅IZ>ھ{2,+*Qib.".ebtSVIL=MeNE! ||8SOb[ţZbqk[u>?_Du-Gcp0 #Ct,-ai!{л}AC+F1taG #AwzN8ڬX{IYO0lyj%BE0EйN8ذp…th1s9C7#61 ρU]3h `MEW8D{͠z6=Uڱ %Hq`z]&pYTA 0}â +endstream endobj 5448 0 obj <>stream +HlKN0KX4#L/`Wf5ɨ,Lt5P.4q% Bh_צs[TieMYUYʔH:31-rU> +}RJu_չQjЇLOQ}~NyX8yZtNtYw ɤƈ??mec Fq(>stream +HMN@,Hf 2hjR5 ]yuB;SHƋp,HǙ7:Dc Um^'|$a*2:50F? MeI*4!|a[>mĖ7\D1[.`-xf+&d2%VHf[P\x?K9Apte +Frܸ߃/mlrT͖{|z#Kήݸlmbo~l_;9.Z&vqc{VcTd3wbosWiZṇ.]+qT/E0شc)nM)mg4ou,ou-JߨGG)%5w#lh8d7Bkw9gbln;4Ĺ͌W##C>*ؽ:̮ +0j +endstream endobj 5450 0 obj <>stream +Hl=N0T%G/$,D*  & :TJ%\FH vՒI|dQf4,i K³ f9O DY>r:HFɢ$=M$iNe$,o)/ "wA +8Vb El5RBLp-E)h댲R6^YiPS +?hFh=uǍr%wY)[hP7~6Ώ˸1~֮=tBόѵ߷zAGm͠c[۱g #UIȯm +endstream endobj 5451 0 obj <>stream +Hl;N0Y&G/@ޏ*)%Db&>BQa,=IuR2:+TQY~H L3&}Z\=c'jܩ&Zݽx|ݍ@)Џ43@MKq p LbҌKW"pWjl4g:\{jim>stream +H?N@gdsa3PHaPK ֐xd@IAv| ZK *%U*LUiuey^T:R½x&W.G4|\ҹ|xF2"e%LDȷ'\ +q0q:@>p bdhˏh]`L{J?simheڽw{OsϚezTq |GhEBXaQ=jĝ`nV@# +endstream endobj 5453 0 obj <>stream +H\MJ0.Y sBeT AW@]Pt'+g\҃ ]4@?h{/ɣ0MHCD,,eb8LQ=,aRaI/a-.D>M7bŅXl:p.=Θֲӯ H8VNGP:4 d}U9!EY|8t()wԘL{+Y?VJgM:uY℻,nFUc!qR/+7Xp` =ρmWۖT{2mub7b7# A+*H= /NC(4@Q>AiFlwƇKAbG_ +endstream endobj 5454 0 obj <>stream +H\ѽN0pG,y#/@kT@"L}`dX&yuzپvsri:i䅩g&*i*|5IT}( 3<`uIi +>o:/j۪NZ?*Uӭƥ}_oS0G8$a a?v 4Mb8bgcXtaa'l8n1u#x E2a@ aM xx=#dH7#\cp Fϐbň`СcGbD cEݷY 0< +endstream endobj 5455 0 obj <>stream +HAN0`&ؤ0dV$&0qV@] 7!dAm7j]<>& I2Uns)V 0FX6sB ?8`Ɂ#rnpU8#{dl$@¾j\/\fe$!B*xX" yɑ`v +3$&1 k[;0].xZP`4AD$CH1W NFJ_  +endstream endobj 5456 0 obj <> endobj 5457 0 obj <>stream +HT=o w~V iU)RB Ps΁Cj9dȿ/pU~p5 %̈ &-62gL:͢# p5w{"9?g; .,NW+'ּ9VkIBH@oX{%ΓhbN=IV^uJHL(ܘ(W;ňxb[`kb + +endstream endobj 5458 0 obj <> endobj 5459 0 obj <> endobj 5460 0 obj <>stream +H;0pG)Fr# J&Ii)% R$2El`Cl묶کCVO]vxK dʰ'\voh{1Ǫ tጫou8K%38$k,ni\Q.Gl#oG/G~m-ڀh=FBRnq 5!EkH64;h9-D<,؅o43Uk#g՛(W a"X201sjHSo[4ꀦ/h_Cix=iYB +endstream endobj 5461 0 obj <>stream +H=n0lj2 yB @RJeN=@۱CvqڀgRu(o2agh@w~QFi"b?$fIkH>H,(Ap6tʐ;dL]H6TDY@>HqKze͢[gб!k;1g`sԣpa!p5۪$R:1oFrӉTdWΰ~,3D\u7UO\<{uLyƢKUŬaBNJPPX‚+Gh[3I(CEhH +k=Eq(v4Jw4RBs0pFEkQ]-Iݾ?GҬ/+T VkH +D~P& +endstream endobj 5462 0 obj <> endobj 5463 0 obj <>stream +HTKo0<|lrMڻlѩF6d_Fpx,Ws8T.i_̮<G/,Ïokyy'swX\7/Y~29~Rƫf2C9,kֿG\系> reb]ԬL[8kO n}, a9vVp%98Q8'wx=uwn$(n''~ wn `;7u?(n 'h/p_+'=EScnע{'W>yv/s;K_gf +:g&$w`N-+[8 kK\oɏF;;ksY)FAgAwA17 cS ?̿oAo9^uZ֠A@,9g> endobj 5465 0 obj <> endobj 5466 0 obj <>stream +HMn0`fɛ!@3I3IU@U,: WQ X"O4yPUu$~㺪XźmڡeW}SVUpS6}5l(j_PqnzJ] Ϯ5__==޲XW^Nط?kji.LI4 D$!c,3IiBr:CRQH4)"T]2(?P47HϤDxOl 1X$dA_tD-ޟHd*OtA|nuJɡߥHEmcdttFL&l0H7$)$hs>stream +HԿN@k#p/PZDR5D'@4:sN +n 'wpgw(SzhIF9NH~J%7:ZarXVXGz<ÛWv]1n. nrNnqr|.Ь(m# yӎ)Q ErJJV. #{Kɚ[h$k~ءլb$HiR9 J)4,!wJaOxD@yU)RjbHS62y]QFR@K(&+eziPp'‡|p'OȒg%kv{tRطg.0:%1I':AF&0g+ +endstream endobj 5468 0 obj <>stream +HM0`W r&4Ӯ" 8du##Šh F|;=ao'v}#;4G6mt`ov܏̍N8!ua>һ3ݽeKq@wW95Ml0R"v%,ydVa`~ 0ȿI@B`°Fha%@ ܗMLs웇K~Dh/ap !%4|B+ 1  iVH`p8!v@@+6P^`r;D[XH+yrU&OԻX|l і`ɹCٺC +=貤$qú rKK!W3ai\7N:ڤ"Ҧ"&P:se sKtwq7G& FDPPM^|.MnP/ %Q'"! AI!0i`Yo/XLJw6t/hKs*!@aǐrvLzr#Z!s 9 0( +m }qoO? +endstream endobj 5469 0 obj <>stream +Hսn0A+ aZ)HIIL(WGQx_Ǵ?xզ2.v <3eK.D!o]QX,Ds(k~(վٱ)zLoZt}[{ݙmlaۻ|l8SlFڵ2AdaY$7Ho/uzb+X8n9ovAh .qsaqCpg|s0dz4%Z{oEg2kYls%wp_pX/1WOV^XO9bd_ 6Mmi\W7Wכ~li i|e|Cُo*!Y'kP $ '#G/m3==[x^Ca\Ϳ9 +V[` d +endstream endobj 5470 0 obj <>stream +H=0`G)" H.d3IH HLJ + \%W\01yvbg)q13^ISe]5?VmӞrhnonw HS5Mݗ&f8νݏ}Ûi[zxz{l{zyFC )pALQB%Lcc @juSpfs7v3Qv(+ |+C jO"`5sA:Vv+5H\T,%]Af<ǘEfVGO" 3;z^(J{X_FWaXl ӲIA!\Fa5#I6q'[ }:Ի@\ 2'J-x>stream +HMN0YL G+Qga+.]ht 7*#ELFb䓶> +O0c.8.ey|dloMa>]':L[va{]2tyx:\z8DkRz=xB; {n= p [}a +щ{X;L[u#Xzʳـ`^(zq7Cc5 +endstream endobj 5472 0 obj <>stream +HA0PYDq&*] ̰duXp-sp/jίʨ)ޤ/-m,yEYv᷻hRT|Wڊ QM + Vnl=Ewu- h{~}U[.ꆮ޿₮?kjbY *cpg-8&#$ÃGriN(1怩+DEھׄZ9z&Len Ts.>Ǚ=Dh;tLb*43ӶM+dVu! +JυYE* +>AqC3u℩1QNt;)qȍ?)͑c5zH2 ՟96أH~zmyCpC8ɟP 3w.!y`2V}͡f2Oo!0M@ f :OyT]9)(PS_FO !N7`/ +endstream endobj 5473 0 obj <>stream +Hֿ0_!?B6iHHt@V$`dT#^,G!J$Ի,4j/}yTܝ.mYGuSV}]+򍜛mTu +h<\ٽUj{xryM'ߘ e &6&ptI\X$S%Z4 &HP*~O(HI=@x4b^N1E(jI׭]c.nGE0=5KE$@\^t. ?lGsv+:1jnew/TPۉ I:f^ZL|_L> a>zȮFInlu+QOJ,ЕYi^l%_7MY_wE +\ys󙤿ZQ!b,\ +buMe7 +D?"h"}@D\O!='i Hӛmt%2k(ҬL;Ӗ :@藺Gleb듴Ƿ >7ϠtcDn"ۅA$zGD`~QV}ˇx8>+" Y04$w1U"o.' +endstream endobj 5474 0 obj <>stream +Hձn0Ћ2 y'"TJh;vhi|J#KmoPUK|wp(Ig:/4INEHSʢll}䙈8*8K~EĊTd$gK9PdsMyf{APחGR^%!TTH$˹HF!QX$G$;7JRUR҂.[e`dxخ'$;ѬJym"a`4 g +u`~T/AD6,Z_Pw~yKjc{ȑp.G$H_G3R1ͦ 3 +pzLK1G q!!W%# ZE +endstream endobj 5475 0 obj <>stream +H=n0`#H^8@0EJ[ کh;vh b#0zbYH'!Oy66fJ cI%AQL,qDcdAQW?p,IlA$^ug’pX>7\b)#Ul?*DS>ńZPZ#+٩H#z4gG(f}^'qY$%M&Y:Nk:z q/Џp)Săŗm!;>stream +H1n0/h0>xȤi CvڌڢIG rG O~& :E9~2'%)|_6B+~ԥjxJ!5UYF/}g;UVʚܩJ($B_m>q)+yeݰ\=;ac.iȢC" }"$"cMTE!U.Y!$4a*:e\XDȰpmЀ)4AZX=LG>CtF>:ooAMMp[頡@xBgI>=#BMN.}^3$R-'S&-uhuӆ-r4p^WA/aVe0sNvsf8ǭ@rtTڙ'8E*VKZ'*b|a`V +endstream endobj 5477 0 obj <>stream +HM0uɛ\M}I6Ti.FX:G7!G;KX1qďW<$eQe^uɯ}^U]ț}{l1?T?3ۼNW(j?~͙xd7ZyO:_}dW)HaK@ĚP b)NIB t#Mx}, S+l`Y,,} +NmOj/vO=:dh5-S=[IzVf@ϡDOesg5`<"#.GvY<^_\4>j,IU! ϣv=rv36 wX#d-<ϰ>zEBfbծC'zF5;O=?=8h=.]/yGO&俉=8/{GGF2U8xY +o^ޱ? 9L +endstream endobj 5478 0 obj <>stream +H=N02D#h4i~` WQzld= /Iswi̗I~<8X{ĩG4p(6 o-čuC$lc{E[]s?%h`)&}p' 8C"IME)D*"HDHRݎ$jt{o vY')d?B[)2L{K HrLQ!m~2jHň*3JK,Je!ce bt1.HAD` H*X 949eJѐ" C.B~.&vGFFY*ܮLs>2#XZCHj,!?ühD]j|XS0&;T8D>c_  +endstream endobj 5479 0 obj <>stream +HMn0Gg GRJEv$Yv]EX~4UZg٬,iIkCM/XuhÊFy Ǻ8mST}vnE|Kz\FT+2?}߾~'wDq;C\YL(h=@c؈_\fȳhΘgLaC9r3 w1bG89dj9{h0IqqgS.8ǙT{:0TYnUe:QQ#YAO3lbEKD%,a f{ׅ,6ۉ+U2N7e[&}?zzN#5m5ŇLdx:bj'9*Θ(q['}_ ![ +endstream endobj 5480 0 obj <>stream +H1 PF.,pszƮF$RT{$eDIs4\Gt11?8b&.֞guUweTBVcU ޝJ6GOev=Tӯm˃h]l㩅4 >\עxMw5FR WHH{_a>~=hX{R/-L/%3V`N !F ,m!@YE {`l +lOEѾ4U`p)p&=n^.,ܨ@6Xw+5{d=RErW?@@,2tX !Bey^JWxmeԎ[_L (i,K`fJCTolmO1P){~s,^HI' I*?cFB> GBt)7n ׀ 3NUuVj:]$ @CZa_Am/k +T +!L\[K} +endstream endobj 5481 0 obj <>stream +HMN0YL Ghɨ,Ltԥ q;Iko,[}2J %v,$wÐ24b g GK fmЍ-@ЧqL>-a[K‚{3"]cq$,9 pֆK0FDPp4W)VJ_A+mF{^.'.-3 r.,`h=_5ZsZZ=+5|ztQ3+*S^ҧ^o>`}?7nTt{q[7M T |!: +endstream endobj 5482 0 obj <>stream +HM0eaɛ!*] K X'GQ "%q&=;ͳLd+t7S)juQ\˩j1_x]<=4O1g~w +-Cwo_};߾~/k(HHHC4u &aD0Z"z> vH`$E2r vYn=U&}DD/R)}0K[XB"3ZEGͶoI P|XKu]J䆽tmJ` md0wP 1]ٰ4 MS);֎S؉gk'It +CN2yxx2Tr+7r3]V3+K2*ͅYbfž$O c%KTRcy*3xo|]`H)sx$O$i\ՙCj +endstream endobj 5483 0 obj <>stream +H=0`M_`3q2T * k(9KQދh%(p~gxY5EU{vמ]ڢ/Ux]ta9F/Ut_33⍏д؟O_Õ?2~-MI_1Nw_5]h"a#i'-]'6O d~a 0Bܳx2Fz"A$@,yϾłٿL}*:XXF;' (#U +e1,LdeJ"Qy*&D?WY$&}$xPDzZt| y 2 gW"v @F\":WC=YoN*tVF32 I 毭sDGv5 FH|7J'W#9bvՉ-L(I.iXA[wDv|D'ٰ׏(3`(xסH< g{ (%6-Jԥ2>stream +HM0੺䍏\MdU] X:WQ|"r86_=c;OTvե on4Wp벯ڡS&pKw}S}}jh{\yOoPmM/Ǚ_=-ew?hHe\қ&'$2 <p2gd"QsBa^ eղ3F4Lx&"4l +@"*c,*IW+_,8U3)2o.˄{عޅc,q.BY蚈,lw$ xPEafAH5h BnB:NP@̎LԌ{LDꙡ)m&&+\]Hti=cPVU )8H.xVOHeʲa2dՔ*ʳ)UT S^š +º/jhmCKߗE( 2kYC 5Oca ( +`R<.S*mM^!u},L}d@ُcquZfE<*'r·D&!%fd{?^Q8 +7|` +endstream endobj 5485 0 obj <>stream +HԽ0`# +Knx²OtIl))rU)J< +=xەBŒ=cLe추7UErdU2n\޺$-S^AǏu.?~wgqoN8ܽ%3q8O0/_c{`{O {4$oM<W>stream +HԽ0`G),#/pfU$@()@P'WZ~.?d;Q@."}3oUǢoh۲jxw,ں<׼jT}fsqjc{W7ve]6 l׻? ;5n;v{Wpyÿ~]^29HI9AV {/$`$Yy^.ۀ3>stream +HձN@5& pT$&RhFkH1>O cq&7cvYعA +~t/EH%~"/>Kx,TO1"O/ x2KB@q =c&ZƼyа\S͘g8I7&TVGa丝<`VpߣRߢ9[b4gE.!{'2ԓ"ףJ1 uVZN 5ZJV2w>stream +H10`W"y i4)Ht@?02`7CSN#l?uU?voʮCٞOwe۴C?3eS7QCUCW h{~?ѧ =>4k^7==>}kz_|H//Y6 )R{1MK1ha`b @_L9}\S 34a)¾#i~b8?^}g ϘAv!ʗݶ[Ymf-Ѫ2-L@f~ L$ TlN-r>' +fTl6>W؀a&n1>8e̍#(7۩{=E:=4-ύcU@+$" +#-+~ ~7fG-?NP.X1!^NXGP⭱{P܁G s[M +v 0H *zA }u/ѐ" +endstream endobj 5489 0 obj <>stream +HMn0p#Hp|0xfRgQ]m]J bI},,\_sĤTb2 7(HAX^ޖ"7!/dXq_SB9|>۪!drӶ+g錸C2-òyP#y|YBAP2m2<# +HAe e[g+bSek17 oVgMEȳ([錈Bx :ZL/jIv-Y"(&JL@%]).bxq% GIEPxk6(I!'PJo;5jcDk+xHMIΫ/*oڸF~77;q Q{F !ե?+ηK$4WSPp^ h뙰B jB1 c- ra'`B +endstream endobj 5490 0 obj <>stream +HO0Ƨ"7>B}mHUHt+,Y`Šk(^za%OҎgJ),4VVvwة}SvmUMUaSunUpVMQ?ǓXSf#VVVaN2kzĿ@ge=|`m tlRi$9$ފ_ PaM +endstream endobj 5491 0 obj <>stream +H=0pGG/d2i)@I:YQl8(9B Y1y/ X? +14fMl;cZ `ݓ'$,Il.AyVK7&36#}OsE9 -3N;O`yM`+xtYF[XdsǴcw~TqSݣtۃsrc"r;A#eX28'f"qSswX"p{gHfD S{vS!g +XKzmsrhhzguַKfV\ihhBCo:ٔ7۔ZGY7ȷ YqXa fih N.VwRϏ5)g0 +endstream endobj 5492 0 obj <>stream +H=n0/b#=!TJ;jgr4ƀBlcǨV.6'4 IHVA<%!_qU'^NXH?ka>z8<>M[Bc_H`.;U(*XdFL,WJ3ON3'nIԪEeVuZ%;/Ū/E&$4Sk몏_=U?j$+uP/UaR%<%Y4pEfI.N?K%R >FӠ=O + +endstream endobj 5493 0 obj <>stream +HN@=G`_e)'LG5z߬޼ȁ"t)I-iZ=عegAxx΢$nzB$t#{ D,`O,]1 0]zŁjH~p6/ίNy=.=Θ5*5ƲȈ* wSDJ"D4z=KՆS+: +겍*z"U;>YKӖI;QMB FHh-~e|0~Q쟤yw4XI',k*)9}[Q'cI%KA;"( oTLHİyƮٗP + +endstream endobj 5494 0 obj <>stream +HV;n06:xkժZIl |ev-#d!8F)$ΌTWH[M}uߗ*'2ʮN9p[Pm֟X6}w[8H ߒ#o>#5_>K/ üwyUv)ACgRHzT֣%2]B;W2م](gB}(# lⷡkrc0'yMfqPmeC۔\bKJ)YLU3Ey/{wЯ,R C{aIXE$a'PXL59Qo܏p <0ԧQŦ7H&>stream +H1N0PW*y 4k8S`9=Z1b` [$n_ hҋmPByHfQD'IsF0Ә%{0?|sҎ%9KuB;ppE٪8X\<><+u('Hr2yN'4@%ܓ #ҟ۽ѲDQTfNr=IR_* w2ڙ*pQåö-MES+Q?$u]Ԯ/JjĞ_=cW)x}b%;2 dwz۴ _YKL1H Uitη#J\Q9Ⱦۙ$ .W f 0h-ׂ +endstream endobj 5496 0 obj <>stream +HMJ07tQȦ LkUBЕP. ӣ՛Yv1$v>:Mq#l +xE4ߢS]$oR5r+vͼN0E>&S;EKPdRiR7:cb+DjOn;Ⱦc;H\"7CaU +endstream endobj 5497 0 obj <>stream +HU;n04J#/§B$RT9@2E^Q|Jb2ocN8hQ&ぺ(j@ bXT C1ՍY1& >C}EfZteDxb`46zzmK W0F= È<ySz\dR6Y=RUmwո2+upƈL:;3v,u@k,RNX ..JC5V\”5X\Lz-s)js)SO៺բȡE0.RAؽ,]&΍p bηF̃}Asg_nh +endstream endobj 5498 0 obj <>stream +H=N0Wu_4*F$@07GQ &0K lڮ*OoE%)XJ#Ȟ=PoIu?, EbٔΆlmB滿t[*G)%] +SX/ਓD")xB@Q0cRe; x> *`CBC]%!HAMa iI+-Nмjf~U?OOf2ؓ[đRٰSc3v>h +endstream endobj 5499 0 obj <>stream +HM0`MP_`t:*] ,gurp#xE?b'T*H,>;/s>stream +H;0`RG/3fIi) kQPrK.]D1mPH~eXІwݡezյөa-شまb [ +fןΝo|H_ RvSo? /軷_cl/-rsD2 ɬSO Euf\k9V{\(۞yͭ +O34X!Nb!'0D?|#RpD4^A0x'1lq\Ő#?k^1qHXCJ$鮶CKFX?X4kX[(X[ FGUE (7wz9kj=Emgbk_GaU"|Y>1:( /G `s?V$ױ?Akmw5_0_ 4@ ~ 0ۃ{ +endstream endobj 5501 0 obj <>stream +H씽N01TG_iOH$2 FɣQ3D1vcljH ./w3FE·9E̊TTd1ˋ4?_8~"6+{g ?X̌pw%)O9Sh{ZX)E@:֡ Z-ɀ[}$PH$ =?$$C$ \F; r$ <<+z9tՒpezzƲ; +]@HHNA6h4Zق3Ntא"ͮu+ɩHm05#kb418fs[")֧YFJ6O +endstream endobj 5502 0 obj <>stream +H=0";6/&~RYZ@Hlu,Qpp2HQ=c; + +@ |c{<ѪԷʛK[NjePu{NR8kujۮMw{qȗoU^HTc#wI%gXqe *Ns 3cygFK\ciJ'8:b8NkʏUfJA +8Z +*<},+[#i5rO48D1+,P$MɈg*q]OI_60ơ`4%#H'q +M$0N$'q +>І 8@2q(Ot }oJ^1o?&ڰ#fMˁ眮 u u5j>stream +H얽N01TG_I\|L +Hd@FɣQ,=D q> qK}_y O4c4 2'Azf{0ηqԲM 2y<{9{x;^3o/ay^ߟhyIQSah8CuxIсZXBHP\1#κ˻¦A| 5 Yѡo1BF1Pc DF sޏw +M%@VzAs_#eضpGz=.W2Fp9s,^ 6k蹓5*d^UU ART#|D}B@? +=~:3BVVt U5@co0$1ئK"Qgo* -T.xRd>o^~ +0b> +endstream endobj 5504 0 obj <>stream +H=07LaP#XX52&)IL +KƎ'6 K@"~{XQЂ6y]]Mo2o=e}*yڒ>0\eM'n&/.mU箤한>PrzCYu!ww-Gr}E߶c&(`Ogl GsL9c[Q.v6pN# +=c ?d(\J eF!PhY1OGQ3<:^YHܿ#ꙥST =HD9xpk'|f ÊED.u6ώQ +:ۮ/~S}6u78 +C@[`R*tԧғ8N9>stream +H얽N0]uŏ@*Ȁ k,GȘ!Jp|TbsvD|ͣPYeB"E2cRY} EiO-sF,$t[=k.dJW g {}%&棯gt0R#M]YHO~Q̐lwړ~4Y؀ZR#ryX5󀮣 Sn1M,*ki}@ iV?m0u?&oڨXz~:@NA3fw5,  ؏Ddw" + *Dp]! jTS-<0gEz€?I 7ĶI1EGn +^~ +0F $ +endstream endobj 5506 0 obj <>stream +HԽ0u-FrK?bw"0565l y +endstream endobj 5507 0 obj <>stream +H=0`# +$7_`;I}jeg2+͝𘚾 +8HqLX{g>{THf\'Doƒ# +͸2ڌ8S-ppQ +S8.2 O".l5Н9U pH}  ^"!a>stream +H=0`G)H6/~Im"- +$8PR~8G*>J&Egb"DH)',yɫ=įnթMQVEUM7m]qn* oFn}_gzxE]3.n_<\"(B$&IA Q,2>!1>2t$XML`Xd$}$C_*tJ!NٜF*cNq$ J +2X@&5,2.̔.*&!mCt'XT$2aAЖ."UۡEqwNyAYQ$\e$V2 LCY ;&r1(7,dHCTLtЊ#@Q,} 518cn<6e͖í(/L<*≮܂`L =3cwQ2|=0sSX6+# 9$] ^f7ǯV||t@Ԃs$_"Y$-c [._~3}I 0< n +endstream endobj 5509 0 obj <>stream +Hܕ1n0idEG/Y2e˚ -P)H;fh"q+h|H:QAP|Ҕ'MQj'<g&)*Hmٗ~%OML*d| O9{x{gYJr~|JijU$I}y4I}t'kBF@H&50j":jpr W&@I,FRXjabbр`a$'1rt,X?b"O:^XS;DFN0i[jLchAv?;5wewT:Eyԃ~:@TVhvf+* u@ZDpdQNh=̤+uDYִAMP,"2 58!/_%, ˙LLlj$c+X k]5·9~IOIaj :7Ȁ؝asM݌Ӟ Ez +endstream endobj 5510 0 obj <>stream +HܖN@ /Pw'MRRbT BCr2Y1B. 9U=ac$`0[;3Y-{0$4 H߲rF cLeIJVW[cY0í%j"" .&$YQ!%F[IOMAߍ; X0cf&%IZXwXC +IM=SSKu: ++W뽀!#[m>͖!xC Ĥ$!DRMHͰV ugd%Y~ZTIRb֘UB7dClz^) +endstream endobj 5511 0 obj <>stream +HԖ?N0]u%G/4IS`9fb\%2fbNm@Ub0H@V~f,dq1I(ʳ܆#?L<ɸj HU`z{)q$2\aT^;\bιǥ$UP둖|IR+2Y᰽+/"A؃T|@ʶCEP!]s1<-2|84|'LW3P0bkCrʄ4iI'bu՘gu(rHCNm17tO@L}P7޿5⯅ֺfs dYD'+I@@UդV9н)"կflrKNC϶j~zg4Yu(̿NWQI23DGA^Wu1l + +0 +endstream endobj 5512 0 obj <>stream +HAn0`#Hp|`4I"R,hac*zn^qgx`G(R+O 8Wr+%n2&YRRJӓTI<ݬV<]7%*M׳|+}=_RnI׻wBQ|{nƑEHˌs֌3j + +"e!@fR3lYf9 fdDmȱx'!jfl/.ցU"B HZK9%RC'sv ȡFK:j.q,jX@!}`XG2q%vm,m,)S66.U 'v>p_)zc!-M </o w\o +>T +endstream endobj 5513 0 obj <>stream +HԱN0PW@M8i)R H00GfPipPAP7Dy((%D<#,8)'f38a[']ʸmn+`7m^qO؞XA7XAW߻i?[ +v-^gv[[x* +ZV=rǎkz7Dqo}{~Vy]*/꫁J|_qJ +endstream endobj 5514 0 obj <>stream +Hܖ͎0PɗҖ][s6uՈ/weuK0"vD%&tLƄ(DDukD3FO'=kdHȘ[κ q\QyL #Cn& kLr$ҎDf  ]"X;Kd1%؆!s J=DC?QV2 1W̥K 0#R89kV콍4'"3j kO5峭;rD2"CҀ{"y#bnm@ +&Ќ"8R`ԎȌeԆtk˴'"8;ä7-kJ:Z |98Ů.@'| 3ӟU=I2dh%HsmD'!z?oEoP‘i[j!!*et՘NtߡHetfWD #D/2㯮%uI +endstream endobj 5515 0 obj <>stream +H1N0PW H^zM8iR H0q`d FTdwRTRc?esc'apg7tRqR qT.K;1'͂5!mBx6Ua֘ 8@+ KrVݑ؄ |P$S $ՂYLn+L +bX`VlF;UU=eǫ6eSfk3Q=rJՑ=xx]=ff\&Q|v4^>}5KMr͂RJCfnhHin/VJS!=ߖ{q{)Qx th7 z+-{;DP +endstream endobj 5516 0 obj <>stream +H=n0'x0:x֟#O;$hROr 80b% +}>|1l|9?+x&/qqYY&쎕i>6*K ]3O|Iv67јmW;jGֳ+:%y$YO-캔}}+wz{l|@+4{}<[Gk +endstream endobj 5517 0 obj <>stream +H=n0`G H^8@ R*SvЪ|ȀL ~"UҲ/z~6=`JYu'URRf 6',Kv6#/x%)M l]IQ%s R^f'xsMؖ[sC^ߟps.Q [Ph@ B'IM-P G4I/!*gƾxt:nDJJR*6NjPBNVmeM#_Q +-C#ƈFWFN`z<^'gZ]QlŠ\RTA߾ْ:;_4]/kuP~9UY?5kL;P]Wy#vtnz=*I͟p#' DV9tH8L2rހboՙ"_*NU +endstream endobj 5518 0 obj <>stream +HMN0NfA^`2oE2j" ]yuBvkkp,-?mbo}%(vFIQ ń QCtL|' wHH{y +A>-ć%> afWf琏ttiAIiH=KӅ3"m!yPMn8fHB))E֥2Z$]9bR.836~Cs]l1 )t$}Y*iK'cDh#ISnLl[n Tz>] !!?&\faibˤ֕@Ɔ=d'~&!!: `'g +endstream endobj 5519 0 obj <>stream +HN0ᰤ^d}* Ɖ?;P4zq5 8r ԲR(ͥ1arIȏX:h~^Eȅ0f~pMnE% o% ',\ŹxDdq|yBYl.ٜ` q*̠ #*0U07ZST:&ݖTo? Hh:ug? +:U$Ժ$5#ginCA5:4 J %qZLT j 5ZL#׆Wɳz)ugjJ +?AUJ@S)*wW4t|C@9yO#1Jp iۛR'$_Ӌ Tn.ĨZLEƕ(Ƶ瞧QBo${:C7=D]>%NQ94b@o $(Պ0B6|0`fЄ +endstream endobj 5520 0 obj <>stream +H=N0`A^rIdďD$802`N5r;?BbS5vggPcQ.04+ +S\>ºHu +YGU ++@./+tOpy .ONQKrgσpo<Į0E-bP@@H 懶=iELm35B?\T|A@mOpWH=jf/i~I]`/YBm!"4tJJ/B$@>҅¿W%r# Z&r"}U)xձO"[:)%8e=!9tEdEb逓l'1S\U2],$F,5ҪeSU7`r3FtW0a~e~!s+3CLkB~ 2l|?jFG:|:A)V)5XϷ~0{v +endstream endobj 5521 0 obj <>stream +HO0LHCY<5q&.4ą*ހeJ)CU!N'~?ZDIUYR#M">E~2ɏiQuw*XYA^-O>HȧÛ\5#=z52JR##Lބ;y slM>zK7!nb( +9 *X(9dnHe.S L-{S ư}z;k54#N(M]?ȼП[ywmѼpۜߤ +h(s@X FQvɎ!F[F_2C;O[6y\hNW_FVw.kp2F2`WhSqPe `p ˉĀdOfSߐߑ c0B˶X_~o噾C +endstream endobj 5522 0 obj <>stream +H10`# +$ef*M"eHI$)S*Ar%M q116Gh)JO0<^`uΏÞ˼(*=ӓ*xx׷}UĦnNU^EY~=gл3}`t޽doׇoUk$»$ )H' (  I1V]j)q4F-NT@3mdcM'h'9} 4cA:ې7#``ElKh?hVhBJo (S/H2d3r@N GR}sʎsW&k!]GplB}XίQ.蒚钚']tYX^pT!/P!)Pd@j }58>ةc`d2`F1^R̀=y5# o`53}O 0#8M +endstream endobj 5523 0 obj <>stream +H=0MM_`3f>H HLJ +ԓoB`ƅ`??;~^f:Q_~tm+ZoN~/Nc/N}s8]/Mo^|7~<6GwpaO@Xx ~]w۷|N k JaEqb욠atJngV#EIqN>3R\BnFԸ`*K. 1Ò:ʐ9X;jd8JNf|m yEYF4_+ݮ^^! UT18md,8cұZ%] +Ex5:VXh@wcUDgwp*e#X#544͡g!^q8t~0, +>Eu-pv¸C:&@"B「Ey.̰^n|X7U}}.2ѻ0VVt 0 p~uDwH=U:/Ȉ`jV:b&Jǒ +endstream endobj 5524 0 obj <>stream +HԖn0` |Ȓe mz(Ny6cl>E[WgPG:kRwDnNCujcGcuX-Ҡh˫zn [M#;=i ڽ'Mۡ7D^?oZ\Zpg9Qo%1^ KDHܶ!TXer KAՐ9ޒ 6,)6& +OLdKf +bGh@ܜ 'o=&7GXH!:}ְR^0XVGBY>+-/|id93"ߍsHa}YC^?{^> h%bvNq<>&s'*M)hQPn'14"LGd#Xүd/uAV 9ԙ1KjJdtQa)Y)^Jšb]#Q]RuHc_|C%~b +endstream endobj 5525 0 obj <>stream +H10PR-]|L iHPq܂!%M,qSX?l($oN=?P~#nӁl#o +!JCQ||F0@-{?3}eu7}tw~~W_iŰ@$RP\4 ,`H 꿃GǰLCnō:^Cf_.GZA!P " `L+`[3/egm $Cv`ɋqB bS"W @]Hb\ =  ` G6t ,`$ySSS]@o(@b9`c`bpY!RfUlI>stream +H=0(pjM"eHIdSHԐJ +҅3oY)VQ(F1`,e)oák򦫋㱬jTZ>ThmXmԝ_j?T)odմbV~8v;LnCCsvRHb*$ƙ#ĩ9ͿD 9RxspJAņs^NDXq匡/G,  gb.4p9qR mL' +)@1b ѷJ +* +9w6u=Gx1N~!Qr͛(("Q[] **yԓYg=>2Ot@Nwׅu=T9'; +Drql4kR3$9}aRCN҄\9_%pJ)g +e|ܙ=秙 k/>yg^`pF +endstream endobj 5527 0 obj <>stream +H씽N0Ǎ:DG_h6*Ȁ G2zIξ` K|w%qbWIlaZ,+Vaγ>stream +H=j@'4ǨI 6Lr1,-13Adâ:N.mFIڔ%Ih^&!iԵFW'30 rq!xY=7ݢNo'\3@_Kk( 3 +,//2S+3 KS9,]4kůtE +U$"J~%Av(MJl55bnZ:R$\#2Bb 3-MB!Z0zi"FRRJTصȧZAwzB D5 +endstream endobj 5529 0 obj <>stream +H=n0pG"yv焔R*SvЪ77P\ !~Vj/{V8cѥ-ynF͂u)6?hasa`{!زGz'Jع|bgu{A䆾=a>Yg>si2ەb q+Mw2FP큖/|))2#9ACKXx, VC!TPf!M)cq69Duu @JSzgB^ xQ ﷳj Ԣ3S2IESy/t_QIzA-:?&Y^Qb'/ |1! +endstream endobj 5530 0 obj <>stream +H=n01 yJp!5CvڎZ3ܠW(JVMH $b˖@GU聕fXhfW&_=%,T Xgi29P_Pr Yɑ0s,b2vZ_ϙ!\`iJW^4e-nh]vK}*mrtT\6cV8Xgto/];z7H?e72xǤݷr%C +endstream endobj 5531 0 obj <>stream +HձN `.שɩLtPG꣰vC +&%.R8SB08"ǫ|EҌ"Ia xMrZ +aJXUsRl) +&;LY +kL)ST7VP?8^RJ u{ҝDC~I}K=RE1QhZ@d?C2S~>am`"(@"W)F ƟG#sD& mZoIe qW";ZoЄY̸9.X{Ww>DS_ȱ+cǓ_~SIͽFM% +{Zuتk94`hŷ +endstream endobj 5532 0 obj <>stream +HԽR@e(G^ BD13Zji5x+/21ƉZ |a6"x~9ġE h." {b/fRĒ&˹/g2aC͏K~65b6BFl>M7WmΙ"4Ft130s: ʒRRfsH031S2/wJJ{|oٲH;ji*SQ:TPw0[&Ta6EQyofcVbaflW2i/T_3 ^^Ք:k˭7pKN>WnCќhR#zzi'W#wUYتK +}جw(sXtA1N>~Z?G۳ hcop{o`v> +endstream endobj 5533 0 obj <>stream +Hսn0C!i`BJ[ کvЪ ͹QD g\t0?X_96 |"X2. #$`|C6~A_6"N.YPfd'o$"UAI`" #’RR!)HjK)EM +𢈵ON&RNMd""Ѥi0U.uZcQt@ Gx$G:I&#O֥v;.T3M5բdWcuxk-LMNr>(@rK"f@2,mH1s駟ܐR2 6B$ | +^ڡ4H|>tĐ% #gQӏK\o(dbR$jjDtX?$>I0 !|kܓoň +endstream endobj 5534 0 obj <>stream +HAJ0 . +MijV]҅^)G34d28 DC5OhHʜ%EzʄD9dNSQB{w ݆Lpnۻ\*qvI+p># gyy~}96;Ң FJ( ^ǧM~Kb T: =;P4i_~ߓzp՛V7?M'㙩'(K'oik'c&w (B+JPc8@c!NP.D_f~xc[Ef5|_T 0v\J +endstream endobj 5535 0 obj <>stream +H씱n0/ @$)m2Tj>@۱CvR_G#02 \+٢ 34kˢ0˄4 740lCB?YJ_y'i1[G UT iy3z,ڐ՝~)#~~|솈_ ]rqsՀc: +a1_c9""&Uq٨:*V,Z"˾`,` Ӡ4Jn\ UBUbpRFr:i US 9ʃ) A5g>W7wdDpG;x6:R +vVx_ |wMj@%Ma}L;X薓n {)Yٓ1Ĩ2:oy9\1rJ7 B."<'#DT-0!Tyی<Ȍ +endstream endobj 5536 0 obj <>stream +H;0Pa ^`eGe`q rM (:K=Naʟ8!uUeг/a:6-Vӟtjʱe~˦=4{ANt]CY;V;=&sO89q!B +0r +煉FV-݃]ȫ&c<$w;ƒyuDyuw|D<'*q|H6l~#f\3y ;A,%[  TLelIvm9c2 17yÜ: I|&lή lk=fzQRp1W@c6 `HK0%ͥlHI'r|lvQ !\欠x5[r1+W()Xe4Cjn70w>,%ѳ}/П*d 溑D?WW +endstream endobj 5537 0 obj <>stream +H=0`@r# ,S!&RT9[$Jj r%\Rxq;j&Quc3~EӜچtU4yQѮNM[(*P௸=emY7&ۮ:SES%>|E;Z##>" @{!xBx&//JtNI3ZJGrBIlhGGt&Ƴ"eVt^5WW[z|0RĢ)Jψ9I +Ck=sdlP;Tmps +xK9?{^m_@Q-Q1.*,'yJpy7<0.Gz =6Wfqm:G_^dWlq1Tȸ.ojLT_U;_1^򥼢 +C/,~g ;&WZ}O#Ћ~v/:?o8/53 0 +endstream endobj 5538 0 obj <>stream +H앱n0/ A|bbd iz(NY4 E;GQ߀#A.eRyg$FLIxdUJܕ*RTeFdyW)يg~ۺ1T]nTmnQȍdBJ{~b0`G'P b X &ɺ$8BJXB*J1K6C/)₲.؜\' QIF@u9ut.6syN4pR|%E)|D?@<9"v79s9tL:k\څd6d&<:Yi<Һğ1)?|C_֤('S@ ɍkBzB &!$"/M1H= +0#d +endstream endobj 5539 0 obj <>stream +H=0`G." lf䧊Hlu2Dڃn2;~mP`,EO{(aY*n$Lci"J,> +>bXVYӓ0n,4s>#[1;ѩ`7E.L-:T D&9]]+dY"wMW9pXcYWKwսе4_cma| +wvvKmC\CSW1ku?õ"loyyV|0_I_7kԻ.[RIO 9Vؐ6{8l +,6 +,nf8M,ǽ l +ɝyk-4! +btog_N^:~5; gOlN?f]"v7{_{yaw}``l +endstream endobj 5540 0 obj <>stream +H=0`G)" l&d'*S A`K + ZG 7t)q7,K.&/ӳlE +VCslU[UY5;yQ{Vys-{z)uT!/Z69kVt|iV-==ҵra0nk$[⸧ |1Jc&L!] |hbGz9O#N9 䅗@G| +M-$hQQ`ҽ;L#DcJ  +$nK3&=[ɇ# NާطDj2͚Ib&d:f1cTD!RKV6dž"H#C /=7 <P' ɑ[B$)P善LѸYU%NCKzTbvReaB=/kv,, !"#D_բ7w~N /{~MfppwMfLn$Y$8a`$R(~l⯺J݋cy{QH ?1g'`F& +endstream endobj 5541 0 obj <>stream +H=0`RDr3G/dIH HLJ$@! ڋuD_ifg?"9MזMݜޗm5m?S_V]{f쫦mgW5yߞWwIvxw : ?}yOmlRxEfɄSbx1fS,0ay!4zSa-E`0 =8ߋzIW)U\㙷s@Vx3y4| u:t_o.̠\AUaxR ّ1M2VS,s^T9YݵeR;ӓy!@_ 7T'OX.zC~,ٯ3OmkZ>t{o-K_OIN* *m1s&b`%!^;#gh՜!O&.tPHM]#xufqHX\?^v\ ?;WS>= +endstream endobj 5542 0 obj <>stream +HN0 8LM`'@=zhm<#p@@`tk_Ͳ|[VB,,R2<=Jp1$ C1W[y @wl2 `e>y~}`9k*Ŕ$N0kD2 PMrJr/eSLCDmѦܠѭᎺIh,FC]_,1F߷s)5(%86mREqFv_ßrbQNGjJP +xL$)nƮM:6[͜!PIjtE@ZT#rl*u-պ}Lۉ^𾕜vP&ya)a &u +endstream endobj 5543 0 obj <>stream +H=n DZ2Dbvl>stream +H=N0`W"y 4MuT@"L@0'GQrebر) 﫟s=ץ.9~cc'# h~GOx;^%QBMs\7 +wɴ4=#>dx{KK1heWg1Y|Т5A|XdXs*!bES)2,'lZH'3D֣t^ +"XLDD*.A+ RSDr]AcOјb-KkK 5ܤ6[DRQ ) ` +[vEމRG[* - By371N0~q@iO&+i o]ZC))|Gj{4.㯊gՊ{UMBb +QLU!L6! T. +endstream endobj 5545 0 obj <>stream +H=N0`i~H$2 #fX3D]KST'_c׎ԧk7,$qYf'iЍn0^ӱ*ene0@z҇g), wCYaoswI[+9|P3[!.-$kKZKxm-eHRZb%'cJy'$1Aɗ)ZZa @u[V-Yg/D35RrHu'y{}d$r i?ɸ8LM`'áŔ %%rro榨^t<n )%1:ݪ a +/%+ S!/VfRlO]>\oo +endstream endobj 5546 0 obj <>stream +H=N0`G*y 4_tT@L@073D1yvc-̀R˫>stream +Hֽ0` H^.n[*SءUf~= hcEʐ/lꪢmPkʶ]myjN~7>;*z諲۪kL7͏Wx>-߿5_~xu $!>stream +HԖ;@Jaɍ[:{׉itHPAA,-RFb.dy]ZHs&Pcwé՗9tnͩ?}[|*.ǦV]NO(t|{},mݞbR~Z|>/ 'SDɓgHCX$B"$D E .Y,>,\O6EƀЄ Ye)m!sC~O"DGd[#Y,)jT;bʑlGNHYv 2r@7ȂwYU ˊpQ z1{dauZ"zT8X}vX, 2;:Hd䋲ꈌ\D%>QL Psil.Ԥ+GLd0Fz Y D qp5w$a!̒:6.̸Kcz[ͫ)&cj1!p|`(dl{!Ȝ%!*3r6D59 #$V amB1y{%O#iD$-]Kn:ܪC! x; !tgoB:2bfzBz9+/FbcZ1*{Ry#l&6p+d\]x~{V^2.p24dMG1Z +endstream endobj 5549 0 obj <>stream +H=n0p +p D%V&ICvڌJ,zmYt@ GOH'p yQ|]T;k(65kx׼wNv53W󺬛-/J^q5.\2NW[D%2.$oA=!!%!mhqT#^{ k|-`93K>9I]xic}t">.LɂYS=sR.Cb`(D%7cBƙ:ĄB9B + m RKt).Х@'o[aqq=ڐ-̜qϒ?/jƦ>ƦQOH*CWIL= 엞<ǩ_MS{ C>K?s՛Q"Ԃ&>-P:!NAGѯr +endstream endobj 5550 0 obj <>stream +H=n0`G"y8`Hi+R;mo{rzFmԾO0 Cҕf<t)8iDpN4'F>_0;k)U*IjGl3IpIY`{}F +@sD])‘; {YiмO :HGcN6vƙN̵$ܬ@ + %wBN|^D+>stream +HM0`GM\ISt] K !װ,Rd_)U`齤_xlzoڮ:uֻ۪-{u;v;=`{po޲ۼn6Ovw`珪mNϪ?aL]mKLfa Bhz }!h\ hOdaÒ= +CrAI  I:Liz Mb݊f"HRm3aAdqL_ls&RqA$<1YIi$,Hxw6!K`@Y\s(&hS̺+S~Ó<@ddS+ ᢭8159)$U;)\WQ%%u{ e b}_tG y7?1J k@!q[ĐN.4 +&i5]= z̉Vbtz1L ?W T\ABIqCtFKĠ3`IJt^t fɰ&Tv?I!O`2u>/Uxgka6Ӎ1PƞQÀ`c7=9V6 +endstream endobj 5552 0 obj <>stream +HԱ0PG)Vr.MtH\%: e*~ThRX6xx"n99xn8z܋.E+njQETGy[GᾺ:~\5صm[>+QUG}.]˷/oO/Sno]jC(قk)FvkƺԌؒrԔ._!DrB*ʁR#1]P#qpirBX$UF()rE pR !aI)sBT$#Ĥf¯ןұT) et-g l>stream +HMj0'\`Ԍ3i E]mk=GY7u! a +}#/D<[?IіB F$ű*ald`lN#~q!R\qY9kQg VZX`h3Щ>96" +pM,ebIYQS|`eJfճ.S- Դ[CIrFM(?,o[[9%eI S3&8x" KLs%Nu +BHkvŵl#'t醮-anY@w"zeюځ,%kG8rq:+#X{cuNhЛf6ݲ)O +endstream endobj 5554 0 obj <>stream +HA0`GYD&G/0i܄I4DHֱĂkq#d`~LAPh5?ueYR4E[W7m]Tj*>*n 䡨e[7MS4us-PVNPkR݉_ )|LȺsçc6QcE{kƺs- GʙP gBz^/nK236(Jy vfϴ:Rаrs޲9g^g|,BBqKBB@H)VFΌyd$TY':.PC8,\7e)DgIVj>stream +H=0`G)"L>stream +H=0`G9l&HKXJ +V\H#! +է<ٹloMWYw*nkVe{ۜzPOmFc7}[]s|S~GzwwzzxͪswUpyþ|^^RtٰR +B4!lF &9.cA3ẗ́@*ܧڐ Oi(7&K״"C(A iUqBT0C;Da8@r j"CV戦)5%Jh<+M g%qb9=Ceiش#s씴oɬd8ЎxkMoJT.r|`%@y])m>KXp, ZN;PL +Ȥg3i5:/-$2Gcȁ)G|_ Qn3Nv9,c:$qt D18w1?}uoohU +endstream endobj 5557 0 obj <>stream +H=N0We%G/4N&"ȀFr!l&6R@ Ҽ>W$ &$&rUɊ im*$T'T8IVX"6- y$\|0A +<>j;<~VFO"Uy⾨du]ѥâ{G Ѭf 6 +1j +z)I3 BT隄`!FoIzpP!$_hj:7Weō[U( )ߣd}v}Q_;NPU튖dnjLՄ0;fz"fFrhfpXXi]y;s$>NJmE?Ua.?Enл@p +endstream endobj 5558 0 obj <>stream +H=N0`W*y 4㐟)R H0q`dl(9B E5vjѪTxh/zs^Dd3' =H:=F4fWNL dDZN^,Oy| >vo_%n3ή0@ԔHy8-xVf:2:B d +h+a@R"[F\Z +"j3]R,`tieInRY*O=)3)XdE=Vт!jq9W˥AuxamޕEF-CjEɪZյjd)\x +IO|+Duiu"mms j}yU6d!kF1U)U.ri~j:˕؄jiļubm9Bg/gbw:U{ +k)|6(  _g0Ŭ^ +endstream endobj 5559 0 obj <>stream +H=0p# +$7>2x`*Djl"n RJM8 ˎ &Ύ" g`D2q5/Tٟj5v]jgj MknT@f^PilWd!84u +)W aB}{'opȀT!51eCS`[:8QG2A2`;X̱wcJ2~G3]b?i ֹpZgO;pٽWM%kX؛_}{/ɑ +endstream endobj 5560 0 obj <>stream +HN0]e%@Әq3U*  &@0'G xv\M,hlȒu!y,NȒFIhN(~yQʲuBTڒeQ&?T`>#Oxǫ;BHWsBjM^_٨ l6i\9B|!B֢|(&u !藊L<"pWOJ Q-@V %p K;ۓ(5N$q>n I秐2=᳤s"~@gd;Ow8'i!ٚ/9&!ԲNq +t]\\9ia +CoTp +Z5%IfeLW$Qd FPFIǣ mE!!2:O [˱0~+z8Ö;D =+"C:zc|++ i'i1Ͼ]mڝ C{|?y +endstream endobj 5561 0 obj <>stream +H10`\ a +i72EL(T{-(),l߮Rm7HAedVu{7eErYiWTyT%!cԩ4*S~.PT6,K]ܝAQ?]}Jϟ察ac`"ibbdZvt!itDz"_ޔ>rb;BOf|R\iT:I%3Fx LWW.sZ5Tܐ-6ۖoKmKoz@jWĀ D_ +GD(mPIPfѵ-KÃB'Ʊ \s}fejE\(sR"KxZ$ XRĦB &bSĦM9MP^\QťK'4P4G"zGus|eQQ͝_U{s[Y ?D,UmDSycgU`(* +endstream endobj 5562 0 obj <>stream +Hձ0PG[DJ? n6PE:@b $u"QPKO:qCy؜ʫ97eT-뺪*|c[wm]==U%ۿllYYg˫/k1ѥDIhR Eo!x]|s%p)cN1coB m4a]BRQ !1 >H|J-GA Ȼ(Ϙ27,z>S)%F +jpk%F {4X˘0"Y¸B*b#4Άr0 El79u  7!E! Si咘{) !OPS9!<:0unލH0BLC5>#ĕgDz ˷N$kmsK`Aҙ倔Z*wa )q:|QTg-V3M$w +36_|`2a|皩y.EXKY&5o< rdGۆ9/52.a_S9uf\{ N{+ySW(泯T1arz&5.(%.B1uًKv`[} +endstream endobj 5563 0 obj <>stream +HON0KfA G^`N?+QYK]OQ,I$T3&/ A_qLbmdy02Raƶ1#t&4gg\0I,'MyRY$|l» +Gw& GWGsBqT]חG\]`1G-!_$?@9LD c"g"sty&2]ybN1jtژkqm. +1o*+`>aբ 8αj{hN]Y| +r^.unnPյԩ7:\Oro4˦90<w'%)迫sjƈ]`N)>stream +H=0&G/xI2ՠ$@b+nI+\s.`EDߓEU7ۋ_uٶy/p\eՉu])7܎:eW_Wu&hv=۽㢹|x۝/_ PBAI^+sD G(&Ms,uB ǘr26s Ip0Rc0r)ѱ[&{^`C=OMZqH(IK-ŠP]xAW':NcHLQ&Yfc\#OIK6ty^hSVB#^v-hs4Pe@:΅i9j<1qK*@b"~wo緔{m8& -;j-GOn?9N1ф8b]S +w?ٿx<ze?| +endstream endobj 5565 0 obj <>stream +HԽ0`G)"#/px?[ Au%9b9QP6eco}S C @=IQ0!1!(!y̎d85ʥ0'Lbv"o1]g$hR 4ݛ G; Չc({ Eukg9x|Q% -e9Sq 6*8/6HוHёf2@1uGjloA%fSs3[GfLgs}TvYW56s4ܭ/P{xh0uffyuzW;KE͸6suO(Ν9O/SpTYγ%ífԆ<:2npwoH^#| +fq,) *A_[\Y* +endstream endobj 5566 0 obj <>stream +H=N0`G"y 4S`sV +=B QM~F*Đ7}vg1hC4  RCd cGPT6g!,tVXbt/ +* +=?f| ?6@Z2.: f-H+<8xC) .pf{@t"IF|@)T1F䪘Ēe#Dlt$9.*2 *!Ĭ&J94XY>stream +HֿN0K#8 +B.95D'@4:C+< 8~-&]}RJ[r<X_D!BD~$dDpC?Hx9}i q"k&~(NO~HcvH]JJHv32b/ϯ4;ϭ%+KX!CwO 5͖ +Uۂ KwvKcL5G3#)lieKH/N̡lP*%JIOi/*kਹQZ,mrb!O= ;$7z% +endstream endobj 5568 0 obj <>stream +H=0`HDr#ؙ%SHPqvK +ԱD,QPrs\bϼ畲-L>ű{~^JlfWU]lM]4Amf[:aimŻHoE^KQjyL+;z|N-,q$evtЄ $!ʬ3`}'($r ?ʬ]M>/>ʒ s$Xݡh}?D1+<}`&< +U5oɰ uKhΜ}:>|Zhɰ י 8OK{.gTi,Ώp Qr`=b5_X˔SM֕rb$kC /CJߡ5 't~?8brH[=orOڗYU<"YCݳ/ >Ywvblq ]=`ܝmyfl/sq?4a;#}C 0Q' +endstream endobj 5569 0 obj <>stream +H͎0 dzBb\ lgs@bO<pshM9PMvƎ+!՞e$ߎf>"A7yZTy[J˼lJ]UQӬpi[[le?gy2:+jy|FWy~+}FDLjv]A@^b=#d`12123FEm5Av/UkBB` b@rD%6[L֪wᤏP4ptAƙd;CueH? 1a0 ƻL LL@K1/IYRb@GLX8\p_vFd0{{Sy-tqG̨:xc !#6`&yNrA!Pv 1"HF+a <]<`c\LX!q`$^f1 ##q݈j%c#D6ל\Ka#_wg$0~ +endstream endobj 5570 0 obj <>stream +H=n01 yO6V*CvڎZs8G0 \$1~f|g QҐ}%qJW)!Ob͸)"D~’"g+{H}N_&'^ +A%Hg_eF(+ +N.k C R>THSC.B(-,-QR٢~)Q9R"dy\yZ^[Y ы \ AF1熤t! ?oqf{\j,,YR","([*$H)lCvL)MTe, q%5}"o2 +iܐN-vܓot +endstream endobj 5571 0 obj <> endobj 5572 0 obj <>stream +HTn w?ō:`I%ˋxHZiwgoTcM{l>$;k$m࠴-rwqlM?AUe쓒w+<]x?{w +67nG4rkPg9 {#uZpBf@x^CՀF+ %eӑvwM*D-)5&(eAy$[(NBOaTrqyF6lpV+WH + +endstream endobj 5573 0 obj <> endobj 5574 0 obj <> endobj 5575 0 obj <>stream +H<1n0 P p`^ Dɲ4 HS$Sd̐(>BFEX+H +8 +[I; 2$E1wxpJeylImMA} $;Zf~_?'p+*x(buHw(bUv[o5_#OǁSn-AbVe +endstream endobj 5576 0 obj <>stream +H22ѳ063S0@=3ScSK8bU嘂U"`\.'O.pC.} <}JJS<]szrr +endstream endobj 5577 0 obj <>stream +H=n0m4`_ 7,VHD +E$)S$Jjr\@I)X5LY&v۵9ˋ$VsS-66,>9+%,kp[c3^b0o#֗Hx+"cADAh ,EI% RysJ;}0,h;Q +WǽD:< g#[Qo +^7/'辡l?_:L@;{J&?~[:[Y/īS,tQ +endstream endobj 5578 0 obj <>stream +HMj00@m'v2)ԋB,Hh #dEdF)^ i7Z'(MOqY噜}tuZNGj?W4k(YتV)%g+[f7%5sb% 1#P8BQ" 'у-!|H M[:]>stream +Hҽn0C H^x'RJeN}cV G#0f@q,5ev'K5{#/6˦rui^>)*j+mMnTUW+AvEzQ<ȶō4ZKDg_ k|d> p'6R&zoi(ÀˈC81fH)!IgFL'b?D p6X0~A1_~b\W !ZpW.0q\RySB_@3>㢇@M usj<z6!S)n Ku/ŧ+q +endstream endobj 5580 0 obj <>stream +H= +0I\ښN@`֛_ڂ={K~ R"HFX+!뤻.t\ HӌF[4ZL J]esb\ٰ1W r /@Zj + HMl*% +TxTō>?ܙF: yyyR5\B6`W +endstream endobj 5581 0 obj <>stream +HL=N@ 4!sL~7$R J +(9”)4>stream +H;1 DSv~nHJEpRlA+3AHƮ+MZz5'uN"uTsLw͜<4UnPǿ=1w+#Ia>Hޯ}`gqW +endstream endobj 5583 0 obj <>stream +H22ѳ063S0P0345QеԳ004R036003U0憖 +)\\z `bBC[X*$r9yr+s{IgC.}O_T.O???B P @@\\\C2: +endstream endobj 5584 0 obj <>stream +HON0p&,L#L/0Oɨ,Ltԥ x1#dA@ A%оR>VzJ8J>stream +HN@PL@ɪ&Zji/ƣ3$ Z:ŗ{2 %Q4idB:aZH*bc,3'oDhfR#=bzHRvTB%镜3=RItK4$:_)/$&y? ov?Q؁Gi ii~- 5x JJ*K`f;Ds96&V~"_IwV ]ŝlnlqˍxݬݲlSfrD =Xm|m+m@`#t$@'7y ? k +endstream endobj 5586 0 obj <> endobj 5587 0 obj <>stream +HTPMo +|\ i6Mt띂"-9higlv[cAqg2lYrhp`i9;^fL}D8p +B7ń@<F[0#BS<FWy~ye-{mf ?> endobj 5589 0 obj <> endobj 5590 0 obj <>stream +H=N0pG"y)#[}MGBB*  &  '#GȘ!HIJ*/ϱ_s0ZH&?hЏǓ(Tuiu`WOk +c]Cuq-3?WA +OOD_١4Q i"ޒO;ewRnl䃝Z7\~Åu ֝wZ. +]a؉xhYG:Ư(05c( N0n3u Kq:vYct .@:M;R245/U. PCgsk$xܻ ]I}~%JSp)1~"gY COiok/fL 0S7 +endstream endobj 5591 0 obj <>stream +Hܖ;N@Jaiaql$Ne) $rAmG\0wg14~Y3CND&ryr.G,WyJ2)2^]%fo+4KN=rxTW7e=Ƒ!h`;P={uqE Py(΀7+F@* fB +djQ`à*f MbNabt77 Sd d MCГJ= .}j,sX38My!51N;BRkƙ!\ "Q=3dzPC~ATؤmѠ.O+OAJkxŽY1N,&`a@@ONVK!ͯ21#` +endstream endobj 5592 0 obj <>stream +H;n0p H^r|PrcVp 6E MLUY$(A$Β,lC$[ VzKwY^D,L$wS9k8G8?<@_09ʈ{oHOr] t Ajyd-91$!SkTyP4ډ-Îb#M:@Mfpc%j$$r@R`PH]}zN,'X\| TǿŞd|n6s||roʓ=X eȹaBU\/d|˜bTӌY/0l`fZenY鑺];V]&9Fx'-}l\ +endstream endobj 5593 0 obj <>stream +HKN0.HSdD&҅Fp9% ұ-'񕨙?/I rr xPD4M;J7rejӂ(ذ˗nȺ$ %%Px}u +R%> endobj 5595 0 obj <>stream +HTn wcOTu.kwR!C޾EWuû.`L0:o A<3ij43> endobj 5597 0 obj <> endobj 5598 0 obj <>stream +Hͮ0N8I7<} ?#e䪉0|uBkx&$;Y)n 1fG)ӟ(8,*byJ]^DDsI Yc̿py$='q'etNLҏEI~|# ??1?^ފo_W|aNᜂM&*6 ЧrKݍnר4 TISBPW*l45-5HvRytS:F/ +cEž+5] +J2A97tm;n10$3ED=RC̈l'W)Q_PmӢѐӦ!Zxy!ov:(˖Akjt(Ԯf f#y7E ϱ})j .Dݖ:|U{D?(u@ ~ASWph9 3ntJے +endstream endobj 5599 0 obj <>stream +Hҽ +0:  I&*APGEg_[PP4CH[p!EMEƩVĪHXf3EBSCR0Q'H;dJCB& f bv<ϢJyQYCӮi#nh&ٯ^{ꍃ5r;Gݿ0<YJ`m `P +endstream endobj 5600 0 obj <> endobj 5601 0 obj <>stream +HTnH E +-g0 ,&ݳ/Kr@1g0F/0uH^R t6\_O_;,ϧsg]7_^ڥ۬>nm]v~t}q{:/y=_[/ru{xxlmeGۥM˵ޘZq֮wv\n^씱jbGĞ,qT*wMƿyqQwG'bn3O؋8K#lEl+o-l0[Gņ?ovVV{O.V[mÎUcr~O,D.Ysߐ3E|g ۑ v]9K/1p^c (væ@^J=!^C/ =Q x=>Ag/Ew0x?&|?H7Wο.%(YKPS48_**PΫ.)KMRH*#wgM^N FaIc2*;+2$6xyV:b9O4 Ҽb^3)zWE#3Þ?^3 4#?2Vbo<Иg \S.tPblS y9eҾSF>]LTCNWlթ;-qc8kR`g]dSu0"Z^YL!^UCcbuxQ vƋ3 OԏԳ~uYL-)q +s}6br,^}x1lY x},ϡNq3_wS|w޺^3F~J1p + +endstream endobj 5602 0 obj <> endobj 5603 0 obj <> endobj 5604 0 obj <>stream +H;N0(#$`wsZHA܀+Qpt)SX1~%πX VŏcglNv$7\&{Gu."omŮy2Wx%|PLm.Ǘ'Y.FNRijaLI˦RN[4P`ɢ`KN7(EIE]_bjD2 TBφ ET,jC? ɩ8_"6F~HM4F*%_"%&zTEڒ1t-um/!h@@}Eۉ L'i&i^e[\M8xS66d=GIt ?j=_Y`k\uL.8h|[-4bG(ʻ!1a 4&1Rݟ-|R06S=Ʃyv$t&X _n -:kZ+Ρ +g+O 0s +endstream endobj 5605 0 obj <>stream +HֽN0CMvďD$x`ddQ3X G#^:9mYe9ݟ˽h.W\Lh>sA{ba!7x#fײ_ٹ,bv|y"K1\ȧ;9}fOF9N`=HW6Ac 5fn[fC~9gR+fmBqh6FzgzwdKrOp·*6Y\秺ڑf޹9Y^gdō=/#:I0 qC{}}(?U!Tէ^_1̢Џ~tX@5;?&LNϟ|V߉Ԛ! usl㾩á&{]wbC +M75?dzA10qEqB%6J 0' +endstream endobj 5606 0 obj <>stream +H=N0We%@/дi R H0q`dH\,G2zbq^*UM%Wy}_/If|t,YwH3WIN Fgnfu71Bb@jбjvǀzW DbF ih.;WvXj ~ט,)Ќ}`ceun+0 洤VP S N4ʢWOvζNF G]}0A +endstream endobj 5607 0 obj <>stream +Hֿ0pG"y#$/pm]K"  {`Dsx1K l,lbC~N:vnWǛd<ߎ#>5==0^0a/6lEy%}+VQ4UY.BKFxc";$~f&0ɀp},*$Ol4jbS0 ʓRndGSvJ]]IKo0`ID eĄ&$/-$5ITZ"H(m>Lf$EZD,("Sp7;/J)v)rUmϮ1* 'LEr^%Wc-6Pu*J ڍ(!B|ꞅ04H⶟!\;b295O95/th}]@A{D#WX`)Z=yL%?D*rp6Q{UNKQ+jBoK_JQO5->0 +endstream endobj 5608 0 obj <>stream +HձN0?piwߠA8xyJM@L~: f񎆉g<Ϡ7R qڒAY, k5qvI`K(ѮpkKHh7ƃ+rd]5MmtdZGqk\z0:LclZ]ѱwc&OKA˧8-La_4'tJj-)XXׅTܩՇqx`}B!֚t TBL>mHɕ!U 0m\z4LK2AiݏpR"y|ŷmˊ{Ǥ +endstream endobj 5609 0 obj <>stream +H10Mɍ_`g"- 1T{r tH8Wq5r)6,-.&y3ciZժi{uv|ثZC#>u:v.oIUs~תy/T#v7/7RM2Uth.hcϙmONEN6u_Z e}{@y/y\68n҄g<ꂍ14=z`$HԹ%[]Y_YSxr)?M)zp#o,(^`u "zP_zzֳT}9C=N<[fG=tL-8٬#0 +({x7.hDWԆL/J M}cRZ?pm{WT ɑ3N^3y z =;lxu/1 +endstream endobj 5610 0 obj <>stream +H=07"M.\`o-i)%Z>%.҅`?/eH3o7?~waso^ӝr{ors8:?~ll}ug_ ^f V$tH3PGM#'ƇR8%IIQO!*JȈxQzF:rfdc9crĿqHi D*a,QWZ& SM⯈X@{g2𼮗Wu]q]L\M0$ב oH73Y{LƵb&L0A9$BZquT.g >eOkEf&2X$R~'aBeq^$ê.d)vDRMx&Ѧ5 +#oywH-!1_.${ߪL"4{ʴuCAmvBgi;7TFPnާ)^HEōHU4]UdXcF.ES\C "ZB2#I̓h?PP]j^0Š.؂ #_x)OwntZ~WBA? Fee몭H-S +}` 9 +endstream endobj 5611 0 obj <>stream +H=n0p +p Ķ:'ICvڌ bM5b%O2TeHIPٔrW^췫ӮֺV۫] ߟQAVޔ\חEZ<^DՔuD9o?&p2:N='@,j>ep%;0?Hp2@)R\S~Fx`-%:dƤsI!q F>sc8dFzRFMH#zp?#i(h m@Z7+3J 4<% I |RB)X|,B> +B, )B +B,$U0SK O&e3FjFd"zjSF&c FðbژQ;32Ÿf2]c/7k |*gd}1a0 xȼAeN? >9_4Cx-7OGM`V +endstream endobj 5612 0 obj <>stream +HԖn0)xE @lˉz4@<h<@۱Cd7##(%yRݡ@Izlܲvae{kw[?r 톡v;>#ktuǚ]]a ]o& hBHx81Hb )?OsϤ*3Qȼz|{tF0p.6YL`0~oq\wQ ^ Y!ԕsH-0r'>&e vVL + 2H!G(21 #QG=F[ DLn"#*}o,k."JArC"}3.0p9=*D +endstream endobj 5613 0 obj <>stream +H;N@!$p 5&&RhVK ְ( () +b忀Gx4ILaBMP,n7 y.B1t MbJpwI([+y +yRPmd1qy#Q\UTBbxp9*M*瑿,~ߣZQ1A'5EOP Y["VuF[Ǚv}d?ҺrC98<> +R Pp5]$٭5P4+&}TB7Wq<TamvapzנcM83|n +endstream endobj 5614 0 obj <>stream +H얱N0e䥏P@%4*Ȁ 7(y"9W_!`:˥rɗ||)lmϓc^ضW,dJ=%KyvVg뜥 oOd +[pPUltzNY H!4^5)qȑ0Ah%1,spVr.A8 NK#$Fh@_Ĥu0QEJP p{@PEb\vU vY{TՙguQ A  $o2i; tU`9!;WZ7V1"9ey5UIkG/ : +X啔ITij{ȋ|R5#B-@HBR8@?^4uW#1\BGjTktO ENBE1E_AMεи%DQB3IAc_}4Lδ@vU;) +endstream endobj 5615 0 obj <>stream +H=N01D T/дB)?`h 9HD E4C/_lϹ0;X|y,緂=>ݦlik `s.͎/O`~zcSl2!_S=j~`E)kv@I~1_)#n=SB*KaEۼZVd9$_a0V>2P;&b+E +;jX"+7&.&$H(T˽poOU3WZb!Y W4Xo2FVUjLt"k7K, P*iK +XBve[(Ž%تYVYNUYbh;%of,Y|B= Q֎7vfW] N[ +endstream endobj 5616 0 obj <>stream +H=0`HDr#$ؙɎ]H HLVA#o\bĞb Xısw]k_l~35XwMwyh?uqKcM>~7Gv{}v޼vr{|~N_˰*UH}"s:`"A d h R>xY7UqT 1j1l2^A<FQrd4k;^̦ACn>z`O]+퉧&w3+r:GU3c _=T>\pn1v9f12*ρ!]Ŝ#sPےsDC8NѪ{ +4)xfb;m@@VC0/#,f{X›`zSM +gPVZᅰԶJmhaαFr>y,_Pmr𴜦p\Ǝt@k:G fK 8< vƑ-zd8+b&=cM=5?pCM1tޜ5t |:`ۇ-Q? o01 +endstream endobj 5617 0 obj <>stream +H=n0'h EG.ʏL͔$3$h׊7z&_ݴ񆯾)_]~j;}m?OM3[l, +P[6P\ 2`[AGXn+bKEiF2K[o`v`'jBKw0Xzo5SU3Vc8%£hj Xc_9ޢS19`_fs"G!;dzu72~U62[Ȣ +AB?1 Xp4ܘLe)+tf#Sjuˤ=5D/c:(韛</ïu㛖,s6V.NĚ؊q +endstream endobj 5618 0 obj <>stream +HN@!HՓh)z"DO>z3ģ`|9F?c[Lt>vgvȆ(pv2rbP<\N!E=~(neE"a<L"HH~E#d3P +5K`x ! 9!uwi^yi/YcmٱscUsRÕ MލJZ|qw׃Ϡf~NᯝҸqae[nm'vK +|}~7nwVo̸^Aj{:V'q&Wܑ3ߙ2NOxSc}C\F| +0hS +endstream endobj 5619 0 obj <>stream +HM0`."y#$PFHHt+,Y uM ecgv‹;/{Nwaz{~p: N|~=k8oNoƯ_}⧗ܝ]ēZ/U#IƆRT3eh4ⲰQv!P p!PЅ@A:'L + 6t+?,oU_e+ ȓ'R;uۀҩs[]UH$cR Vj:Q_;[t?./ +endstream endobj 5620 0 obj <>stream +HMN@Gh2h *]5DW@] o2G`9&q޼ (tΒ'>ObG:['ue{G[QlS5k U"0*|y~}П,|J9x$&KF89QTVRFUvRoI"r(3RHA@@>C- TuC(dW +Ť%J ӌ +endstream endobj 5621 0 obj <>stream +Hֽ0Se%мMJ`shM3X18*X R'u}Oߝ?޾>3;A[</l .l=c7._}dLKN]3\Dp!E AYh*3qG;>a;" Y-Bڡ2HZ0Ub2T Ղ,VNBf33tpУ &Zɢ\Yרdr$}VνV YBfx!*Y\JE_v1k">{1!TŤ^Z$L)FJ+LaxU #SQλOE՗/C +Cs: +8 Yʭm!&3h4ʰUl&kQqx?wF!$`DL "e,Yx+"ĄT5OR~O(EY("nWrW|jhҚZ +endstream endobj 5622 0 obj <>stream +HԽ0`G"y#$/pӆKh` G󣘉Ո%Cs||sRxhvݮ5o}]>뛏"5{u->|G}>훦˦񕴩Ebrj!r΂^t&(gTB /iBz_!EZ{B0%'"r-g1T08@#)"s4wđ->1 kCfAr`jӞ.;(Ya2X6?Sh\HAvԉgŨӜ`QD\B$vJ!AѤ@b)/7%A9_ӾMN%Z~Hݭt\%:S)uX3=t3'QN.3M]7n6Uҩd?|tLQ )UӍ\GLaަE85/Aq>>se߄h5vBx*^8i { Njd?8*QEV0 +endstream endobj 5623 0 obj <>stream +Hֱn034O~ ĉ5H[ +S>혡E;&?i#y"5-Z(?']ol]y榹ssWϵ*jvi(bP7[y77bsuY}ۏ/FL!X%xTҩ"ɰHBA.j/Z +dr3t^9(29_aNfTFr. +$SQCĦ2\z˸Ud-{bf"D058')SY ѳ(h#y[kK^,B4EOzQ/#) )Ɉw $o(=d(HWQ:FzUHzQmŮaL :ȋuHDb8!y뗻I-hS6 pQ^ܨ˚dLD~A$և:* )Ѳ.y$APc߰.ӎUGR +3j:yqd'sx2D;LD'E%28 u2o# +endstream endobj 5624 0 obj <>stream +H=N0`W"y 4iچS`aZHш(Ʃ4/Cԉ74//Kd0M"݄kݝDQ>bLw0aݴEw?}N;o4bjC9i_חG_Pُxze$33VpuMqa{4\Qg0Q71 a`j'Rv* LL$1B3DQ=NtݜȮ>yjI0)!WY5K-*p,56(wU d#FH+;BT5ȗ-8)t{hJu JL&(jT!*eODMPf `R'C$0d?I? +/7T}V2 G!XL#-p_2W{, +endstream endobj 5625 0 obj <>stream +HտN0Hܝɩ &:}%| RGlhoگ & I^4_dGtE)/WHˆ4ZZ\\us M8^_s+Xȝh<`O3 +BӠO\Ky|˱+JN6VcY+0UAp we) +@sa/QXnv肽նKYZ\>#QZL͡+`wY"F׳%jo e +endstream endobj 5626 0 obj <>stream +HԽ0`G)qݫ" T<PRx ^ňpG2Ed_fn.vNflvmM<k]Ӷ뽿zwrw qgWk![y}M{M]GM+7g͇c)gQ0+DI#'X $j6j6HjQp4-.Tڈ'X;#o|.uyU̾žg ̎װxiV_ݣzx@OOPjXOO gMra]ኒ'@-[ʞjUscĄ&D# `Z64'N ؔ (8ߖg#az9GՁ!0 `*jhl64kO;WG7 : uV`N7_ݸm +0R]lZN N.Ol|85y\my?A? +endstream endobj 5627 0 obj <>stream +HAn0PGYy TJ.ЙI!aC+Pdu,`(>YEdcO0vQ(yIٴvݫ˾uQŪot>篷]|W{mb}mۉؿm -k(+ҙ̘,3Ft^QtcHB]S2BkGNvcǵgbLƒ:szE'hStd*~4!93 K^Khi }jh(M^yٙ,؛raQe6QB[y=i-_J⎁!5D7Rbꨪt5˴UNHRiDT0DDC C:"ao^#J0/ )HE1g|ND@̀4P~S̼Iv Ž]'KNf{5|p* E.J Qe; +endstream endobj 5628 0 obj <>stream +Hձ0Љ&n8VV *>࠼ f$p²؞$'C"&/;n}sqx~yb}ӆ+O½v<'bi~i'qz%of4XJNtigFN'pghxIo&# goLH!mHEBvd|G? Y،9II,+)R,bp$9Px%_v̥4HBQU|C'gQu4021݄x38ܜ%MB8}BGxS8 >E!̈́iߥ#7@ތ>?$SҘ)Cx1S(\~jN.0 S%"6yTdt#5BaTB~5/mZ\gY-%Պ #BfLh{H0o˷>oQFOSR}ONx/~ 0" +endstream endobj 5629 0 obj <>stream +H=0`G)" dw;SHPquXQPr! +&y~N^@@f&_4]y[/7m}o鶹p?o7mm[}hߜu\yݴW|{IEçw?^q)#c+TQʗ}S0L0`Ď!ATi +HR @&Thz B"dLȦ椠,HzDRS$X$Ɗ)^$6\"xI$8 At(g]HNt5='^$Y$VqD$U$AW(iBv>>}4l[@i $ +d,%e.aI=/p44QuBv=UM)(tURy 3ͶS I &M; g=ߑYJUx")\G73:PGDw* +X8B d@VTá $))$Y4!I#,|dB5Z9:>So/__ A. +endstream endobj 5630 0 obj <>stream +H;n0 ] 6=HZ +Sf śG8r Ȉ/)P75~x=\]v<|r&ݓ3+dwfyx|yG$]&}I65*]AP/k+jHm Has%Zꝓ}PHP3ek5: 4=钴wIT # iS +\)8G:y(M= bjfm42'֨y)ձ5T̯&'Zi$2ZJ  IG;3)4CJvqJ$/2مb9n]BaL^$1'Dj掄& i%VF5 /V#h?B)erdޏ'{Z[:!7Qt! g0Մw2Pt&+y`,y% +endstream endobj 5631 0 obj <>stream +H=0!L;HPq-6X(>!&,3 mE(/lwn6fs}wOzg}*{fȶoz϶/wu=_u?}yώϙ9s Jih P2I 0Q/rF^U v&|5t>$1QtcCiTE2LN/:[USp{hxVI`og|7.<_!tK( s>#ց(a@s +TS\p>Nɡ-< 9H E΁}]&a#)@>= VkjM&ġ׳563 {g0+;/"ՠ@5hԁ +%E eh +>۠19Gtq !GKߏ)!fGq黄>Q:? GkG2o2_C<pgA1cj>7o롋n][F\n$BJ:WOvH% ƭ2őaa? +endstream endobj 5632 0 obj <>stream +HԱ0ЉRDrOnX$@ ]P?ş҅ec{l'6'$$\$zwOnmbj] z}z͓m8C^]6㏧;vxWa$)X + @]Ԫ)6tSY[jU29hDThSs!dT8mx絼tM=[(#bu8 tOӕFNW,]̇~RKW>l^_h ףI$"&3ܧ.gWi5[IV!ٲD״~m`+p+vD_jM8~sk,kG<4Dc*KͲ%5fvV2^EHY`qGOXy>stream +HJ0YPK?mwKª`a=~OChD:E v.kvqb [d0MXL?B~;[LW[ͮ7)0a1`|b:e=8Cma$=~h6]S +JI(WS3{C7R~uWKd`ZXKK7.ʎr]GƲ6l)Հ5K1}Ǚ;_$l^WGsYލQP#z?O2': k|`7 +endstream endobj 5634 0 obj <>stream +HO0 +#_`{ݥk=VAГWA=zPzkE"#L&vR\ ?n̤ө>/ÝPաq/Zv0T_ܽ>tn9Ӻaݴ<>stream +Hս0Y&ncZtH\EWR{^ňqGk&㿙$Lfƻvl}sqحz/W2Juq]_=7$6ovWM{/7o7؜^7_>iq~?l "h.6PAs9 0T ,,fK`0P<!X(@HqWI!g,; ,Z@@9 K`yc;"^ໍM00026"!N⇻u)˙&ЫrvdYkS, >83 "0>~5]eb&5;Ac:v\ҩU@Ӽ:Ɣ?N/Db1ct)eG`0E]'"@N^j24b'z%&\>8B11^Pq(gGH[~JS~pP-O*O}W 0Tá`9 ԏAϣ/OV` +endstream endobj 5636 0 obj <>stream +HԽN0s@G/pr{q"j"N>A3M|-G@|(9 !AÙiIӿcsɷ.ql֙um'g'qCڄ\fN&_YB,-v;)| +endstream endobj 5637 0 obj <>stream +H͎0ܚضRؕ"- Jp#\~4#^ď`K${>gvbߞq6m7W7}s{7v4n[o67w^5qo>W㕖@^rFqH\sI94 /tk %t!u TEMz †=U.D16 ="H`Ne` .|l{ ,̠v@#{q#\TfπL1)!| ٞaO6{R1Y21dGC8<

<`9\|=K+MX +Jh &n+o58POY {Z<#qzL8q!ɬ(.B7-UP=9A [є+F&oQe_p9QrcC Hc^X:CePI/NH:#N>xJ1O5ʦ4M2hLf4`q`&~OzF{_Bf +endstream endobj 5638 0 obj <>stream +H=N0e%G/$ҟ)R H0q`d9W5r;v +*!oh/ʢFEt81]1e劑[2WTt9 elC\ސUJ ،'1 Wg0=w$="h>stream +H=n0pp Ė +'iCdz4d$Q|\?R"(x?l?VjTߔ秒>f]*7t-͑nxYtex'^+=~Z `B!)IB:BXBid*H3 d'W,"m LQ0'QS˦MEjRRK&G. +Q >E]1٪2))Lz#  2uGRw |wđ oo,G,>stream +H얽N@Bq6<qU$&^aZZh7G [Ӱ۝̆t7qg*.KNXRvirj'ޭַqzذ,Np=98@5@a/YӨqQ)1$0hB;0ZyOe2!nk +XrZ&T.L졆"0|g) !2ct֪;"RĢTLK̽>+:Ư"gTCM!E^-I(k&V㝌oiKky +:@ 2tFeBB9Wvʧʹɲ #ZXQi[|"EU. +QOq&:_3P~hVM9v*JjW[W}KP=âڣהZs1!am+b4Ӣ= >ŁݰO[kt +endstream endobj 5641 0 obj <>stream +H107JM\`3 T *RR:YQp,,qpґ$߳fZmL_d'Sv.+'evȟa㷢B|̦g]wQlf^l_fEy!We_en>B6Lj e76|%S˩8 i׆R-3rrRH~xB ARfi9 +hdt +51Pa%)N]sKVhè5 # RR8XDۜM#H\!zBr[8 gouDIODI=QRDzISQ҄ ~wJ |jA2@&rMpՌK {DMGHR3 , mciÌ>.Q;>ft8o!|<=@z'ጧQHsryd +'wOO鷾/#R( +endstream endobj 5642 0 obj <>stream +H=N0`G"y iJ6S` ͜+D9|:~%ljzZTku\w%~+ӥMLaQUq;\|\q:K\.秗{;µw7z`AsfhP h`"3cuQodZ8pfRrTfMt\.t4Ne^ir)9m!j +no`FL![ ZOE6Ԙh:`Nhgyo]jSW/jQAykԹ;<@5Mnx6KA'$:;aLBt70Ly`YRֵ>A4Cg/0u[z3H27tlm6MSO_o +endstream endobj 5643 0 obj <>stream +HսN0*yʀTmS()?`sxGJpžHT|Os\Ljb,~7?6Jl)ߍwG(~z o@֮kZ _W?5Lp .ۏ2sy :'[?Ժ5c`k\ee3@ָ1uH_ + 5an[24gKq%>%Q +endstream endobj 5644 0 obj <>stream +HK0@ՋHf ^E@HK XG e,ە\h E>/vU~;NOS~˻g/vAW_$r@V][@aɃk@Pʐ Ii@&p3b>ryX4UCUK-E|SLoZKo:hy Chgi?z5R 5H"d1,hB5EQfsZ0%G,4R0HKYU lUjQS , ]$G@ {̅^ Q|OԜ@/dӥ%@  >Vc^HOba#C8sD ج+FYJ_ X!04Yl/Pl&o0E=go|j# E-ކ'"K)>N/4ՂWU+(P.Aa9=Z am 1vIMM2N:i&1srՊJ&[ +endstream endobj 5645 0 obj <>stream +Hĕ?0&G_`g&3[EZ@b +$8PR^p"󋴋$?}?{ٴ?fvs:gv?vf_&u9+Å>iwlzuD__ Uho1!-a!}DHHƈւRE[bs.>;6!XjCdC )xѐVrAA= /됂+. %.fqvQvgFvئ +RATrBJCH7Bdkȡixݸ;"dX8[L5zHl;ѪoXTڰX:߈Xd%):NF +^#4 6?Z0!$GUM[fu ~S6Ul ]xO$%E%(': ^ 3"ETZc- W$' +鈺CshBĴv/Q exhIB(AxBoLpđƧ3䒍 RβE}{_Se1 +endstream endobj 5646 0 obj <>stream +HAj0P/ #8ʐYUvEJȲGUtn5VoK_0 -lIsUoM}bw}g҇aS)Dg:s;;8 +~sr%-UΏK ΃pb,3L!z3f Lzy}y?sĔ;Xy0[Axkb@.vqAPAmťCe,ubn䒺o ] ւ M,>23mHl.Ot{%4?ͱ_;Kw y +endstream endobj 5647 0 obj <>stream +H=0E"!L١8KI:G"G\bb=;~;J0n/vv^oˋpo}'ۣ{j^Kl|"Nbbnb{eۉmۏJXLi2AY {6EXl_3u`}<7)! eY\'/թ\$؁GKhgM;AMt7W$O k8nOU"]-11"PK2`.*|l|3|>dw w:`R/3eS(|\>.F +%iߥn|wWC{^A=ڬşmىf'\3-ϻp1bHaU:2os]EH J]__~dM%Ż`>Ojgf-SemLplXCx}oǾ$ +endstream endobj 5648 0 obj <>stream +H앱n0b@G/IaBJ[ کv(x1́S)C:4_l>N|A:d(6Qs˜{[AUB-C>qmXG?}bՐ3͍B(,,8J&9QnU;A͂h$Oe+WwɯL Q*L$N7w$ׁݢ1rQC2PR(kCecy?wܩeÜM\v@*A z4'")O"Ԝ@\. |Hi$ +endstream endobj 5649 0 obj <>stream +H=N0pG"y 4KC)R H0q`dĖ܄D t`]?!_Ð,bف?П>#vIzp19zuK .Lipx4q$g&T^ J2DbL.Z8!Au1B9SIlD՞~TNp?)*hRRF$c!VI|PK~f]T`ZJPlw#@R$+JR{EP!Jm4~hӉbL`7E)@Y6Lo`OIlPi#@m̮3'  Go +endstream endobj 5650 0 obj <>stream +H̕n1ǍƏ~.wN + qT<PRAMkQf&_Rxfgny|kj^>} :_u~,zGَ_boˎ嗛;<,6"=˛f4+@x²UA*0 +r+S%a$̱'NI@lTPBP &N~{JtR{Rc$e$cBm*6ra0B E +l|WX< 9坂.&7ÌJ2]7:YP䵨AȣohRcBhV*v hμeF()c;K +JaCR ;\<PC"504]ClYEadYEe`7ޢC!ڢCXHڙ6AoacDi< +66u:AL Gd?!J= PsB""ԜOܯA~k6fRl6! a}`D J}|\O|MZf`Ms>_[ST2 +endstream endobj 5651 0 obj <>stream +HտR0_,<yBc&N}utWMdsG,M_Tm%' %шhMN:i@S/l1H̛j8Oϲjt@񗔝NIYq}F+rOs" R[fvR.qC_(arL8(qWSjJrªhH2:ؚrL&!N NT(rr~vN)4 -*gE|}HS&5_3ڎe[ +R5oQf*3&[ +7L{N*8ܝz3exC LR6\NZsJ:Hҗe6_4.!! do +endstream endobj 5652 0 obj <>stream +HձN0Ы:DOIܒL +Hd@FS 3D Ns\ Hry8HSmM.<>b}XxyL EQl+;,y"KYr˅رxK;k>stream +H얽N0]1DG_mRH.D*  &@0ǼY6^#1qR + Ur8"yY/IrǘMܑNc;^Q=3tq]\x. E~^ߟh~I+<$ԫbPKASyQe9)4jC|"5^)Fg4NƢ0ESh3NҌ:F{Py> NW܉5N+f|[Dv3ڭ-7Z~ uRPCd 5B~„ew5(d5o!? !O/*>stream +HMN07aA G^`: +3j" ]yuBkz47 q!Ikaq#ݐ~0!t틣DZ>hz=xS~*> w6e`ylX9禗^חG^07и`A 0HI:)/&)v%=B gAóэrJJI(fPNH$1g+j(jJݝ*JB3Q\ Qa*MjMf{MU]q,;MV2&'=AD {#߉ä DKTڕɢMH4!yNɄDD'L-`G +endstream endobj 5655 0 obj <>stream +HMN0t+QY.]htM'^XbB@_N4ąoK>Ga%^l|xm}Hhy1 +-UL]cyw,\EԿbIH9kSv^_hvA.%@8#,FPbPc + /-쀨tp rжwܿ C ` pPs@> /5lZT0Hs߀$`abxAF!>5jCnB(FT"@Ґc? 2i8#C/uuvQfXgeFo9 +endstream endobj 5656 0 obj <>stream +H̕0 @!~s撛 \[ +=@۱CvޤG#x`m\Q(Y"H9Cu_7wt~7t>7|4=?IZ*.r\5rjӇU#Տ?9!녂km"Ȧ  QuRl- +f" DY iX2 kX 򀥆E{wP`ow_G3`ޝ`l*Z]YR')BKpP?`)W#.M89Y/2OHoBj9 `uIDs$.(:MѐXgd,H/mK3ȠHȡ!ѐng"-J[CgЪEzgx_{zG}`acpdG/hצI^ Q[d5t.\ X,T \2b[aOѳQQME,m!|}:‡VDz֒BH4BNt-ErHMǯ-VW0JhQFFiM4EV9i+Vȏ&8S +endstream endobj 5657 0 obj <>stream +H=n0`p *] q2H[ +Sf쐢ŢCţ威GGHS4@0,Hnm[_ugWޯw|>_+P^j2To|/|~]W]7w/57,*J@YVcl$ز6ke;QH&T\L$4 +RH&4c[\$1H'mI +g) L7@Sb*XcC1zOK&T4R"S]H9vc_6}H.}H +UI*'y$YMK֮DN~ Q.0]ZID:PLIaBOO8AuO.O4B'}!x '5ƪsY$úX vsIFO(pgϐJ(mV- eNX"cATv/k1 Z Y }Dۏ%HbiEKCQn$+$)M }u1HI=#J/`?_@WmUtiJi%Sĕ=ƽ[7* +endstream endobj 5658 0 obj <>stream +H=N0`W-iD$z`dH+(;D1q r(,hDWAL$XoclUL .#/8YmS[&L>Հgp,pxCKpҷ']aF^hSnLo8ӄҮ? DQVM}_! +qEFghI|A2+SM# iW4F JU$:P{8؎9׻o&8Tt\QQ>UTMp u,T[ӛ]C6AVCgҴus-CV ]IM TAql7U;]j"D'q/c4g'__g 0a +endstream endobj 5659 0 obj <>stream +HUN0C$/ hJTH0ȣ2fls>_}@Bb؟>g9r,UN,]izig1S/j{H7ߠojqdL-,[E7_*˭jLo@Lq0ah`}<#TzhP5Lհ2NۼR]|>uj>Ol`5"H6C"dPPh9FRC)5ЖbUS5U J8 + +B[V t8ૺq<ҩxyo<,"aŹ)>stream +HĖMN0ǍM\WJ|H'ޒ&\G4ev&7*u<3ckۦmqۜ,o'I[FmnnZ.R.~7\^5\4\F0#4F^ވgH:*@s4G#Ǒ()"_D="-*Z!FHDg>h1X+#TRWZc0rҙVqln܃E!TQsj&䫶MLm'a!9QF +<\7Sd )H%dp* ,FyysQ)O Z}dU(0D 4%X#2q0 +WœۑI|INY>QqBgAiߘRFJ`Q,B#QE6pd9r;n&2(la +Qd~**H *WF#ÑꜼؐ`]B$ GZ^7,p +endstream endobj 5661 0 obj <>stream +H1N0We%G/Ц)ҩR H0q`d\-G 3D1:XP Ivblg|'4/25L–m6I*:c=ON,V9׷#T*cwK +endstream endobj 5662 0 obj <>stream +H̕0'"t n’j$@*()@PǼYŏ`HMqۓV"3x~s{>o]= x5]7yUvya7K^-'v~lhN66Po3~Ϡm"+2}t$sb~9-o]6W"_+QW N Rخ`]PO8Ԉnҋud:w> HN_ HR Oȫ9Z'=݄E(R)Pb~p;fS@TBg? &[JTAJtjZADHDyG&- A%D澶wHF{纏2GBEp6war\eF/:DX[AAy +N31"?'R]y+׫$H~D_{IXxSccU.1! qHT$ Wb:o/V2Tixmcj\43{`@ +endstream endobj 5663 0 obj <>stream +Hֽn0`p#H/ۊc8-Pҩt̐G#h HxNI>N$NQݶvzwطzvoo6]b_oݾǁo>ӗ_#_Q5!J$LJJXSf*dlUGJ J +4ՌX]Yo"sn+l\" )iDidosA!X,Y +"{>$ђ j'srb!FD!&,bޞmXwwDHe#)B&Ҝ$+K;wExt0?Mg$ /Z l>#;3&?HITe5!y5%͗RK' El !#p!%Vau +G Ҩ#& +Z6`q. ګvH^Sad}A"L#AO $HqGsY {@LoG0:6& T4$ O* 򤝬#>?XD25f#=G|O򂖞r">R})5) ?WG: +endstream endobj 5664 0 obj <>stream +HAN@Я$LLz#\Dk銤j" ]yuB;#^xIK0IT75<)3=h:g'#/nٺsţp~~ pn=΅N<̯NmW8r/3Q,-1'09AY]`J彃 $92N9b5;}30DC28fMjC`ŰTbuEjzPTPW)ʝUQLo)}(ҖSj01uv[RNKD!#MeNNχ](m$A rFjy%XI"\]6 $+P¡sp$=Я(ZPֻ\th+4)6~T + Zrbu4VTT3u.hqnQ&ksC +%&}'/N,<Git +endstream endobj 5665 0 obj <>stream +H \X#/pltIl))%h () 7ۅ`apywwqw>_O׶=3㟿 ?q>-]>?/oz)VrSpnVLXXO) ˂U l<7n]3lSO VȪ`]:?Msz6MΨ͙el^dʇ7Y`QXŴfx@ ʻ>>stream +Hֱn `"d)mPmdk!Eغ2z@PlһJWu(>~ ܵM/7]WﶍWMWjWnZ6[/G>~{~ۛ]ۮꖮ?kj^*K"!؊mH{9oj/S2_n|g~`*6@E%:K  DKL)$t s4qU% +S /g?Qe"⎉D@:9@bIrIQM~B4:;>7dr +Hvw6;`t D2'fO?_ G~ +endstream endobj 5667 0 obj <>stream +H=N0pW Ho /D$zssG!q;THyCqk$` sO:]\6 )E@HTX8/5v}>2cAaghȏB@y du[d3MalpW@>stream +Hսn08ࢵF@PI-ڭh|m]9r wCP6>"w$=?nCuno]ov;Y^>V>ޝ??z-ֺ͟9kZPx1@SȱzAC433Ec~Ar6^rP4֩p徙zz4i->_@j'*s$N<0P̅1yu.L[+3nB% 51/Shm4r3>stream +Hֽ0eŏmBu)$: .a(X=)}J`a^|qs⿴]kCsw|yqhNE?쉝sw:nfǻÙm?6a϶o#>մl{~| ;f:I" BjR ʜ%Հ>9S4RҴ˹1BVLGE@HA 'p%֒x{G"I;TJQ º)F"9+T5W +&o}K¹|l%bFBOF]N#%L$gI!A Y%a7h N'486E**3*aOPE +#lN) ׼aLق1B'JHxB6):[HHdMyMԜL`F;@?eџ*r1P@8Li)=ƒDGh U3\__Or-Ut!􏅽9 +endstream endobj 5670 0 obj <>stream +H얽N0PK@?R) "Ȁ@@0׏G#d`8vLϾ'UI~;]-b!yiQr^Ƕ\^"v'^dϯE|s{!̥/o(PҰQ%q_ (I14VAԥ[gq\H(%Y`jokkx.|Q5s*dJAh.⑻Z}B!WP)RNZv" +~%D(:yCV}qiej >"D5B'ߠqykC+8ݷOC`й~L1kv/1jtN4 CfZ!I^z~V6ztJi=Awh)EMF{iZ$;Ư*~S^ +endstream endobj 5671 0 obj <>stream +H=N0u@#hBکR H0q`d2zl_HТ 9$s[uVG_f$}Ҫ icr5 =p~M4P7 חXn:&mql 6eHW&v :ɣB1bhu1!9D;Ӎ(ca5`c[FFtuCvI#O؃2A2^-\IAQO"(0_kg5ƣL.G]pm)l ` +endstream endobj 5672 0 obj <>stream +Hֱ0PG[DrOX&+U *>J +]}Fw)$gd"@BE6Lfd몒<94l#w(iWMptVU#o?3߿߿>jyB7=?>/~9z&(124,3c_ FRr:J=%1BߦmH<+ǭa"82Ҕl,VI%i_)#S%91)]] !B%!-SFjIT%gJg-!ӁzF2!igd 5H lٜT|<{`"k.f"RZExqA O+Zc)6:Xj>_WW̗m=ՙEЖ +endstream endobj 5673 0 obj <>stream +HKN0 ]D&GH.O +eC$>] K X7*9JeUc;'!!i^;$'ll8gb:e:>stream +H;N0QDr#>stream +H얱N0/%P@%*Ȁ Gk'c(>[|䓉8r4 ,'wx˩EѢB>u)/Dv!TdSO/<&6حhHd +SC`l292{Phk982PP R7XZ0ʵΉ0 G#jOߦMpxS >5P +ڣHs0eUU݁ 07FmRD:!#Eu;BW-!#EL~| %X[5m*BkZ>AGKR\t, 4ʏEGqd *.FZ8/ŵ`x +endstream endobj 5676 0 obj <>stream +HԽN0YR&o@nX~HHlpP^qN:J^)GIacgl V:]]TE(fUwcv*UYJۦ?ߠb~QV}1?:-+1__߉u(("Em2h7q@MOz˚&))egW9]qt6_mBc;֖[3ZoϺ6v i n- +lnup$t[nsX~J_RmCGkq-^z +endstream endobj 5677 0 obj <>stream +H1n0` +p Ķq'N CtҎs#A(熮 @XE=ROTZjqy).r)OY.ÿJ_oåzqc?nϗE_rMQxz?q4)13d 121>M#T}hsxayknL`E-KXuCܞ$V;&a3'<'n&YNǢQuS{=u-usd',Ќ:G̻b:jS1Mć,nrl&ˋx\^-۔8@-ЦbCOtgw):;A+bt !G7v`J2Ctm:+tS7ƺO]VZ +t]x~hΝ)JenkoWF?&{m6MbBV5 +endstream endobj 5678 0 obj <>stream +H=N0WuGH.PҟRH0q`d߄(92D Z??CQ=$9sq09I4`9 )>aOl9 [QN0;w=EA,lO3ςEmY3dF*DŽ% TJ2bYT=[#,U(c +EY*N>m+U=# )LnR1?ioE4wT$jI `=oDJ ?;|:Zx:ԗ:J̑&Uzx9IJF' pHM." *LMJTrIo${nQ͈ ۛ4`7$)UzU֨Jd7 hSjȖ#$9p'T;'Ԭ(} a 7 +endstream endobj 5679 0 obj <>stream +H=n0`#$/.RRJPzcV 7<"ٵ?OJKlrWyUlU]*ߕNʋT?>n7=׌<.rۼ*_%]ztH';Chr#FKl̑{7v"(h8#j]Jpc-l2ḠܓDگ%UVה) R& Һ1xf$^v%Ⱥ%ELPNezSz*8%$RGݡzf*-<ƭZ 1X]xyHY䬓Ysjo =լsb<;t?q׸wL_Yb9glqP֌ĪM=e?U xfa}ÄgAxj?>stream +H;N0&GH.Ɇ}VZ@"T()@PGQr)?xqV4pϏ=OV*^'6^7jD<풽wyrgrR >&pg4-ϏWZ~~t%BmH<C YDH萚W(rb_(dŬⱶe0"V@S-, JĒe&t0>JgIĵnA!@bdg~6䉈R!d#_ױQun! 46mʲ4!o9h۾T LDn+n"}q#}#--V,^4!sJ 8qGPaTc"Q`WЮq^J\MG lAUAa U_w4@=/zPp^EC15S=h*Gh2h7b&agt@A*5է6ez4*`_ۂ>o4O +endstream endobj 5681 0 obj <>stream +H=N0e%G/Ф)?S`9RnecNTur^OtJ鬠IyH Zs>'Od:l\DG)IvK9.4#$+)ωtMC8$&(F5^Zh1D$XbybF|XK.v_TO6S>L~{Q5,ɐPdf Kq/F#O;|SHUt'(R2$qDE%A2޹R}A;LwR(Dm^ltJQu#ޒבK#G;K#3:9͵OW*NxЍjL S{Gp ( `i +endstream endobj 5682 0 obj <>stream +HMn07"7>BrOQgV +H Vl+A]`K|pK\K/R +Odıvozw/}#Ḿ梋cPq?<ټwWdZ~G67_%Q1*&HVH`(EMu8DaaH8q! tA&ɍ(, ,Pt}XGe"ϒIYER)Rr/B̼ϑ6HWBF&02qe@jIܷ(Dn`OI%Z|G%ahuPPcH8XE/J+d'S2؏v{D/QDj%b$A;->stream +H약N0ǯPK!~~PNȀ+02`-Gr|ua@‹۟};b+^l|_l7|zß +vn6+-y>L`ꆿ}\J +endstream endobj 5684 0 obj <>stream +HM0`G]y L۴*tV VXq1︆^D1{Ͽ̈Vtտ:npnSw WKg_=uip~w]<ݛ?ˮ7͚XBdae*(Ɔ|^Z\>~, 4`fr`l݂ ,4){`vP%H!h#Lv +7?%&,%r +C2 Dpm\>+C&va@ +aňG Gy@[s)a*/6 +2lYvd5W_ʯ;6h;צ[Ѥ{QVjbaMlbUIr +}Pp]4Wa*qT  j@CvG\OU)b4ft!bu ؃ +!(P<98yGir|σ9DDX4hF(̫[jaV{3 DrlcnzpF 0_u +endstream endobj 5685 0 obj <>stream +HMN0Ytɨ,Ltԥ i¥W(w,Hk;}/Ĩo1ee[|yT{-oή3zG ӧG͋bo$gWt]%ve{]Ϗj3pxCc*? +Aj$3$h! N" -6RR Id@" %O$@ls#pĶ i6/⏈(H + . '!4س 1`tRL٢3ϱD%,5$ $z(CÙSwjTz{V2C"4})t^)1"J(~[~O8BsEL/;ZuHHz+OV+H^trVRv#!cCQPI܆lx4F"IE/r +endstream endobj 5686 0 obj <>stream +HܖN0Se%@N +Hd@F͛G#d$}w*v?[g?/r)2V\AtM\uM"_".6QYur%;ű2av/??^Ev#2א֙a"4AF9"\+qvvB1dNET$A%N.=٪Rd\@@ }vt9?nQybTTBt}K4#_ GIO8ÿr27weJo5 :3?g(-U6a(\{a{);;z_xr-"DwT5825q*Zc8 M+'WcG 4TN<#lFǏTbHXJL;ZzMK*?9))ӯ(n3(~ +endstream endobj 5687 0 obj <>stream +H=N0p["y 6"F̉nC#R~K/b-bvrʎOf*;_V9Ǽt6\ӗEQg}/:WK>_^d9on'zg4 P *"D 0 ";?c48apaaԞgHGEeOʑ@i!UHHMD`Q /`YTHw&R` &ڒwGWq4FOW((Oߨ,}Q1EQ`ˀSGX8G~{jA@ŀmlPiXmS`ǀq XW)Ul)A1`'m +l*'Ro(>%PW76 0 j +endstream endobj 5688 0 obj <>stream +HN0!Hz#X~'U9&уFh< +ږqq\\0 C;M I2XE 9)%dFˌJ֩ $~ oXxswRWDX Zs(?SBhn aemuǶAgPMc@6(Ab+-{Pa'9̩ف<ԁ~B9C@0G뀀ctF Tzza9 o +D  + pe} D @xȏA+ޛI&"{cMܓ/@4 +endstream endobj 5689 0 obj <>stream +HMJ@)]f#$m +B AW@]Ptfk@k4(*Ey3yy3i6t1;˽b.ɮ˛vvewhŧWi-,8tu>>< G="XB9&7mFeCRB)mUX2<:EcYQ˄4)[KƲjU㉣`i贾c<_X۟p{ K3Az ʊle"XO z Zqte' +sl( +WK9cD M@L(Ķ># ˀNwVS"h˱K`"\ vb՞7*!61V,#uBnTa"9S'r,s@ᘇ.;LL`+0x%` +endstream endobj 5690 0 obj <>stream +Hֽ0XMZnݫV:@b $R"b~?\D1o9I4n/3w~_oNCuzq?7'9⳸Eʝ?=־O"ؾv?b{E˛o奰0k[ +:N4"P64uOIrr ͜ɔ`3ptt6er1>r +Oh8[K`ӚhBiģ%9(M!iUh95^[캹:2~4Ԥ9)`%FӚ$2i5cHcZ0xOeX? $ؚFN@:1ez& $ˤY@ma.cDOv/u }˖lB +pM M8ERؿ4RM@HCaλ/yM48J`fR䗵i7e蝭iQzxh*~2TLR` I)?$XV~.<3%jhBsPBV ygQhSxj.ulY3XHsh_e@?" mMc4VτDStO W|!״xuw. +endstream endobj 5691 0 obj <>stream +H=N0`G%G/$nRH$2 s͂o@2D6#xO~?l,exbiܒb^qzKפM q_eA Ƨ)MN2,IqiqD>$ϖ(gW$YbJ +l>`uuS0 +3%5qtk1Ն ꆂP(0SbBpU$mD{=Oh;ؚ{zmS{ԧiuEiYW4Ri#NyV(^m:A['ߺtf lX7tP^LKy-7~=F0@wEn?e7>Md +endstream endobj 5692 0 obj <>stream +H=N0`G"y ih D$802`%.d` %BHXj|_WX,Ȃ7gk2,VCqGEw:Ͷ|wH\w7yuM^_qu7M8*c`(=4 +4(QAT Ъ:ԪcH5bPQ#9: +@d` 53#-C9ޏ&"BCNM3 =PMZR%]H`&וݕ[=pn%Md a^>iV抵+ fCd7/͢pd_ H~Q얣'7;5%Cn-;w7{c/Aeh+Nddžk#.~L"eo +endstream endobj 5693 0 obj <>stream +H;N0KP_P&S`99Z#xCg/J!RZwni33IFZ}q'7UjU亪E=CE64Msi>g4%ꊾ<>O zXj w1Rǒ 1e%Qa6mb/&q2rm ՇDS]oB&0dc.VUtnhFӀ'3ۙXԙA;V SĚ'4u's,rv/vꄝ#s4k HSfd.C7ܛȝF78$=>Xw|2a/=!Hl=ME"څ4>f*~~Vw#U`{ӰGd0j +$'if8iƿ7DYbֈB~ܐw_ + +endstream endobj 5694 0 obj <>stream +H=N0gu#h~hӟ)R H0q`d\s2"1p!cvZVOgɳell4'-`d.sD5"KK鲢-^|Rty}rVWVTxm(AaV( (H 4:zIRk5ASAfE`Xc0!38b[>AA%z/tG8C#p'@uAQ5}TȽ3 +V}:(ٞğj1bz},tHP{ -4$,=j' +_j+ |{(au`x@u4ħ)^o~ b" 1w  ps^ƾpbm3`h1t^T~ 00 +endstream endobj 5695 0 obj <>stream +H14# `vL+ 4 +aAO~Gcև1IU]3xZafI*Unm=?ܵ{k;x'yO.aVpQ^} 7onޮ޾l;9\_> _ +IyM AN-@̖1O̞[1$58$Z35fz.`lzLE7~ 괡G=,;qV~AaG4+y&l)jPH+oq&tQmg:LY>stream +HMN0$pzCffE2j" ]yuB;q] BKK$N^W,I2Ol}6lV UJb7)xSOx/D9'1^dnԃBjlj,j +a%@*!!QH_'TТ +i;~# FQ4P 9hE@S[vꌕ#XՃvwDʰ`D~"H4x1=hL-$栲̠S5)Tiɓ#9"btИB@oH_1(Tq1T +/+&nv'$%gPW2 +$UWiWCzqB0jC#H狀 tH*z: OiNPh2ǷS +endstream endobj 5697 0 obj <>stream +Hֱ0PRd!솻=v$@()@P'HKQgJHGs)vd<IݮՋ}}yi`ZU뮼<=ۓ~Nn޾Ur{zW(h4P61,{_0l1/79TJ\d7ݚhKLIihGiv&^"{D.tJK6"!hp=ȤT/d-j_$ *TƅFn Oɦ2=-(PC0;NlN Ag@ߗhPF0P$N~*و/SrS)L =y$5{3.x |LTJ0 .'HY/^,0Y'o3ثze4tZ{!i[DԸA*͜ LI 6p 8ַ9eF1)0 e$[J?Ts4#a,zQ&mS3 S%6OV}@+zǃbE\E.$?ȿ c J +endstream endobj 5698 0 obj <>stream +H=n0` p Ė:'i Cdx4& )I='Y>O~OjdGf~ޑ}i>F4?Z0Zwݮ%?oIiۋ/D?ï[|UlLVs@/؍!H$ $:B콩/#3#LH=:(.3AVWA#ITO 3!Ԝ5T.!S.!ti4cr6Ll'ُs3V)IHz$U2Pd*Kh(O +PUQosuГ $P{>O Ѐ1TtV9&sf$ +`ɴ*2቗SXKB# +~q Be$l}hH^5a~xHR@I;&^DN A! # +oH =CR),"QwÉϹ'N2)"1HR?Qg{퀯_B +endstream endobj 5699 0 obj <>stream +H=N0`G,y 63EGL@0;*FH6֌ۉWR*k_^$ͲYv8_d{"3(ߟ,=ɱ흸HwpwtɦFs6^e9.pKd\m?]5L+v%iʀ֤__>DC-[`qɸ vfע[m֓{¦ɸLjݶܬ5k`A \aSh@Y3]3Xm)_]6Ц +8 w[~!ݕXZ5EN+dBgtU -)/>Ђ{RCGM (LBCߐߑtGߘRCX ^!M], `^'E +endstream endobj 5700 0 obj <>stream +H10`y\xF>uuLfH [qڢZ>˒gv]"Q(-'}ױ m^K]_W:ܵ/.κvvFu_o/wgzt}Ǿ^_%93AM14R0|ɘ 1(  Zal%C@bP3|C mgy=, ʵ" x!g+,KQ9v(+61ՖB@TDo%ChFA4!!>O\!lS6!ғ=DWC&`N`ms"|u#`/nA`}̋C]"P#c&n0t@5E$,mU0Wt@qrDӮ\$ˮL6'!EeArP°@bP`GUA8s}a'j?p H GV +endstream endobj 5701 0 obj <>stream +HAn0`#Hp0C"2 +)I΢R.HԮ!Q|/YXvl CCR Sž8uqqsUVu.Uyo⇺> /n˛;עcQ5}/N:~*~>Ώܝ@kviF`,KHUĊXĆؾc oj.XtX>:bE&d׉qtsX9C3ji;&qffӾ[3i-3t'=>lr6+MD&mZf E܃Yndi6q];;Akb}W;Na`қSВxJ':Gsْ.b:1Nh=:ltuV9Z6E/Cgh-ЪA6E eҡ!n{_N7:{rƽuG`\! +endstream endobj 5702 0 obj <>stream +H씽N0K~IKJ3E*  &@0'Y%P S'Eپ/cU66eh{d%b7]Dkl N<]A,I,wWL;BkҍK TQ TAk5c 22׶ +:tSѹYjw-(&lb zH)tD.8QJ Mq}[r`f+[al~Fմ>iy +P6scƨA( ˵@ `HDAxc/G }ct BjԮJFNE'oygxňkw>stream +Hֽ0R&r{U$@J$@K<G#.o|w{)V:xaqq:=^}⛸9mwyuh/,v7F ^޽bw~;8p* +г?3TZjFUyM-+dYnuG[D!`z:L]^{he[^? +]ȼľ +r/Y5-嬅&οAwTg``.Wڕެldg;BxZsm|uH ୛0ުM,v'ͦ\([myiI UW'%LP*MC8b>q#Y֚)&ǝ zs{ѿ'ȢT[3OoiYތ.|>&6OOB*g[6Z&ǯswP:MHMN/0jJL#MJ>jjZAOorh[l_t^y?y! Y }0!88ӿ/ʆU]vD'= +endstream endobj 5704 0 obj <>stream +H͎0rKymnO N<ps͛" |!3xTrh/dⱝ}ۛGz{{wv{8^o{w7:[w}7I^w=w_7bwI׋EwT|ȳ-&S:=%6nZ8!tR4Z={)8QZH:n_Lr4IWd@Boݲ|]\p Y緎ȋ(}mp 渉jB +oն$dNU؟ߠP9^$$C:Z +)^G)1OᥙSO42Pe«t\S=~ISZ? ײOY<;W + +endstream endobj 5705 0 obj <>stream +H̕=0'Jɍ\`{[B$^-@!*9Ed8JES$/gXݡ/UǮKO? b7{vwf9NN]ۏ/̸+mS 4PPL@HJJ`MyL6%hh0&9w`` Na\v~kLb 4Y@OˉJ + mxm@h&M\_CvmF,@4?j4v +z tF 3€cM[\bX`Y&\l\%>ŴTL]XRyԃ#o +|F@2wZG֒ӰWC\F-,5vWSh+g"˵3%`:3Y{$⧛$2ձjQ`eq L2г)4VJS85䀮:ƣRbvhu:jM)ݯ<`r< ؛30R!{ +endstream endobj 5706 0 obj <>stream +Hܖ;n1GP ^ֱ`ZNT9҅oEx- H.4K|rv;o7_vn= 6;={G7||fG»[`?l{wl{ȿ1g E +~0@Dq.#Xʖ  (楊E:im@"и &Hb +: 5ByJs"hUhaRdKHPj{-m?&Ն5"F' HwƣL'Bz jx1)S헇q>֤'BDh2Pr]WІ> ɳN)XzvZAUȉT̥]ژE`U>Ws4Q$U ̀.)B-t[>stream +H=0`RDr#LHPqA=f>2Edg6X=[lggv.pf/<=i_z7Yp#oׇ󼲏_ㅞ>ް~w/BO5M CW"ZNH=&NH3I 41H +Î]7ˁݷn0Ӏ_ vͅplթ7Oy +n: +t [ aoVlC za)&%w F2f*r`u0C1 |@@-p@pa*w4@^ N%9!*rT9j5AQ=z(P="H>/K[{oku:[ rt  ? {= +\1h B +W"j ^.$DxCT6 +endstream endobj 5708 0 obj <>stream +Hս0`G)" %/p;nitHPܕ 7fKQ=c;Y"/?x<Cx{YÝqw{=O$Vx#^ܟ_|{WM;YL X -ցtDlW#dV6uEs61 -RK6~2Կb64 , +Wgǻ`-^c󙞴!Qs\ScfnwDl f]Qώ+yelocxٚY1{A5EA|xACE쳡¾w]gX,S⣵EXR[x_Zb_)gTOb, +I hRˎygL P=SbM:EA3|EG*y|k-7Onk^_Ц:Sw?Mn߳21kI? +endstream endobj 5709 0 obj <>stream +H=0&G\`w&β[EZ@b +$8P"z(8^^$KMlgotH(~vn 7Op7(GYPǻ oFl_޿~6b{x5|8<~U|],L *@ I pO<"}|WAs0' ,q#88zFZ) +Sk}/`R#_r@W#Zr^TZ-q5qFǑ`ɢ:R `K([W/>stream +H=0pG." dW  * (;RX y6v$stjO\ץCuw~W.yNޗ|nOgu|xgu}GW1Z9i'D_p.8 !˫?n.9 t50m+m6Dttv{}@-GFXDu3ULHq5Pp޲١߲/9m9@4F֕ZNS:,0ҭ=SP&514(q+ E$(i+0z hIth +N X%pz40̾ 3qȉԌ!clc! 2P_hsdBF[2-l0P +Q""M"TdZ33-7q5ь)1o9uKHЁdt5Ռa{b[1mhzq`,HTy85,SXR3 ~yʡ^_{[FV +endstream endobj 5711 0 obj <>stream +H=N0eG/J)RH0q`d-qE"JhJ$<$d?vl f鄩'ߛ0޽8%Ӯ Ù5M[ͻ!%]0ΧdtdBFChy򈴾"FQ!KBJ,`>F Yc6oi,>BG!Ƅ (uV fOrDIh!րĨUR<( +?Ğ"xJܱL M(QSR54dfzұқȀ22rMO/Vf_`rH3kS#KrN +endstream endobj 5712 0 obj <>stream +HԱn `LG8Իj&uhN}c6팏FrzB_GaHB.&$Ic =z[hL8{as^Ht}A$tK"Gp-+nDLB + k\M͔6uqI|Di*Z~zBM%Ss$|Y.ḰZG2d9jf=wʍB*YT,&K~KܾHenP3O If:UODt\ +:.b./CB="s/1>?Fֵ>~O +endstream endobj 5713 0 obj <>stream +H=n0` +p Ķ8'iCvR`kh* B +)I ؖ>#IYU}hޭͅܞ/fozOwr^k';z/us9uCo?n`d. H# 9Ha,ez:#ib8;ӇS2g]?6Zl!6<ɰ\doD HKU 2QtɆ0$8.XFUK_|`=B=JY UQߪ +!D˜~3Ջ;2_^b nև$Bx&#bnv>׳_vB=_iy ÔJ{-=ӫ=LF +endstream endobj 5714 0 obj <>stream +HMN0Ytu\8 ]yuBk8Z#살Җ7qoA7-S^ˣ, +VMfp8ᚭPgZ53UζCQTmꎕGtu㒮nl#^P#  AOH2Bp dA!#A 8BḰi2!(&Dw7v" 6v@=6c6zߩ1P @6}!#dDHVH"Qz|A AG`"E1puJLu_RCoAjѺps%!!+;K2y¯Ml~UX( cR?ҭ[;hlѻvHvjOԴ "Wn^n-}`m +endstream endobj 5715 0 obj <>stream +HձR@e(GL2uhYJ<=%qb|-ph>~,V?[/y؏>bOl tX,$=펅~YxG) g~ݕv9xp0]J`cdu b< 3|x@nBnjia3`NYPe=[,,U$%)d&gnG|& (ĝ}1@lLv$`OگjurvA3"#+qfE-rCWNLΕ̤ aA#Zɾti1cc K Fi!_ߚ(Sr樃] `) +endstream endobj 5716 0 obj <>stream +HN0.Hz Z&LM`=z3<GȁP)MGKP w!r +w=3qlIߴ?IiJ^1щGwGLJkpݓ`V9+XP!U&. ת!5橳\4z3@s96t<@b5k)TG|%ZDp%\% ԞGp5Ӗ5%_'.wDǗ L$zMBr1ɿ3tV@t_Ѫw$ 5Hr]dRSc\l R/-*ϐ݂ ՐZHγ8mo3{ +endstream endobj 5717 0 obj <>stream +H=N0`G"y 4mZ-S`9>2fbI*j%ʯu0:|l<BZ"WC`XW0'།΀RMs mjڿ/ڜw^C?0b:Tбg{9#`0Embg# "X`r!OchUF/szK +endstream endobj 5718 0 obj <>stream +HMJ@( B\M֦ Yvԥ E3GQr,gg7?/R*8:yyLZ+zm"Z^~-g\lzhmhǼ]Bgq!#nɣF;RjQ +Ab8`f1`Ca1TJGQ_RmQQ'2LvsudJTXv$ei>!7rJu# "YaAMS0uu1FE{mi U +d9Z$s6dR*WAœ(":NH9&D&ÍM̱0)"={I\7&zoT۴,߮~]LѬB!2[TS[OhSޟν%:QIXr\DF'? ٥ +endstream endobj 5719 0 obj <>stream +HMN0GXt# _ʌ"5~h iD-Vyr^Fv')y&,ʙ 1cOdSg$bixssR푔D|(\P Yϥ!>R;ITi>b<$-&1ILj8f/O đj4TEFh n(G6 +HL^b|q6-^E(^ '$SAX +endstream endobj 5720 0 obj <>stream +H앱N0*y#/4iN +Hd@( G + KuaW lŷtSϋήǜ67\.4! HLI@掐%1j$FN>@":b$1ĨŬc$HL4R& etjXᮌ;ShatfMLWo +$`9,ŜblͩJ-=hxC ++8 Ug* +MHLvT  [ )XO>W wIs8DoANNb" j -m]vwp H7!nYZ:#3>.oO `>stream +HԕA@ ui69Br6-}H Vo, W'.2 ;g+oq ;Gg}潺}Qwj޽|RTj{yQ|坺9O){L6N=3Ji*J44TWԐnMՇš*Ptˣv#~gn*a0rX"㐎 ꢃ6:nl6F dHeV&dB-;JNFoe_ ? zo40YamJԅo/!pZ>~ ̿x[-0^? UD́ +endstream endobj 5722 0 obj <>stream +H약N0]u%@RN +Ht@FHlQ=X1_cqC;_|yb^-j3_d= +#z5_&-74rmNӂfΨ&KXKv!HңBbar[AHqO$KF{FajG5)$x^VbȫCC# B+ I+QHD" G]uKVd +~HZƌsjBhZdH`~n +FC bpMl)zJwPA ć}%D:tKJT4aœ7&L;4T\M0JC|?O 6`^B}mv#S֦Ԙ+Ԙ5 :W4:%P֩귛Z]"&ONKnCAG2!iёF@w 0c +endstream endobj 5723 0 obj <>stream +H=n0'hEG0/J֝ P)H;vhСǪz^Gȁ>0<%>Kz{|{~qھ:=xv{p >ݣс>vxv;ۏx4X`C\È-|` PXȱl~hldac!l{v13B,7q1 E_W&[[3 Ċe>uyn ++3el`6 PX/>9;m׆"^y(=QKSi^%,JSB%]Cw,oc\) Sߍd{j}^a!xvVn uUZV~ճv/ZOh_վѮmGm K4 TOa /w>stream +H=07JM\`g& 3T =lZQ,q)B{/O_L7=6WtlΏo[Ng]=toEnA^4Zn^=mZl>x'.τF'm*32}pE|0!z#K1`3yq/p;OYS1Kc`h[5{tʌLFQcqbcJ 5'0-<^9s xyT]"/:_<Ϭ2ۂ{c2ǐ`θO&)]^6f%獩7p7 5ǹ4;oxdSՖzf׺/l}"Oa}m͜g,Q*LGG͉.#g_ed +XFS/2%o̘CsyzMs+ %<@8ڮXp>stream +HKN0'ʢ79Bs>-4J$@,@%U| Y1y؉?H*Hx8_=3I OqאG +]D%/‹Y' P'P쪭SSpI 0A ''(UΝ1h-^q&JN#jzP]>;- +@Y$rKaU~#&(I1Ph' @ʐO`o8)zKjFd@R7F#V M($Z.i4}h@jP˪"ɦi(B]T *5}CH F_/ oߘI8 +D⬊وc[v>% +endstream endobj 5726 0 obj <>stream +H?N0_rm@ "ȀFƵ|s"$k=#uEClr-EUXeE/炽uaѼqm?f%o,Y +7w kMO):"pF`@Ǝ<u1D#HѸu#I*"0V" H5$$c E)O/DbSl| iFI,;ӦsMHLpj8 | +xi;ً9z>ݭSΤ;[Wp96Lݸ ++YڐZ_6H"΢Fta6@K1Ѵqb1m\E߯>1C> "V?DBShX2 P)rE-A/@bgO1FeG*r`7 {`? (˚ +endstream endobj 5727 0 obj <>stream +H=N0`W*y 4Kӟ)R H0q`dT \) 7b8~ڗ!H~ϧҞh-zӑ D\ϝ+!c 38;x|x W߶k/KΘG1Ƃtd˜D%@R)`GTf q)0)((fYS &E]_G1ւ5EQZ."@R{KvF\c;Hi1E + Y(Gk⭣L29" @5{>"R5|#ztgDfVj6V%k6 Q\0'H_4G@yIީOSq[[+=B&rϩ{=P~L-@"E胒ZMwJV6~K%, +endstream endobj 5728 0 obj <>stream +H=0Pa ^`m mJfE[ śGanRA.)@RLch Mk[]wnw|ή;LN;smnNOw[G3?G-ٿ5?vd~G~DD_ +!C%`S"0zr\LDe- u.Vw,f i&.ѪR%88 PKJFViѠ +y0HȪqEټ㌶$o5T|OƮ{Tne B!)ƶAK(=&4½dTȃXM۟-zWdG}tr Komfg^@HTfNMrk /.U!77zKDbCvo2dE<5 +%GZ.k݌uq|m/-+fBrD +ub;KX"f2VT_7xBi ,h +5s؀4"AK$ϟV]CI] L>ˬ +endstream endobj 5729 0 obj <>stream +HM00h  Lό;) E]:]LKnVߤ:Zq IYYzݷ/:noLz۷JoQWp!z{v?i;;~h&7p8V4 #$@橰f5 Q:aBsc0C7࣡Uᩰ o'<%6:qh0)ŜSAhf6.31󟬣YE+Jl¢6۾dvˢ>8P 4z4:z9iKA ^ C۩ oasipfܚ,%e|!?oznæKe7_)VvGxx)! >p$^SSxgi<{צ[ (_2'BHoVx\3sokv,aɐ~vax,䵃nrAe#-- +endstream endobj 5730 0 obj <>stream +H0 SP)B#/3]V3*- 1$8qE@pn-I{ˡjI@;D~c;p8;7;9 Aǻp{߃͋]ǧa8~x~w/Ao~WzK[>KKTSN.CTTmK#;!0D\"F+Hכ2چ(R٥M<:nEr fsTED>F 5%ZAȥ9@RH+i6%cq"]B6&HFc138FF(˅  Hǰf!bt͈eNHBJME;<$"r#EՖBRUysD.5$l +Q-J`g1D QatXɂA)GKx2t&嗊egVRڌhz][k)+h&#  tNl,%ڢYht2[[i4[#]!>5~.5fդV8 'J|w#>'yƸUF4O`UEҕa*^w%[-OAlZglnVbÓzUZ>tG]݊TrLRa 9 +endstream endobj 5731 0 obj <>stream +H=N0uG/Ф)MH$2 #PE|,W,-/l=[NܟEtXȟϻUJ5 O$TaF I^秗{o\0p,&j!c;h)T`D/P!a1:kUl:ё[SaQ]hSU$;;(ӱCC Ǥf"b +L{TQ74BQ]ew!*G?Jzѯitibz$DsI"WB'pHdN $eSӫ!Vӻ~ظ&K vJtsO٬ю&iYK]M,;\| 0ȹ)" +endstream endobj 5732 0 obj <>stream +H=N01D#hڴ )R H0q`d\ƚ1Cdcǎ@DPNO?'9ӔNOIJ,^&Y)OV 臦qHi#YdvK4!Kլl}}F]qE__HqN ҏBLy$'=9db*1՘&9?:D nގC%@S;7aI]iI."L&=7dQءv=7 6pC5&I#v hHxG].~Srr w0mcbH'>m!H_h&3qpiŪm Hi)lVs2ID;yHDCOn[=!C̱ޓd9"{K=Eo1} 7C% +endstream endobj 5733 0 obj <>stream +H䖽1g"m/p,w ))Zൖ7Gpkc"J$[x<']_wa0*uá/qOF_uQ<?~Q_(Z3l$Q@łY2I`eE#,J05aKqqX-'21N Lxi VXNJ{LL>5 `ͼKتj`CKP DK0]Fh B U+NԠļ + +Ay8,<gY] F]S_ӜC5Lj:&aDKYA9Hfgo |qN'@ŃPLQyV-皸ɎCneiAC'."چb&P{8Tq=Լt[?/R74eUOKH5ag$Z4 'h4&@Ԕ͟_zG>M\{).5HyEy-l/ ? XN2Eʾ`#|]>-gV`~ +endstream endobj 5734 0 obj <>stream +H1n0PW H^r|NBHi+R;m,(oH? a;eX,CH|-:i^ o<]0pЎXiO|QՆGB%vWB(/<z fC*{!k e면u jhWE*|BU lmR>jF$c#O?f;/)xl @g)XdIdwt&}]ETϓo 7Z%yr2 &jK40鎋ߠu<P@GCѣ=FL:*)UjB9}$IgRO}@`)~ +endstream endobj 5735 0 obj <>stream +H1n0ohG.9JԞ-Pکh:fhb*k5dER)1 +B(/i/6ywrj/M}ےdlq&|#7{R-Yۺ%_1 OT%bfƑ%@a b1s"{@,Gj"7`b2Nl$&i3~jCbA ɃaVѣL#猙 + H-1H`&Vz<΀e{5 +b_-MDY!x9d}J*?wu*\baܔ5lXl8m"E:TeD;Ʃnf{ӹuLRL*KW|2|fҭ$\0JoUbE&g+841jfDKd2KSM,{uL&}bR 'c}d"7ua +Cr֘>c4nG̫i{A1rB'~#AgGy* +endstream endobj 5736 0 obj <>stream +H=0&7 LXT * c39BJQ9.v7:og{96ٓݷhC>s;uys? 'vawM;M/> BRI 4 PsR<1)9*R#?G;H${+O]? +iOoGS|; +VuFEh2yS%DIV +wB:]OܯD* Q]x'\r+kҦ?N9 +njt1mxHJ &d:(REKLR0%HJ g=Yݘ{BjEͭvIM*s:LI5^kbY/V߀V[K&Sl d//--? +endstream endobj 5737 0 obj <>stream +H=0`G)" 7\`g&2[HPqؒ]\%WRZَ"-ZmovmѾ8r=?;v}ï7~g]盛oNϻ?n+H`)0REd blA[(FwoJ`0LTJ N U,L )LTEA6 >.V&K-AP& + ~ B].& @aǖc$a%.`(u_,Yv?b+eҥ*b؃E~)@Oxri<868.8Q.8. = .T&oҟ@a$.a %@úwAUq,3LM ugGa=`eprL0 rm=ܹ!;llmR{_mQ"ni 1[TX#Pfc㘳|(0ag @О `X,Va5RbF:E5,F@k[<Rs#( +BNUl4OO%%/E +endstream endobj 5738 0 obj <>stream +HJ0aK֭l;M=zԣ7 " +^r(I 05`7 Cޞa/XbkD=%{"mC}"vt|4 LE$0R-OL0?;pƝ/p'0{!K#l| 0 kz +endstream endobj 5739 0 obj <>stream +H1N0`G"y iZdD$802`Nnҫ(X;vl؉Kd'dB't6 ]">,V1LI@9nh>stream +H=N0`W*y 4ۦ?S`9o|ҼJ k9{y-B>Ol +7mpU.ErÄ5gMm#e4e|%㋘F3i]Sc̻qEB\E#(& +,Mg4*?SM|R_ѩ4>&_ lE;q1D=~JFb@AS$x(T' *mRx?[1FB6!YVRbXzK͒om҈֐$(L57[/ehE+)(n_A21 ̢DQ+@սgSK":aI` u0uEFoE +endstream endobj 5741 0 obj <>stream +H̖͎0e?B6uhW VlH`]#^̼%^KKX1 ْM=7q~܏xsNx:oo4mOD~{ޜzwt8ݫq:qa@f !!@ˑH ԓ@@ ,@h *:!^@<f±j:dX +`WYy +w[g M(Q[FL})116 6%B_O6 +6 +(Y@^TP=r@@l ULt"?*s i 3бfHE+_$,xhLg:7OZ(s3"3道f +<~Asmv}lrW44y@ I4.ȠKft k&,[S E|yy&yS(6b]s\G+ΫPJ՘vq"ʗo1 +g&I9 9QbIx5HQj5i37a k +6ҡ 6узhm +endstream endobj 5742 0 obj <>stream +H=N0e%G/$n S` )fٸFA T*P<4{YЄftEEJ3efQJy$y$,;NO=q@+P2_P$^^RF%}~z'38.;0!Dwo.k@{{#xhٗfN\rP"$@t3 P zKCF"1Sz%#Q w`Dp65L03Wǡ\ú@D?mY޴ʪ);fXacM]*]qvi- n?`d(-Kn +l0[UkD[Eb"ʋe롑K +hd~G_$vhPK!ݰe 1 D ۾#{D߲{ IH )_=$i | 6F9_k%<4 +endstream endobj 5743 0 obj <>stream +HԽN0 "yo@  + +02`x,̛c.M(HxpۉG#9\ 1(dNUoxuXlk>u{ y6dhۭA/\Gۼ4N۠k]8:awн]1% i>mzYďTfM0IMh2E@NG &IL0H>ʇtD#RaG;iR7n=hԴWݎ&\L~eskKYԵ@((RL8Gd[W[MnKU XV}6'7E 8Ց;{HbGݙBF* J=)->ciz,aԭ-cEäQvǑYNqKLzDr|$u6\F.Mz2ĤsMSkˈ/h +endstream endobj 5746 0 obj <>stream +H=n0'h0EG.J֞-P)h;Hvr^GU[9d%=O c9gOnͶY5Wu{k۫~ea̝;}}޲7M6͏?[f*J}FH%HIQ2H*\H:JO94@NS]NO70By9 i |'IWBd%$(H 2p +H +.# uF#|E +RCFB" +3ԧS1DS1ܺNl4DnI쪌>stream +Hܖ=0MM\`w3U$@, pbA\l?gg5.fyv9=5̓.nnX-%:hflX+%J, "52v΢0`9!ђ2r"%ZB- ECBy HV ɀ/Iq KD5vA\ZlP*Ҭ +$1.({)xx rJ)R%U@:48;EdMjey+;F/ʑ?0RL{QxA>stream +HԖM0'R69Bs6v] -du8G"njI-43g& np۟hpg/K{}f^nkۯ9>vo~Û罽_߾~Ԟ_& +GP#㳶4+*9+ )B2!":$&d=ѡD3I9r +0HHBB!I#$!x( tHJ&w9ɐ꼮m^;y%Z9R2*4H+‘)\hPa[Rj11c/T0 ADA/&Bф>stream +H=N0`W*y 6-dďD$802`N`n@Ng=JU6>stream +HMN07I7=8"5҅F4bG`ɂPOFY(y)<^=y,pB|'Y:g*O_]76Qlv#٥34&jnΙO<>`= P`Qh&(RcP,7^nR7Sk*JL +L%j"H?S-Y:RldKeI"2Z!׈$$ONi8Fd$DҤ0 K %"BI}O9i韥4F:g(>LT`1T}#?_&0pnSKvrLXɰYX[2%]p +endstream endobj 5751 0 obj <>stream +HMN0NXt3G~FtdD&҅F׭7#q5HKB)?ЏJH( IHbOb"Tcr$rV_δ=ަ8:9by^yz|)#;Cg/z4,(kA sÜ|ZRMW5\+`́sr/Ljf Tawvվ0FL.+h'ѲsI{_jMLO.)6֛ʝlazX&(g17돮ٸb XWviriUXfZM |+!rM +endstream endobj 5752 0 obj <>stream +H=0%erΰ*S A-)@!%7H(>B\D s7 +؊7{mvzWC}u٨[{9͵oOinv>ߨGz߶-6;uo9CyzY.!("DF B,''G:i.D"XQ@ "H-/zw%QƐ jnCW2Qg C9NBdNF3eD 0JT>stream +Hսn03<G_ C; HSS!A;KZ "ڲj`xǿK H6@8lGߝzȣvu]ٷz}4O;za—+qϲ>ފGY7X]X/\dܙlb2&(.h y<2kx2~}x2>hG=3<9o\"yV5zsS6)9ѣ;RBnE|$kѢo$wd tzIl|U[w'ܾ2weeg`;ć$~ 0U" +endstream endobj 5754 0 obj <>stream +H=N0pW"y 4aT@"L@07J>JQدBB}C V;YYuZ,bEeN85G_^Xw];gqyOpyMbeo/r"CfvwGh +ol!d7( V +N7yL  sG[{eT#gO8݃̿ekD!0 #oGSb3?Uv=p@ +RB"@hJҁL!RW) +j-3R`IlH`f_%<0MN tӀ$)Ã"6Г$@D͟!)ɲR"ك.mT@STwK'}]n +endstream endobj 5755 0 obj <>stream +HֽN0.<}zw7595D'Wut|}4|#CҖ~AM_@-hQf,V.K)BYI(~zbSJ@?qί =Z|8^>stream +HMn0`G䍏\3ӆU@,Y`H\R/#xEc=z=~~qD(W&Ɗ%0KF#" xWºrY<` ++ +KKZП (/7D{.J<+>stream +Hԕ0mMZ.9EBZ-+@PoF%Edc؎g +w8Cw?y3|7yp{uGtG/O*~0CGy}+fAY?uX!hąa-c/5cx/a%,1N9.fJ qK$%zWT<ւp(2؂܍W(0m6V}uz&ӔjlMY6ճ*g6=OlŋU&aI=tH\{&NZdY_{_q>fyn ̪uqnunN&8,kDQ +kr6;7KG6 +06F?8Ї~=_\6p%s1S>stream +HN0oف^`cD&zԣJ_G豇J[_6Yx4]-du8z5䞬\*#DWa<_:I8c-IO0.Qr1Cp͊ 0qLMdh=?Wg00qL%#mgԔO&)$ " +endstream endobj 5759 0 obj <>stream +H=N0]e%G/|S`99ZI1C??"T'~?V$a [DEu6htYFKf-<xۂ,_NJ ҸaoO>K2qPCAVN"h.jET +Ko(! 2\u͍PQI2Wm9Hm3=CqTQ> +6ߣBY;o}*~j1aJCBVYpS$46;ru +uҏz"_#Cѡ$<!kB@b⿱&GPMRH=\%Bj/ŰMԗGѣh"y|a߮SBBGV +L%TĵƨAh!qu*l%5zF^)xB'%?F*bf}kwS Ge +endstream endobj 5760 0 obj <>stream +HMN@.Hf!G`.P(UD7TMda+.]ht 7U' +$@,TfФv[ |x} i2;)EY̘̣=&fdR x z@AB~F[:ȱ*] eeoZȁkq'^s\c+?ڕG.'o o\Ay`oEإw shWZ'59w-~j2q-o֡ZZÞ>*|'f9CȹJީk\7̷u vFGb[RpՈ?XЫo8 +endstream endobj 5761 0 obj <>stream +Hֽ0`G) *[ A%㵂5.]X1gB4HOdz8ƛiˇ<wzNGyxxbf'yy)]q Ǯ]tV/-a<b2**ʶ^^fb5RDkADhM\M(U*k؋`E;̇[{k*kޯ<0[dsYc$ʘ~cDSzS?TLY5CJǖC3a{ҰD~+LlA"k.UNf)뵋ٖb0M>stream +H1n0P +pQo`] F@=HءEF̔cGQoQA$E~J +(&??vͮٷ}sv8oзڃMGWvEk>~oH_3n?6]NWn?~{L'B'ejIH,@,yfy,Y +jc6Nnk&~y_?e7o},ҽh&'l& Jxzd|^^`i+.YfGdA4M 饮!h٥ҋإ3l>I虼B-:DPL5@,}&:O=Xɝ~N۞^avϡwAFzn(S`;<›U7¡!<\m2p4{wE* 8s9[ԄE*ڄTJ[к]Կ55m=\n8MVѣ_Y6aåV,3<8Ŋz⥮Oyś{Xo"3yP[z揼2#L +0d͡* +endstream endobj 5763 0 obj <>stream +H=n0p + p Dv'i CvڎR٩cĭ`o*{Epp%Genٖ]vp=Mv4#s^KxOotsdžyˆq@7wo55ih6P) rτr/RMHN ee]y<ٝ, q@`~ >|a~l% Ry.dT)g?I2T)#mi7{7&AxƋ +8<#3{Ya ){-GWx*k(W>]AAE>stream +HMN@GX4 G\i+,Ltԥ L'*e0Yp>) 069e,^tYuJ78l3u'6utGIrHrIYzBe$ɯI3vHu) D% + \]d Ww9ĬG|,vS2BDcGs (e6WPɅBSgUkJM +May"BUؾ49tWEX:-yMt=LEnkơX Pxo3,pvT 3s1ȑ({3ݴ{p>m:]y0dKn 䘫Z3<+égӮ il[=>stream +H=N0Wu%G/Ц) S`99Z#xlb?yy. +e(H}C|ql~/J48\b,l92md/?U,Q቎@#Q>zԋK8!Q!y/JU}aݳ/[۽ +endstream endobj 5766 0 obj <>stream +HA0`G^D&G/0mRiYE@ $Xq,ͬJ ,,8IV-|&eQBV[yWVj_K(vOy=xQ{A,7~/m!wRӷ{qx+\ˁq2& e$]lX pW,wĖQmr75j2_|5&<|w'[Xe/r]b +lu ?N uw9p3؜Xo]n?pˉS u+4V`$qXn rV%}MІ=7[BO,vm9ڤG.C9ZWFVGsM&Cw9ڡ.+vUE-GZ;q񋋅xw_Ι5 +endstream endobj 5767 0 obj <>stream +HJ0Y<,̥мͮTXAГ=(zNm#챇ژ6v[3D\!%ӏiHiRyyWv-2~̘X |BS]_;q]arqɥ[gL`R]Ѵpd4%6JFQҔ@4HW4CaQ9P9CD~B)MAh12tȟFK_{uyMvԗdkzxRM%$&\O=lgQ#$!@ߤ)%񏨤$ K` +endstream endobj 5768 0 obj <>stream +H̕n0 zȩf6@<h<@1Ct6G(UNvˏ(mT2mu>]QnK-F~*x~yU;Xզ.$x4eI`A$"Ո"Qnw:]TaL]6\0fT!CVC"5ӪsK>o-eH GǓbP.ф?lNv +a'C:G6} пޙ"x* hy;4qoGDPQk0sDS;稑 6MEHΗS'{!#qT0 r*|ȑhyjJCbD47ȼ@QBh #‰i[]`eRˌ@VFq-U݌%8[dK"a栾 ctV_ +endstream endobj 5769 0 obj <>stream +Hձn0p#$/<@\V )t̐*q>F_GqH])S*xgb7ba:W./t[{~<;^9呯obô˫7yOna + N,I2Vz2y,AR T_<Ո.e^Ӡ4 2TO;jXVPq|&*KoHżD,SSާ&@ P T(J@JwfFF,s$5J)L[lÚ@b"eulPSJ)MѤd/J"b*}R Ȅ'd%d2 Ϗ%ԑM&ud: "T T@M$S:r\{}"l_Z|o H@"W!?5)bЗ+ҷk=o;*  ;O'w +endstream endobj 5770 0 obj <>stream +H=N0pG*y 6:ECL@0'L\G0&qN_^r8\4_ts3]yro}0Nv==5ϗk:;jIgǗ'<z|Sxh)0e!eD,7$0)L  +c'$O_,jKO-tYX@jү -z5cTTQ$F #"20 2B,&s5#*cRDd3 1B)"[[j |GҾ6|G/k +balxCY i^K52ڝޓJsT1D܀Z@<iA-ܭ}u35pC:d+eg2OPxrJw j$CGPAou]R废nR膶iɬ(A鞡mP^b"k\݆1')E*Q-BU8>0dг  +q +endstream endobj 5771 0 obj <>stream +HĖ0Ən;ح" Tp ʣRl챽{vCE6g]6/_vW#> 7ힼ:\yg>=#wMϷ߁o>}~|埮J 4Xa68*5k " Q'Q$!$DT5!XꔻHfDNl~X@VG&# &(kpѬvc"  +r3~H`ADXxnk5"MFdmfVLl>e Vzbb-b!w2F2dg`W"@|6CT, TgEZHUnH "mCcEE:㴜)˰zm%W8tA x"#F=bMP(͖rYE"I)JOB4a&s3$WL7~b2$32:r>;r\`(JioI`\]ₖCq% +{P)(hJ)"tO v1KdL36bz"BN>ȴ5|."b%]HM:F} +endstream endobj 5772 0 obj <>stream +H;N0XMKQD +$8PRN$.)SX N8BP 8<<9I'|ˌ/6aܭ&2c^ny%ZgvY|,a5O3n/x?u$@1>50l(V $"d–TM5j ,z0IYirrp` L944 + +*D  RRIQ $y 7 54RU$; + 2@r8;qg"3M֪ժ4O.iRpO-:H+Nk{'KD>/&I@Jn8MkK>stream +HԽn@\~ܝsrrTH\)S"Q +~-]v<%Qwό]oնjn}Tzݺ>mOVgRvӀW6t6oi˪V篗J 0«@&h+(^V=agJKfHSi`#9yr9-iã'=n-?n| +sAԫιzDx-lpSp߲|,CX=pAXw ۅCqt::dKw+m U(:d*)vLt%fEa0nm` ?5dÂf뒛j_ɎjɖR<܆J7q; +k2nG=9ynVh;c|C;Gt7}'{s3 {^` +endstream endobj 5774 0 obj <>stream +H=0&G/H HLq៱c{!VW%ߛn[ڷO.{znv=_f#nD^<ݷpx{OndsKkyI~O67юl9&?'{~8@Ey,3ϙUfOX= HҮb`Xn\b7f֣ٸ|$DSa1ϙV(ل`N2N0ghP +*oPNyC)^>6(P KY`HЙ{4&sz]r.S,3Ajegaub{!S\ͭUcfln^laյ(_}pS#1 S'̦xGI}M=Y]`ˋ7;z_kKg}3,q_Fz>֫uiH>?4ko㹍87 +endstream endobj 5775 0 obj <>stream +HԿN0HZ=cjrj"Nљ>r&R,|צOQJ(mx4M:{d,5u'wÔyx G6 nf%a8nήJNDBCF YIjڤ@-ӯWT4\xzjS,v3W<wP1#ܕ>LT[^>-ϢPe'1>A|M*y|Df!~G Tsc_Kj7a`RvfR䔆, |˿;@(3|ąy2 +endstream endobj 5776 0 obj <>stream +H=n0pGp@4%%RJeN=@۱CvpzCQ?!`% +C? :E +.b4JSD_& 긹rYz=mF~tk{wGto4u +6F!2DŽ.c G+pOI O$1Ujc*&Qyq;ΧPwhDX#3:ZjA$\ّlhfhXThJMRi wԌ#ZLْ$=s<=AB_|Gp"ᇎ!A+=bG&q  b{|iVlS4opN< Ktt GI |@ IFJ6rŒhG1keBDRR#tov$XghnU}nGo=~jS&7ch/9GJߒ"##Hk:[E +endstream endobj 5777 0 obj <>stream +H=0M\`w&+*.HL-Xq-K\G0պlJE4Lc}3ߏOá;tW/_]w2NC_/xtաx}_?ߋGqpbz}Nވpc+j-sM ;b4 v́{d֫m'D?ɔ!rClR.ȱ;siU,0;fGfavu3C2+4(^ +{3U8`νmڧz.s)'b/]QkZ1jn}J1r Fu\[b15^L)Yʱ2PĺR֤^3?ccH߱{M-W%-._q 8[^<y2dXY`;K5Nrdǂ]Mm?-ʖ=JAOf?Mؔ郝eeU#7zذob~|/mpKfoO-gc +endstream endobj 5778 0 obj <>stream +H;0)0FG0/7&"@Rٔ)$'+(< ! "MOOr}ױj };>C{cO}{`fZp躁}FNtݽw{{tw~R"#G8fR P<4b1+c$ъ!ToLr8U3-0$!Nr H䤲'ydM@&2V ܆s D_"Ha5]n8 ,lS㤖U74Pf| ":Cd#ȵ +p2//!p񽪯ˢ5]!4*Dg˛ H2Ҍ g' ,%Sy&(1ͅ^xHޢwNFU;^ LqD&Ԉ|nұCmc3քJkB[t³wXK܀tٽmVV] +բQML]A#ш^6#noS2+F7J Q*EM*ER!CD0N +endstream endobj 5779 0 obj <>stream +HսN0=B&mV!!y)sZRv|9 vrI#V8TGŞ7Ҕ2JyC{. JoH #>vCH BhDKcJ'%O%t-տ:둹m+E(ChJPP.0qL32X3_"~{%#$ +endstream endobj 5780 0 obj <>stream +HԱ0ЉRDrOXn8,JHlW:̟D\b{Ʊ'졕#8v˽lg9n_ޏ<܈ql{gdӽ;GlbwlN~8cWaHe +@2 ``r.otɁR&0~ U#M0`#h`qZ0L`8#װr0|4_( ,`r(3JC +sd< Efhﻊ"*_ X5rvvQ~ ,[]djPkUH]%ގW?c .+mIMO +,*c`Lϝ~4Ŕ.:3d1'b:U +fKCLCO_ׄ\z84G +ThU{*#?~I1 _t5O}t&#}Oy5@KH(jQAu :EmT% ˮbN9%@ +endstream endobj 5781 0 obj <> endobj 5782 0 obj <>stream +HTQ=o0+nlP b(͐jː!wM靮{Ϡh8Cۻ&4^E8c;hz;oڃ&:8];BQ(Apw4=`]GLy"pt3DP`tRz@Ћ<^=YkvBal[JqnW(VpE8NL'‰h$(xW*VK$2[svű;.ϊX1+C8aU©(Sd2zʣdld-Y&D(#q&[Es VGI~m + +endstream endobj 5783 0 obj <> endobj 5784 0 obj <> endobj 5785 0 obj <>stream +HL= +@ ) !kL!hB:f{2 "SX'ZFb3h&-NgV{TU]alvYb 2Ru@g4$@Y1|xz-BQރڸQ +endstream endobj 5786 0 obj <>stream +HĔ=N0mmM_,aH H@%r(3㉔m)/~~RU)sYR^(Sk!>DyH/܏㷹|yZdP^F"ohl$/1p;1dȶ=ٶvbN< +" ,%܁ +6pls/C۰3RÔxK} 91w/Tv/pܚ;,hߐsu͂pmLr15֛W9k'ԫpE>SlŢ9H]! 2Njs +s» Yp¸œ`sS +endstream endobj 5787 0 obj <>stream +Hܔ;N0Q&G/@a}THJ + %WBD"iWl z?,z˹\$9^\.Iv,.<$ +C)n>stream +H10GL֤'D<::htFo77?}}}+%LDR(C_AVx}]r?accS 06s&0z1^ jChRA`V[$5M_.׹srָN~[87Gk\Ě8[W=#mT>xh>.:%_[1n% +endstream endobj 5789 0 obj <>stream +H22U0@=3#S(bUB]8L'r9yr+r{gPRTՓ+ : +endstream endobj 5790 0 obj <>stream +H|MJP#io`r~Ҧ+ U.]yuBѭ x x,Ǽ-4W^?S:bL$e2Yej/OzU4]c*7q{Emwjif7I:_2Ilw}<ݕ1ڇ~yJ;`@, F+#żo86'bp(,}c;k?ȍd,9$9<,)uGAS?xx>stream +HҽN@![l QHa7G()e(v&C۽=XvY_6mSj^Moqg/XInlҦ&odz>stream +HJ@ 9G辀IӚDO+U=yԣALi;mDW?!U?Y-ŏt48V.df,fCQ-jBn?TOk:\K#0E(x b 6p _ -) +endstream endobj 5793 0 obj <>stream +H;N@'ri{8~$.@I{ ҅YC$ +@X^}yy)ˬUvc 8)l[2}GS|+V*Z9f}cܬ/6թŦO/=3!Z DN#u h o,qq>,Ѓc)%68jNON9̄X'Ie ?f _^})sW44&M}T4AEtf +28f0*re~/E@_h%4ϓcsޚk! +endstream endobj 5794 0 obj <>stream +HԽN02DG_M"T$2 Fďf$1Csw R2ž'y*S\xy*,xHkVeh^ Į^\,[dw{!37Qԗøk{N˼{;*_-,zXiRA[Dcw{ƎwИE:MԳt"',7,lճ7;J-'SJOZ̥NT)ߍf|&Lvv2T5򓸤BВ[ 48S4L>4Dve0|jTEMG}i:4]E\N| +03 +endstream endobj 5795 0 obj <>stream +HN0 ]P)K!yHHt@FJx3D Ӟ$*۱]y5jqQQuTT[tyl|^-EqVKV}sY^J ƶoK<@9e:v1`跸Ž`G, +endstream endobj 5796 0 obj <>stream +Hӽ +0H>Bl['|Ӿi~lWݶoln]{<7Cl!72 +endstream endobj 5797 0 obj <>stream +H;N@Ǹ`_ 8Ig)D +$8PRQ + ˼6AAݝv|\|4_5_jV0/f^5Z,ǥóYo>_:/+3Y^䅙ln'4{y~04i T=iP(.aa0mK&5J3JG#.ҿH@&П@R= #. eˀJQ.B޷xb/ksVfatt^Ce|Rhkp? dKTHD:@5(H +G}[[U!n*DpS;iovmz0 W@scMp(:܈b3,8V,b23<)s1w[hjH +endstream endobj 5798 0 obj <>stream +Hӱj0 h&!h@=کkHBJ7Q=wuh g,iO7j~od49nQ5 +^d٣󃦩x +9-95T"YQ%W%{Oλ${媣 Hjċr%(ap:>UbpH65eI}qSV,JNȈjx:o̽+qSm{_0ݽҠz`jG* +endstream endobj 5799 0 obj <>stream +H223U0P0342U5367U0233 rr! s<͸= BIQi* $>0 P= l(@ g;H@D` ުGv=?F =8 %c``?r +0@ +endstream endobj 5800 0 obj <>stream +H= +1/JXL!hr Eks3=Ja!3 F8RdEh\O8Q~=h>stream +H씱J@pE`XZr.v)lT7oQ)S{98@.aٙb5=-z&b5͗vUm`<֥>stream +HAJ@?dxa6VCWU,]u+h7Ur.s eY+ν}gdtY_&}KՒ}9Ηdk? o$}l5;=HkB[G 1Wםp`#l7JQ=)5gJge+闓*SjҡjUNl6lǀ*R#_m-#a͵ +endstream endobj 5803 0 obj <>stream +HN@Pl#/pp]S)LB#P^AYT8bm矅Z>ו^ҫ*O*]J=PH'<y\>stream +HN@LJPL#&&RhFkxr DžFߙO[˚n:,U0.+Y)R}o0M;\%@!#c RHFfD8J242!SNB7|ywa<qb(h)x@ɆS0C0)+xpL + t 8XR5Wc!U\C+ccD6V`L%A&!y>h:/i:Y,G޷ a +endstream endobj 5805 0 obj <>stream +HӽN02TG_MR5?CUK$2 #&2zrܝ Jԁ ,1'|*_Ѓee/zQb YcxxVJU|mҤT¤*n.boхWk˪ =;@@jH1!БjAh*ذ4~4f( n4syqr@:xq>݌IQG1JΖvIݗ½wq=)}/v\dN؆"| MZ˖dmJ c9?duN= %gRWS !Q[ +endstream endobj 5806 0 obj <>stream +H; +1_,i WBVQKAEB=JB' I`~+qfEJ:YĴ!|EjhU3ꪘtv1;f! E&C냒NBa-pu[Ō3J/'}YN̟_$ 9r,1).m +endstream endobj 5807 0 obj <>stream +Hd=N0'rMn}*)؊%&{%Gp"0 Z<<.oҶjږu~+.sUmZM|^g/٣ml~^##  06cfE&B+'28te78'X +;G%0|#l= +A ,. +"NDFbXgr[X +(鬤E%ְ?0$A 8jx^'bKxtFg%A%M#yZ|]hz}п bV= +endstream endobj 5808 0 obj <>stream +Hѽ +0pKy~L`A'@Q;Q4 -l\$|&Y) h(i' +qyb˗ ~Sj/evml:ym!;%5ِ2![Uj /Ͳ7Ӊ7Jt> ^ iSlEF \ +endstream endobj 5809 0 obj <>stream +H1N@1bi +|<7P%RH@eDIsGp"0?AB>stream +HtMN0GXL ހ^@> ̎dD&҅Fwx3&e{}'Bh~m MgeUj2Td55ߔ<_EԮUެU~7w׺Ty{??^U{G$X!bLObDHKpX*AL40hѪåDdm;Ul-kHDՁKg^Ʃq||kH~G/S,6?(v(S7>iG\hK?ů3L1h%R!9 UG= +endstream endobj 5811 0 obj <>stream +Hl=N@F'6> tH@%$87ۣ\<|;aKg{ff_; u?tz \o3/7k]oV݃~ݭ&\'4)fGτGj?J lVCϹO:مBIJ E'[rDg!Z2ʗR _ɴu~P8HuI +0" +endstream endobj 5812 0 obj <>stream +HlAN@d ۂ&,Ltԥ [M]vy3oJBK:;|e*i'Y.2 1"[MFNbZi&߿z#for'9O 1[˹mߧ<Я DA$Z=T=]U`֒Bʩ7hA1jI=f$4%F=V[ +V,eFBh#:Qc뵳QdU:ԃSB6S dE6oMQҁ l<6joT#%XS71W^uҩ ڷ)3V7(d"sɹAg;?3@:k/>s?C:jnoH +BˍhH;|g㖡x܈W/K! +endstream endobj 5813 0 obj <>stream +H?N@ƗPl <S)LjixGn ¸3;Q +~o`UuV:-AV{*R{.ܯҍE٪ڠ$Ff<٩*\f#UȬ9S79 bk Q#24P#[SAL 9ؠ(QcL! ZZ"m&@S^]ozޙKd cf2g_3d;`mF.3ޞ{}Ahs>stream +H22U0P0ӳ07U54P0342U0)\\`B`fr.'~8Pt +pV0U()*Mt0o`"{4***jr +0( +endstream endobj 5815 0 obj <>stream +H223U0B]3K=csS# =C#SC= bU173ɹ\N\ +&\ +\N +\ +%E\.\;!) ?@H{B#?lH$#dF' CG`8Kȣ ` ztrpi !r +0`4 +endstream endobj 5816 0 obj <>stream +HԓN0p G/@u5Y5|Agx4 ufBبG ?ӖS2iUMYiyQHY7U$۔;Ob_QnRWR/9:u=>X0ByuMI>:7Doq0 T|kc X@hCa =9tfMw_#0b +p~{30)=` fC8g^Ɠꖖ9N +2T0v\nqc3ZaE7x_ +endstream endobj 5817 0 obj <>stream +H?N0D 8)R H0fI}{i(bïRjβ(I7\,n(W&0M/ {gi[[&z(k!Dɟخfy,=ze-3smeUx 2j`mP[z*\DS&%ZäVj5) G{ΫFL;BP4WW޿{awj3}Ӄw3C3?yQΉnIUX?C l4哲J yhVhR{4Ѝ05(D]͞؏ ^ +endstream endobj 5818 0 obj <>stream +H=n@A.,M xq$R("%ULa+Mq=–}`HP|2MřE,3rx/w9#[*=kQ 0Ot()xiBҔ((k:5 ֠* [ϰ[KϓTC@ڢ_ Cm0T0Ll +6̈́q'MӨXߢ7ZϸX3T;E\G^lf7k{ ,,Р2R%3Ʃʙ|rV9a3_BH4$ʒ BǸnJz/Pu +endstream endobj 5819 0 obj <>stream +HĎ; +@'l!LG\$hjihx%GL! ffyqK&E22Ś x'aa]|uwGVnŎ8]2NN\WsƒS}ђ]wӜFO^EGelh@E}AMU馏 zph]Vi_(^T֋ +endstream endobj 5820 0 obj <>stream +HMN0"79B}& 4iwF$@,@^fQafƱC" R/7?6[徑v_6rהY#?j*vuy(6큄f8vz^T7:]ZTݭ|xݕە@1 91 P8@`ZRSͼ7x{5S="PRq-{`:PC6p9yM@B3('A@j?[# 0ța=`!C!;>h`k@FfAk>stream +H25P0P04Գ07U533U0 +)\\FzfF T@!9ɓK?\ĐKCK)YU()*Mt??&?`DDddI&o1v4p Փ+ V +endstream endobj 5822 0 obj <>stream +H;N@E4^gqR HP@Iڡb[RdFـ)FwilM*ټJK>+y7 fie?u7szfˆeYvË|%/X'\1T(;F@ZZCK@A]1WmhMa}dS"%[)YiDlc4$68PC hmO޶s|{OR#O.:h A \ s}r- +qBX@ЌHhSfOh"c͹nfo=~ nzHy ~vݰ{-Զ +endstream endobj 5823 0 obj <>stream +HN0/ɋ!~MiZ6K$: Ċ G2fjK*R~g7+rM]7nUl]ꊿ\\Vb+j-of0G]孫Y\m;j&Dy¯'`GT0D1&= B[%Y!#&K#'Óf }؉|T33?gk)i~lF#Y飇\|1X5lŽ] zhrʤHT̢3tMȌ:i94v?.|ނ똩8pYp\&Cı( Ed.G2=bLJ,eO{Q{ +@cdZQG~p5 M`:vv }{ԫg}>VAB8@hfG{din| 0ڶ&p +endstream endobj 5824 0 obj <> endobj 5825 0 obj <>stream +HTPn0 tY Exu]hG@L <#N> endobj 5827 0 obj <>stream +Hj02b @d˒my-Cms_Ńu&[}s%(f2Kl*ic,-4IJ*G/SG-OE2J,J-~a]@x&p}w +_?"OӪ,IiK{KA-%knd̲Ztf3_/ob&ܹa`!5oIkG=;̩^^PA?)ʉC99-o.` +endstream endobj 5828 0 obj <> endobj 5829 0 obj <>stream +HTn0E|,[u6! !h,PvfH YT]q5:3,Iq~0y8y0 $t^n'뱱u^p<~<9/ +'G\n0gğ_^8^tafD9O<]-䳸:m5挐GQH @ fMi{ݸ` H +sU$}[Gı]0oH F[4cN(b 8 լܷ%=5sn1d=^eZrOwTSīaJN˘ _'IF~I(is~c x/ ~ׇ + +endstream endobj 5830 0 obj <> endobj 5831 0 obj <> endobj 5832 0 obj <>stream +HMN0KX4#L/0"5وBPkkT:h +, &xk&S`#Tb VEv.E`MNC,aM4D2Df.C$jEa_Eg¢(r|$J?16>j>Ex}VI]@,׺>.t=ϰ_f;L)0v$u41;9B_0HkP`E֙VOTrrِ[!%G +endstream endobj 5833 0 obj <>stream +HT;N0&JlH|$@%h>2fSh*y̛_.벫ڦnW]CUu|i;ʪfnx_ Nv|soˆo+L;Tz"GCDP嶌 +J}X{`=rwL o 0=V +endstream endobj 5834 0 obj <>stream +HT1N0uKP_iҤM3U*  & cn=D60aE_޳^ʢ`,6]ݖ͆y4;TWnM]VMˠjM[>l=㉮XY4t}n].YIק[LOWT4¨;Y! PY `HP?JaB2HP,I'p%[FFR]7ƀi40M r_6lw%EܲPv[: TPH;݀:<ҀYa?s, #m1q-/ьeWc̱|Ҍzȝ$*0P>)3>stream +HԔn0et@'|NHi+R;ڌZ3<Ȁpm0n$ gL}$ti (& Ca ] +Rw{J!Hw/>xB [hZk[M(ȱRgІ6ۘڴjTe VJR3%K2sEeWdc)*M95}Ҫ‡F K)^]Ƌ+TNStI;o&7f F.>stream +HԽn0` H^x'Pmڙiε`,gp@ +l:5 <j| ҝ"`]w`65kL]N pg̤SN4.nM 0sXKnDŽmlG>t)̋jnO XV~=a #ƀMͱB׌ Ŗph( O +/Ẑ{Vh(F(< +b]@oj +endstream endobj 5837 0 obj <>stream +HԱN0pI&̩ &: 'WBrC_~\˂yp6t0/I(i() }g^mHa*]{0E:ĿĿ,H95}}y{$+4 qސ jh^Sz՞B֩U汪xCf3ML!/%e3B3 ZXuDI,\R bY/CU2u|AY: >c N8(G9ܩT?vqMTaMRY%eȽ7].}vPx8T*!U"1G()n)ՙR`b!ݤxƖw].2@5Jbx&zW@Jk& eNnɷ\ +1 +endstream endobj 5838 0 obj <>stream +H1n0PG"y1RJeN=@۱Cvqё )JϲGXD8l)x 4IX ߬bXmR1* v-37ٚ[n >L(qwTm"!KB=uu-::Y4f&!9x?042'g7[ | +j(P94>@g0sPgdEp:PiQB꣼Iςz +r/o8Zvu Mu}j=Q&[Q"6$igne ˃ p#IL%TSp% c4)`W̷ +endstream endobj 5839 0 obj <>stream +H; +@ a@MZ +jihG)kHPqo9rQ_pnn + 0RB~|!LQHUy~WW'f(6*,QKƸ얐 'U"%mɱ5%cQ__s9Niq*韕ΟӷԵoVC4X2͵:m'zKa 7P6 +endstream endobj 5840 0 obj <>stream +HMJ0W(MwINgڮ +]҅ZsrOPa7G()IJ$-R%2SR%tMJH3WR?jEZ&8A<qS`xKJ^6M<>`qfʛqCvJKUY}_K*_WwA:IiZXk;'s +CUɵl[2냫vT6c'>rPy0 ʹa[N/\K͹E7%Á +endstream endobj 5841 0 obj <>stream +H0PG=T%`IlE$8 G Y$~$cg;8Uy=̤mZ׺kS]]]nt>_OS#k:kXvt8:/&up"u#wIzΧ1 LxIB%(3 b'K]²E>C%Jo71xeP`ڠds"B" +<8,PX\WdD"pC3&BO`# sk>'(91fpG(B@m@@xQMDk: %asB5f@Mm(鮠NZo"xH<-a E` 4vC ,/zЀ`ͼ_1Xۈ638j&L! 0 +!V S&,%Pts'|smSJ +endstream endobj 5842 0 obj <>stream +HMJ0. +Y M:a`T AW@]Ptkzx.(E +~ 猳IQt + Bϣ=LM>Ca|0NbvD>% efTrܭCAȿA"\`kk( />*L@h6:e)@JD J%Jn @m* m@Ʀ#~rfDiHPX-q4Jh9P뀚LizՌ JSQ"rUWDf3аeFoW,E +endstream endobj 5843 0 obj <>stream +H;0 Pi\~&`G02TVjIډ)8GQz{^`~e8` g(ux֋U.7KpH\3SN<"# GpT2kZTKNHe'=m^jTu>gd-%kjR.5UIԿ^Ԅ:p`OF +endstream endobj 5844 0 obj <>stream +H쑿N@Pl#/p+pɩ&ZF7#E ().3XX^ul?*Id"8UIU,qm)M)xeO}VNS+B ȱ(3*vXJ +Z*狨o9ՏT F `4ϨQ ##DP1%LG7=) ©b4sEgsQ[fnrS8+l8T$p +@/3/8aiőq½۝~`R#{p;Wy$B7/FfԛK`-П9jUQ +02. +endstream endobj 5845 0 obj <>stream +HMN0`&pza(dD&҅F7Ûp <[ןx)V|xn!{~8]߉!5J+Mn([[,@cVԆ}DQ^Lq5ŷ A& r )a͐j!(c-9r0#9DXE;( c2 sd%uP))b 238j- 6F 4GCrYY%8RAC ]:i } ^e D +endstream endobj 5846 0 obj <>stream +H0ǽD"<.d7tHG%̛u"J|qZ3)B*][mw_תkKyn:UriS)US;+2#U>~WyʲnW;ۏZx҂GAF{ {L:Ğgiɳ4CDSLDd3xCDHTH8U$~9D QhN-DdS0* (B&OPwH'¾-0fL3۔id F0WEO$B0mLU-G:Fr;@iHFdi~wUz7 6%k$"vܘ>stream +H;0P#Hpq.`b7 }o x'3sqȹB ot04Vs)vz_7.`[VCl05*:t>9y-T)~y[ݨ9+ʫY 8 +0 " +endstream endobj 5848 0 obj <>stream +H10P#; 7XHP h,\@HL[FR(E'Ic+0Lq7%fVPhε)7 b@4ԍDq%D7(['EWܼGi]:W|g>stream +H;n0)FG /`J'E)l*7QTATGyJu9FߵjxԝOݽ]uhF?,ۦM4޺ F&,w?k +?=|ܝח_Y}KY*@͹Ll`@qG0l K@ WX7vGX0ҙ(.+R$`(0\,J wU Вng]7*@ SF@<%K @fЧM:Xbf*4J3̗*EgZ`)/ptH >stream +H1n0P/!V*CvڎZ38 +U/ȀLmDB"Rk)YӘnW$,SyO8>QDe>3F}E'm4*,c%}z%l)KI+*UяR_8Dha;zA`_<$C+ EohW¿|plx| Fm/4!C"܆ -aj6 >`rggp 35jXh V:(נ7mVU-1h9FOJ6p,q(hU|j k7*Pz/gaaP։*McԐq$TC!hi td IT + ,p/osT=MMȯW`AF +endstream endobj 5851 0 obj <>stream +H1N0PW"y G/4.ML +Hd@#f( QCU,lJu.26~RQlOEU9D)sQ0O=rB2Rx,yn0.JLw ]3,tܰ'\a} C'u3X7 +{$B&eS ΈǚH(1-'ÅbP堢r\Fca6K% pPFtģ^So)$WoI2q ` +h׿>bͽǀx s`{].zC8K7ܐFF%r$ + +4tcY{Al3ZCF]Ml P5>sE *7~p7UCkVC +endstream endobj 5852 0 obj <>stream +H=j0`-@@;!H[B;m-f:у$9 +4B=Dﳝ'=922Yٲ:O`,k,fUUo.+WV7 %Z4Y(%|a.::Sw|eKں +endstream endobj 5853 0 obj <>stream +H1N0e(ҕ@8MFBT@"L@>stream +HN0K؄]MVM`'@=zy9`}G P֍ɚs:ә:iJRy^J,i\VT4Ί*xn*D+.X~K3Uc1##΂ 2 +}扺%̊gX\S, utKLZXg:שg|f#G:b x _.|:#9?#/,̦ 3Zs9S:7 ]Oct,0!=`(w +endstream endobj 5855 0 obj <>stream +HMN0` nÔ$&0ѕP.4o7t' Bm_[yř4G_ۄ1hMt'VlDֶ45 +bEj2/lMv`mo3WduOյ,R^Pꆾ?R2ЩN. ƹ?1D39 +_:@B }d!kB8aj{E#f95\,lD'hb h4:P_P~x> endobj 5857 0 obj <>stream +HTQn0+ت HJ.9&ݱTeȁz=c?@ps68܂A`9HLnM=Hj>5Tt8Na=% ߂йoM@]FGKy=Zj=X6k*Py 3Qpǥ1WW& -g7s"1-|VW*"c-u) d%SEƺi$x{a2R +1{X|ΒV̩sx??KG2 + +endstream endobj 5858 0 obj <> endobj 5859 0 obj <> endobj 5860 0 obj <>stream +Hԏ; +ADJ<}{~;ݳF@74P4:`D|ɃE8hYR֘Lԓ8VR;bI^u8؏{i> ?0;`͜L^|&<{n¸!Ib| +endstream endobj 5861 0 obj <>stream +H223032T0@= NrrA8f`<39ɓK?\KD88(O_T.O\\\ +endstream endobj 5862 0 obj <>stream +H223032T0P3320T5537214Q04@CB. + K L!g +ɹ\N\ +\ +f@)YKW4Ӆ2f'W @nF +endstream endobj 5863 0 obj <>stream +Ht=N0 ]uGh.@4Ht@#旣(9;T @b$/M;Xӭfh/\onmgukhzҩwe/{*i)y0T{So0I;;=9<\N5ӽzUӍJiL+)t:ʔ(B6Hq8AG*#p:RLHsj?زbeC,s(aŬQw#o x _lf ؐ3pgt%T*2投(bźMZS!_X +5շ +endstream endobj 5864 0 obj <>stream +Hѽj03|/PR$KPCB=ҩvQ;\O(d-TykT6PM +9Z9!7V[UhLE;Z.x9Sn^/vCoz"g`loHc=+v-/`e< v AhjL $,ȅ|Biy#߰e%kEBľ .71W6yUQn1 ,K& CuBNd;ϑdG`aL +endstream endobj 5865 0 obj <>stream +H=N0gbirgH H@j [XQ@K$';37e llSWdnP-=ƨrq$vV+cm׎_a~$[a~K5ww5z7–`Øtbd ~`#$"9`HYOO i#5) )i O@"z_c~i%6cтsK~f]`kL΂m8XX3P` +endstream endobj 5866 0 obj <>stream +H=N0g"4>B|liHPurGH"0c m-S7M;*e>6etmF)-*s7c㣌 7<]tc0mF [$!!026D̙)‰ ^.c/a,I8LճF")Eb爬J%Y]ύxN@K=ypJPh]ݣ +dG+g$ ^--{ KE"p ݀O-e/ +endstream endobj 5867 0 obj <>stream +H=N@F'Jai=SpJ +Զ|!e ++7 @Td=ϼ(eHu*O]&y7gIݞյ{\SI+US24hNUͶ3齫r^2­Mݸ']ݒ(vD@g잖"FQ#ى c1(:C0XCأ7A6~{YZ K)}t+DF=[;RNJ><l Y1J?Ϥq p"*#0gVkfU;tesՙ;)YAN +endstream endobj 5868 0 obj <>stream +H;nA `- 0;3 $[ AHAP8 +G@"ACO|XBE +SE+ѕ98_^aM# 0a\N`6c6aNh\`а%9k@ @HC%t0 MGHNWR^a> r{$e>?ysZE|RY +endstream endobj 5869 0 obj <>stream +H|0&t \6]6U$@J$@ ŏG#L9(wZowvpnn87}w}s<\LJkܜExiCTc~k}zOU?;}}M+?-T`UHTF<.BZ(.@YR¢OVgTȑE 7&-}F،bJH rY \tzYzfߡΔP7mHTIdK>Mt Qt A#Z@P_SR5W!Du¹eBDK# `+_]rlǙЎ8M)Qy$a2s*t "͉4EڅȇiU!]BQ:R~5(HxfyF.&Z2vJlg #蓩Ta|79 DR f\[("Hs#Ho((ϊ bѷP89b!<&Vi,Z +UI۲,RM!-uga,VjN`.*e痨8-{Z䌕sFhJ "aƋE48MCBۨü#~wӟ?ڍ" +endstream endobj 5870 0 obj <>stream +H;1C} +_Ll)@I/D@Z +~^: 2ISg-{Zs<.(d}^G+ܞpk7n4z?.1>~!bD +endstream endobj 5871 0 obj <>stream +H= +@ 49sO6V@--Ѽ.nhl{/IQfae,es5ȋ2ix@.E+\\]~Gg>stream +HKN@6@ 1DW@]9G,YiH _)]vƥu횂.ueӕ;VEZT.$i.WIo0{Q[b&pO_8 ^a@$a+ $| #hc1c$ڶ#1Fb$RbZ릀7[Fbgh3fRIκz(b[ZmN`~2 d'zgb 6r:yi*P)? _hL +endstream endobj 5873 0 obj <>stream +Hԋ11 +nr*$R A"] 3QKe2K;JfQ%G͵-G$^…ք04vCo8#]- +endstream endobj 5874 0 obj <>stream +HAJ1E3N$aT҅랣QL8ISkW5!2.Xk|ׇJ.fG ptϫ'YwRiXIPJחG.l3fhG6-(l>|oA& MA)?F>gv?;H,̶l5g{ƈ2 _DV>;u +endstream endobj 5875 0 obj <>stream +H; +@ )i8tnmA*JQl_[gCk!@ƴ{ofJ2t)G00G$3:+ā3@@R C!ti%T$ @h"DWAބN ܅Ix՟Ըז +endstream endobj 5876 0 obj <>stream +HN@\q4<.˞ɩ&Zjyx RgW@YUj~+g;TVXmPYxzxFե6Rpp8x; <`La~OxC>?߰fh?=ĨCYx1v! ȡv? 0@=r3dDtHXҞBc~C2.K$ cwU?-ZFdLZ,CV{j1K#7[d<.]֑.g]5 CU +endstream endobj 5877 0 obj <>stream +H̑;N0'bir։;q$$R -J +r=–[D ~$_|ֵ(* T ZNB$-ES*H”nE;*Vqo +5PjZ}S;̟HߓQ7$1[d avڡgr !e(Vla ߞ<лc򓫿eiG s.aL]żKgvDasQ7q𲙂 Vu,ڝ]G{;%!:|M +endstream endobj 5878 0 obj <>stream +H1 +@,xlvDSZ +j)(X^xup1*i lE^Xc82J*֩HsYm:\6IL8B6xxZQBz^w?&5--x +endstream endobj 5879 0 obj <>stream +HL=N0'JM_`k,-  *PRv)SvXʧmuژ^snk*[:Pw`l:Y9nLˇ:ϪyնUͣ{՜tI^!.9:>stream +H=N1Q"i +r2k[ *H(S9ZG2E?>iB4+4m A.3uWm%4?/ ǎE4!OO6>stream +H91 @H +AHtX*w.&e9f ZR(*ֆ%hӷPo8hOp +R{>stream +H223032T0P3320TЅ1 L -r)\\q K +CNrW04N +\ +%E\.\ Փ+ } +endstream endobj 5883 0 obj <>stream +H=n0 Fx%G0/Y?dM3hh>CV2-KodZZfq딨*4j/ +<~*86"jCH+a7J :=~`BAJZ(l_ G  +!S2늹D6L"]1GX\c4pYsf>a%aY?ܛ?Z27v[q3u$짙.yL^; JN7CO-{B +endstream endobj 5884 0 obj <>stream +H=N0FJ4{*)@I9%GHjxgP@GdY7t%:g-n*H1W +U6wFL)$q0>} vO=wzp +??^HoDE4nÚX2CE"ro9aÌCya +3s3E;}?5̲k=ȝr陻'o/]&wr?d&)[aC +endstream endobj 5885 0 obj <> endobj 5886 0 obj <>stream +HT=o w~Ǟ:-CK^ڝ"59dȿ/pUk?5ܽtw'Ǹ-€'86M{Tw;2$;9$;dOSQ>Я1@pjruM{p"Fih$?'1۰U*Q|>iP+s6ZvOxb[`k + +endstream endobj 5887 0 obj <> endobj 5888 0 obj <> endobj 5889 0 obj <>stream +H22U0@= 3#s8bUYU"`\.'O.pC.} <}JJS<]szrr?3 +endstream endobj 5890 0 obj <>stream +H22ճ0320W0P0ӳ435U5346T03406*rrYX4("P  s< =S!BIQi* ׇ 0P`ø" +endstream endobj 5891 0 obj <> endobj 5892 0 obj <>stream +HTPn0 [tqP<:ͮH#Y#Eǣ<ͱ&j1@g8W썅4mTز5A:$n)nbS <ҧȿDs?8 c~]<5OI^JAa'_cMqUzvY>g .8#|)wSj_&%N {ej95'tc r + +endstream endobj 5893 0 obj <> endobj 5894 0 obj <>stream +Hn0`G fmO +H N<p+ICoRq+驩<̌6D+ıI"_b],f>/lŞE!=\ϖؽ8顽A^m]l7m:U:哼Hߥۧ)vv:B6d7afPtQ* .j!KC("qPzj'TԣAMAA?B.@t x0oz +j +~Hf`54'Bw + + 8 +*!j d! Vh!+d#WlCYr]9N8$Ia\ l'c{HSChg f v`GQEÎf 4;Ptp> 9\6X61A[+*=*GXNS&4A/LRrm#kGN>stream +Hn@raɍ~\ι䮲@ +$x@T (l*J^Q^`# +0~8[+%9;󟯽unvl6'piئٮv߼naS'lw]z/ͫw<}ٴ۶<}ڴMyzQOgޔǥk6%HHcXd1=:&9 Io. O蹎HGDTFYDTHfZ)hQ 5$&Lg"#霍H{ 38=Lr%dLΧ[2&2Vړ,K?z*'r¢]fuHaܑnd.=AfZ*J(IMe7T牶 +hTS TH'7 +<rͤ䳕4@)tIo 5 OGۈ9YLP_V"!yBXISi4ILkkAR&>Cn'k!,+qTGC܆;I|d#3-0lzn>bS Qδ*r:VʎZ֊ #x050R뜉Ah*lu$ObbKޣ+&d6A,v pp=1JO;啉ł@i/;1>stream +HԻN0bH}"`[Gds%qN`p͗ 0$!I딬XQ0\R|ϋ,4i~q'L5ޯppNhx$8?=  !71BР=.hӾiЅ=$jbUYm@^uf@mh `Df'jwQr `KEȻ7!8d׀KPLWZ3tP{VLVvF,ߺ3NC-2լ+p1U&2r`p̻pM`k2u۫|tFFx ¸- B t.  _cC Jջ0{x|T3-1~ + +endstream endobj 5897 0 obj <>stream +HMN@!,Hf f"DW@]&]x,e ( di%bE_B'_y{aJ822I(hF$0!7 $#&YB&YG}vyy;<+pp՟qpFXDq08& 9y|x 术_jloԔBkWcmK-!0igWnd9@.wzZ=@jQiQnB, @TzZw`LO]Z/UejlSvǥ ZsXP-iVS&^ +5*yS5*y+NR^C}ZbCU6r%mf^x_, +endstream endobj 5898 0 obj <>stream +HOk@ qv@Upу71=WJ?7 +!޼ɤSA š/;oޛɳ,(fpMz%U,g<}ǯb2[,:W,[_,<}2>ӧi>ӇgOOKxyyEw }`AȠh=*q0ܲPt@m0344VP[!>BM:N>4U]l#baXYUH&gW).Z跁;ʴ>N`"H]@sUJ8EHd}Z`rgZTX(^ tf:^ DwD41f&pF{Me8d5m(m,D;2u: |pɂ[Z/Ŵʵ҇‡6p]܇/>]èP_+Akc3M +}|P4_V|ԅ U"Pnhu`^ݾz'P`&Pd*40B*B3 JQPm%3*K^EvTJ%2j([ h-CPAr $QjNI@ìA +b/ds:0ߚ~|8rcr19&>a;)~ĿT\ +endstream endobj 5899 0 obj <> endobj 5900 0 obj <>stream +HTPn -X 7E/9X;H1F,ƹ(0Yf{KlzL08o#. G`IGf8mI8u~A)ߩE> endobj 5902 0 obj <> endobj 5903 0 obj <>stream +H|?n0p#$/p? )m2TjءUUG(уKU> BzW7$\o~?z% +OkpC%/m"PkPSG]%cg\UJ1TkV oTG{),}UHr^ꇃĵiA+\wu`q-XX`Bt[ ;$\6Ut +L(P8L1EF !Hs1"Z\i+7UG*`aq+BOPȧdN9Bj>RuEjBWw +endstream endobj 5904 0 obj <>stream +H22U0@=3Cc8bUU"`\.'O.pC.} <}JJS<]szrr0} +endstream endobj 5905 0 obj <>stream +HѱN0T"ݒGȽ@;tT@"L}`dl}2fj'l,[?G3VI-NfX%JɪRT(XQ\ዀwBTl.]. |~Eא/oP@<+4`ȕ.{d7>V!c4hڙ&zog59zuS~Hx '{2$'PCfY?vpg\஁'`W +endstream endobj 5906 0 obj <>stream +H;N@\XG^ R HPqAmSq-K\Kq(n(ǚ٤:(,J2˃MRN ,QqH=uD9O-M<}yնR[ju^H+s#?=, @GKCv$$rZQpå.p𹓋}ɑd-\(j;ӼhP}6h7q!JI'%n㽔v:Gy)͆YH# 37h$pIls`ko?p eá~VyvַҞ;~j?]m6G1<[2:~^oRi^U/3TQ ߊ5 +endstream endobj 5907 0 obj <>stream +Hԓn01 y`LV*Cv;jgh< +Ȁp}w$!Ic-l~D&rV\"Zge,rT[\>+&2iD^G*)v|zJďR"*)EJ՝x|յva B'hc6l{g٠?yҎ ؎#pL=`hC2 ۖg£<1Oq +fp~{=Wnh\?fbWiSz1n}#= JBt G=XV&hZO׃!l荸ă` +endstream endobj 5908 0 obj <>stream +Hұn0P H^H&Pm>{;@JZH/sTۢ g.-T1|Isćؕ*C.qֶ(cS@3.NHe~ɽTi!T"os}r΢EN\O`8E 1Fd?!hEцn9ߣ߆1&/K{#,W;;4IX_rfG}JĆvva-% pգ-4vaߺ]]q=h*m蚀-~` +XFC~giBb# M='S`1Z6#TGZ<_"o +endstream endobj 5909 0 obj <>stream +HN@ǗPl#/ _U$&Rhu7#50]wwv0iieR|6줪m9+ʸ&?flu1ӏ}YF뚥qjb7tdϪ&,K+NYF==>*F_&B !h" 1рӺ4g.Jو>BHc KCd[p H p'z;/5#hW ?~E,9 s '- +$&L((&(P>>>5jFԼf +Q9FDN[ -h~Nk t p8zF5>jw~ +0 ^ +endstream endobj 5910 0 obj <> endobj 5911 0 obj <>stream +HTP=0 +<>ĐKҁQx{%u"7IJ'ӝ-w$@ +4 c5`cu u<(>`WR υaxf-@ [jqD5x)@E!w只CIᮣGXOj6v{4! _Q7X6w[;INys6;EH|J% i" + +endstream endobj 5912 0 obj <> endobj 5913 0 obj <>stream +HT; +@@'Xx@3&k%DBͣx a̮t70Ǟ 9Bm*þrFyˁdO2}CyPL#6أ^ldQ5] +`&OrQ0̻L1&O%o2 G? +endstream endobj 5914 0 obj <> endobj 5915 0 obj <>stream +HTMo +7Im"M{aZ)8RC!4c׼Wv6 n0Bk 8S쬃,cu\%^y$n1b_vdc 3<6OG0P|L_GA`ezS]> endobj 5917 0 obj <> endobj 5918 0 obj <>stream +HMJ0Wd#4i;UlA +BЕP. +^G貋Ҙ|Ȉa`c&}%I&4]f2tq"S4w)~E,dVTrO͗LFozf9/h*p:)7o0q;fׁ$B:!HK-6ψҋ]xWV)N{'7Gjp#bGNi 8TrLҙ|u 2x%mdbHd,,t$0 +P(-I5@nJ-%2$c3 Cݘ `kL,TsRT3ΑVbJ%G%upubTԎ4hĖ.+tg -r?'ԅ#d^GzGZ[*Jh4>ahtZ25ˠ|Î +endstream endobj 5919 0 obj <>stream +Hսn04G_ elj=H  +$Sm-Y웱S־A4j Q"y xd3E(KB]^/0.rt.=|gw+Iom׮j;X+:ŽV;X\ݾX/O᭰.Lmea +A=Vf06s|`-$`Ab +zDh 'dS@jYXdl +'9hA%šQ&`pjƠGi.~LCwIP`Y. r_iYg LEWQeC6{s: O~Z,N'R]=-)?>؇k"lmEAIK나?8SPP `6&7h.)aF`wpxBUA3P8q:鄪d4 +̲ Gks$:;x>e4ey~ז )N`Mߥ +endstream endobj 5920 0 obj <>stream +H;!a:iu 6j b@)*nencW$.YǗ>{2ِ} .? +endstream endobj 5921 0 obj <>stream +HA0B=A;3YX( +AГWA= +* +sXo`=d鼗9A$?$mB/Z]]\˅w?ZKKgW-/zu\kf?)4!] ݻN-g=X:S+B=.w<χw]61?}5}l츻c7`oЃ;ǹիz +0 +endstream endobj 5922 0 obj <> endobj 5923 0 obj <>stream +HT=o0 +:$~w%F*! +0Ub=y~͛w<6,gA<Ăg5]i*Zݠ\>E2 Wn'K^_{ + +endstream endobj 5924 0 obj <> endobj 5925 0 obj <>stream +HԱN0*} MZw@ @?߀G1C!b/W[_i.M~XYdE^,_B%엫l)a>yZK=GoOkCѺbSLIC!1,arNZqcd umPLGR>kI'i%$$CB3VHx6\N\qݘbZx]F AL$;$W~o8틴_#{##R; 4fX3*C5 UʅXSͰIj(ЌJklτSP\bsb9*pϤ o=1;ި뱿-Gy +lm;\n~`z +endstream endobj 5926 0 obj <>stream +H=n0`[-=B|8"83-G22 \q%M[_翄% IȐgd%yQJé` V 8͈rؽK c)ocOfSl_Pu(UD'!~i;|_㎒|wtWxU#r՛BӃJUhʮ n/S +m-[W9tA:}ntԿlmUz臘iO~_:eFV.#u^6*%Mk# +endstream endobj 5927 0 obj <>stream +HԔ;N0&G\``YH H@%h>2HVL J&?x*ˬPu]eem?=VL i}V{^\gNjEV^ߟx{5ȴ#[@3E0A /,-$yBG"IDa) p  3RD@`>Jl%V2,D8R,Z*Vґbxfj)']K|DY 8 '’~,!Eڏ#l{)̸P593eNagؑ'ϑD'$ {':H8bDd;E2LM`~m F͖mʼ^d2mֿ1.lo=^ ӷįX5I 3@ +e"nGSY%@4, /Izt#VZQV:"i~;)= +endstream endobj 5928 0 obj <>stream +HN@!0{Woʏ--$&$U9P4z7q#:eibC&:._ofin஼u&`}ʋ!^l@Oxqū%98B7"0M ݥ +?u>Ev%T2DŽHǜSSC툑SRJ,6YeYCg*0Y'!<8JK>stream +H=N0p %h>W:ȀF|s]%bBRy'tUl]2S:-Ox&W|xV.s/bW!)nO.eRM>Eu%td8Tȏ=R` iuXИuvmf8 ,> wnI@yHG?f+TA} ҇$AJddbloav9:6UqlY.V\j~aŠzAHR }ugl-,Ivؕ4L +)[i8ؽ9HIHER&7>stream +HԔ1N0]u( h +HdF 8WQrgv#|ogXd|yY$yE—e3ot'ܼ[˅6OtSlE;rnie/MԣѐىDZK1#$%#VTn iHhr@1yFt +lbO BEAMBE(h^v=^eK~ +};5f薵@om)CN˓fZ$KIZ1imm+c gDR@h`b¨aj4Ku6k\hd$ش6_$[@2"@!qI +5Ň|m`\ /O >}a{z[# +endstream endobj 5931 0 obj <>stream +H240P0P030642U57UԳ012 L̀b)\\Fz  źf.\.'O.pC3.}K .}gC.}O_T.O ?c`'O4O>stream +Hj0-ď~: iC!9Zhs lKrZ>H7QȖQ`C{I>f44Md9+<9Zb@0#?2SVfQÜ~8<[$Ks9]}N29_}I~չC+oO:w* +Ck|s@k\: S z vʤ1f`j~N5PYQ0U:!:i8Wz5y횫))= t=u(B촥L:`/:ڀI$a՚ +3Jk!B4>|&Md^o՛k?Q WTjwSMm3_{ ;T9tF!s+ZM& +ls N}HjnՁ]j_J{mQ%HE8Q^|`˦ +endstream endobj 5933 0 obj <>stream +H=N0'JarĻ6[EZ@"T()@P'GQr),Ď]!B4g{cӲ$%tS~ow}Ej7\Ru]VW|lqH(V-%.W⢽#/J'B q,D#D(b(j i 0׸5.{kNAK=6golҔڵAΠƀh6so:sC.xqd!-8IB=%Cn$|ባ_H%doqRf-63p> endobj 5935 0 obj <>stream +HTPn0[&Ja\\I$<.}B,cS}^>f;=w7= (˼:pQrPZ=KQN x:34~w}s +6c ϯ@?8sP8ھ*&fgpBfDh!Ш5R]'.\;S򀳀ˢ#>VsEN?Ğqh9 :ƨp3/W¿ҭh\6 +0Պts + +endstream endobj 5936 0 obj <> endobj 5937 0 obj <> endobj 5938 0 obj <>stream +H240P0P0133170SЅLM,Mt,M -R + M L-,@u\NSss\.'O.p#.}0`ȥPRT @j! d`U( +endstream endobj 5939 0 obj <>stream +H=1𱦰4&|D +$8PRŃ(#UQlGX-(Ŏa=L"%?ϧv$l1j:LGj̗eZ.E![/FS:>骳|XYAN7m6~2?ǯE6>$/Ewi`Z329oiE8S@+|w]H 6 x&? Ut4ESDnsiM`̀H9љě:2wM#Ze0)ִٳ>0&xj:RDYD5PFppWז*';qXfEy%ag`)i .:}(] +ipk=6 ?${]ợEXwƧWX^k-{^eE=V +endstream endobj 5940 0 obj <>stream +H@7Ja~$.s%,  *()@!W#(Dq=(dOavt)6ٻ3Eֳ|YdGl]zl5[${b"dth-w`sww>q<I3)T\ױZu Ugij[34$(`֦"4&Gnin|ةI ;khϳ%Uy@w'뇴^tFVCk`>~ +FJETⰛv\ FE#l"b)9|!5KᢗN@73ctDǖ2qwa:0 ͍g +m1br #ͯFkGl!ۀwCE0\aup埗n= Wk]|.C eWI +endstream endobj 5941 0 obj <>stream +H=N@!$pºbEj"V@--4ZVaXYz1^N #7,{+~| PZq<;XI|DpGtQ:w2ewz&MszG*M_PJ٘ h+X:@kD'@ڰB-`ck'W=_)'ߖl$ޘ4Y2yo:J mvz;MHM~k|G-C6"ڌ{1=' F +endstream endobj 5942 0 obj <>stream +HԱN0`I@-pINMd09'@4G:Gp/m!m";|RJ& Ky '4iRE(eA*3,Ͳ8 YXq}ϯТD#cJc.<7w>stream +Hj@ Ճ0ynM +=уxG|Ghă:K +a>?3D@k2ߟLZ,"_E^NjY,|S6EQy9;Zn"{f(rup\Vnr0<;fyQ}E6>_|,?}|GW{FL=&whrmp2 +[&piz %Nz4XY8xP=֎8/?0w̡m %u )^9i 55 +0/!c9-oSymFYͭȆE"ۢ}Hu7m\y1R՛ʓ޿/FRzh"?:dR(mx8V!ݻZ!Ӑqȱ O.1hACgw ҩ2={PHhx"{:j骹ORt{Jy)0~1keô]e(_!\Z޽"krPmDwhP<ܥ')b#%SmZN%_֎> endobj 5945 0 obj <>stream +HTQn -P$%<` a\0c]'s6<- \ppjٌ֙:'i'hpNqnFIi G *h[3~|E<ĶƓ9hQU gLǥ7_:*ZcOeezôFUs.~{֍$TC!n-|5Hlo1뷄)~U= + +endstream endobj 5946 0 obj <> endobj 5947 0 obj <> endobj 5948 0 obj <>stream +HAN0$2TGpV$&0ѕP.4܄#)љ0 64mQdʒ(͚ѢXYaO|7 x[ x@Y_ۧ[~_?︾X`KTY"ibb%P6w< +zyס==jBNU"lgI\(,RȂjAs m`> mCi#.X\Y%q7,)X+prA6At'N}{;w3wl6]ͬ?3˱S91YksSClGwƭo[ƞҍIs + +tʦ/=gGBkaN +endstream endobj 5949 0 obj <>stream +H22U0@=scS#K8bU嘀U"`\.'O.pC.} <}JJS<]szrr@ +endstream endobj 5950 0 obj <>stream +HҽN02TҎhv~J$2 #f&JqwI[ C>:;s+s_pEW):,<[dOevPM*>CUmǻGhLvdXL>Ck +__Lsn?x{ CMV! Dk89-M`K bW1⏈,Іc$ZLĹaH;]n#9갯S-hJV܃q(Hh/hc'G[.wMZ@rը n17S +endstream endobj 5951 0 obj <>stream +HϿN00t PH`}ut\/k:2j= 4xО~_˔FYu^q$yZ5y+f=NJ<(/>stream +HKN@ G/0dZIzL${3m![ h +[˘LG;ig$٢דMl`=U9uS%X-^ +endstream endobj 5953 0 obj <>stream +H22U0P033473U5437317T0Գ054Q03170TH1*2432 . 1B!9ɓK?\ЂKL:8+r{*ryp70=pzrrf. +endstream endobj 5954 0 obj <>stream +H̒N@Pls-,BErj"V>ZZhGQx+) ,wXJHdgOt'"WRf|"#ŕβKLs,SJHiR&{#!50g?cɢG,aKwZۄ@El +b6;"S@1v}{ykd]klfYmpk} {&v+=dUbfz+N6[^l~<;ܫ=tk[l#Iz \>stream +HJ0) +A  lU=у7}>JJ̤. M&QZ*,I*(¡LeeU$8s"DEIRYkb(hbl.P*WObӈ*bUu)+es>?_Ds-Z @KQh՞4h)ʄ 'Z [KpY59llv|LV8mO%;.x}f5H9v H A3RNMp!c; 6kOi‘-!.ָ=tJb܃p<;'ͬ]Y8EyxwPՒ5¼x+ʋOyi49u<,n | +endstream endobj 5956 0 obj <>stream +HL;N0Q4M@QYZ@"T{A܌%Gr{y'q)[{C~ZXz2)5_SgC8m O/Rߒo`?3N*3@SP}%s+>stream +H;N0ЉRDrJ* |H HX * (9–.V13)W!ȋl4JUYTcUJˢS]׹Ju#2, +3IquXPkl]u"ɵlHv72I/o3Pxv/v-񿍾|vYN996XsJDO@v\sDa/0` K&gӴ舋E h@WЉ8 +~8za5!SS6Gl4#T8j7~7pggW\uV 0 4 +endstream endobj 5958 0 obj <>stream +H|?N0eG/iL +Hd@#f(>GQ߫(EUϖ*Md%W CV-S-^EMkTVEV>$ C'ݥE9g1] D3 `Գx!І:Y s 5 )\ ;Ղ&q-e_ ;vI>f,ҾMhJBI4\r|pM5#(a1111=rbͮ2O/9w9w#ڑ4!=gTZaJlh%']uP̄kI)¿fד_ k&"z +endstream endobj 5959 0 obj <>stream +HA0E /4@+Ɍ^DY,H|% W$ӚH mW;jlfX XjuWu7 gTm_wҨm/mM}Sݝa&ʇN/:}cNOtNO__A,DBKT2(> endobj 5961 0 obj <>stream +HTn0E|,[ua !5E}I7@ Y Q.kQnR'\v8OWZQ zPmǫOx4eO8/ns>xw`z/$//f +4v_}#?ZmqRG(ð VF? t+i;-]Y8-*ϑ&cҋ<$N< ;͟2<7iʞl#.={r%QC Q4+ ֓g!{^|({RP ~\ + +endstream endobj 5962 0 obj <>/Height 229/Type/XObject>>stream +2S /2kM@'+B+h5 ~~~~~"z?A&&/տJxo_&Ou׷׿^ { ؅xk^ (3|@ + +endstream endobj 5963 0 obj <>/Height 232/Type/XObject>>stream +2[ /&!2gjI[oվoҽ&oVoI+[aޕ7zM7WޓzM77ޓzM+oM7Z & ZL-֓Z UMl&,Z`7@i/K_TKZK +aXXV +  + +endstream endobj 5964 0 obj <>/Height 230/Type/XObject>>stream +2U /pj%AAAA~?F?@~~O~~~_??7~~߯~4 ^u_^{{uu~^}K0 ׵/ xaxax`/CfyU? + +endstream endobj 5965 0 obj <>/Height 232/Type/XObject>>stream +2 /A&xeim??A_/?'/?ld} 26!!2R8d x` ?*2(@ + +endstream endobj 5966 0 obj <>/Height 229/Type/XObject>>stream +2&j?V@mA@A> "PAAA}I}&M}&_W܁^WX?_~]w_u.v׷]iv v] Av% J.]  .  5s? + +endstream endobj 5967 0 obj <>/Height 232/Type/XObject>>stream +2 /+A*т< D=^A&oI&~Vo/[}+Vվo[&o}_ k'[[0ҿuۯ޻]Kzw]m-]Km-[ %[ ,0 ( + +endstream endobj 5968 0 obj <>/Height 226/Type/XObject>>stream +2$`Uo]o4__??IH0o0aM Aa | k5k | A5 }M}_d,X/k`P +>Bx@odD xk ᠗A4 k_<__Kv~/B6 +°Q a +<=Ja 2iI_2}à  + +endstream endobj 5969 0 obj <>/Height 224/Type/XObject>>stream +"%%k_/KujZKTj*T*T+ +PaC +(2A_K@ + +endstream endobj 5970 0 obj <>/Height 188/Type/XObject>>stream +#$'; Ou_^={t:yo0ääunAIL:AIIAIIH7IIH07A0& & tnaL: H7& Äna 5 5& jjMB +MBPjMP 2ZGh !]i& + + +endstream endobj 5971 0 obj <>/Height 224/Type/XObject>>stream +% l.J%`+ ˽'(b0 (0P`,2$x-JP wXqH*ذi d&? +RT݃dZR + +endstream endobj 5972 0 obj <>/Height 232/Type/XObject>>stream +2Y /󳆌K&R< xD4xDxAA&oI&o[oҾ[ҿ'__{^޻]޻Kuۮ]KimK Ka0kj- ,,0`f +vpُdP +f@exl;T\L CP +@ MC$2;٦C Y S!(&@fX E<6_ xg@6Wd$ٞCjS  +"H?  + +endstream endobj 5973 0 obj <>/Height 229/Type/XObject>>stream +;ڀ(vp<3 xjɐ +$8@8D +CAMH8D + AAtn : AMA&tMu}_omo[[uVi[JVJJJa+ a 0J V aWYP) 0(` + )ہ;<gDP<v\ ``;UQ2ASXiPk___^zzt^].Az.AtK].  + +endstream endobj 5974 0 obj <>/Height 224/Type/XObject>>stream +2 /E@/& vj9YUO+@ @Am8}fz0oH7 ޓzM7}7i O_n^׶nk%K털a d5..  (K(+r\bq!K?d2o + +endstream endobj 5975 0 obj <>/Height 194/Type/XObject>>stream +;(2N8,0< x p@D P"AA J @Ap A0& & &7IMMnޞ}}uoۥK}[]uuVҷ+i[JVVҰҶҶAXiXa 0J0`0`Cl(d†@8PPA +v NӁ㝄*d0k ɨd + +endstream endobj 5976 0 obj <>/Height 195/Type/XObject>>stream +;*>/ProcSet[/PDF/ImageB]>> endobj 5978 0 obj <> endobj 5979 0 obj <>stream +H4˱ 0 SbI잆7 ʓީKH8 [U-"2ggǩt+Q;+MMA6E;=  +endstream endobj 5980 0 obj <>stream +H4̱ 0 SblNOC Fw"t3o(),ڑ)` Cko2Q9sвul7 +0Qh +endstream endobj 5981 0 obj <>stream +H,10 D^ DH,k4!i= oH@mRlLMȫ Gjj(hT>88<[^`lh +endstream endobj 5982 0 obj <>stream +H,!0 DQSZdiyHaRO'M&Ag:X-N=0녏O*58Hljrb;3^x3 +endstream endobj 5983 0 obj <>stream +H4̱ 0 DS$񥧡dhb EɌ@SL}^ % MkN+ΟO) '| +endstream endobj 5984 0 obj <>stream +H< @ L.$E@Bt.l 328wlTfXN"G]QĕYKȈ`v + J +endstream endobj 5985 0 obj <>stream +H4˱ 0 S8ɧdh# B`H%[&Z+p4 V.еd/xĦ?_p Q^# +endstream endobj 5986 0 obj <>stream +H4 0 E{O= %3@!oߡ"$jiJ\yE] Cj`r莳Ӽw]t+Z +endstream endobj 5987 0 obj <>stream +H41@P E>x/Hk@k7 "-]Rn mtLJsM.ҩyE]>؆Q#C0b>e[k- +endstream endobj 5988 0 obj <>stream +HḎ 0 Sx8힆7Q;eX*b=9h\LO"ނ_U I6׋vz6 +endstream endobj 5989 0 obj <>stream +H,10НS @KwGϠkcbL"4 q]0t7USTҹKK5i;cO +endstream endobj 5990 0 obj <>stream +H4̻ 0 Sxm_OC F@ +fd,bUsȂlx +IR񫦁Z zp +endstream endobj 5991 0 obj <>stream +H<;@P @>^C>Fi o7A}fdt +RlQdj&XNȤPI.|N\|WI~\p0,q>`K@\ +endstream endobj 5992 0 obj <>stream +H4̱ 0 Sb;힆7 {jE`͚-<uDGq6)[xˊ }Wm֋vz ~ +endstream endobj 5993 0 obj <>stream +H4̻ 0 EўSĈӤ v+ٿ $=WĄI`dgE4 +.7UsS|ը*~87=^[頏I +endstream endobj 5994 0 obj <> endobj 5995 0 obj <>stream +HTPn0 tN /!ivE1%࿏8):<ܑr|7"{bΑeኽ#(Jĵ[t> endobj 5997 0 obj <>stream +H2231163T0@=csSK8bUB]$ɥ`ȥ"ABIQi* .WO@.V +endstream endobj 5998 0 obj <> endobj 1 0 obj <> endobj 2 0 obj <>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>> endobj 3 0 obj <>stream +HW w6;=pRF~JvIBtb%V}_&˽>I3,4]ݕ3jk~=1d>(=P`G"94PI@xi!ҀFQj_[Ep:OAt|6>0"\F 2~a yF9&yDpu-K^H. imq]$e{˜VE[TjnwZAN"=Ń*(dJ #Ձ\.Y# bxD&=7o,:p v# %d`=w7.9!BB05ހB!Bfz E} +ȖXIydc{jUеM@PR\m鑂q9K>ry:VAZ[.zQ =h5N+-9 !7GY˜ܽL8 3]cٰE{: Ļ~|@G#Lg-Jp:kk%$FDz4\1Yx9Ny AAqI7)ܟr:`vU㤳 jƖѧs_b1+T+ Bp3?m* xQ3PC* CN@E xj-.'lϵbq!OD.F ?ܶk%>~!@zi&PX0(iO:)7h[?Kekb =X4#J#y<l%1xXgJظM6?P+W"fcT {q:N'O0)W,nYFL'@s.dO('-p\Loߺ^}\;?UcL_ sӉ[_ml8W=9,Y[ \uk/,^9mCx7 Fͥ^1*C)Vśvv}rkz-l9pr ]j֯lpjo-7AGfHIh͇!>UG">B_9 4NyH$3TetSt5P[e wB?$ܜ$IbE~B&ÃX LE+1Puv U)3 +g]|^US|r|\[g»1nxfpXo }ߞM/%b/3B/#f^Bh'hM )UsBg%ܴOϖ}a_xGL3srE̮;FhX?{bbvi]5b^^{dk9>ExLЫQ`rXq |RbgaK"5{^9~@~ȆʱFm"!ƻpSaU~i\\3,₣>ii6Fe @cÀ |' Z[ UCs5K s _Й6߬" +J5YH"%yVۇU D("s0 >{{r$x`v dFMLc̔1yаd?iz"+9 +_>U?KMl$ 2Xܸo#ޛO#9\ +Igss"2A5}W*3[zK`ǽpfхQ__B} Heu<87+G甴Jݼb|ѤnuvCUI ?๽.>{O|Mv@s>mFښ8p9(Jkj{* +ٮcʆ^j%V^T'r!vpI>33ӽEM>9>j".36OnUupm3E-Ns_rl(?[*11xaS% 2C'eTVnTHkbkӼ4wGΏ5)Z1wo ad]l0"f#ȔY.U+kWlg)w\RNGd{jUGwgsչl lb6Çç4I#}yFV~nޭU&p1\# +Lsww$Ne,ܯAOꖓ90Η0`9 BR(XHKzr|JDyT'Ju;nlf7&K6:U[ʎHY\k4yd/8 ܌598Kiy~P}\,,"ͲE`bw纷oM|NgB'>2/27 …ߪn3/RzWul HqK<I6yLEdh JHrޯ.͎bznpWACgw>'N QՆ#羋|oB{hqi,cSN'c]Dqi PrmγN9cA TY'h;ɜn{Vѐb5QRBC"pGu(''l4;!9Cur(x4>}^8Q S?urw]$LSv)Z*%OtMtr|h^d2fԟV 5۵M~SsC ew?,qQT2 L Mn28!w%E("?RE)xZyfE1:AHw߁0.8mC, +moe*0[v?N:91ǐac>6z +-E R*B*Ha3fby4n긂:oq:?*D˘q˵:&i2>Oi}b؄e0js ƨ䑧PX2kxCR)Gӎ9&ɕ,]+xp}{R sG&#&,{[dI.Z2m^ȏcO2+>Qs>O@N?xck53=И)dN[%hQBUTazޏ ,uxR>ڌ&a%̣drIlĔҸn }*+H@>"ݮt- +| +.̌]1i܋1򌇪a1lR?J4R##b兾h + <). +f+T,#g?7=L+e7^W<$/~~PӜ}b1J'/\MSѨf +%S| 9"\:N+sx-m\y::zYPZ H Eh٪kJɪ<;3PQtRLDžy#V/^ +-Csfڐ):"̓tP>Y41XɕmtwhސR ̟+k:XM96( ix yuVU'o>n E㈗_sYkw%U"Oʘ,O{vԤOIބtqݪQ +huin F0}<.Q+XDqd3w&wDCfhiu41҅gJ<|ծΐ4MYV#_K ̽rjoFRY0Ar|/JAEJ@' ŔJLaY[hT繳t9aCde0gN~wm!>5QO]28.qք?F^k^M[\ms YERAf(\eZ77p~,p 0J< +mdp4i875Ij4K2rN2~ +tnVISOT!fQD:+)*di`a.pT]A;*I:R \y,jHi%,Ѥ5N" !,bPvh#grdJ`5CaqEfe*}:AY0{o@GG2́cz'R1 _X^FI`m4;h$w`KVDz83  EJ0Ի9Yz@be l?EM ^/mvM{/yHx1K*Z.Ǟ%=ƍz 5%p _ Lo_h]EƋ!hh'y.Kw34OiѐQc_KG +9W|TVYYɼQq‹x=22 nce6=̽% //zN GeD5M}V3aCYp1yd?5^7Ѽ . O"W–5ڇJEsbh<ˤ`J55$!*%J~Ur Uܥӈ.cDf-3s+2qi<3U}[?p9UO޺ ; TX<UfdQ'd-,CӸ +]qe'_Z KkM|6DQ R$ +Lb`\dΣH!!Ei,`YP~J%߀ <;wv}^ص{Νs=wd/Gpdw 1 _GPJǶVO:rwP9gSV7}|R:'DInc +EZNp5\#8˯ks7&B͉0USAF0@!Td:P`>X`%S(v"&h +rǀE HX^ )W s +[-NR*!*Cn,>cFH3<[3p%UJЃ _ =jYW5)AHӪ#jQ@NW߫X>1+1y=DGF01Re*r +SG|'GnYb0%D\˕Kn=0aU$ǧz +wNBo#3X'1-[B25ujܽ[qb7*.t. `c#b%{sR{LA`F+4۶__2Vj?뮧{N7A/zH^A7Kՙs2-^f:wl/BWɠq)d4+'Yÿ`MWo[_'*^v +ʐE0ԃ-NfI.b03 RIR!5obµ&N_]-{ӚzFC-9],l֮̊UZ2E7i|ƽg`G.C'\Ԧh2ۨ߀3e/~ʡK!~0QbhvQD!~ը)B64^4@>stream +H{PTqi]e{.hDhEujNe2ZDm:hV/(jfԊjR%1xbPP+.Pd +awy멯pܽBGsPgMٰtъ-x; Gg@,LJ52!183jUevǃoxVEƆ=q=8UYy+)˫'j[=h\5w0W9RY|S^SU;*)RjgùtJi?*oWO}ߺ)Iu/5HYqIQi9]q:5C?~φꛩ}xzScѫ?v *ˮ4֮ ۖkPJ-kn}>^鑈;]!QXiI]]o~ZPF"x#wr/5F٤UJGٍCZYȪFgBWBF_>Hsj +>y3G;, **Tn/=4O7 ^2:7Imv{>ͪwIzȭolXՖҭ~]ע'L-GNGmjs|=UunN֗tWl{zm5Rx$S,rft}"Oi>hyDu}FWnPqW{KeܑegmsCʓW37_:wn7 𔼕sm:Zם_VQ0{SyQUYQqu+Z{ţm:oG'd/ռ_ݦvG$gzCwX]q矝<{^c9sN={; <*g;{Ҷ+9cOxO~?_KIHOɼlbtPzF?.ٚ6 +>r޸t9pKjxW.>i*~0oQ#b'L_ +k:3iS;<[9l%o_Ͻ45aSg&e?:DWңO!2nSfAt{3o[AC‡}pi _!5oK;2*b֦ +{E7zH9ߩ4F+_˺w['n̪hWE;og3/ucg-kkhmEwJWw奇D&AL {<}^j a;Л,ך6 +J벒'=[E"fzA2z"%WM;sKcc8F2-q}+:!~~n0`Ќ=μF\+F]B +2EG|/T}|$! v7sic_xB#5]\_u*q 4𕹫tj99^ۣuӭRyATNge_Jm CNR!~`]{D@ָN$ƨʜYcc1;W>Eb#'2byiݑ> +&r=:T{IF$~~q8"&T,ό$sUji޵_gff^Vhl٣f^bY%{&Ⱥl_EAY' 㳙.e^I1p<|G='e®̡+WQ񢖍>k@Sq`~R*%D '˾v}<+ykW#1QkO8sj*̛nn4ZyqsrNа=Ή1bAwm_bpʢ{dъNzL˳M?}<%/V8:2H$$qOEm^ +QTJ ]&œ%s<(rtp0B^kgնc$ WU䧪4=7߾:R) +φP^1;sPM< |ٻm$t|P[\g2"F6*"@,IZ4Y߅p8zx@IG|G-!] *l ?۩~A{V{(9j݊LոیO3olrl Ӊ,`u-݈WxQSK KjC `r wo4?R6=N[QoDO魕:H\w}[M1R¯j\fxG9t|@ +۳[ Z=2E3V3xt=M']x.tqpv7Ѹ%K1^HgqjooB]'~(P'i^['; 4ƝK4DPg5,oϊ00Q:} Z#/IooB.߻>R$0HKk+Dbg|5<6ްQ+*Dd{ ɼwEbhj#] 0 /w-[֜UƮ]Lҵ[${qQZ8|8mW#1tzUAG0nRor1cWf[vćQ4jX~@ .#1ß2|XkN~+ׇWΉOf$];F+DA~Ar-(%h<Ο/N?֪#G/8xSn&ϩ'3:+=#qէ1 +IT λN|'Ҡ׫'1of;:6:a?X_DF_f3k"qQ&+q8%0[tz5/>le `L}"s&Pϑ^#KSxjLs}/~;Z 4ӦEOLim +E]ly Λ*xJ؊{ު"q2_#pjڕ ,ԬS_b5菵m݉O dQJ5^ϱ 灯Y2_Jײݎ/TLEu4B$2Fu/tQDnn%n٪h{F_%HШwT-WSicx6GRtQ yOADĝmՔ3Ѱinޱ}۶7hfxsucar*Qsk^ʘ!Bσ$_#L/)Eoֳ UI\ٻ¢יI%UiлGt7k5,*]ՏqpD-׽T7=o8gU!^w13%h~ypqf:ȵW6 }j "XnCBU*m*t VZ<Ã[E!hH8RZ$`BHs@b҄$+0$K|Ȣ +у2o1h<;]l7jM&stL94e(1aUM@Enq힒SlOP_ܸH(;duɦc_A~ -Jޢ.9`H#˲Ȳ$76b4ͮqSkbڲNUkT$>7*֥Hj4Ir-xbb5y--~mqz7Zo⾶pK(+$NQh& OrLam4=4Y1k%~F5=Ep^WNA^_yv#QDu b* +Ig]hW-+\ԣM5K4)vs.8fdlS?!jFmsLL=_9Qۦ:)?t;nm'ޜ57=lgJ鹾&\w[IW>hQVٕerNշ[^_Sx`n$dؗQT/ӌ] UO֤0.SW?(9#ƥWns1cԧ<7ooJr'sCr8WtLܰeKJLJ⻵=g'8JbwIˋXd:d]ol$~]`"VD۰7W +&tA8ilٺi_rh&u¼}ig5̬Ԅhi),&{b\QQszc05$J3*D1=UёxRj5aVU +AVب0k|vѭ1mG}~7\?8mMO+&WI[ j )5Ŋwއf&\-7 ESuR͗nQ.th5U#ʽ'y6zokH\]]_JXK$c׏v!Q~uS5*v^^oɩ}a:(ƽZ_Cɉ4Q$-%JkТTnUSe9OHM C#1ŝ_Tv6%'# r뙏r>z#W1#>e +Z>qcyH:1,-' Zˠ#0"7^5#559m=]uCCQ{ S~ۙ}4{o@KvjfWr>7//?o}sJu^:sgٿ|p.+Iu,9h;Iu5}W@@sAk.cM"V+x?|̲lA1`0ĖrbiUjcAGn.Q]˚*"~BۼaS<b SCuz +0!ǶN_-9ڭJ~va .➕X02Thw +Xd'.V%=nQr=]JcDcu\kǝsyuO]6q;e`m1`C'(_>Br[c.3VKTJu~߉ K6z2DurӤYЗx +0]?b[s#|bk6V'ѿR8śS{&Oy'1Ǐk֚ *KcE|ɨ+w+#iIL'mʫ//wcJȕLѪ;d O{UvEJ!u)I>|AH?tFXjt{mŀAhEņZ<#,l^#ߋ0Ŵ y7n˺u<T%b +%L˦s~4AV?'7:yҒnˠtw?[MU-Vݹv{3 i'L Vp__/wa[d`鶶CiK2?k՟֊;41hhod:=w<#{:ULѩR}.:ԨJ\|:ffgl9VnȒgy ;0-gԭg9b#FXE/:~"ۼW xu\裶Q`ntm.)'䦿qw[Cս|#ҟwY:GXeZi;4NZa+i8T$Hܽwޭ711"0G Z/ +, + +)톏i4@+5nڜkODl>~Xg'%%Muԙp8R!,IH {rBqo;G ō3#ZW\At.; :hUDD ,Ze= !$sX/7_}GkgڵQdwc'x<Jqu!C+5z-Ga<R3 Uz-Fj*yc]y{V' +;xP(|x'= m6kV^ 2$p NXzGX +]0-ح?'nې6?E!""1j{bVZLRk _2_z]kPˤ2YSڲGߝJu:&ݟ!cBgl} $?W&eKEaCJswo;نDkA9hiUZoEO[]!3NČnO˵1=u S4a ndЪ{OT:f$[3 85dI ^U'gVk҃0.R  HG/-~61&rlM99/Zdz~M\/EQ~ѡͨ[nĉ/t#u SB:sڿ>&Ʈz\lI$㼚AzfUO_\ڛԚ;196>n>OP,<˅0>eJ1@TΜYynk +iCMx\΢Pi,S2zL~iZi hb;2uBL0EX@(C`QV$K{6.N.\ߌSs~z=nbbз4>81zR +{b@7h5D|mI^ $FtQOK/$9cMPt֍jk5*u04=#|,"2|PaP,pyxVw.+O,5X gO&1?2KPG?kPAY ܧ2=Yp*aB/E:Lyiw?Oۍ1tNqvLg^2ޢP876[;>oN(O/ x}:`%o=4]k ҷ̵M9?=eE{ /w"9б~3Wl=p,gcIW ~H|Cc +c|]leS>aTG]FSWkNs#Of%M]d{R2 $燼EqUMmK޲N8yڧydwZQ`B,!]99[' ʼnl87Ȋ7J]#$_62'P!Q_s +~̣0 Q`V]_ntQ~0/Dua>uePA"ٰ|cO=l613Ck^n.KB]Y36X\q)kRdeƑ3)hMrth,*=jf72aB.2-Zg|)Ţ @i| 8 u>VdxQzA3 +YL ܅AWc+y)rXD+7-'ۺBb>7u_a7`%4D<=uv>J_w0hϳv}ru07uQyZ0{cbsOjEuNuQVv=a/kGݝee5Kɜub4% # 1 O.m%=D XA7mԆE\wCjIcmjs~}j.]Yfwe@|g"L,pg?+NlTna-eJk`2;߲bɌ^CWXvq׾o sCuwe@tal&1鑅qQf^m~qBƸ˕*û/|Ȗd<褫 7tҫCy?G,¥ջ~5uq3.`a [< +BٔU@* ;VZZV>]P("°VA@ ~@y\sw%:'5i:4أeV 1}~Z_59j +4 "m~Y|=呲ې欵7+B9;e.LWo^X?{'1R=*lI2"@O-LK +-IGM񷆎Q| "zˊ[ʩr1 +Țy%7tf,F> a^xex5bě~Qm䠛C ͅF:,* Y]n?dsFSC!~6`P)?z)?>p'_d0!{?-q075q_<. %Wn:AZO{cvA ~HOA^n-%ѩT7*?<8< KE}GI.'ńj@tj/K t 8zoqs p"i) +E ,p[ +baDӽWU~QC<4Qz!4$e{+=Okw]`%Ba0Z&S9=^hsn~_9hlssQpexm@H)LwҼowj&Uo570 YDڪ?0ң[)E%! cgȁgbr5dmѹ_.iG3sW- oE}24S>'4X(H<2Iv| +`X "`K&:CqPr$ & w0l!B)o?olQ#~9c=@3 .{̙,&zW&\8{or+Pߌy?D;, Wp*܋8G Gg gP#՞ Dx;󱌗RӼf'^1x g1$( +&i5aM_g`NPIk We6I8on5VYd*\vqԎz;ߔ<+k&LZSPbqx +q8܀v\@ί߷8$}/P_20 X]c?6e 3p ?+P>gizK,-a=W.obYࢅ,Y$(x +?Ў ^v E 30' "vRO0dq᫦ZhYF;΀5ar*,:KD#5 ]44\8+7S~rUG-c+X6aZ+6Q!dIGδܣv.7yjqSBOn']%phD"F$E;+Îaa7sXz8U:4*<9s[zјNcm i6$e}xwgÉT`eopCrד7SD"Tvbl_N^ +&4^K_*Dž|7Ɣ%8z},ZLsT\xlFcorWԼj-'4YFԖΖVvҋF UzQ|b/J9u~y/.?}:Jz a3Ҿ"]$2D"z?AYd"19 qDlֿ% }K" Fʬ[Π^zg +yg7Z铈([Z.޿n,GG7c jʵ\NRJ'Ǯ4N2'Fc..I$*){Vr'njRJgP(MpRHKM^jcfL05`GLO6xI.mfYIXC1h"?==Vc)sNTOS˥[lfht%UlrjdWK@Z]8L91o8c>>k8ci[7O:(+#]\Puc [:U2|ž 5B=m2u }N#d`IJ18hQnֈj:3j,鑜w㺹D=٬1H$OՊe:8gM0iwTAQ0JcW\׈.%1*5Ʋn"Ƃƒ5 .*q Sc`FXi|]YB}{ zkmuY@>^^7)\߷oEW-[kgv?[ˌ# 2DH %sR91և]B +&pۯU^`{EklH]>lѣ)۫Mvh2+HPk3.JqbHō}1Wl]dpyw!9`>F,{WEIM]bFIa  #H +S^ Իlc,N,}ZxNJ+Ag,2xcGR$ Qx&a؍A=` )|)*y`dtG8BJ-n`ՙ9F)>TR%Z"`/*!pXA{k hƠ}Zӛ"p֢ S2鿟gAt-&_ߌ`HgʶkU7O! d.ho$zLP IÓ|r04>ՓMv:;R!iT鹒#£$kN^ё3厞i]ih}yIeU7p9t `oڌ`lGOҶ<'}zHƸ6߯:mJ =13y 2{NWC8M"̜b +km\ X6 +ճW(7,Ye~4+7=Ӫ/~lstX?|TsO+W{_p{/ѣPȪ`aD𯋚}a;'AĔ\qz#nƕ|PoLNt +'$&uiyn "bdiW`(A +ZiO2w)*s3bgY Q V-Oę +ht+'ì?ٿ(Mbpȁ2SY4 .=[SBkJ?+1-+( Wm(e)0Ȭl\ʾqsGW|hfh vw9Ċ5"o[$`?OcxoSUE zI\)&#)p?dSNUov_>Mw[Pnܡ Xuipɪr'QH(脲C:"3*G|r sy]ζj|KvfM +vObN1Z;C| 4 UmyR{HL"}?M14u DReOSYG* 2|X(4džI*rIXp>).FvFb +n,R+ӸpAų))hZ^$H(@OxH191oF9ӌ~ѣ8]Cc.hM :|FoN5殳Mm>tMxE>VW$}. sZ^LC$x,lT;#1vYQ߈`c }ӆ +3W{*ÄxMo +8CQFt,W_zw–ۼ `{+ur!gu#-ȹWݘC iPFvGb"ڵR$`eϏ6t}qyoç0,}BBA9tN +pjļy9GOP+񑃼)%hoUn bcmҸ?E,s-hr>"Y6o>W@fR8)iN`r[VF^¤I[@ (B8"iyuyi=fegc@BxU)^D[kS&MHjb#ј$xKOl *ZKsYv.%<337;EUJn;np^Qj knZFwϗ..~CV|b*Ε" \Uc65񄺳d{NKH +d"[J e<' ީTB2 < 1go9~Y_踰ƆFnU+⚱::8ICŔՓx==0N2h' B9rFA9:xǞpMO5eQlN b4RO{rP%>ogfxžY񤇫[[="Zɞ{ULV?Ē:y5aR|]'՞'̓~IK[Jo1Qnf,IrXu BXD}ܺ~`TK%Vl̔ +ɧv/VJ.TW[:/SW$gMxCUѺ{Ƽg#Twns{iw ;WlF %T-%)90Fͱl@TlEO߮RWeԉiɂ\=LY7Ν m:b9jLP޺\;J==3zRFM)dIʹNch--qrMsvٕPs%).gY)DziGXˈ֗x?!{Ue96V^o-RwKp'irN @$G&絷QT1c~` KִY*Ž_Jy1;TSt_22y6& K.6bß~MH/n4LMF}}]MNWUotVñ+mrT%njJDkX5o~}hDF^:x1kȘ*'T;Xa^H)$Oр;pƂCDҞAkgiL$Aēwt:]eZ3'9yU/CX/:&UXF!be3%ޣS-=/uZ@CQVX #Njy@Ջ.?M[aJ0NB(5Q9sXo'eAӖjʕ.xWQ1N+477#Wwu;-9hbo%.Jb-G=Z%xVrlOD%[HRMK:U5Km֦-X#K]xwf^g{LB("Otk&\+y7T`j4b͊)s MueDWzIxNⷩ߃/F-W=d  +ON߭kĚ:?|%xVpp,c)"ػ񼜟MEs5AA_٬] f!5덆l:+A MbI˴K:v1+Kym*!?(Ý `k2;1](Ms SGvGCplCyV˛ri(xo:8M,EbPvU R^u[FH5u3C fе@:@ܦ2hhʪFS˫e_f7bD::@(JLM IQeymSO +5d/e)[EO FW6_DU2Ĵ~^g'W ( #SX6@ג'/R;8Xw>jtz4};KW;Jq*4==$r\" %+jhoz-M.Yi黯(sM4Z-+CETbS,"aqVS "-"( +8d_RJNXzphk`9w + }pǸoҸ) 4x saT_W(Ȫ]cܐ㥉ldi5ˋʿGyr5#(75ðrbZX7Zz{G|WcNcyʲn{4-pl 5|0V<?+$$%%,"ؙ h8-ZKg\ꊂ (huTֺZ `\F%, KvBVhph9~C{TGSUܳ > Gw&kMv3E҇ғ{H4Uqds_ߤ?rK]8KS=K~Y6L X/M%ؤDCܽOj^^7 ;*)6E@\%&1ƧQNyi~v/ETG ٫ӕ_8*GEuFNV)-T:h2#9bEɧxc}Haksm !ٺx'\+A$!5u!M!1_ؖEF8a+m+ɕ}"-TbVȲWO e?,ڥ;6(4V `(8BFk7h*4 +_%(Kړwjt&Ω# >BĢ-*+wG9|͕qAiS VC\xqD7:Y*`w%WJ[V]>DrU':"~C8̪ h8YeXKuf8\c:5SuBO1Rp u^|__l:Kj>(p3XB1)q/,4Gq\$9QWlouk8eo/ftmP# Y.b( 14,~" HH\%v<Йl -Wb}6 ԥW'+HTiΤ']QpY.AkTz@/"qOaAtsRt4Yb1b0A4SY!">?Z*1$JzE(+[H[!@$Bx߉@gb;T-D{a])lrlhd+ :֤3e^eF :.aG:CYE1č"Q@ (B!"d "$IB +D}a[Pڌ@:,׶rVz1 {PwtzZXr`8ZJD$@Ȗ+1?)"?F`ױ{thB.ÀqWga0\V#C=nl!s46~Ffe%t"-Փ"q3 +1n5O7,!խՋE0]7ܠȥaB>1=cWGЙG<*1I:%MX|ۨdA +;Y9@WEtUpѧ-//wP ǥm0o9$s|ȱ+(-oM5kR!M ̚?Xg]]1{Zيw(/o_4ixXa^GY3k`>:&@ۙe{PRm VK{V7!!бrzDl:G֍ +^ZJy#-][|z[95/jsN;a޷?./))-.?kKKKKKZ.M ҒV?u7o\2s|Lܴo*q#U"[$LJuN6=>$dÓ_ ӌjʊQ?vRipහ[lt| lCS,;/]]ˌ)ՏPg3ݞlci1h-Չ")mo)~TQV PA>GcʲekIqj=;OS@?}sQߗ}~\iZP*To8ͅˤXva,.o8䋿>v>VsPyN(nߍcNݫtV9@KpR~/ԟ/)كy9ٙG޲v动Й~X4k Z-It!b>?ڌqr.a(;XǦ& +5x'{>cMHiN#Py}Փ˙$^9NԀ>>сjGjK6'5b#&9LWrY މ+K &/cT"(6ZNn8Ҍ(`~gth*e\*i76=u)?౹Z)U'Pb(4欟7F1#lgZc0Rœ{3);g:79qރj /& KmZw +Gl+NIH ` ̎n:F E޶*ZfO.w+DVD q=㝦fR>;ЫsSX5`6Olgifp0<]rp,lL(jC [0҉j(D❦Ħv8/b5,HUrBX,Cb5S`(4"ܭ n w g: Nt)dZ!\jh|j>,8K:QJ|*1<:"Z1G/dTZ]ZKaH}!|UGGܓ1  }VE TznJ PEcy\'8} Off+b0]8d}bƪu NA{Hf@qYˀ*~KQ:kJ(d?8hq`8Wd:U,&)RIZzD`7QYcQW%8T΂puW4VU\ai~-yQlkA:LTb@(ZfgAt9%>|\m! 6=+0_aMgmA%@9'9I'7*NAuPM7-:PPVE磨-KT@Q"@HIH2b uw9o0Vcnm>, Dܫ1WBlTbin4a u?d-xeP=qG}7t/Dþ++=6VsAj(݁2fC"Lj%@%XٙHg pPHg2K7v,K}URK@ov#=\)>07mG( =]2vZoG[ j*1Jh +B]=0hLrpDoX`!*M].ٵ;umZuj# ƼvTߤ5'yJdP6kHl TbPsŪ m^ނk'4 .7O6jy*ፙB q% ڝ'w yfO -J Ɯi^Z5A]PPJaa? +8ḩmV~o5q ?%}7?W#7k4+h'[ּ1Pk96]'BEWJv'G?2q `ۏJW^idK_%$*=*1w-Շfq +4p7/y$*}Y`p[̣;<z,m;N'y􆹓\-nޢNSb">/`OKޔ1GК56 *1?wrGn5S"ɣ:]ΧEN_ Q}@ҞU=V@؈WRevNE#KU?zoY`S$9#(;BX9H0K%g iv~?%x\&FKSQP̝Yi{S깼E J O*AXX҂w=09lάIp@oaߋb914SoV.w1L8-MHuJյ_GJvA%~;Ƌѹ\ƴ+ŠzU7D"9G_WtYu3*xZh) 8'EcRw@utJBoFGxn={:2mLҥQ-hDO\)<]?W*`:iHlMw9|<|㟘&?kTJ*h`qkk:.5-Q =N4g+:|?ŋe:'tOr]hexL/1CU{AU#k* zw~MsNդ¿lTb+G`vøH fg2/h:~XdL:.<\VnB94aG'lO쪖 N@%XΗAvKĸꕃy|ZqeaJtm.3 N3z986{ |;JkKtA%X֕h7⤓2א:0ΐKVA\;0y))FzXT?,ʥ,2VNFX@3" /Gz1IA%X}Cc24xG1N]Ja`\ϝ:>uvȇqPOߩXtUl!{^isO#dXroVؽyt`1ڦ8qxG94.t/trvǕhuqphoPTb>5M\T9ۯT* 6̞47' `~P"j +gu$=k}m}{,_i(߼-Re^~~ɂ"UENJ,@nwo_Ws7_ӣ,XBŔY8@TbeH?tpnx*jB7hiVI%"G.c@$Tu(9$ᕥ}$G 25>[xnKѠ{_^;B",;W禸+6zNXo#(Equ5|ݽעlD[l?Q); W$ Lj|pRF5ш)[ky _.Ξ>Qѵi}}mb8 *1Ī6$[3(\ C-҆rD#&)xPb%/Ia=52;fJM{ZN-J 8/YK+iSG UTuY;imv'y̥{'IT=Q8d8>LTfx7#ĤF)KR`Xn3) +ؐ,L.B.d; +t eV{([d!$~W;3`mJ Oo/?K ~Scwl,_W:vI}f`/oV_y/2Tb`\ٶ.Bic4I8pa2jcf[G~&RzR~5VXJM?Chh7RslyZR6Tb`.ar_ C:^3eygj/N) RvK%r Uׄ5$EPrT^bC;Y +g $bAn C%.b 98 yΨ^;}=5"L>8$P2OORSWU݈>RCXoPNa_/fx5>r8Tb`-qkN;_~cׁ̊ƗlGF~΢[UhH" $w Tv#HR%\UiG;K5!.b壏 +]evSh*10D3fCuUUᱵ+%3ק~X);M}>Wb/9r{t- 9G(9YeF Sn~[J k|Gp,G|Gm˦ +IR^~bڦaMfֹj,褉L=6VTÌM |p$ uI)Vwb"nDOD1t&]AhY`-P!Qư=4 r8IBc8pCEf՝fcU;O8h +W'7֞/6s!Tb`pk ,kT]v l[ٳCj6G=Tbh?ڬM(,QM8ZwGviTO+mnbEl гzM#?;C;CI5% ʻJyK-S:w3-FqƋlOPݴ|GqlGǓ~4hgShqԞ&IW8n̓nNf&(*T+"hj[Q(P"Bc!X" +AH8B٢EggѮۅf6&Lu.=}w`f,Wn_?؀ij,.7?GReee;=ts(mnj5.g7g\&{L*LNAY,K&`04 D&I2J"AbYJ7[;M\FxfIE}e5 o- G[Da&z~‚Qֳ!E..2b##">Yڕsy9n0pT5`3 ,\T?½cA"YXhY*D˗ƒ1"(*6HƢPXBD['[GOU}c}3 r맹lxayffCn}#жޡAc{y + Q /3f8'^2 i y"Š+c:D@tX Xbkosprq8Myz-YB9aMy #V. WUU>3 嫪v>}]=ú$]~S%dx,‹tV}~+*t +B!)1XR#I$ +A;htkbl.7ࡶ8;'T:uB.x­ ڼ%d{xT$.cNIS]8o. QxTS|8i^=iy} ];Cy|$`1LU1Xb{)G6Ξ%QhLb Dj xDbX[X Dgܤ03e +BZ`jܪ!a;¶s/bc%|~[\X6kn2jlhln+ϻ􏣻/M'b@ihKO4}gз}6%HDɁ$`3[&z38Ë Q&;aO|WqbHŰôdqF<@ m]0\f(xzzʕ- Xyfݑ{0Oǟ<w2)Uqr:WPhPT\bPZ1ViTi~% +lX}%Xy6|eGDBX,u^pxW}HnF8n=&!Z'13dɿu=!+rY4*li[<mlv99t˄ĄS)ϧgf_]v..-śL[uS73Pc8]́6~ĥQ^qZ_S[__-BM d.NQ(08b;JL7--]Y1R3ѳ`r P?CBفDbnĸ3P:#uOv=Py|GґCRH&W(XkGg7M{R2U9 ju~Aޘs ?54oڠжZM٫^(RMJkr/w${E`HfPH$*.tZ+ͭ;ƽ zR}%Oe}; EBb7qb;PXO=l,<3P,EB>Mg. 9xRNaEHcT^yhh׶|3MLD戰 6'"XBj s5-}zo_;},Ef:m1~qV S$1CRGmc^{dWˍ;,RLC, h4*X;|ׄD;yEꪫ *F)qK_$!OJ_WW֖\8}4joXȢQ4* +19\~K:p*{_AQyS+[޶KSA)E0sK"O=cr`(%$%(²(܂L$h^}>=,ߧe+iUF_},xwW1%`*@%>؄YYxvez b<ڼo/Vsn?>f`DR9):)/媋**K4$G_ WVɬG{{8`0$JD<G:D~+WS^ETSΖ 7M6]!GNTbr1Cz$CRbc+hOܿvt7%0$Ș3wr(nAjt/S.,4h\ +eLYu-PQ۴!fl۹ S#E?sNd~>jSr1'/; ! +2V;Ot@y,bb7=t'PJJI8Nw5@7,NMf^M8~`džs&C"dKa[A VE<}9nϪt%{ʆ3iu7IfQSWEw +bɮ=Lj-Gfq EBPL2ngVT e@ߔ_YIhteUEdzJblmRbZs}z\MEƖcnHSC㺚l93惍u5yo=94J @歳.9Ƌ|CQ%*A0ȭ|P9z߸;lPS$ItuJEfgLbSXD,ݷ BN>Z}*n|qO]-M/\c:*-;Pgٗ.S]tj^g\0AaJLcp8<˟gUw"N'p{_銊Ԥ s8lds8\6-pѺI߽-OSh2xW)4J olo;PPSɸ%L>0 Cs +^Ғbm>S|mqYYiIqAƵӇ]8e[@rDmid33m2.QTQS֎wښW'!fb=7'Ƙ9;P- E%tg +3w.t! /Û\ ?u֊cɷҳIE]K'AR*ݛ/  8\H,(" R8yN ^dՋwiJ#+FuopdcI޽rCs]/?_2V&䳙,a + o=wFk 8mtpx=R鵚~b.u H{og8J?N&TbzUc9<|^s Y7m&d` RJq1I:MieEI!E +2̅ IUaQYUeQmL^NJϲeŕ)l9rviS[W[P~(s.]} `@%1P6>Aw*\'s]rA`!kuI0<8$ [P`IEUp JIdr4!v3GQU\8sz9O7g״ѝLٓkܙN25?ɘ-,f"h̤5@lygc"Lyq?d[.hbHC[p3;ˆ=I!mb㋦LᐈŒO]HC`^^A'tFjŠ->{˪arD +E~uS}@,?Uz +OgS@&K诚 c~ر9DgozoK>,B.84ktY7!eG?TB|> $q[kkk~ˮ{O?W}ߵ %UT@e$/t}<`, 9#n9hl}NF9Ğ&^'(~„Ih)2r`%x9oyf2~?>Fx<mUjSx@E=ivybgFFzzy.̬ݻ2wg:Ǫ{nY{M~OƆB^|,Fd +IRv?UV[MS`ϋǕf2#=I K8Om>T)ذr\¢x{.qT2|0y,#N#>aWslCj%C@w-~Z`~ ~pHvꭔtHhZr%Y p<׻+J7%AbO#X"*K7,8_:Fm,4Ԫ6.R A#u*%{ܪ$6xjӇJ#-ɋZj1h -nA`_>XQ`j|d%Flkw?'0awXc^0ͥykAlG=S7Q'9mp +"Vr _gƾw  <:ꡛ9ܵIAoգZn烻7qSTLJI$0"4t:vrjQ=1Uae/$Xѧ`;vNitF/$gr$&'_rEQha+ٵ7,A*S@7m.w\&YI-% "ڇ ]Y U!BqyV^>ݗ$"I!e}'$d>y% kֱTC헗p j <=}%2A s(up:jL8nggM7NVEQUeUъQkULN]q-fWx`QT,H޼GlbQ ȑTfڮJ$|>hf>#_Ds6W4:Avk&$?^ ț`_1J򛞾JM'! <|fS:veUSCĜ>~|D@P[w al'C!͠g@Pw#P)AWrk  /&3t=:E("M̓hjîl2ƣ%J\IYw}34O+kJe,*傾Md{V +zug0Q BTą{;,wY!I<{t#JDX $M[؅2ѱ M=hc YC[w$pw-$kv\ٻhR& 718E}s#BCdX6;3.8 W@PwTys4 }8qlV/&'!^WGnZ4.Bx\^*){TT,&H+uI 5 8a3yI-kl&7RKFyҌcU|U3dzIWBۨ- Hĕ^t|(W@Pw-с(!GVAPT,u-^luTdEb9¢~uvT:$m(_]I?]khӬ^G Ͳ녎)/+֖OH#bVd5Bz*spA$~o)}v]S +zu_0R9U7+AR} +/{HZ8* QR&EĮsi1w>n$e[.߷sr4Xr^d~CsYDL!Hʮ}81Wb?>/L`U 4poo\0է +B]/-.񲖛P #qAm>P@VJһBҖ8*zyWIZEǬؑ1dK;>L=G=ʢ?AV+dlfO$ip*B1Wr=s\Ԁ w@ &[2cPoo#7 +^݊š&+ cHa?_ APB=/)J,WLgp[ѕÄ8"TS):CP >%c@;hYA GeQR,jގNmZ^6}26q}H_t-\$ n^{~ +^FkN|=Zn姼AJDgmS67 +` p%w{#^i ^FyxaΒ[xS.o o|GYX c4:I3cз&YG??CZmq SqyfUhK2ڢ+6NK(\D$vfЧj6>h/nWU^ 8ׅց-\f}<6߇#J\5hTU@k܏!aL2$̥Ԩqt E]>  +K*wnw:ձ9,Xd檉zGJ<4\ |-Wj9Agp64ALb7LVeRl ]N̒I!IEr60 A + S|mIei +D.xp +_\V/uU==[!2w0s;w!TAŊuYdlj呕 ESOv6w47PPy/ϝw0ۖ񵀕vpW|iW/J'bf0c֤qI +AӼ%SۇS߬*uhG}i j cH5+ziYZ=ET%.hA=eEVq k_lT =noė68Y 8,IC^7%5 +R)R^dCA@BibIę/ybH6ECCp A>OL\^&Z>0,tN:R90#o󓔡)gWkjȧUrf& ˛4fؒ*)xqrЬiie}qEyJtLO@j-G&T_:zꈨqG/K%Wb^Qȣ&,-j Fyu o~3&AJL #pE'\7weg^hM \u[Ѹ8ic5E\K` W +TFoЅtU[NHmC!}\:[O6d "05 [9k I'M.,39MuYO (2>yO +u!B_߬JX"4cm&&!Ƚ6'gxӫR'YyoÞ7F#zFbb*nИk6?:,ia\ yx%Oga?vB W1U=,op5ޟ?,mL'8t EK׃NowJ,Bs}qvEZb +F"u KDL.:Zx\Z|mޠ;NCّ="3T'hqL{GݞK}owm_|rA=J5@Oi!%RA\hf!s?~!ġvEg6|m'@6]u~56jţ4>bjE#&JQYk/f,&>ZFQ!]16+𼟇x;]@&I<'$Z_V(Wk@$J}/VT |O2\Z5X@g˳d}p _ڠC0,[2VVbFJVhql,bөT죹^ljMݻKM|lKV,фDac|OWў>rŞ:#"${sؓ@+]0S#p>w>wWRd xs_8[UhNwJUyC1Y˧7L9{揣Ưg8+O徒aq `}3+}~WV E9x(uP{ӷT~qAmېKbOHb1% 7ĞZVwR TG: 9xu)2~FTL%4e wmZaȍm乴0VifcfXc[hwk&$8&xVւO]niofPHHQ~bo6Gt:!KG<{h-BO>ٱݭ)}up8?vj`eT'Z h񨖐㙇jN QrTO\-XvYL8%8.NQW0'}bA0ayBs[1Ofz,8%!u]nAA]N$Ⱥ,(u!/%7Ş䷻姖-}TEq}oZw7bG2>IIL %STb+w?iι*#*WY-9K(m›J ߝ3 U?aT=YD3ٜi=U}/*ֶu?q9/#ZUϐg\ BqgN $'EP(q5GV|=L˾( hc-R& YGXj5FG{/8 +XFIvK)7mD8sM3?9D<(R3t^b.w8&HL{v+[hT.߂* LjƾWiU4,1H:tN U7Ua犿{h7ƗxUue=QqZT"$ +}ۋxaa1-lPJ%gfFnGSSf)G:mqG I٬eݻ=-HC8=C`X#_$1*MbvcoZQ szR'3p.+*R;oV+rÄMeqG(ޥG $I0@ze{hIӳTT:p׸Tg`V)f>,x=k%5~r,%I-OIE׏MR<4}cZJoL" Yk_ 櫮}Fܐf#}}Na޲9-|hrxpREtX9ܔPٯנ4(u>yQA(jK'1Fgb P(! A=.m-j|j,LDas~_vvg9w z ^.lz +3-v|h87fcFf|~d+Fz NAnmsnOH (0!1#JPPfFp5N_TthdL!'cS_eڶ 0tB:+gEa962̥3#ur{+MU\K?D4(b@\sWW& jC|'f8rA#puۦzSO B?NQ Ǜ{hckcL!}ԇ #wNŏpq5@P74y ipկқA]Yiwͯ +2BfMٺp83ַhiЄYhbM, l6Gd+]'\, _rkF4H@JLʔ2V 5wyW59$9/-{L AixŠ'Qsߖ: +uWN}Q!pou6|<7+^XVSy0}Ækk eOxF+\2boq8F,}P:va6{IZJ?ѬՑ3/XdQMR-פ/X=R>S:ԏ>)ujy9wF ~vx~IfxpyjW +Xİm;zj}eƃ/ m׉) y+yQL{X +N&b*€Ejmß f VbO43"It7A~#F:F4[Y0)@)J*byhF`̴wDV qMeJ%EηWVՆ.naMfۿ[Xv Lǹe_`1ϳ/_iu"6VϠ̵X=Q(CL@PZFˇP _K;ھ|Zdȸ&o 0Hp+50 Yvtw:^U0am?M`ēWN>bᘞf5UMxO+\Kc Ҙy'H\Q AĞ8(ʀH:u+7NP'|AkʉF#P\ELy +xv<{i#pE!FGי֏;$7 $/ńL*qxs\>?,)>7_0`%>Sjp;P44W,Ա +uBCn! (e2Ϛe\.9u}zJ"1 "R" M&8W +3𤗣v~aMo\52ǓJ!OCܝkgH[(!,,1cbcZV%k>/]Ҭ-WZ\ GnjE@dk%sݬ+%y"}%FmPX^>*y9@[=O7XL>^y|Bj4t91R쭒&rv\""U +#OO +m`F6a︀%NhR c.2LFVbI VƻJL' PCb<66x5sL`V3&-OhU.al%ͺ;2Pӕ K,]#k/]dA:%^5eѬ|iO;wq4.@R)1~sf+Z9qQ2 CR]ҞPeɚ>ik)mY7T"Z +O)7(]<[+HUDxO샽jE/f_4/sN-s9%p+q +67z<ܽOZ?~p>e_ +]1]uQ~Bj Nm rOj|tJL1cZeXZ\BEYnX*UdjE5RĠAϻt^%asQh#?MB  tW)pB8V4F4|C.C!Yg|l(y6ڮf:!ƕĝ|TbzIm62#-_ojBdA"7֓]c8*D9dZyBL ^[vr$dw ,m7nR_>{Ow09vL UaҾvޫK8!N]ܸ5EeGӦ1~AOHak[ +KQhaoZ+q`rJlmwfxdz삣F6hJ8"K٘D&ɤ;}Pm90ګ\&@2ԕ&j?{{+nv{{ʾ@he-lCJʎWt+q+Ct|Ga+zOuLcO̫02LJrq%3'WtO&0A(<0gDs0]:y ?>pLZKϯ.BP ]; vSi)ߝAD#|gE+ҁ8NN(1Ѷ7B!D-~贲ǽWc_=(xSyyサSW%jJ&*8ID&U|4fFChTl5(I3*&qoVA`zWgRD`م_α~s9*$bwSqfG-%9 r%Iif?[K.ZSJ;f\;zhј̅)%RxԘ 7Py)Y%4e&x촥rWK[@5r'a8nJ0 &'aNpL72mM! AΠ>uVbKp +&"=(z^bebGi;*IWP%ZW+-A X(}y,ÿkTHDNw~ǑP=5مQ^kh DCCTQL3*J("uq8'\*9z^: į$LӐ׾"V'5'qJRko 0Ԫ3ǂ +t4H~h0{'cG8,L¨҈@nbǞ +0~WNqTbu5xḫ")ԋtnn_& "ӓeV^a : UC&RB1Mm%86lRrUbUW|jfH(rٮtOc2HȶMḢ@Otܷ~8 !˳J'u}ѨVW1ʜ FA   HPU4l[5d=gB/-w& ;J=؇x |#W8ϫN(iZḏ|/Xt'u"#: n) "(H&n,ڜ^< )ױCyu:=a.=!JҊv:ڽu#rXCGVb a/ 5cr8nn81t'\=*JE|hҌTIѰ *2JD4u[yQm88Hמ7Oc8:0-J-<8z?ݞ0WU%aXw=?jJ0fgq0\;y񸎆ҝ_TbvT݇Ұ EP?s¦g!Lc !0qSA=te y3C%2^SQF_yTiWg%}pw !H`:*t')`"sk+In<@07̦J9KU* KηRNCkOo^&HA<]3xJ?C_eJGg6?+qC^/*yXig.Fnh鄷|x\7qB 9а2"+>2h7MæNP KcLj;=8IushRLh@O7ӵ;~ޥkƻ񻽯]w._͏MJƺ-m}vA^~N۞-7qzC㝶_oj}SEW뵆/>?qF,rJ|/ڙ~fi<$?H u6J˦ȘJJ RF< a4|#($Rx_4RdgtMs.O? Z~NS?JEC}jZ3Ҷ;ce_.a->}sdm3Wܘax];nڴgef2jL8bH dOnn$IdG@@rTTpA ȩKST:jT) #(t ;A;2 2~y߽'Ϸ蹲ݱHaSy]wd˿Py+bcKo"Rx|0v r 5~S^.+RmRֿ|r{k|Փ۷n<'RAm6Vl#^wT'swmMNV(6'w?%%mP}{V12H9@1 +A+>3f f', ?&)*Ә: 4pQ ‹fR~-/ZΓH0xj٠t$sar=])ذASP3+ۇFGO'zdL.tgqer]]6*$LiӺھt[ݝOf%jOO p)dBpXx/ OEl|>2lxܻ~+eT.2Wo5x}[x~%5m3회 7B7cRƋ+C71*dKR#.@~0L1ˢ6c@1"1 +)v +0L,Ja%p> 3f@uwL]:+l.Œ>W}MHx@#K+LcK0bm N K~)dJ%&o>-5}7 +m՞a B)v8qo%\b+hgeeme58ώ +87u8uC+"Uׂ7lSc"9MuӢOBL[kKXkW2q&҅c!L#h=^@28Ǯ/!J"ٮ1ޣ(*5jTaZH,lrIy1^j3;6XZ$s:KxxCCFK &3?-oƀW:|D!Tr5>3fto v[Jˠ ͼXX6kՙS9(JM 95̍WYe = n;*MCMn +dA{}c(6t;ؑ12L"`ړ7\_mm*#65(1ERH8Վ [P*`QKC%ķa1AT\&087: w+$IJaQ5nlwݬ6 !l[##H4LҊf qy̖BRLP0CDym}ۋ\)Ȥ0F* /]$)4dvg30{"K 0O?ᆟ31 +R<̘y?w`?jj=Q :FlEF1uBu A5eC$-MKC蓵e90rςQ 593Y"ybo q}1v$sCAG,Y8YJtKgWyWtES }as)VBhī0HLؘyF2뺶Hܒυa6$:vdwEyD'%cMGl/*?u!ݒl=S=S- .IB&P,QR +N3b|*sG, +iLN#\^0x͊ѹiذ9{"p(?6/Z +HR ;ЩYJp4cfhquF~ቤ0A%[յ9d(7~lA.+5n:@lY~E  WW I@@|nv!2;LA9oHCV5{]+Ǽ""A V5N\~ASZ*aq;Y& V.2HO44_3f@Z'{|}t -!sֆ"mTeXrSуktÀ҂"OV8Ja@sR7U~//&4yzBrM2IL tţT۪]ʊ׊GSo<^(VV b`áxCHghv";|Λw/9#PB+l.߂IAה^jKTP볿RzҀ'_>2nhj-NbTn-p`(1 +ˮ5bwEZ]z'yNvF"dIOL+ۑQpZU"![ḐayrFT=9%O̤?3S:5Ӕ.2}%G`Ѡ'勺$%S.o*JL:TKZm&T*/#