From 53e24a25f5358f9410a98410ec47073124be2273 Mon Sep 17 00:00:00 2001 From: Preetham Kukkadapu Date: Fri, 21 Aug 2026 00:26:29 +0530 Subject: [PATCH] Add Preetham Kukkadapu: statutory financial statements builder and power-of-attorney generator --- .../preetham1930/poa-generator/INVARIANTS.md | 196 ++++ .../preetham1930/poa-generator/README.md | 266 ++++++ .../poa-generator/config/blank-form.csv | 12 + .../poa-generator/config/clause-library.csv | 21 + .../poa-generator/config/deferral-markers.txt | 10 + .../config/forbidden-phrases.csv | 20 + .../poa-generator/config/jurisdictions.csv | 4 + .../poa-generator/config/notices.csv | 3 + .../poa-generator/config/poa-types.csv | 4 + .../poa-generator/config/powers.csv | 17 + .../poa-generator/config/signature-rows.csv | 4 + .../poa-generator/config/signature-table.csv | 5 + .../preetham1930/poa-generator/conftest.py | 78 ++ .../evidence/poa-durable-financial.docx | Bin 0 -> 38740 bytes .../evidence/poa-healthcare.docx | Bin 0 -> 38755 bytes .../poa-generator/evidence/screenshot.png | Bin 0 -> 219728 bytes .../intakes/durable-financial.json | 28 + .../poa-generator/intakes/healthcare.json | 27 + .../intakes/limited-real-property.json | 27 + .../poa-generator/poa/__init__.py | 14 + .../poa-generator/poa/__main__.py | 146 +++ .../poa-generator/poa/assemble.py | 368 ++++++++ .../preetham1930/poa-generator/poa/chunks.py | 207 ++++ .../poa-generator/poa/editplan.py | 146 +++ .../preetham1930/poa-generator/poa/errors.py | 90 ++ .../preetham1930/poa-generator/poa/intake.py | 188 ++++ .../preetham1930/poa-generator/poa/library.py | 383 ++++++++ .../preetham1930/poa-generator/poa/notices.py | 161 ++++ .../poa-generator/poa/orchestrator.py | 297 ++++++ .../preetham1930/poa-generator/poa/report.py | 105 ++ .../preetham1930/poa-generator/poa/style.py | 48 + .../poa-generator/poa/superdocs/__init__.py | 8 + .../poa-generator/poa/superdocs/client.py | 329 +++++++ .../poa-generator/poa/superdocs/decisions.py | 123 +++ .../poa-generator/poa/superdocs/transport.py | 172 ++++ .../poa-generator/poa/superdocs/verifier.py | 180 ++++ .../scripts/verify_live_output.py | 197 ++++ .../poa-generator/tests/recorded.py | 38 + .../poa-generator/tests/simulator.py | 247 +++++ .../poa-generator/tests/test_assemble.py | 179 ++++ .../poa-generator/tests/test_client.py | 179 ++++ .../poa-generator/tests/test_editplan.py | 198 ++++ .../poa-generator/tests/test_intake.py | 149 +++ .../poa-generator/tests/test_library.py | 241 +++++ .../poa-generator/tests/test_notices.py | 239 +++++ .../poa-generator/tests/test_orchestrator.py | 277 ++++++ .../poa-generator/tests/test_two_drafts.py | 153 +++ .../poa-generator/tests/test_verifier.py | 101 ++ .../INVARIANTS.md | 142 +++ .../statutory-statements-builder/README.md | 179 ++++ .../config/absence-probes.csv | 2 + .../config/carry-forward-rules.csv | 7 + .../config/event-vocabulary.csv | 3 + .../config/new-note-titles.csv | 4 + .../config/note-recipes.csv | 7 + .../config/quarantine-markers.txt | 12 + .../statutory-statements-builder/conftest.py | 46 + .../evidence/screenshot.png | Bin 0 -> 123955 bytes .../evidence/statements.docx | Bin 0 -> 43928 bytes .../scripts/verify_live_output.py | 110 +++ .../statutory/__init__.py | 18 + .../statutory/__main__.py | 98 ++ .../statutory/bodies.py | 893 ++++++++++++++++++ .../statutory/checklist.py | 189 ++++ .../statutory/chunks.py | 183 ++++ .../statutory/csvspans.py | 101 ++ .../statutory/disagreement.py | 51 + .../statutory/editplan.py | 150 +++ .../statutory/errors.py | 76 ++ .../statutory/events.py | 170 ++++ .../statutory/figures.py | 375 ++++++++ .../statutory/ledger.py | 153 +++ .../statutory/money.py | 91 ++ .../statutory/notetree.py | 282 ++++++ .../statutory/orchestrator.py | 254 +++++ .../statutory/pipeline.py | 167 ++++ .../statutory/priorset.py | 335 +++++++ .../statutory/report.py | 149 +++ .../statutory/skeleton.py | 275 ++++++ .../statutory/statements.py | 432 +++++++++ .../statutory/style.py | 33 + .../statutory/superdocs/__init__.py | 5 + .../statutory/superdocs/client.py | 290 ++++++ .../statutory/superdocs/decisions.py | 123 +++ .../statutory/superdocs/transport.py | 172 ++++ .../statutory/superdocs/verifier.py | 180 ++++ .../tests/recorded.py | 38 + .../tests/simulator.py | 213 +++++ .../tests/test_bodies.py | 144 +++ .../tests/test_checklist.py | 159 ++++ .../tests/test_client.py | 156 +++ .../tests/test_edit_plan.py | 118 +++ .../tests/test_figures.py | 163 ++++ .../tests/test_money_and_chunks.py | 103 ++ .../tests/test_note_tree.py | 180 ++++ .../tests/test_orchestrator.py | 152 +++ .../tests/test_rollforward.py | 130 +++ .../tests/test_verifier.py | 101 ++ 98 files changed, 13299 insertions(+) create mode 100644 use-cases/preetham1930/poa-generator/INVARIANTS.md create mode 100644 use-cases/preetham1930/poa-generator/README.md create mode 100644 use-cases/preetham1930/poa-generator/config/blank-form.csv create mode 100644 use-cases/preetham1930/poa-generator/config/clause-library.csv create mode 100644 use-cases/preetham1930/poa-generator/config/deferral-markers.txt create mode 100644 use-cases/preetham1930/poa-generator/config/forbidden-phrases.csv create mode 100644 use-cases/preetham1930/poa-generator/config/jurisdictions.csv create mode 100644 use-cases/preetham1930/poa-generator/config/notices.csv create mode 100644 use-cases/preetham1930/poa-generator/config/poa-types.csv create mode 100644 use-cases/preetham1930/poa-generator/config/powers.csv create mode 100644 use-cases/preetham1930/poa-generator/config/signature-rows.csv create mode 100644 use-cases/preetham1930/poa-generator/config/signature-table.csv create mode 100644 use-cases/preetham1930/poa-generator/conftest.py create mode 100644 use-cases/preetham1930/poa-generator/evidence/poa-durable-financial.docx create mode 100644 use-cases/preetham1930/poa-generator/evidence/poa-healthcare.docx create mode 100644 use-cases/preetham1930/poa-generator/evidence/screenshot.png create mode 100644 use-cases/preetham1930/poa-generator/intakes/durable-financial.json create mode 100644 use-cases/preetham1930/poa-generator/intakes/healthcare.json create mode 100644 use-cases/preetham1930/poa-generator/intakes/limited-real-property.json create mode 100644 use-cases/preetham1930/poa-generator/poa/__init__.py create mode 100644 use-cases/preetham1930/poa-generator/poa/__main__.py create mode 100644 use-cases/preetham1930/poa-generator/poa/assemble.py create mode 100644 use-cases/preetham1930/poa-generator/poa/chunks.py create mode 100644 use-cases/preetham1930/poa-generator/poa/editplan.py create mode 100644 use-cases/preetham1930/poa-generator/poa/errors.py create mode 100644 use-cases/preetham1930/poa-generator/poa/intake.py create mode 100644 use-cases/preetham1930/poa-generator/poa/library.py create mode 100644 use-cases/preetham1930/poa-generator/poa/notices.py create mode 100644 use-cases/preetham1930/poa-generator/poa/orchestrator.py create mode 100644 use-cases/preetham1930/poa-generator/poa/report.py create mode 100644 use-cases/preetham1930/poa-generator/poa/style.py create mode 100644 use-cases/preetham1930/poa-generator/poa/superdocs/__init__.py create mode 100644 use-cases/preetham1930/poa-generator/poa/superdocs/client.py create mode 100644 use-cases/preetham1930/poa-generator/poa/superdocs/decisions.py create mode 100644 use-cases/preetham1930/poa-generator/poa/superdocs/transport.py create mode 100644 use-cases/preetham1930/poa-generator/poa/superdocs/verifier.py create mode 100644 use-cases/preetham1930/poa-generator/scripts/verify_live_output.py create mode 100644 use-cases/preetham1930/poa-generator/tests/recorded.py create mode 100644 use-cases/preetham1930/poa-generator/tests/simulator.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_assemble.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_client.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_editplan.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_intake.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_library.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_notices.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_orchestrator.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_two_drafts.py create mode 100644 use-cases/preetham1930/poa-generator/tests/test_verifier.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/INVARIANTS.md create mode 100644 use-cases/preetham1930/statutory-statements-builder/README.md create mode 100644 use-cases/preetham1930/statutory-statements-builder/config/absence-probes.csv create mode 100644 use-cases/preetham1930/statutory-statements-builder/config/carry-forward-rules.csv create mode 100644 use-cases/preetham1930/statutory-statements-builder/config/event-vocabulary.csv create mode 100644 use-cases/preetham1930/statutory-statements-builder/config/new-note-titles.csv create mode 100644 use-cases/preetham1930/statutory-statements-builder/config/note-recipes.csv create mode 100644 use-cases/preetham1930/statutory-statements-builder/config/quarantine-markers.txt create mode 100644 use-cases/preetham1930/statutory-statements-builder/conftest.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/evidence/screenshot.png create mode 100644 use-cases/preetham1930/statutory-statements-builder/evidence/statements.docx create mode 100644 use-cases/preetham1930/statutory-statements-builder/scripts/verify_live_output.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/__init__.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/__main__.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/bodies.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/checklist.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/chunks.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/csvspans.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/disagreement.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/editplan.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/errors.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/events.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/figures.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/ledger.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/money.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/notetree.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/orchestrator.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/pipeline.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/priorset.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/report.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/skeleton.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/statements.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/style.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/__init__.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/client.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/decisions.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/transport.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/verifier.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/recorded.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/simulator.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_bodies.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_checklist.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_client.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_edit_plan.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_figures.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_money_and_chunks.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_note_tree.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_orchestrator.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_rollforward.py create mode 100644 use-cases/preetham1930/statutory-statements-builder/tests/test_verifier.py diff --git a/use-cases/preetham1930/poa-generator/INVARIANTS.md b/use-cases/preetham1930/poa-generator/INVARIANTS.md new file mode 100644 index 000000000..635240ea7 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/INVARIANTS.md @@ -0,0 +1,196 @@ +# INVARIANTS — `builds/poa-generator/` + +Written **before** the tests, and the tests before the code (TASK.md hard rule +7). Every invariant names the test that proves it. An invariant with no test is +an intention, not an invariant. + +This is legal-adjacent output and **we are not lawyers.** That sentence is the +whole reason this file is longer than the last one. + +The thesis this app has to make true: + +> **Every clause in the draft was selected by the intake from a data library, no +> clause text is ever authored by a model, and the document is not exportable +> until the counsel-review notice and the external-execution notice have been +> read back out of the live document verbatim.** + +--- + +## Section A — what this app must never say (the legal-adjacency list) + +This section comes first because it is the one that matters if everything else +works. A power of attorney that is well engineered and quietly claims to be +executed is worse than one that fails loudly. + +A1. **Never present the draft as legal advice.** Every draft carries the +counsel-review notice, verbatim, from the moment it is assembled — it is in the +uploaded skeleton, not added later — and no code path can remove it. +*Proved by* `test_notices.py::test_every_assembled_draft_carries_both_notices_verbatim`. + +A2. **Never claim the document is executed, signed, witnessed, notarised, +valid, binding or in force.** A phrase list is scanned over the rendered +document and any hit is a hard error naming the phrase and the clause. The list +is data (`config/forbidden-phrases.csv`), so adding a phrase is a data edit. +*Proved by* `test_notices.py::test_a_forbidden_phrase_anywhere_in_the_draft_is_a_hard_error` +and `test_the_forbidden_phrase_list_is_data_not_code`. + +A3. **Never imply notarisation happened.** The external-execution notice states +in as many words that signing, witnessing and notarisation happen outside this +product, and the signature block is emitted with every signature and date cell +empty. A signature cell that is not empty is a hard error. +*Proved by* `test_notices.py::test_the_signature_block_leaves_every_signature_and_date_blank`. + +A4. **Never state a jurisdiction's execution requirements as fact.** The +jurisdiction library carries language that defers to counsel; it does not assert +how many witnesses a jurisdiction requires. A jurisdiction row whose text +asserts a requirement without deferring is refused at load time. +*Proved by* `test_library.py::test_a_jurisdiction_row_that_asserts_a_requirement_is_refused`. + +A5. **Never let a model author clause text.** SuperDocs is never asked to draft, +create, add, rewrite or improve a clause. Every instruction is a replacement +whose complete post-state we computed from the library before sending. Measured +on 2026-08-09: asked for one section, the product produced six, four of them +invented in confident, domain-appropriate prose. In a power of attorney an +invented clause is a power nobody granted. +*Proved by* `test_editplan.py::test_no_step_uses_a_creating_verb` and +`test_editplan.py::test_every_step_sends_a_post_state_computed_from_the_library`. + +A6. **Never grant a power the intake did not select.** The powers that reach the +document are exactly the ids in the intake, resolved against +`config/powers.csv`. An id the catalogue does not have is a hard error naming +the catalogue; it is never dropped and never approximated. +*Proved by* `test_intake.py::test_an_unknown_power_id_is_a_hard_error` and +`test_two_drafts.py::test_neither_draft_carries_the_other_s_powers`. + +--- + +## Section B — the export gate (hard constraint 1) + +B1. **The document is not exportable until read-back confirms both notices are +present verbatim.** The gate is structural, not a check somebody remembered to +call: `export()` requires a `NoticeReceipt`, and a `NoticeReceipt` can only be +constructed by `check_notices()` against a live read-back. There is no other +constructor and no default argument. +*Proved by* `test_notices.py::test_export_cannot_be_called_without_a_receipt` +and `test_a_receipt_cannot_be_forged`. + +B2. **A missing notice refuses the export.** Removing either notice chunk from +the document leaves the export path raising, with no file produced. +*Proved by* `test_notices.py::test_export_refuses_when_a_notice_is_missing`. + +B3. **An altered notice refuses the export.** One word changed, one word +dropped, or the notice paraphrased is the same failure as removing it. This is +the case that matters: a rewrite plausibly paraphrases. +*Proved by* `test_notices.py::test_export_refuses_when_a_notice_is_altered`. + +B4. **No notice block is ever an edit target.** Notices are structure, uploaded +verbatim, and the plan is refused before anything is sent if a step names one. +*Proved by* `test_editplan.py::test_no_step_targets_a_notice_block`. + +--- + +## Section C — structure is ours + +C1. **Never ask SuperDocs to create.** One verb, and it is `replace` +(Decision 28). +*Proved by* `test_editplan.py::test_no_step_uses_a_creating_verb`. + +C2. **Never issue a mid-document insertion.** The entire final document +structure — which clauses exist for this intake, in what order, at what +numbers, with every cross-reference already pointing at the right number — is +computed here and uploaded verbatim (Decision 29). +*Proved by* `test_editplan.py::test_no_step_targets_a_heading_block`. + +C3. **Never let a clause number be inferred, remembered or asked for.** Numbers +come from one pass over our own ordered clause list, and the two required drafts +number differently because the healthcare draft has a clause the financial one +does not. +*Proved by* `test_assemble.py::test_clause_numbers_come_from_the_selected_set`. + +C4. **Never leave a cross-reference pointing at the wrong clause.** Every +`clause N` reference is resolved through the same numbering pass, and the +assembled document is re-scanned for a reference that does not resolve. +*Proved by* `test_assemble.py::test_every_cross_reference_resolves_in_both_drafts`. + +C5. **A cross-reference is structure, so it is identical in the skeleton and in +the target.** If a reference differed between the two, an edit would be +renumbering, and renumbering is the thing this design exists not to delegate. +*Proved by* `test_editplan.py::test_no_step_changes_a_clause_reference`. + +--- + +## Section D — the library is data + +D1. **Adding a power type, a clause, or a jurisdiction is a data edit.** No +clause text, no power label, no jurisdiction name and no notice text appears as +a string literal in any `.py` file in this package. +*Proved by* `test_library.py::test_no_clause_text_is_hardcoded_anywhere_in_the_package`. + +D2. **A clause condition that cannot be parsed stops the run; it never reads as +"this clause does not apply".** A silent `False` drops a clause from a power of +attorney, which is the failure mode with no symptom. +*Proved by* `test_library.py::test_an_unparseable_condition_is_a_hard_error`. + +D3. **A placeholder with no resolver stops the run.** It is never left in the +document and never silently emptied. +*Proved by* `test_library.py::test_an_unresolved_placeholder_is_a_hard_error`. + +--- + +## Section E — the wire, copied from Phase 4 as an idea and rebuilt + +E1. **Never batch, and never approve a change we did not compute** (Decision 35, +and Decision 41 for what comes back). +*Proved by* `test_client.py::test_a_batch_is_split_into_ours_and_everything_else`. + +E2. **Never use the synchronous chat route for a gated change** (Decision 32). +*Proved by* `test_client.py::test_gated_changes_go_to_the_async_route`. + +E3. **Never send feedback on a denial** (Decision 34). +*Proved by* `test_client.py::test_denials_carry_no_feedback`. + +E4. **Never treat the job as the decision record** (Decision 33). +*Proved by* `test_orchestrator.py::test_the_decision_row_is_written_before_the_actuator_is_called`. + +E5. **Never believe the chat reply, the `changes_summary`, or a 200.** The only +evidence is `GET /v1/documents/{id}?include_html=true`. +*Proved by* `test_verifier.py::test_a_reply_claiming_success_does_not_make_a_step_ok`. + +--- + +## Section F — the failure path + +F1. **Never report an edit applied without reading the whole document back.** +Three classes, all detected: **not applied**, **applied wrong**, **collateral +damage**. +*Proved by* `test_verifier.py`, one test per class, each driven by a recorded +response from `docs/evidence/`. + +F2. **Never run a downstream step after a failed one.** The queue halts. +*Proved by* `test_orchestrator.py::test_a_failure_halts_the_queue_and_downstream_steps_never_run`. + +F3. **Never export after a failure** — which here means two independent +refusals, the halt and the notice gate. +*Proved by* `test_orchestrator.py::test_no_export_after_a_failure`. + +F4. **Never re-plan.** At most one narrow retry of the same instruction, and a +retry diffs the whole document. +*Proved by* `test_orchestrator.py::test_at_most_one_narrow_retry_and_never_a_replan`. + +--- + +## Section G — the boundary + +G1. **Never import from `system/`, and never speak its vocabulary.** This tree +answers *what must the document say, and did SuperDocs actually say it*. +*Proved by* `tests/test_isolation.py` and `tests/test_domain_boundary.py`. + +--- + +## What it may do, stated so the boundary is not accidentally wider + +- Say what a clause library row says, because a human wrote that row and it is + under version control in this repository. +- Say that a draft is incomplete — a limited power of attorney with no stated + limitation is refused rather than drafted around. +- Leave every signature, witness and notarisation field empty and say why. diff --git a/use-cases/preetham1930/poa-generator/README.md b/use-cases/preetham1930/poa-generator/README.md new file mode 100644 index 000000000..0113c2814 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/README.md @@ -0,0 +1,266 @@ +# Power-of-attorney generator + +> **What strong looks like.** *Run the intake for a durable financial POA and a +> separate healthcare POA and confirm the two drafts differ correctly in granted +> powers, and both carry the review notice and the external-notarisation +> statement.* + +That is the grading line, and [`scripts/verify_live_output.py`](scripts/verify_live_output.py) +is where it is checked — against the two **live** documents read back from the +API and against the two exported DOCX files, never against the run's own report. + +--- + +## Two sentences on what this is + +People assembling a power of attorney need the powers-granted language and the +general/limited/durable distinction to be right, and then they still have to get +the thing witnessed and notarised in person, which no product touches. This is a +guided intake that asks who the principal, agent and successor agent are, which +POA type applies and which specific powers are granted, assembles a clean draft +from a **data** clause library, and exports it — and it refuses to export at all +until it has read back, out of the live document, that both required notices are +present word for word. + +**We are not lawyers.** [INVARIANTS.md](INVARIANTS.md) opens with the list of +things this system must never say, and that list was written before any code. + +--- + +## Run it + +```bash +make poa +``` + +Assembles both required drafts, prints the intake, the clause numbering, the +powers granted and withheld, the notice check and the edit plan, and writes both +renderings of each document to `var/poa/`. No API key, no network. + +```bash +make poa-live +``` + +The same, driving SuperDocs. Six billable operations per draft. Reads +`SUPERDOCS_API_KEY` from `.env` only and never prints it. + +```bash +python builds/poa-generator/scripts/verify_live_output.py +``` + +The grading line, re-checked from scratch. + +--- + +## The one thing that is only possible because SuperDocs exists + +A typeset power of attorney that survives six surgical edits with its layout +intact, is held at a per-item human gate on the way in, and comes out as a DOCX +with real Word table structure. Take SuperDocs out and what is left is a +string-templating script that emits HTML — the typeset rendering, the per-clause +targeted edits, the review gate and the export are all the product's. + +Measured on the live run: 1 `w:tbl`, 4 rows, 16 cells, 16 bordered cells in each +exported DOCX, and both notices verbatim inside `word/document.xml`. + +--- + +## How the parts divide, and why + +| Who | Does what | +| --- | --- | +| The intake (ours) | Who the parties are, which POA type, which power ids | +| The library (data) | Every clause, power, POA type, jurisdiction, notice | +| The assembler (ours) | Which clauses apply, their numbers, every cross-reference, both renderings | +| SuperDocs | Holds the document, applies one chunk replacement at a time, gates each at a human approval, renders and exports | +| The verifier (ours) | Reads the whole document back after every edit and classifies what happened | +| The notice gate (ours) | Refuses the export unless both notices come back verbatim | + +**We never ask SuperDocs to write a clause.** On 2026-08-09 we asked it for one +section and it produced six, four of them invented in confident, +domain-appropriate prose. In a power of attorney an invented clause is a power +nobody granted. So the powers are chosen in the intake and rendered by us, and a +fabricated power has no channel to arrive through — the defence is the absence +of a path, not a check on the output. + +### The blank form is what gets uploaded + +We compute the **entire** document for this intake — every clause that applies, +at its final number, with every cross-reference already pointing at the right +one, and both notices already in place — with the party names, the specific +powers, the limitation and the commencement left as blank-form markers. That is +uploaded verbatim (Decision 29). SuperDocs then only ever replaces the contents +of a chunk that already exists, which is the one operation measured to be +reliable. There is no insertion to perform, so the insertion bug cannot be +reached; there is no renumbering to delegate, so the renumber-around-a-missing- +insert failure cannot occur. + +### Structure really is computed, and the two drafts prove it + +The healthcare draft selects a clause the financial one does not +(`healthcare-records-access`, chosen by the condition +`category granted healthcare`), so every clause after it is numbered one higher: + +``` +durable-financial ... 5. Powers granted - financial 6. Successor agent 7. Reliance ... +healthcare ... 5. Powers granted - healthcare 6. Access to health information + 7. Successor agent 8. Reliance ... +``` + +and the reliance clause's cross-reference moves with it — clause 6 in one draft, +clause 7 in the other. `test_the_shared_parts_really_are_shared` asserts the two +clauses are identical apart from exactly that digit. + +--- + +## The clause library is data + +Adding a jurisdiction or a power type is a **data edit**, and there is a test +that proves it by adding a row to a copy of `config/` and requiring the +behaviour to change with zero code edits. + +| File | What it holds | +| --- | --- | +| `clause-library.csv` | Every clause: order, section, heading, tag, style, condition, body | +| `powers.csv` | The power catalogue, by category, with the granted-powers language | +| `poa-types.csv` | general / limited / durable, and whether a limitation is required | +| `jurisdictions.csv` | Governing-law and execution-requirement language | +| `notices.csv` | The two required notices, verbatim | +| `forbidden-phrases.csv` | What the draft may never say | +| `deferral-markers.txt` | What a jurisdiction row must contain to be accepted | +| `blank-form.csv` | What each gap says before the intake fills it | +| `signature-rows.csv`, `signature-table.csv` | The signature block | + +`test_no_clause_text_is_hardcoded_anywhere_in_the_package` greps every `.py` in +`poa/` for every value in those files and fails on a hit. + +The clause condition grammar is small and every part of it is evaluated, never +looked up: + +``` +condition := "always" + | "type is" + | "type in" | + | "category granted" +``` + +An unparseable condition **raises**. It is never read as "this clause does not +apply" — a clause silently dropped out of a power of attorney has no symptom in +the finished document. + +--- + +## The export gate + +Hard constraint of this build, and it is structural rather than remembered: + +- `PoaClient.export` requires a `NoticeReceipt`. There is no default argument. +- A `NoticeReceipt` can only be constructed by `check_notices()`, which takes a + `ChunkMap` from a live read-back. Construct one directly and it raises. +- `check_notices()` makes two independent comparisons: the notice appears in the + document's reader-visible text, **and** some single chunk's text *is* the + notice, exactly. The first alone would pass a notice with a sentence inserted + into the middle of it; the second alone would miss a second, contradicting + copy. +- "Verbatim" is at the level of words, not bytes: whitespace and entity + re-serialisation are forgiven, because a document editor is entitled to do + both. One word changed, dropped or reordered is not. + +The gate runs twice — once on the upload, before a single edit is issued, and +once on the final read-back, before anything is exported. Tests prove the +refusal for a **missing** notice, an **altered** notice, a **single word** +dropped, a notice **split across two chunks**, and a **sentence inserted** into +one. + +The case that makes the gate worth having is the one the edit queue cannot +catch: every step verified applied, and the product reworded a notice on the way +past. From the queue's point of view that is a clean run. It is not one. +`test_a_reworded_notice_that_the_collateral_check_cannot_see_still_refuses_the_export` +drives exactly that: 6 of 6 applied, nothing halted, and no export. + +--- + +## The failure path, exercised live + +Every edit is: compute the expected post-state of the whole document → one +instruction, one chunk, on the async route → poll to the human gate → refuse +anything that is not exactly our change → write **our** decision row with a named +actor → approve → poll to completed → read the whole document back → classify. + +Three classes, all detected: **not applied**, **applied wrong**, **collateral +damage**. On any of them the queue halts, no downstream step runs, the session is +reverted, the revert is read back and reported either way, and **no export is +produced**. + +This is not only a test. On the live demo run of 2026-08-10 the financial draft +halted at step 1: the product returned the paragraph wrapped in a `
` nobody +asked for, twice — the first attempt and its one narrow retry. The run reported +`APPLIED WRONG` with the diff, refused to run steps 2 through 6, attempted the +revert, and reported honestly that the read-back did **not** match the last +verified-good state. No file was written. The same draft ran clean on the next +attempt, which is the shape of this product: the failure is intermittent, and +that is precisely why nothing is believed without a read-back. + +--- + +## Tests + +459 tests pass repo-wide with **no `.env` present and no network**, 113 of them +this app's. The SuperDocs failure classes are driven by recorded responses under +`docs/evidence/`, so they are the real shapes of failures the product really +produced. + +Every guard added here was verified by **deliberately violating it** and watching +it go red — `make probe`, 101 of 101, 24 of them new. + +```bash +make test # ruff + corpus oracles + pytest, no key required +make probe # break each guard on purpose and require it to notice +``` + +--- + +## What this build does not do + +- It is not a UI. The intake is a JSON file, which is what the tests and the demo + both drive — there is no second, easier path into the assembler. +- It does not revise a draft after the fact. The per-clause targeted revision + loop the plan mentioned ("change the successor agent's name") is the same + machinery pointed at an existing document; it is not built, and it is named + here rather than implied. +- It does not say what any jurisdiction requires. Jurisdiction rows must defer to + counsel, and a row that asserts a requirement is refused at load time. +- It never tells you the document is valid, executed or binding, because it is + none of those things and this product cannot make it so. + + +--- + +## SuperDocs features used + +- **Upload** (POST /v1/documents/upload-base64) - the fully assembled draft is uploaded verbatim; SuperDocs is never asked to create a clause. +- **Chat, async** (POST /v1/chat/async) - one targeted replace per request. +- **Review gate** (approval_mode: ask_every_time + POST /v1/chat/approve) - every change is gated. +- **Read-back** (GET /v1/documents/{id}?include_html=true) - after every edit the whole document is read back and compared to the post-state computed before sending. +- **Export** (POST /v1/documents/export) - gated: the .docx cannot be produced until read-back confirms both required notices are present verbatim. + +## Environment + +``` +SUPERDOCS_API_KEY=your-key-here +SUPERDOCS_BASE_URL=https://api.superdocs.app +``` + +Tests run without a key against recorded fixtures. Only the live demo needs one. + +## Output + +![Two drafts from one clause library, differing correctly in granted powers](evidence/screenshot.png) + +Two live drafts are committed: [evidence/poa-durable-financial.docx](evidence/poa-durable-financial.docx) and [evidence/poa-healthcare.docx](evidence/poa-healthcare.docx). The granted powers differ correctly with no leakage either way; the healthcare draft carries an extra clause, so the successor-agent clause is 6 in one and 7 in the other, and the cross-reference that points at it moves with it. Both carry the counsel-review notice and the external-notarisation statement. + +This is a drafting aid, not legal advice. Signing, witnessing and notarisation happen outside the product. + +--- + +Built by **Preetham Kukkadapu** for the SuperDocs round 2 task. diff --git a/use-cases/preetham1930/poa-generator/config/blank-form.csv b/use-cases/preetham1930/poa-generator/config/blank-form.csv new file mode 100644 index 000000000..8dd4850ef --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/blank-form.csv @@ -0,0 +1,12 @@ +placeholder,blank_text +principal_name,[ PRINCIPAL - FULL LEGAL NAME ] +principal_address,[ PRINCIPAL - ADDRESS ] +principal_id,[ PRINCIPAL - IDENTIFYING DETAIL ] +agent_name,[ AGENT - FULL LEGAL NAME ] +agent_address,[ AGENT - ADDRESS ] +successor_name,[ SUCCESSOR AGENT - FULL LEGAL NAME ] +successor_address,[ SUCCESSOR AGENT - ADDRESS ] +limitation,[ THE MATTERS THIS POWER IS LIMITED TO ] +effective_date,[ WHEN THE AUTHORITY COMMENCES ] +real_property_description,[ THE REAL PROPERTY THIS POWER APPLIES TO ] +powers,[ POWERS AS SELECTED IN THE INTAKE ] diff --git a/use-cases/preetham1930/poa-generator/config/clause-library.csv b/use-cases/preetham1930/poa-generator/config/clause-library.csv new file mode 100644 index 000000000..12445c308 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/clause-library.csv @@ -0,0 +1,21 @@ +clause_id,sort_order,section,heading,tag,style_role,applies_when,body +title,10,front,,h1,title,always,{poa_type_label} +counsel-review-notice,20,front,,p,notice,always,{notice:counsel-review} +preamble-principal,30,parties,Principal,p,body,always,"This power of attorney is granted by {principal_name} of {principal_address}, {principal_id} (the Principal)." +appointment-agent,40,parties,Appointment of agent,p,body,always,The Principal appoints {agent_name} of {agent_address} as the Principal's agent (the Agent) under this instrument. +nature,50,parties,Nature of this power of attorney,p,body,always,{durability_language} +limitation,55,parties,Limits on this power,p,body,type is limited,"This power of attorney is limited to the following matters and confers no authority beyond them: {limitation}" +commencement,60,parties,Commencement,p,body,always,"The authority conferred by this instrument commences {effective_date}. It continues until the Principal revokes it in writing, or until it otherwise ends by operation of law." +powers-financial,70,powers,Powers granted - financial matters,p,body,category granted financial,The Principal grants the Agent authority over the financial matters listed below and over no other financial matter. +powers-financial-list,71,powers,,ul,list,category granted financial,{powers:financial} +powers-healthcare,80,powers,Powers granted - healthcare matters,p,body,category granted healthcare,"The Principal grants the Agent authority over the healthcare matters listed below and over no other healthcare matter, and the Agent must act in accordance with the Principal's known wishes." +powers-healthcare-list,81,powers,,ul,list,category granted healthcare,{powers:healthcare} +healthcare-records-access,82,powers,Access to health information,p,body,category granted healthcare,"The Agent may see and receive the Principal's health information only so far as it is needed to exercise the powers granted under clause {clause_ref:powers-healthcare}, and for no other purpose." +powers-real-property,90,powers,Powers granted - real property,p,body,category granted real_property,The Principal grants the Agent authority over the real property described below and over no other property. +powers-real-property-list,91,powers,,ul,list,category granted real_property,{powers:real_property} +real-property-description,92,powers,,p,body,category granted real_property,"The real property to which clause {clause_ref:powers-real-property} applies is: {real_property_description}" +successor,100,successor,Successor agent,p,body,always,"If the Agent dies, resigns, becomes unable to act or is unwilling to act, {successor_name} of {successor_address} shall act as successor agent with the same authority, and subject to the same limits, as this instrument confers on the Agent." +reliance,110,closing,Reliance by third parties,p,body,always,"A third party who is shown this instrument may rely on it until that third party has actual notice that it has been revoked. A successor agent appointed under clause {clause_ref:successor} is subject to the same condition." +governing-law,120,closing,Governing law and execution requirements,p,body,always,"This instrument is drawn for {jurisdiction_label}. {governing_law} {witness_requirement} {notarisation_requirement}" +external-execution-notice,130,closing,,p,notice,always,{notice:external-execution} +signature-block,140,closing,Signatures - to be completed outside this product,table,signature,always,{signature_table} diff --git a/use-cases/preetham1930/poa-generator/config/deferral-markers.txt b/use-cases/preetham1930/poa-generator/config/deferral-markers.txt new file mode 100644 index 000000000..9474e9b75 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/deferral-markers.txt @@ -0,0 +1,10 @@ +# A jurisdiction row states execution requirements. This app is not qualified to +# state them as fact (INVARIANTS.md A4), so every witness_requirement and every +# notarisation_requirement must contain one of the markers below. A row that +# asserts a requirement without deferring is refused at load time, not softened +# at render time. +# +# One marker per line. Comparison is case-insensitive. Adding a marker is a data +# edit; this file is the list, not a constant in a .py. +qualified counsel +a qualified lawyer diff --git a/use-cases/preetham1930/poa-generator/config/forbidden-phrases.csv b/use-cases/preetham1930/poa-generator/config/forbidden-phrases.csv new file mode 100644 index 000000000..4cac2aac6 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/forbidden-phrases.csv @@ -0,0 +1,20 @@ +phrase,why +duly executed,claims the instrument has been executed +duly signed,claims a signature that has not happened +duly notarised,claims a notarisation that has not happened +duly notarized,claims a notarisation that has not happened +has been notarised,claims a notarisation that has not happened +has been notarized,claims a notarisation that has not happened +was notarised,claims a notarisation that has not happened +was notarized,claims a notarisation that has not happened +before me this day,notarial jurat language; implies an officer took an acknowledgement +sworn before,notarial language; implies an oath was administered +legally binding,asserts legal effect this product cannot know +valid and enforceable,asserts legal effect this product cannot know +in full force and effect,asserts the instrument is already operative +i hereby certify,certification this product has no standing to give +we certify,certification this product has no standing to give +this is legal advice,the one sentence the counsel-review notice exists to deny +we advise,gives advice rather than assembling a draft +our legal opinion,gives an opinion rather than assembling a draft +you do not need a lawyer,discourages the review the whole draft is predicated on diff --git a/use-cases/preetham1930/poa-generator/config/jurisdictions.csv b/use-cases/preetham1930/poa-generator/config/jurisdictions.csv new file mode 100644 index 000000000..b60dbb7b1 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/jurisdictions.csv @@ -0,0 +1,4 @@ +jurisdiction_id,label,governing_law,witness_requirement,notarisation_requirement +in-ts,"Telangana, India","The Principal states that the law of Telangana, India is intended to govern this instrument; whether that choice is effective is a question for qualified counsel.","The number and eligibility of witnesses required for this instrument must be confirmed with qualified counsel before anything is signed.","Whether this instrument must be notarised, registered, or both, and before which officer, must be confirmed with qualified counsel before anything is signed." +in-mh,"Maharashtra, India","The Principal states that the law of Maharashtra, India is intended to govern this instrument; whether that choice is effective is a question for qualified counsel.","The number and eligibility of witnesses required for this instrument must be confirmed with qualified counsel before anything is signed.","Whether this instrument must be notarised, registered, or both, and before which officer, must be confirmed with qualified counsel before anything is signed." +uk-ew,"England and Wales","The Principal states that the law of England and Wales is intended to govern this instrument; whether that choice is effective is a question for qualified counsel.","The number and eligibility of witnesses required for this instrument must be confirmed with qualified counsel before anything is signed.","Whether this instrument must be notarised, registered with a supervising body, or both, must be confirmed with qualified counsel before anything is signed." diff --git a/use-cases/preetham1930/poa-generator/config/notices.csv b/use-cases/preetham1930/poa-generator/config/notices.csv new file mode 100644 index 000000000..68f54840f --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/notices.csv @@ -0,0 +1,3 @@ +notice_id,label,text +counsel-review,Counsel review notice,"This document is a draft assembled from a clause library for review by qualified counsel. It is not legal advice and it creates no lawyer-client relationship. Nothing in it has been reviewed by a lawyer." +external-execution,External execution notice,"This draft is unexecuted. Signing, witnessing and notarisation happen outside this product and in person, under the law that governs the Principal. No part of this document has been signed, witnessed or notarised, and this product cannot make it so." diff --git a/use-cases/preetham1930/poa-generator/config/poa-types.csv b/use-cases/preetham1930/poa-generator/config/poa-types.csv new file mode 100644 index 000000000..0be3cc058 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/poa-types.csv @@ -0,0 +1,4 @@ +type_id,label,requires_limitation,durability_language +general,General power of attorney,no,"This is a general power of attorney. It confers on the Agent the powers set out below in respect of the Principal's affairs generally, and it does not continue if the Principal later becomes unable to make decisions. Whether that is the effect in the governing jurisdiction is a question for qualified counsel." +limited,Limited power of attorney,yes,"This is a limited power of attorney. It confers on the Agent only the powers set out below and only for the matters stated in the clause headed Limits on this power, and it does not continue if the Principal later becomes unable to make decisions. Whether that is the effect in the governing jurisdiction is a question for qualified counsel." +durable,Durable power of attorney,no,"This is a durable power of attorney. It is intended to continue in effect if the Principal later becomes unable to make decisions, and it is intended not to be affected by that inability. Whether a jurisdiction gives that intention effect, and what wording it requires, is a question for qualified counsel." diff --git a/use-cases/preetham1930/poa-generator/config/powers.csv b/use-cases/preetham1930/poa-generator/config/powers.csv new file mode 100644 index 000000000..00d42132b --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/powers.csv @@ -0,0 +1,17 @@ +power_id,category,label,language +fin-banking,financial,Deposit accounts,"The Agent may open, operate and close deposit accounts held in the name of the Principal, and may draw, endorse and deposit cheques and other instruments payable to the Principal." +fin-investments,financial,Investments,"The Agent may hold, buy and sell securities, mutual fund units and other investments held in the name of the Principal, and may give instructions in respect of them to any depository participant or registrar." +fin-tax,financial,Tax matters,"The Agent may sign, verify and file returns and other documents with the tax authorities on behalf of the Principal, and may represent the Principal before them." +fin-bills,financial,Recurring payments,"The Agent may receive, examine and pay bills, premiums, rents, rates and taxes payable by the Principal, out of the Principal's funds." +fin-borrowing,financial,Borrowing,The Agent may borrow money on behalf of the Principal and give security over the Principal's assets for that borrowing. +fin-business,financial,Business interests,"The Agent may exercise the Principal's rights as a shareholder, partner or member of a company, firm or association, including giving proxies and signing resolutions." +health-treatment,healthcare,Consent to treatment,"The Agent may give, withhold or withdraw consent to medical and surgical treatment on behalf of the Principal, in accordance with the Principal's known wishes." +health-records,healthcare,Health information,"The Agent may request, receive and review the Principal's medical records and health information, and may authorise their disclosure where that is needed to exercise the powers granted by this instrument." +health-providers,healthcare,Choice of providers,"The Agent may select, engage and discharge physicians, hospitals, nursing facilities and other providers of care for the Principal, and may agree the terms on which care is provided." +health-admission,healthcare,Admission and discharge,"The Agent may admit the Principal to, and arrange the discharge of the Principal from, a hospital, nursing facility or residential care facility." +health-endoflife,healthcare,End-of-life care,The Agent may make decisions about life-sustaining treatment and palliative care for the Principal in accordance with the Principal's known wishes. +rp-sale,real_property,Sale and conveyance,"The Agent may sell, convey and transfer the real property described in this instrument, and may sign every deed, receipt and instrument of transfer that the sale requires." +rp-lease,real_property,Letting and leasing,"The Agent may let the real property described in this instrument on such terms as the Agent thinks fit, may collect the rent and may give receipts for it." +rp-mortgage,real_property,Mortgage and charge,The Agent may mortgage or charge the real property described in this instrument as security. +rp-management,real_property,Management and repair,"The Agent may manage, insure, repair and maintain the real property described in this instrument, and may engage and pay contractors for that purpose." +rp-registration,real_property,Registration,"The Agent may present the real property described in this instrument for registration, may admit execution before the registering officer and may do everything the registration requires." diff --git a/use-cases/preetham1930/poa-generator/config/signature-rows.csv b/use-cases/preetham1930/poa-generator/config/signature-rows.csv new file mode 100644 index 000000000..f8da2f064 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/signature-rows.csv @@ -0,0 +1,4 @@ +role_key,role_label,name_placeholder +principal,Principal,{principal_name} +agent,Agent,{agent_name} +successor,Successor agent,{successor_name} diff --git a/use-cases/preetham1930/poa-generator/config/signature-table.csv b/use-cases/preetham1930/poa-generator/config/signature-table.csv new file mode 100644 index 000000000..b27e44c46 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/config/signature-table.csv @@ -0,0 +1,5 @@ +column_key,column_label,fill +role,Role,label +name,Name,placeholder +signature,Signature,blank +date,Date,blank diff --git a/use-cases/preetham1930/poa-generator/conftest.py b/use-cases/preetham1930/poa-generator/conftest.py new file mode 100644 index 000000000..11e638bbe --- /dev/null +++ b/use-cases/preetham1930/poa-generator/conftest.py @@ -0,0 +1,78 @@ +"""`builds/poa-generator/` is a codebase, not an installed package. + +The directory name carries a hyphen on purpose - it is an application folder, +not an import path - so the importable package inside it is `poa` and this file +is what puts it on `sys.path` for the suite. Nothing here imports from +`system/`, and `tests/test_isolation.py` fails the build if it ever does. +""" + +from __future__ import annotations + +import sys +from copy import deepcopy +from pathlib import Path + +import pytest + +APP_ROOT = Path(__file__).resolve().parent +REPO_ROOT = APP_ROOT.parent.parent + +if str(APP_ROOT) not in sys.path: + sys.path.insert(0, str(APP_ROOT)) +# tests/ carries helpers (`recorded.py`, `simulator.py`). They are not a +# package, so their directory goes on the path too. +if str(APP_ROOT / "tests") not in sys.path: + sys.path.insert(0, str(APP_ROOT / "tests")) + + +@pytest.fixture(scope="session") +def config() -> Path: + return APP_ROOT / "config" + + +@pytest.fixture(scope="session") +def intakes() -> Path: + return APP_ROOT / "intakes" + + +@pytest.fixture(scope="session") +def evidence() -> Path: + return REPO_ROOT / "docs" / "evidence" + + +@pytest.fixture(scope="session") +def library(config: Path): + from poa.library import Library + + return Library(config) + + +# Assembled once, handed out as a copy. A `Draft` is mutable, several tests +# doctor one on purpose, and a session-scoped fixture handed out by reference +# means a test that forgets to restore it breaks a different test in a different +# file - which is exactly what happened while these were being written, and it +# took a full-suite run to see because each file passed on its own. +@pytest.fixture(scope="session") +def _assembled(library, intakes: Path) -> dict: + from poa.assemble import assemble + from poa.intake import load_intake + + return { + name: assemble(load_intake(intakes / f"{name}.json", library), library) + for name in ("durable-financial", "healthcare", "limited-real-property") + } + + +@pytest.fixture +def financial(_assembled): + return deepcopy(_assembled["durable-financial"]) + + +@pytest.fixture +def healthcare(_assembled): + return deepcopy(_assembled["healthcare"]) + + +@pytest.fixture +def limited(_assembled): + return deepcopy(_assembled["limited-real-property"]) diff --git a/use-cases/preetham1930/poa-generator/evidence/poa-durable-financial.docx b/use-cases/preetham1930/poa-generator/evidence/poa-durable-financial.docx new file mode 100644 index 0000000000000000000000000000000000000000..da56e9b0fe54c3a2914ab565a1d98da0622cd33d GIT binary patch literal 38740 zcmagFb9`mXx-J~sw$ZU|8y%x#+qR94?T&4uV|6;}Bput%jB#hLwbwa&pL6f`{WJ1Q zjqyHs-0Re#lfhvX7t5Ny(H5C*DWC98V1RdC_E9PMDYG&_hpyuUh z=Ay^wX=mG#s-Uzgj1qqNhMC4BNa7=gj9Izk_{ov}E1q~=Hh^1undw{}?CD{OH%fh0 zIRc9DYfk2aAJJ-~K(B8rm9Cihxt?B*W-%FfnQ!rW5#xn9*S^G8Qkf;`UBZQ7sQUIC zU)&i^Q0CqGL0$v2uuIVbo#0fhCy;pRngk6r;*l^I6@dxJ$hzWLuEK6|M>Um`G3s49 z%l!#u_qVA453(n*K#9D!WkxEET$^mT&LHlLD8nmt$#BEwq#>6E_i4H&XA5mI_cd9xYXx3qS=MFZZRQ2;m;CY%K_Iw4Ws~U8c-8$Ow zYJOHHA<}r>4`N6;r~Sb+)J>#%DNtFg-3c|mp$~%(4>aErCs(c$H;iTSVWV*F*gQri$sW9`M@!lePNO`7ZGE`v3gG}!JS zQcty=P;J|+7yvnvuo=|@r@01+Al(M~ur5v0#jHrJsJH>GoitK;XhkyW9lYx&O*cP= zc?T#5N%>Ze=(3uQJzJ0HN1;|%X~RI_lSD^U)l5ZG7HsYkBpE04J29EVnfS`YjvTDc z=PqSS-KuNGqYyDAReev2JE-Yyv>|SbX8y9-_Fbt11`6_NP46G(ZW|$=0~}|F&X^n0 zFLhcHmi=JHXLUI*|1&|Xin(e2z;1pAOb{wCL8cBS%FYgsE=(p4&Srl)d2W(I!f$32 zVPGf6O3w+tE!0rQ2yT|hiB_vntGPEnBlVQAwc)oN>#IX2`FW9d&z;9L!)B5`>9Sg{ zDO^{hEq=@&g@a?2$_XHQZqXYxAU}}7p~SN>L?38@%Z7%cHxh) zn!vx(aX;LE)Lh*PXC$ua+Cqk**j$A2N8@>>Q5bl_Q>2g#KpUM=?VeStIjeM{a~2EB zAR#szj@`QE-SEi;)&#T}PVpz!lx_@k9Wn6CTGUb3pLTksCs-Z{?FVpf2E-SS{6?rs zVy=>|QrIuI5dWP`g9Arcq+q$$;)9N8Kx7_9Df?=}2l1rha2NM`6_KELYT7Fc zecfr#9qOwQ!?0OY+tlUjs&!YMx^~imQ`WF{@AM7X+ZT%QTgML}fQOOK`PSe_w&9rp zl$V`TMtI*-`J%yp7VB3*Su&~uHpf)@FV zlN9_*WL9h)uRbZKZDE2;_A5jO%Z|U_QzRx&I1?a>uZi!adE#YB#2mU=(NZ0A@~LP{ z*=ZfFr7n8(V0HSC%mtznvI0|M7*)^7U}mCb5sCrUHB(c$cEX3O$$09T2-?v_iEhUA zHk}YBXp8!e6s@#iDt036X&8JNys*M$zkvDPYiG5lkqXj%1;BR7|V#nvmQ>~30 z2JTSvwSv~165H>aD) zfkxl#y&UA$qo)X45v+8gQuzIt6_jY0tgn*YVXP(Mo*&%+g#CttwM*N>011U~u*aQv)1WW4gPqBxCvndnsqwHn}9I^iNjPoz6&a2#Lew%f|Ps3g6 zjf`o#+#3yLXB3M3*_pB{E8}e9qGV`jh9Ey1ZvZ}>A}96<$(}7MjW;M!6hAFp^99!t zD_^_MTCQ|M|W9yR>nm?CGZjEn^q?F>KzSfJ46)bE*F_Zh~<|5Snh0Y+B|oA(ml2< z>K=7w`quH)zYI%rIYNfcbk2AE(mlbkW?F5eQnFPI3OV8zChOK*9P^-G?S#M>z1dIj z+*4*!?FdyCE^5A1w$YZ@o(dxNLmW7QO$;x#$=7x*URC6RIoHlQJijuTs;O>C2~r{p1wJyt|~d z^tCINtjN9B51Hm0?Zw1R=lB*?&QuB?8O?IHUbOJyR}78T#M2Jb9~rDmne?c_63)In ztiQ%Zkm3xTL?YLCAo&n&c?)NfK71}8cX{QVDMv=RScO)+swf)1IFyOa2vSbJL68;_ zte?wUjC|~Pe6--MT#j_~rod8gwE4`Oi~~X)pDT?qC9Fm#VEHguAx>knewL$*j_jwY z=SBVC(C*oUW*CmCisodfGAzd0_rgnWGAoA7q}5~}2oPA=&ZIz@_r4Qo=zx5=V|1(U;!t0 zdBQWbZJ08KjWhB~oGGKJcL)fIbhjwLu-6&@tnOd2!CxV|?0n3S~I?aVL znRmtoDHYfw)>=Bhz9rmD@NV&Hwk#@3<+d^<4$Bu;5=&HVV&A9}aB*GSwD^v&*J|-fGzG#W2aZ_&KClK(NhIMV zdVUqy#Big;K|%>#-zf|Y*Pm~y`^`#jqC*Ul&yr8(%n&!z<)&Y3QqfwXLGu`pS*>z`zqw{TUsEJr6@% zcbnS_-S=xM*J+i7^f(4z(_I^Ncp#m7C}QEP2=^nRm!rA#FG^=Rw%r_;b3~T{okcYD zw8H|`4nmiyD=iE;+q+cQP3t2MlmPwx4mgi&@A@w{Rde3$dQ=!uM|hX}kANq6-;Aun z#8~Y4%FpO@gA{FfV^&TyUn}@236dIO>8&U7*jy;El4NRJV^FMe6Ik)BEGHv%ig;FH zn<7D;CS}AD;gh(Ge#?Q6>_zXgRA;Raj0Y$yc$O8&*9Xed!Wr!>7r||*v53P-(x28_ zCRUJb8oL<3{?5n&i?rz{=`HETD8n?>vZ9LPqSuL3=`4`JP}f;dh&AM6$IfPcg7#X4 zSS8QM(Q1{5i1)^+i8q%w68pq7)$NxpIT^ze?aXNrNQOvxB)t!wI_~S4(35MdQ2$6~ zg^Zy>Wf5qM&e=_-#zQ6tF=JA=R3Q$k2(zuL9q8&cpU9-_win1mh1E%WH9Z^sxFwh* zIgnzFHI~DSJ!Mf*NmfdT7}e$QLS&$(xk8}=p?-Lp{R3`@DaZ+AT#Qr4*tmoHNm ziJfBOw2Vv3lKbiB&eCw{)~+#|6=+v&LByO-41pME+{6@x*Dxf0aIW6PSxqgX(p8zy z&)EY^wtjQf4$(?Ss$)pH~;vtc)N^~B1q91dYlIkwWxsd;P_ zWKgzRw_Q5)h^St~TlyYQQM$ZdJsx=ELl^W%5$URC%K#k>U|h8p1h-AuXDTnZLTi(HA2T(?Zft;|6#oH{a)qhh!z)h4w=KYN znQyPJS~hh5mX*@zJ~{XB z#h0gDzCd;7LxLLM_NX5Lq4TRRa``_oKf3L(-M3h39al$X(;l76@W0cy*;GPvx zb6cG!H$Q!^`fGy^hpt&Rm^&5>8Dl<7*$j=} z0y+J(ze!qM#HNM>mbZR@vuBxKRAKhKA=R?_P&P&36|SL!a;OoGqNUJYxC65`uH;Bk zH7UH2O9xC_d^Rb=C?Y@4)w_%Adb+m#rl;CwOK3gIWnfFONhA0>Dn!HbqM;r+2c_Z8 z<>61jqbWha8?-*0dGjKHSm}PytHPH!Gy@2YhaB)hwnsy&iCyzwCLrP-ePZcitl#Q$ zRJXxz%GhE=xG5X6R6}jTaHnm!t?y=2hP)MFU45XzgmwjE%LsOO2(@fJPijh?3VgrH zKno(Fw!?DyU9NFpxF*N_`);-lg6An@j_E&h4ZTp8>B0m8f~F1$g8WaeU0l6v&0PMP z`|If???^WK_4O4M8@%q>C5D2G{;&s0awjnI>B(!jWK&?9Kl}L;+cM$YAb^rom{?s! zT|>4qsgdt{9+ooAzGiN+n%Uc1en7YFrN04jeD>SLI@e$yVsD4;rN&Fy^u=wLgZ~?j zFW=L_g{RI%UfSxL{o6&h(9)>G`%}Wl)63Fje3?+&>8{@6)B1H+`eN71;rub6DOPYY-!NHMUZIF!ha=i)JNOi zeaR?X(WoGO`lMdyLMZngOHkff>RNewG$Q2DfNAvwD9U3qVB;Kla`gOp-H-d{n~>9Chzm5{nTRIjh`L&#w|WCVZ6QDIFsAOV1hX*g zfVcO*Q_tE_5Lf}oBRSb9+wvp>0B3iTuP@Oe)^aG0=4<}OCRV%y=k%S zzx&P=4_6Z$b{_XW8g%+3^YD8+btO5UXrB8yy&qU8sox)058Sy9;dv^qhd;U^38?Ok zWxVv6igEhzHQ=&#U>Y-aWf6_4khjZ`SyYI>s}mbn3l538m|O1<$Mb7j2QJ=6`Az-=~HnQU*2+&s#mjr*bZI3>LdEoiT}>ShLp zM_bxSalZN1%}*~xoK`pEF}tRdsKWX^qHQ#Jxdv&`9x!-Zzv)+tVWMqq{F=UWxbNgr zja5?qoPc`O@v>xet43ozlxgu8^3knV`)lUqeE!+-!Rc*O=Kfab{8BLIvt*0j;=L-oZZy@#0-@g+3)Boq6MoI6?Y(!5H2Y)$-CjWj`4`m=O7no0^=RVe*F1|6v! z!;&mal456Hb-*S8)1$^j_bA4%>TZ_oV$jdF*ZFO46?hps(e&>x>sCE{wUz|tt+Y}CwM{7epPf%{8ES~sL=;s28O-%yF(H?>&7 z>A%`<&?`ps9s>zH!~c-TKmtSU1rPI=>V;&F7L@KkPyJU@iHlhC5BIKK zbp1SDah*kyULp-Qzso1k0PO%B;$K~<{jzWpLFv>fiRJCBp1qp^9q78tb}j_sS_=nG zT`QxLw(s<@0*tz8ub0E?F95d=J^NuFk=MbcDfdFlWzSpP$C3H`x^2(8<3&NG!~KKt zqV31HO(DHDfH-ly{SM=baO#)%OOCI2A1{swVhmn0hA8xF6@(NIXvo@5=$Z|)Cbh_r z4Ks(64dtU|iz9-h8GUTio~%UnqJ9HML&pX!#j@=2$-;?~tC(7V@}VrRO%g>=Ql z7N4n?hwsyN^enDkHwQ%Jk~;uW=5DCC`&>O#Eq@@5w}zd{HhRv3jnb-PZC4ol$_{>e zAG#`4I-F$Pa3zZBmYK*_k)6!mdQGb2f}|1qb*Uf&N5tAR@5LvaDtG=XAV(ZclcIEC zWr*!+alE&BsZJ>DhL|zn6(#HaF%ghF3MhLcT?k~0>e)Tvyk5O`C>zuBfAsUgOmWYM zp6;LgNVA2C*Wa^;`r&)iip$;UyNdAY`F^?cDqaNxu75G-E;YdJKK)qCB8c!N02SUg zi3$;Kek^WNF+H3-nYwVWBXhGCaidHFpolq@!78QJ2Fn{7(*f{kHga`Nbc9QCr=!b< zyCfWb@zQk0t=%JQPnve4#Gg;K71J9(38$msjT&@AgPxZm0Hy9gWw7%b`L$@ldHB(~ zEka%k9mgzSxqyi5e0NPUTeP4YJ$^)2W^x(TsV&J2%VAq~4|?G*M-Fh$udg#J?a6uSEoGmX(_*jth(u zy2_iakNzj0J}dp#VO5>CZ#BuE~ry4sy2Kd&E#<3(qs zDWnOVGwH?PaJhO_tO%TRz0(@$$*3Gd4W+EMYYiAOJbYTY8!&4T0Zj zlZ?ws-j@iF;C{T|myy_P9I+OnQxH#0zlL|o5kSe@C)4hNne1g~8=tB|q?&18bR*ks*<&*Y>wKlmBch8v-Jd+Q)L7Srp! zC=0dGf7AnAAUSwM!+bPAyuxAq_C|7=W2s6OQo?2hiKLv%>4}_HUwrZqdQr9SOl;;s zDPV*l_I3up$I;&Y3!vO8y$2zpvD=vw-y6NIn}WHS6@F=fobMLm%eV6Id-Zcn-W?hU z>w_KMmQ`}~IyDmkk*6efTUCRvPK}ifKc1EDVdd_^_Yq#vL=-lZzgWBbRGti*S4({| zP}eZoGC1s9MPM;qAP67hh&Xdew?v34k;EkrVi&GORl{U+Sm7a@D=& z7ayP2sr2jDu43^l8TG29hkP82OC;6vcIuCiU0nJU{LX_PeXwwsZIC z`Pu?{01#^1oW*d8t`wE!9K50GEVa6rF^=8S7d2Y0v2IR& zE!uil{qCq*i>&$jd0FUm>FM|hSS&3IDy1@2Vdz|%E$oCm#@12WIGy_bUuuzj%->EZ zy}ykW*AfepRwCFjbAm|0MPBf3tn%SeSI0brZ(rRkLOl@fPA-vlduZMFS9p$wHo)`T zBCX=8uT`M`sFm^O7jhe3g8wX(&^CA=U%4B6b1o|?UI&6`1F8?}G1Do!5wN5A+1ihb zIJM})YSUUZ_pG0fcr;1#_mj2yGwM?oJy8X{Btu2qd=!{mZ!*r1(Aj=bxxI%EYw5jO z4}%$4?_8|1nmMwuzGgarA8F2;C;~r6X3um<<>V=Kb@!<+&XzHyd8d3Knw)|zzSD5{ zxy@YW_Ya1a+!#~7HuzQ>@8sMP%F%CW1mxPzYff@I4z9`UyCn;;=oD;2js<3w6!O#VwW|Axvs3 zu>sl?W(C|?x79;%U(g|WuklML%W^7YL5Y=_2=VD}3HA@eCF~Sp1#3is5OLALPgdj; zj9jKO;lrQ!N`_O_iO)LkF}TZp;Fj@yYG8{6`C4{+JFf*tZw(d;YoG_|m#Y^P)TrlU z#WXx4)ULQHe@uxUoM&YV-}rr1za2yAu;#**PckD?cM**IyzduVRVFHAu^&p;0x8Vq z(FyQ9c$_~+tn_Xq_2v!l2*3wp4>!jOneE@UJZKbTOYc}*kFKjT#q^NhRZyOMl=>J6S}Hpi$Hc89RQak8S^O>Ni$(0y`&F}GPG3wspNU$*viK0kw zc(QI(zSH+(N9}paN5H|pvH8o%pj(cs>xle*`rFcz^TVCd?UK6CppEayB}~L&A2|7U zY66RzM=VX>-L9bmDyWfZH6h|!;^$@_0;NKy%gkC&()AU69gsfL)bJ#HzOH`Sj>gJG z^f}dj`=**{0cV;IT3X;f@KV5}UclJV?sn0)h-OkA^L57Y%Jn8|g6eI(br+2{=aQaim>1GYlw!ho0?2r&%gL| z5Rp*Ww`d7mK1B3+*AabUe{oMsagincy|2u1A4HvH!Xcc%!Zw@Lzhs`cz@P#}F;u#~ z#<+!O+0+$CFKTD1Eowg^j6QP@wmBn`{F<|3aTZ=lB5^nJ8*EhA;VjoNtGKPgq^ouC zrM6t`NMTEkMH;M*Anj*%yi0c=SHbi(SR+i&S2NfiTyVu#l2Gcl6&6IrM$MP#4Q0%d z$PFz%0Pay-kCB1P9jAL)T8|+$NtfJ_9dG`Olf#c3|FPCvJI-g5R+YzFwNc2&S=LjcyPXqGWFM^&pFbm$i2K1jb6ic9YoVe{j zrgj8AaR+H4$sH+-^62;D|2*&n)ehYlYYg3p1*O*CQh0?$&*9Fj@{@2Vhf@)EAZNMb zWJT>+8H{4fZ9@Nv$nOEt!*jc0ynRE2k<(8XL3vlYOTe^+=vh@K{W4qr+Dlu zsAe90VXQ)~M*2c7lYW`4r}*?u<;;AxmsntmDB?D_p@k%$w}?JIfz8t@wU1m@74ual z{a&eaU`w2X5?U}UaoS2JrxAHgUb@?L2JGJ&ePEf(LNSb&$ZZd$Hd%RBb=$&mfvX9e zZxT7wUZ`Yw@Ew1e`SYc*prc+{UAbnX zLFmS`xv;(xgKcM|_n&aXBes*j!+ZV}uK!QCl+vH@JCqak^QJaw{l#8JZ?3T5n$toJ zRYLI|o+@+QZ+Ex=m40|g{zCmMu{+0#tR+Cwxub1au(wxF^ISRhhIEW_owX1_PqtN1 zcb<-`8!F(SI?Ik_-eN%xlnLW1)Y}SacJSMYM2{&S#i~s4MgSqF5R8)Ocou#<43- zP4az5)JsK6`NhD^&ko)^mGr~xGKC^(2@D%^ab!0nD}DT#)-Dwev8vcuu#>K(bP_`r zNxLst(yA>T9~VmecwsrTqu=t9>{jKAdOLB%`<9&Ma~+ z5H#L~)DE-IUVOgdz(rV4(WP6J!7u{ugZh5(I^SCc-l;20`t9M@s0hUQ)%MO2Kjlyf zYD%FdF8K@UL4>yDmysC`94z>27^*3wDtmqz|MboMN$rlAr?W#sSns=T&BIyM||HVT_@(+&(o>C_aj8I%e zbRcB5Dwlr{(2_9FQgMnRa#2yl`0O+T*a_N(wR!?CvUwp{fibG`sAqv=0a-!#!)C&` zbko*+D~xn&a&xY{$OypC!IhB5$IVl*8#_aoJTPs>`9D?=Nyuhr`He>W= zyoB>_WChkr!ic*;--Dt9@iqd4sK}>-l~&0}Rut#lSs`&J^C4FhZQFw8H8~i-k{p_4 z0qpl3cs^iBGsoL5ZWy3?L1bI|S0=PzWEh~wN7nykPVK1NjuymdGqg=- zQS1|c16tPX$rv5L*i}(!j>h#v^<@0pa?F)BV@5z4kwBNPo z7UmMfOuE}RJ7UM(j*m)qBkTAKJJ`?e#3sZb?s`p9cdfAfyCK`X-jky*4IMa&VOG(2R$ohv3IFHw-L&k6`j;OJRCW^&+@OE%P(xADnP>J9? z&j=(??x9`HB*rXyMl2Ya=i=smVN+8b;VbA&FH^>2vxtta#w^c78EeX5^JioF4BGP7 zds;*>LHWdT^reZ2hxf)T+pciUv!y7J7-JYQgMf+L4r!-kbjThTM!f&$mZ(N@@`p8rjw_ToY`YY&pb9MJd7 z#Wv74tl*Gw^r;PXOweI{(>Basz8Pep#)9Z&p(2CMW4>aE)0fiGhm$qsj#b+k01pGY zw_17sn@TpWtUoHB3;w7S><6kOhXJbO`=M|c!a|MTX{ib$4p+WPER0n0Ml6hQh68RT zGGyHO-S`vhYSdJxuyAdgMeC`}zaf@xD^N1`{MN7;Hm3Ce407G_y-_l(%||n~A@iSo z0^C8Q#eq$N5q%AZS}G~nqw0X=O}8q~wcAi&jsNPcsqok^SUMxIxN#g!X`?Hv8$ ztxjMAMQypn*@0mfXjj42Pt_`8o4jnkq>?mcOm~}wg1$M#DtN81D<4i&H^go8nZZeM zCubcBcZdft-@|%X{cfz%1cXBNMR#^YP6{a8N2-(KJnzzixz#&>oO()8wiS6?yQX{3>i{!aePG``}6w?yH zxx;9*l=6-lqwf+2zg@V-zFzz_LeG3$yqi63zHPtJ$9|XI;SH5;+BMJgpH-@rs1uTM zF!gtK4 zeCHc+Y}1mQCFKTjKAoXek>R;4pjG0=l|1atX zN=4vw`}O8vQs4wqPT4^nG0idi-qZ-T+XQ(4ZLfuLMeie1i(o*k+gU4^GJFt?6y5_r ztr?fkFPswUgb+FHuUYnV`+BkYKjd{{K=N!}l)uR13W4O?|4-jwGalWq2WkHz$Aprb zNbmfv+1?pfl!#TDEGV2m5$AjScJq9JICVZymOHC{boRI;p{apZGunqWdd5M?O}TC{ z;H)g~;2ZOe{bCRbQDyc=Ll)7S&B?i~dO}|lM}#SlDj*N)XS8*lnfFO~7yd?X<+&Oc=fqAE4o^l~$f=#3l$qz8pLF+Xj5lCvE6Ynf>PGPug`ggmWbI^OyRl zP@mbfZ}E|{Pbe&soRse5$-Suz`BXVJfd@A7DtS+C!d08^Uy>{p=T0~})aWT*(Bo{6y+w5^cD6(oi;3DE(l$od4&9lssWLM~SFSYJ<%Ayib zcU#y;EVDX)u$s#$U92N1vLeaS*r1zKxlEN{uK{OQC|HSmU0c$CCPXJ`Z7QQF(x0W;Lg(E~U5O znDxt_SZUfy=M9c53#!O!zekM$pkFCI&nr9+kwprI_1d;zgxWe^ywqck_>adR=H$Q_}e5;^&gX?+J8)5+Wr5T9JJDT@!Q}i zvjPB3W-%-L*(Dn~zryJ$|GDh6hCs&_waXoHl&-MGh&^L3gxtR`XoXwAR9W&(xUzpv z)S0JRpQUe+ZbZ42I@?11`5i8Ka`aX;Bz^)F2ku97c*chDj}=iIVddpPs*oeipMxU| zMBO2t#loMI7w?`BU>C9l(K$asym|`a)3-BFH2ee=hl3y%-UC4a6Gwm{?}wBrg76X% z7=X2|q2`OlFeDPw3OY7xy5_$c>Zw*}4)m_F_6%C}V~@e!T9<-l!tlw2+Qp%aSv%8v7aO`%zg(gqXUa{OOQK!CDS3w+??;B>}0+yPD!f=*S+`>qv zgOvXxc`tkaB=6kpcGz|#FZ3UUIQpLq4q*&7xUHawl)B`%L8X@24qlB;1` zzEL&mNnRg#t-kw!ZL?RKs0T+Q@Q$i*+}fPU+eFG&yrE*c^D9_BusE2o*xs*g(-B(w zFtoKx_hfZFYSS%nlV`w``|zR(l9fM5MC9D7G9v75w}J@sSD=*Zmz8)v_1*i>)0TR- zUc}ZzNR58|L@D2dN1|&1yI5ggw$~7B{2fm#GM;X=uE=_=m$~m?Rl8;2 zRg(0#Ai_AkJR)$E(T!!^J7%ixO~d14N(u8@;Aj{VSgqb^ zDO=x-d2pIHn~1SLp`m|r&}oT#wmI2C5UWs-2z_>+Nh`M!pyhr zd=C$yk;))`c>j-5dA={0q<3am^++5ny^DbTlRrM`a~-bKr_{^K5Qo!jPO-b#A_eVT zs*Btv#z@{MM1&$GV==K$Da&K^5R`1Kwn=EXUDU3XVS}igJj~5B5mH6JR47QO!n^lz z&~Et`B0r%c&^ur85rQ7&=qN2|x{+sHi~*`_P-~w3?HF}-q$-UTQEnxF1Ux?rtNTGL zdGlgvqbTU9D;zT(vHIm2_P&#TR(tT?tqP_kcT!3aU zD(7316IBd9$0c-BF&4r6?NBjqYy>3r=?>tpTH3wAErj_j&u~|GDD2l0DC<`~Qfi!E zF`rP0Z{?na`=oJd+v~G0s79h{Y>N!xCTKv;>;N<{X0P_`v z3a}O=wt`a-oZq|QFp;u^e^?frUjZp}M^IjetvrCdxU6=?=Jx#gqQ7K`G;L3}rs?Sd z^HtgJOU;&MgNNputs?E?=jei9gC%%>yPUuZ_>Dh;w<4G;4u51$j(}jk{Y8H4+jJ~$*XbS$pWAf?>RzSIb>F1T{{=h>?_WdUTm294+&6NU zZc9$6@~yJEhiuk;z{pd@mWA~IvH+k= zLJHA7T4j)xZ;g8PoF1;920u(Ad(%D|HM30S(dNd1(mo!azLm;*^*H&{K4$7%iOmE( zdxHX9$0@}T^&|e5c_8~oM0KJzB)@oe248!>|V!i^UIRH~aybFMW z_YXSc%W+`V8qHN5P^v2^3nuBO9)s)ZBSJ&xFYVDx8k(9;_8Ji1e0|vO0?1jSLr}xS zJ4rGIIf8BLL2Rd)Kr{M$1s%BJG;BYQ52o?S#s==8;mw*PwP z>mnpKl3;DtmS_G}`xUG^#3*dM2Wos*+P)SiH3&?6n`2%u@QpvYJxcKI$ZD~WV>gZ2 zJy`rMttx0dHgXDmztfdb62#~W#Aw(|Iw`lJV|R?>+Vy(juzjOLlQZwYbrvEk@LfNH zb1eM8b?x&107&qm1p<6l`Wv9D51oLTdqaZT9{S#K=zV0~Xx}w>mjmAvgM0p3evJfT zG_Fl5V9Qf=;tY~tRJ|08fQe{CIdFF?1P3Pm0X81~!JP763h-j7E;7(TAVzhy_ylvf z=R4X(VB+x|65ji-OH)?hx#O$kA-VN!Q@;SCImiE|=PN_~n_iHRZx@saXr2ps)NfS} zVVQ%e^nZopaubbSmbH0qF@s|KeB1|gP2v}5y#~wc^zZNpCzVvQlUc!r(YXc8oTtj? z(Y9ze()Zj#)hMg;SVJP~ur7v-(%Xec&pl?(BdA-QMFbnTCRIJz-%Xal9NQd@)C^Ww z-6>F-ubvC$0cM-@wBEc)A>vo;^*b!V3_~X(kU4(x+3}|S;y4YBFkjFm-crd&V8mwg zw(DRa>Ti#A!Iy8dg9lJseL^Jj+QH0zlyby#4LTG`LHQJwIyzgo6$f@A+vOHY*ZzY6 zW-E-suSO&ZYc=tp(~6K*z#j zY)oio{Oh}R>`E?gWK4x~LyohH>i2!;9@}bAaCGANaXiadTS0Al->*J)`N`8GxBa(a z4yjt1{B(4jocj%|o&l=HW>od8!11zL9xJRUPPSU!L<39ScjnM-fWh3|*lcKmw|`>9 z#Kx#$sh|0U8jUVlx{hc60PNJcLto=pU{F-*u_+?E#zDeCGLKI&sg0;@a;n#WEx!rV zck^}H239W(jWdTy6SFJMwxNkcZS7hQb#pN1f@=Dud``CVY1tmjK`^b%{_>#-S97!C z#Po(!yOr?ZToW^}2D$3$;%eKy1S!rJUtx261!FU`V~(=5wG_vWFHCec{r$k0MkqYG z+b^4WF|NyY`Kb*2Gucs_);x5o7RkY7 zk`_=Ve`Wy+X6gK{@dIP5-p0)9M$A9H=XG`09&;yF_90OHLlt5Lq%!?Cb;<0_we$&E3KWW8TCz=Bwq4MT17Yd~G1D^??9rouX+ zd1+9AVE;!&AA@$AsHe`jEP$XE9 z&^vAo*`wg+W8PGTevq~?Tx4FI(^ZJDLvzO4w-HC?d_ddB`-_8=f&JV}NybC4nM zH@*U^@Z5x;uHbOMX)mPMwQdbMZHnunycndXoI)^MPaqsavkXPKuyRlVm}SlAk( zS%IjL4XX%bFkGiFbSG)vIhU`2!}bwkEpOUCXcP1`nc;QFk;8$NH&Em4Bs__Fm8+dwpIHrJ(hR5DqM{ zbw)S%it_=p!C{ErxrV`n{mEO<=?#%5l$fJTaif?j(TY*%dSN45G+#=>=9L*&yf642 z`siCA6cmaB1{`E1mzd|slePqn+Iz@tKIu}&bmoA=bL%+m&Rr40DB?ozGZ(epEpjcTek)Szb^uH z_W`ALPwD3V0RbPadI28RyQ`CaC4Enf9YL4=0bYUz-4&hpp2cM!JRioBF3F?$q1h2_ z>HFRR?L8i=`!3Fd*?HpfjVT(IyO7**tR>iZQHhOJ007$@9MqJ zIrr@EzxV#ndQ$7js#)V5W6Z*wvuf7s^<2Gb3=dg0wD8`Re!s3g@}mndDR+~5a-!t9 z@^0@|@BP`suI1xl6TkK`h4YS)@%h8E=KZ1X?04_uCDXdi3-OYCh>wc$W(V4f)nluR zDz25mwP}~sVxz&6V%by~3d`g&-{*BzlfyWg^{ey;omQQ540{S5CG5#|%i{>q!*(+| zDLM@w;4bDbCygWX<#H!=mbj@0={JuB)m$Jh< z^0x3AL2T=?Q(9aE6e)c8kI%aG?UuYzsc*FC$Cba>%Ny88>kk;P(XEF_&4!50jD_YS zW#=c8i!-3*xfKVT9@2t{73wUT6*IHe`6Dll*`an{SKzXrJe~aTJG0ii0{}lAZ2^A5 z0Q|)KaL?ziiu2L=ye$UH+Ew{5HTL`7QmiID^C6{88*O#q@O(BrQ_A{c}S}?CX3w}KN6?dvm4rNzi8pG=>2DJ5WXwT3n^%i;a<^#m9}-xUQRQO z4P2!*$U4@SPgUN^4Xw#<13fVwNxhMZ)7AbNM67;mhuc)p>bG!aY4~3n zAs~bZj)EF4EU0$JpVZRmgc|)qV-Ty8`+`BDS_57_Lc9K{{?Yk61G?#{`I4!<=>cs$ z79L5D>GQR5Whu@e=zHsvZHIQH^B6a8938m(#Cbl5pKNsjj4JW626#D=MJ>o8inE5N zq|j=mw}9`vN;g)I2#rpJjp)JFFC}iKAj(qQls=U6$Z_3hCsC`xAmnP1Eh%VK;x!PC zawIF@i6=FX#o$J7H_3htpdf)>TJJurZM1IP2%9fkSED73K=6u6fEAR#0V@d30V|>b zE2>4B!EOEnfwKtJ1oWPY-9@_dr_jH*{!_?h!V(y#u=o#3{U7+hQ2x8Xf1~`LSzxd> zf!ieK{s!TYny~`LfiP_Xg7@%S=d205WcvIZ1`1{5>@SFvtut1nv&!VQ0L z)usz&_!=2H9Jccw9S> zr^mb(Egx;u*gfdbDJ_cf)D)FY;tt%MPTjNJ(4)AwhA$4Ids);}*~E51F`qSTxcR2e z=i%E$(EfhLXz%0KaBOitmod)`Cl05V3$7NA&qMRs6Y~ewilJThFGbE{2_Ah+@yM*ThJ9DN=V~h6q}OU{r2a*e>%}$x?EBUmO(kDw5hNJ?Z|*bH%S-ei z=^ZzYb&NSq%)@X4-mDB183W$T3#BGf@U%X~+UPgeY z;^$J1)W6&5w|t2h+#&E%1-0{QHbMpJvsL0&2)rLEQ;FCm7pW(%li0?{0!$g|vHD~7r7yC6vAfFKh*s!xhyWXV|3LlCUu__JJWL@7+W zpzW11-wdyGkJ2?u5Q_ADO<`M~qN&qvKLoTJ@} z$Mf4;lrV4FzUQs_``qqm?OQ#c78{OKDcxK1$|XBJ4y_e}^}{?%O5~60PT3)strk!B z=jOFcy_f>qK0&L**cu)(s~kL$l#0E>3C=_1m5R)%+e!2W@HetAC%W~roN^sw_)VO0 z%%|1eJ2zL2k~WUPNa&&r=r;yVlL&{wr$2SsY0etQ3rg+WaqX-tSToEgtqdHY6cL$a zz8T@zjdOe(lzK%H5b$rlhq4_(+5sV4fRc9@i>y*)guHi=@Z)nbX-avuGV=*Vx|P0Z|!x5t>&@4B5_a&~p6@!W!U!R&=bIi0U@)!@l; ziMY1Xgdgm)t^Ky)p!p&sv%ctZ+}uv&X&2LkPP1{Ao|MH=@a155rcp-=Sc|vjNO^RJ zry@mSu{;hJ9LXKGb1%Y(=)g|7&&}r998>W;_7`7-cJmgvCHp*}A<0?~|f-9U3Rvo+;y|=#&JL!$< zbUC1(9cy#?+@dm!evN)Jc9fl^l}pOmy;#11SMre=!9H0cM{^a;xcA1n%wY%33d44M z@ysR6VckS<%#cQ?#@d$R>y^;L=;Ey(rrs?l-*cgDuK*7fz^!aMuGY-OK8ZWJ=}=~8 zxpOIxIexlY*6B*Di?_4O6%s${Ni`(9xZZCq8hEi+AHKhG29+W`Pl&BJP!h`DrnAT8 z%Y<)kwK%I^G$b`Y%o!6(oG5Iw;7G1tH11iwy{*-=+sL3}D0{SWjNBWJO4>~Nf;-`0 zys-97OLq;W@>srTAFm?ycM%?|W#H0X@v3^%hlh({L9U=Iy6QqkUSeCF*(iRMiRUP{ z;$}wT*NjxWdyR{kQcACrnZcrV^{7aV(;3&nA`1-ll*tv3ooxGwEUGPQzQt|g}t zF8E`TE{dKSEe{!DO6(J)4~;)@!D|HT&)rC>5PoWc{Hl6ZfwFT|ViNe(?9TyYn;{zO zwg$H)jR*UnZdZOgbat~#`Ej58N*XHI*YG7eFV>Wb4{_~!r)Qh*A&9B9+v_~yMn1dG z!&c7xrGtAteN3+R?D!V&^!2lQi`G3z9Ou{zVt?uq6Ys$X15sTt0hh@A5k^d&{yc&M zOX@Da&D#5m?c>PM`sGkFcHHB4#IL(?K zVsxKH2LuPALaR1G&YIF{0aMZs4OE~5nX@7kibE(!ajTZGlVE_xKbq4?&yW)6Nn7sV z2f%lf=(E2kbE}|ClT901=O`E1YrNde&pjPeavMf7Q%1c@tqTT33OdE^YLAwsY{!nk ziJjC)4Efl_`D16GoMtcz#=SP5xvF(Uqm{K!R}}T38N7_>*txaUMxK<6f(h5FJl;sF zVfR`}?z=0L`TVL#Qw%se{o(NIV)p)-Zp9+&>xuS8&TWs$R)T}cN8CwMucsKyRq943 zt&!03`tKI1XE`NLx+>%J*;W};S1~5TEGW@IOKXGCR#n!7F}}k?H^p(!akPA@b1@Mn zdU>z*44TxYW4fXh&wV%cyCE3T_&_J${2WX+z<)iLJWRh~i?nNLoaY|Lk?FFoHwwl`~ zDwIKAfsdphjGAS>!WleGUV@FecTXcD)KcM6IhR8pM<`5r>+r(Xnko-0*oCyI=hr}Y z#h8P9?2Czc`(I1c?yGrS7jTCf46|(vml|v~)>|8Lg38zpC~cr)#iVQ4I@Ol2y&cD_ zRgr)f5NfNbC8``o^>)-aU_h;<%}V*fJ{A&f>#(n-)$lp4ak{hHxSwrE`4M19kG%-5 zcWhhWU%;(l+YOuLCF(irU^e)>gTgjO2sE?5A1rvH1Q3%|IoXSDZ>VwsxJ8g}Z5S{| zIn8!p4s1B^=wCuCWy^f7rHyny8p)`26}PdcpZ9h6xC_FoSIe0ef(P+;b3cN7Ho4et?U;zJJO&JRyK}uuZJ5hJr=x=|Nrb{Si)|z zoY(AFo7vG2Kz-bIzcjP2z{qDfSQ9N}AxPx1FG=TmT}bm}zG#imsnHNo*L|^XM0kX_ zfa5f1ke96Ct`1l4?Er>Y9VA#w)4Dz32yd@Wt}X-A#0H=y6fP!Y%ho$bi#XF&9`$QY z7sJJ$teTQ~vZ7je>e_o+GOdePki{PtSX&eDcKW;qfER zb{*~zs7T9UyF%@_LKhx`3-b5FB2ajuPtHljk3!KYH*=Jhne7TgdUGE8mZv*r68hG& zdBNX~F6Pp1=HTu7iUYkG=Diwmvf`D*+b(^S6D88^=tF- z&*#JF+F7YPSjT|{5BXO&u)JrO7?e61M)&U3tRsl*zqL1<8Aco?pl5jo8dM}@xQT@# zSXYQ!S9mvh5NQJ_x|rWU?>grDS#xaj+~*l>Hb)){+4vJD3FhGyPmS>}=%7f#%z1yw>pBexFqqCr&w@$^fD1n(m2$=TXiIbgo($ zswid7L!6RE4(pxePVOl9L4`=dK;*Gd>GksU@bb&KhQQH1AxhLS9>Ilwuh^}?%cUNr z20xAa_;PM~_s=$^LV-*@;A&hT>~6Bn-hu>=%oCYo$M}T|f^%bfK}T}IQtp~|>z@!{ zN$~SUqqH8;&z6t(=adWzRQG9$x{n)~7;dU8p^zL5U~U7T&Qf}lduac7K;vmJ4eY)YD-!?(1Uu1Fo?u>d&)x}H8xL@L|0MIEdcLZ~kO8k0X5h!BLE1^5Cp&wZfuXWghd5>(1`i-jpF z#`6-JKNJ^J0H-;UDsup%pGI;I6q4%Oa5m^eC20l>5)ekJ4`Fd8*9^@hxj|RzC}3~+ z@2Xi)2Lo!kCs@KIx^fi+}FmR)3aUowvHR4_$_($=2fer$Isdz5? zeEPhzdXVrxsNr9S2KthwRwp!igKiU7-RorSxLDfjSvs3pI@-lXg90R>if^~Bw0*en zFSS#Mu~Cs3e#8%jpS^|8Qw}!~h0I|mDABegwLBBN4{09M4uzxkNG(&T9}9kuD99M8 zIOxyPdf0V1n5uf~DPx~4!-uTvY0tMSfaua1Ngr3PGLX|NQ>i82UmsKV_d@vWMEcb7 zlwj!`uCE?v>6*q6+-P@Uf~|~-hKuQ$vmTrqT05Iq^B>x59l~$b#Std`qV+)G;d$$Z zq~-OyFEV*4>s<^%tBXsI;g~ulZsjHcR>I;*;vv7?Q!VW3W&56e z=YC8!WDq?CEivT)9ZUac)q?@c#iD9c-q`Hwg|pprd}3o<+U32%{arrYrCesjH|ZNp z)n>bv|5DaNHTA*0@i!-z_d%C;AMPw!i4)1@1nl_v!YTt9qcIgGikfZemAIQ1L)zZc zpry^=C3-$mENugIRijjGlkgntIRYpVl|u^2O-;u_h^nl<)4zo+ZFVkQ0*+uRwX0gu z?fbMP0Mj|VhyM1Da!L5AO^hlpELkIg5SsHJR&okyow1xLwAv%t{ee{mvZJR-4Nz71 zT3*CjU2pLw9x^)hiP?D9Uu z^!>ZmXTb?#yJuYc$F%`3)VBu+*3z}?e70Yt_6qp-B^dF~`LY989^7y&jHIF7E)>lG8d;(ym@sA73h}TrGPWb-m;b!#X}pZcIr2C!iy)T^4eYJ3uvMOfI3X@J-0=<4D_XI81?t;8( zk`%zbUw}&0D&#>ckb(~hJYIJfJt)27{o3|b(uu_+(bLh^P`{#!FNy)HM9_KS0x99@ zx$#ZLZg>^Fa;N8iQZ1gIrObcPEbKCsZe;jqSsa`1$p*$Ssn^=Je{bMWPLQhql?jAG zkuee@=D8_BalKyw9Q>4 zix9JYKWVVhJrHP0pa}#D25G8or@Dw50a~uw15>aO;sinJy?p?>zW2A%e(0HTt>o`4 zekMkTH%q``j^BG{xMvY3#*vZ;QWrpB4X|=C<#ggr2353Ia>}b{82K$R&~o znE*uO{)O=`5hwhDEPq4*SpC06MDz>QNp@)cEr2l_dIpxd9nWYrkLd();w*sj4=pD; zFI;aWauS>lVz`-c99uve<6Ad>Az>;oxJZamXR?VCr89ufpZE|ggsHj#OZg(s$n=YTxMwYR`xhxT$}goA*HY9#C9Pxs~FCxADdQCSHZ3(u!bEssNC2FYr_xK zeWg@h2}S#6RZ#GPlb;_JL>yn8pv?89YO4tspVIwoX~FIDV!A{#-gYqn%llG z9uq;1N9Q;3zY($RMO<0wzY#spX~)7B&$v-e(j8OO3dOR>ZV&h(o!1_Kh0$U-K$_j2 ze(rbUd&368BBv!>*tb5F7(qfo?>!B>f1gKz!}y$IPQAbID0ULx)EX?2d842t2fRdCEiGHX+PWs!FHy9| zSKe7SYh>8O4t~U%aAjigSOc4x$>_ejZednR_*uvsboV3bTVfqMDSToxO__^E;3>aj z*awQP3`d{JTn3qHaqSJyc&(l`?g^2|i!nkKtQ=GRfIt80N^bkQ0~D(;hT?$nF`fc% zcpwGEjA$fQ>l9jtBWUM48f%u16TsgWGOgevLer9i8ee;g9I+fNknbaw2d?^#;aF#g zl`vnQd{W>CpA&f9TF$!S!A-kR&j5>P&OtCAtJ-&a18X>G9L$PnE_NdwW{)dat2P#E zPBIp#`UkZ}god^XwZ8UL?+gzfCy&AK{OLD z-!4|(l2fMkRt2pI%vbQ`SYphF_oHbMNr4PzO^2dIHSb3r)exX=qgxDy!|cZjW>xoI zh>~a5D3_;%^exfiIzlwZpz>iwRcfVOWa0Y{S9#;1obo!*8VEf&@d$j|N_{bKLN&Vx z5Mh8ABz3CA;DqlrzRFHN%aO2|{wS&kD#OpSYE;cr7=}^oaF`}sWa#XwDSS{V8c0P8 z1lEIt^jAkV=b-B6j7QxeSmZA!ERi9<9%;gCxHK4Hl{ghB-xe})oy5u^(o&uttqfz; z6NCT+k3iG8*Pow2Ggp#e&?`-X=jz7_l+cl07_|yxoj?HX$AX7yAX&7TXLF=5Px}rq zO;ADQNBSv-Lc@XP#86hfee#OzgrDf<{eiH0aQ#@c-6uoEX|#*QC*70A1qcs(~i?~x8Y{JEPfn%TyZ%2aX>PZ z=tQgF7ZP zEzY0DUcC|uC!-P*xgZ>%1r{wx>aK7X-JMGy5Z?e5THKZSRZI_p&wxZ;H`0d;OZ=Ac`}@k{4Q~c;=oC$forLldX?>|qrVQaq zlY0;_e<&sEXNQU?47g$h)^R?9)%j2X&IFF7su>aAWKk(=Yy7B;=<_H!(Eqg5y3T&k z`znBIDvgULclsS^8j4ZSUQgztmgd?IzWKlVJjdPwt4QIKSg(CIzIq5|4(^JvN>)$NL zs#QJ~NsFnxb`{!{>RknpM;CZ7j3MpL#c0j!!sYkCXZG-ot>KDJ_5>onu`dVgjmX&~_IRgYtfQ`>5sX#@2#PpVOj59j!fM`ZAA5T6XLEe|m;vQeBL_FK7WM}B} zU|37}g@rzeS8dtbq|<;G6w#6^=6zCZg)BTH`Mn0h9~+AOg@+1?{X73@%_ZD6#8yld z3a(IIEc>`+g4Njwp9Iy0O`NUkb70nG8E9RA{8@y5-)$(I;h?G*N3_l8Z8E$FXqADy zEljuo(Lx3PsX~e+`0pff1M2B^%?)*3&|FiqXITE;V)O#a1lyW{ z8#Y5Y$neo1&_$P9w!H+fPtV@q$dTV%d&Eg`teEKNx36x0fmqf&{}nT4WIbnm4;~s4 zS;lpC7|_umrMz1M(T^UcrkZY5!jk`^G!O=$ltc-jR7Q>$G}?wMhIT^?+89MvVnbc; z9-Rb7j3X(BeT^~5A2c68iPL44CW??-sAmrpO^oAV{*)lWrg@~f-M??@5xVo#+!Mu} zc-uxv)s}*~oWIw{U-4vD7NRpr96UiosU1WgG08?qJt@XU=yy`IjZkV*tW7^0lmtgQ z1i`jLZfIBxa^%+mMsAG$6Z$_+(Fs~N>jUVE`S{P-UkT+gL@sAC#UZE?Y%eQ{hAH)K zlkMIh!}D#bKpK6?TgHYC%DYJsRC|U+h9^nCF`)c2kh>B#THK96Vz&)dPnYmTH2YVI zCI`sG*nTe(8$s%qpeZ1dbQvWTX8GQb-sPyGwRdoUS(AoGaL(g|+d!1jHg52lHfVs{ z8&i-p(Kcr#*5YYjf|A0=BstzZvIx*+ZvPHTR3fMrgYs8cu9V%9%LE!KPaj6Hcm-6G zpQU!C{I5&_3gD?pWnokn{PnO@S-@f(8O3g9x&B{8YgCX64F119bG!7KVATquNb&0U z`0LBAn<_vDrTr@|Q1V2S-XSSYBn}5kiFIMh+&Kl?`W)xY5$6OCnTJ6GyucR)x1ek| z%n8A1|1||_Lm`KneRh!dJZjcv6+=ze0r5O=`MmXg@{}TJ8qr$) zmRBdiY-L|_Vu>uGp1lr7z`l8t``zIe)mdi4BPd}<;Bdac$i{NDXXA+D(nr#po znUdEI{L5ovW-m$%q$ES=GAKoW6V%k80M1R@dN420QZR2aW7qsHLL~p&wn9JVHDo_N zqE|MH?~@@O)9Fv%w6;NM7CP4aSp1 zAKp)Xa2&#Kz}Jv-jG<3}C?9Lf*MNuKlP(Qf)`R%~f%yIo#1oP@c@1fx`5jO;h`S%Z z70c@?k78WqCkOrf^DJR7pLZ7FhYnavDue)oo^b%F7*N;~=ng`1ozsNcA8ysaMZtUo zA39tjNM1k%)A--N^w|v3Y4+Q-0sgN<@?y~UEE&S*@p%$tkza8U$%PG5tBu^c9j(0f>;QIUl_P1jg0+sDLY*}@Dbhy{I z#Z(bc_Gs6T<7(~)G=GRKdn^8t#DJRzkgaf3^~nGuip1?d_)je!K2Ls8|4}mYBnTMv z63^zCx&jF16MXA|vO|sf(}nB$<%##B07>^xZE%`T{gHDGiERU@jU9wSeSjP}n0G|@ zgSuE*7ga0v9fWSy9R#CsdH`VavgfmD!)l!${P9X#pXokFpG!37x=cEuY~r4YN~8lO zTAQ~^dYYkMpI3ldqxMJ56kMTELNjUXB$swW3M}h6Sz+v*n?-R;40yK*0h2#583c|w zZe1wj6yEDqy^H2-w0h!|;qYA*uCS#^9Rdt6Fbc+Rg)e4s+YAqO^4a{pyx>JXKY;s+ zwX3@s%rHdMx}NS7HmV2tc)OyV9ci~H`s5q??Bx3F^n;Ng&i(bcPfPr1uO|9*-RRaaJNP-O0n6AsXB+@vivGY9{SEU| zrs!E=4EN7=8tDh7h+uh?_8ToL+x`G(Oc{H8Cq4R`Ao#Z4nz7#@+bqt@rg*Q4Lm9XK zXRvQFMVzNIy{3V476EhPigM`xqO%gRzKk2WeCgp@o`E4_ACz$*eC6c-U#9)hB=((>Q z0Q1KTsX@{YJUJ&~PNyKxR3W~?+N8FfvNZXCb4>ii662V7fXy$S^H7E4>%f&F{X)5F z%&YC-+$piHKUuiRHmOf{bvUPGs~A13q@4mTa~(S$P_I@bL$NoZC$;8rYgl3Lz-T|K z*re41y?|X+o-e8pdGFLh#BQgq50mXn)u+DveuqDIIf=);2!kU-I6qA<@=)#Fi^Ahu z-~ll!#+v)1cHiH?(q$}ffLu+WVbl_qjXk3@ibm3f623xL&=_Jf)W<2x$b&NDGUN~q z{?^KZv_9&3S?W9E9nZ-bGw5z1rtmbI@M;*dy}Vw$Yq@%}5C_>Q!vd8VvXkmS!Bz_- z$>WDeQ_L%QY2$#Cw8&Z%*nU6ketv$tW{TZ+c|-V7@$KGBN^+u#F$(fOA;|$DIggX< zkY#fjsLG68+MvI4`q+W&3&C0&QIYS~Fgq__!ZDi@3UIchv;E+tgf-Re;Ka9*=QIVi zmy_8eSQ>qph2VFfCZC~520H^2q6swgvqK)NyAFSMr8terPEXEBkDte&QIw&Ofcwr# zscDjKr$E6RO$?`H<(ecf1C#J+SJ4)*Xc+%B=7AKjILJxAlj6i2hzrK$9bOvnu_G!^ zx1pDKkkg8MLe1rHYITK4R-_TAjtW zHyZ8X`F0zP)t8L{GJ+r8uEK)e5=f{?gduARQkOAJl(Vl^fEa2=2cxzCXOVdO9EN$L zlA@$ctuS3Ke#zfXp?R8PKVMFrSQP7R?S?L4hFJTYY3ZdH&|DNj05lhw+@M$x_Vcwz z#Zd-5$2XR48?v+0p{U8%!eYP8qm7EI#D6tDn;JZW!mNYBiH{pIRZNKc7biAmw|Z|* z3Ua$>bMidVFF-SyGkpks!5PdE1JxFCGDdkeC^g`8i^Kw)jDnn$l$GiLO7p4#e^U|? z!a-D#5pux9+!xCK1r1oZQjotZ$m~!&5V5I@bcF!+5>?J^t@yK?Mmd?*+;p&yVf8$1wh_l}*o~z;k zblz!nEO)yY(_3Fi=-u^S&5X|(UaBlDeBQj^8->r zHzz#+c21JA48u1OjSh_VT>l3rqT=75pCSTMtcZVZVaCSCG%Cz71dCipf5xPe7CAQL z*6zjU%ApLKZhw!S<3+mw<^=!ivtWp_rv%CwvE!dvs|~DUNx>F9w+!vau;rC+G-CRb zGm?h0pm+e-PQ4j%uRf{)iC;!ogh>rOH-vwZ;_Ha%Hm2R)9hp7$Fy(MbtbDX0?%sS< zu1$YO&N9DDo*EDA3JCn|Uj$sY>h|J^pn3j9fNURv6BTpBmLFb|1ykRdK|hk@cFg!MQl+;D-s!&SRG8qL3j#73N+x?7aux(d+ccqiNq-x%0{e$g z0s!GJLkbf?0er-l()^j7cp_4!mxb)q0rkY$Ax;f%2V+-i0B}k4W`yBJLL#Gv4F!AH z^hRth*4JTN)XY2m<5!zoIblJNj2g3NQX{G}tj#a~s)XnN}{${7(*J zXkkx=NYD)ft=+anC$@#xoGkEX`jl45R)$ zDa^wR_g0h8IXgk{*9b`+{Yp0>D=#u|Sov6IykQK=N>Vow07Lj8Kv=@KwK_vsBu@u9 zyagYJW;R-;K2cca+miE2cv?zD*%VD}1OSJq&xI|srVGz-0u@^|e^8W2C&tkdpxjx zn4g+8KEi-u5D+xAq$!FqBFY;~A7Q}W9Cij57C(Wj%N>?Jb?6tX(xrv~k7g=Hu0BR2 z3K#~jQ-o!Rt9wWhZkz!i86ZOT#ZDQk+7CdWm0ZU_sT%y;$)O3$7vlj#Q)@<~simh; z;2QBer(wG4t0qaq`46Fd?sON$hc@DIO`u;!F_w1;506Bm+Z1|9E;|3E4%7FKIzhz$ zt++doT7e4n=J2@bUqm z+FwAHckHpSIkKC?tDz%_M#<-As5j4(U7hy*L$dVga8B8933|LR^u9AdLX^~0Y_~H@ zMHX+mEDVXkp$wEdyjHg{rE{iJa>qg9|X7tqng{FA|+=hI7(3u-I8 za)}6kAtS{1U#o56!}I|6*@6E|1x={4(F{>>`%B zC|tsN30w60H9vf9qo~+MBs3SAzkZE%aaSyAGl-e=R994vEb{7u3_*xug(dKH^q@f!FzQ{i09YY5t><6)Jp zxMk8sa9;UOPJ8`!ZNFg1V2s&lenYIk3B_vgceFR6&Sme|lo8B|-!W_U zy8l5W%l}zoSfTDv6=HV0`=giRf~KSzA>O^EBy3Fsdd)^uj<$4)d;PA&dWyF}tjh=% zU#xFk1ik;s{2~~#e;HeF#N!D8bCb`>!I;arJOEX`)X<{_`cP1WT?3noIZPY1vur}N zCWMQ|th#t06+xj@Pri-&u6B4V@~%*e?cA^?X`th5c8tzlZO1Sq&eHacwerk3UC_(n zrolk!BIl)S$P3YNY)$%6+Oj}}zqoWkoCl>X&#KbJO4-Y-mQ zThgH50noHW8&q|04ay5D62Th~Kn53of@u=C;8^Kan9;Ju0$cZf=`vZ`1pepr$|LoC zMIb<`sYbxxpZwi_rJ8a!)KmPg13qH9$E=S57HHbNvv)66an~T6@p~*225wutKqG<; z4k=>;U^3Li%OkTI8%En58R0Qu$%Yr!1l^W+j%6(qJrjiTPWIWn0sdvsy8a*q6gN}^ zTIv~0{P}9WRw?)k{P*C9ej>dRrdshsM9l%t5zO#x9NcA9oEdB+y%TaPf4+=$nOs|< zVl7kD%C|}iyxEBBCJK&7iC*T>I$-QY*9!qrB-)A5nW)CKT-qbZLwOMS_Ms!|DJVm) zl$#HHgZMi!jzAu~I?H+m+jg48;iGs=wGJ7yjw4nUQ~y~uT^dh0lZdg^>j3L#Z&BEc z=I*bKws3He1%znkABU@!H{kzt?3FXyULTO~`4x~d7WvheF?eiupszU9Xsa>j{4KMuliU_j4CmG2ZFiQ@Zs*kF zB*4BDA|qD4LZMTQbVXzIeEoJFYqWL=A`TD<#wGjlh5zsb)*E+hd~PBfB;v zPz`6roen-p2?J`bwa0c14ucZJidG>K!`SBP*i0Z{iu5gGj93`8bPHZp8rG!|=8tF6 zFp@tQV%RswK03!>#1hL9DmFJ_ehyWjw&w;r4i;`4@I6RFLPtg*+F2bOw$fHro}5f& z<1{M#@En97k|&y%&;Tg~3nZ#!M-!H^Wbb7P}2Av9u1tsqG^kw?@o^}Cix z(-BW9qd5wV3{NcvUM}yMq?&Hpb^<>iFR@nfoiD2S=@Y0lV=DyRyP!%_5<}7_zM@H;q zVP#wOn-LCL*PL4$kmBJH4dr&n{ccUebgRsP)WCsdhJhnbhd}NSW_2v zD)d(Rud`VL1_*u88!@_$`n_=c(?Cmx5zwDth~JtR+c?RMVJb zScy-=n+$is{MP%hXZ&nv?$RFb%Hp+>f(?0_x*UN^l-q)%jO;j8zuimi+2ce&+x!nI1GU@o1)Jb+)c4kM2NOd}L;Am#jDKY^Qt{hJhWi0GNeqf1-i zYT+~p5BN3MR_gsheP6*9j$pG8{$c36!QJm;U=eEzsnB>Sr@d6%@KF zlvwjrRd?e)43Z{gLNjd;VV4l--dGEgAs)(2OoSx7A7tzzae-OO8jUOBE80~~(C@|d z8K_k#Sd;?Ca}Xf=54;%s&Mh+xkqT<9cC_*&3Qq%W+d>M2Ch!Uy`$IC7lw?AI)@5jv}KcfI%fjuiO9t|?U0v_uy7Gbw87MD<%C|kF2?0|JjmZ~0D zm?=9)#eRzYCYRtI;(tBG80YT_YMza)P`gJIRa#c z+&v>kNy*xJg_)|nd9idSTcXHfdNhMvNPLCGq|^F!Xq070J*;e`deX`-(0EK9Bm&Ja zJ^ius=@Yky@v7ddu#mZyzi{}~^dc8Ke?9N6CQbE}iPCF*d)Z&u9jd~6*97Z=ez!&b zIC!Q1@O+_sI(>Gw&h%&i8jkDU^m;jK((cmqMilQqt;%}8-dQ=d=KZ`4A{TCkg9s8| z19A`c<30j9lar;=t|!H+CCvdv-yw16!?!;Y1gG=8_b=&3sacQDPIO^ipLONm43ShX;~(LUiPu+k}J2O@^_2lKft0} z%ReQOPOv1(gNclZ0+X|MK*%9_0}W^Ww9rkHmk|Zw>COhVts#VTp%_0(TaJs&L>r|O zl&Cve{574EZ__7TUnOYJ`dy(JnkW_BsyoD6t44X6SbW%$V(q)IQ_=V$&QJhsAtMpW z9ZtV1;S*h&8P$4hgROvpWlTwtU~;#6K=f+#EQvwfh2zg7@Bug7O=(}=9d{;0g5|FWYPZD5fRCNv)~FcO+VAUq`b#+BW=6d*_Nek5fl?z-x&2P1ghp;@C(g@!%3Zex zZIY`N3GXtEF-x1g@7-MMf%Uq&wMDP1-}zh194YOuf@bE~w);0ln0%((f4jh z7txkWlwBIQErniq-L|dII8JFetYYhacN&P#Ue&br)=V8d9$U$qG2z(@qtErB?lSuw zAFdjL!;AqBA~Ha(+T2D5n=ge*-O!h8UfZ8Xkgd6qMf;0NyZEm4=p7|75gwiO?opHr zA4Ow6WN~5iARs6A_Lt&LVtjiG)%^S-d4b6JjcJK|2!^a!l*Deb+uqZ;_AydKey>!< z%U+6Xq8O!g9hND19WwG>F@rStGf(=IUAT%}Iv!H%w7`&BQnhD+Q)6B+LrK!~V|{K# zJH_u_U+d!a&_#AMhaEq^#)gezHX5D2Y|Dr}Tv5+^kQRKE3Z&SY%|?r!*%`Al@S(>T zt5-+++#=8l3-v_vhxE#?vk>87(!=Y)9GVG@bk}f&rEDjoq-701Oc&JW)e|0ngdL3_ zZkw=tZ)6a0)uU=~WYMQ;b09)ob<6-|AoS?$kr+~0IXwdt#XB%|zQxWl5X zfohW}L4m%%U-1dPmJD(xk?%}dNbdcR zI@@B@QDxKJX7Xqn%TU2t}x7L1k`z*TvZ51k>lKm+CRL@$ORbsob4MNiq$Sp zGn!j+E@B34^J6lrBkH;$;Zy(FZnax5y^d&wykl zsvOLb>RPw0XW^1*5*D|&fpI?kwJ>b7Z3^)xCfK0DMG*L7b zRdZ~10Vk9NMW-4WLD|)0M4Jaw_6ISxDSA&2xrR|&zPdtO1CxlS5T)eMtnVCTQE@;`L(j_xmX0Q8Upbm9F&*WX!S z|D*1&6tEFfHGuXxBE(jQEMdJ#si84Nj_Lz4$jWyc!m{Y8#^8dlt&(CJowS`19CLxco~ zFk2XktSd%n5ovxT6C{rLbE?wmXFAqi=owF^wSLS#cM-OcE8>U{-|AZAW8gt=)YzuB z`{A~ldk@o7)wdz*a|GQd*RX{49AGYsO(A|LOaG86X;bEVUwQ}mXN5MjNl9-51nU8W zWBfz-AIWld);9Kk)oMm`jg=n*im-EM@6FB#V>XxxO0XCZrTK!yPh1yoQVZf$p@`BC zcWEwG{UPT{hl>Sn9KL;jE1mLbD)JdJ5-6oI?wr8%_AXWy;$f~zE?!e`&K5t?AtDY3 zr%l_Tia7{6c#<2u{gjF8qqTP9@kU}XD|R=C{XQ-1kqo|xZimeZ$H>=Qw`<6pW4q(Gp$@4NhT|^Snm8(%aWCEpzX3nBB+;nIrj+I zsN4&w*cb@@gl`W$CArgi;@MzyXQ?e3g?zeABzpQEy|n1*pb&Gwm`n(ub@!h>hn1tH zo}nFJROV0XZbEh4YKH@(W3z}qMntnAB#s4)r9PKm`DcU(alsaK@cS#V^ksv-(iuHWSUtF7~nYAT7scxa&n0~m@3A}$i7 zNw2Gbhysf=5kZg+2I*BwC^7UBdXX*&x_~6IE3nk17#8Uuf|P_}qy?mkB?|jwb&(wN zM{+XfeZR@fIWzBm_nyq#cr~Gulj!y*wO<;G}IdH=y@Sw)uDo5TM3Mg)iJ-c7hzk=XK?cnC{b1D9(QJ8qJ6*&A*& z#{f4MkT)|LTIn3U{=~S=+vH`jjF4%ov$k$dFWF~HGoe^3RxiHyl$?O4sJ*QC0C()~ zXcavrWtHX841ESAVcpeeL&Ziz)O7!L4hpHj&nzVxo)dBAxi}6%-_MZb9M9sd%j`KO zA+8eoVW+QDg)4wyFsaTk4cAtjN?MI;zJ9X;_8Dq6JG3-9dIDowN#HIYYqPGl zy~x0(Mn&e|GuN`QExLhY+wIJ*&4aUVo7b;cYhjw#HK!Tdtjw%BNm0m4Qeck)F4c)< zSW8g-5eT%2}@mcJr2gJT?t%dLIL7$$CvyW9yFTbvUss6;h5po}5AiJn_ z`_nC2x$>fFu50l?v>W9e0DnC-zuKE;STY_mf$?559CYA5Ki#LgS!1O{VQK-xZJOt+ zyutIv^5jL0+>rCryp0<*rijl>Sm+gOG{&kxCA~GYm-33sCeSQo`b4PeLPgS@y%vc6 zWmZ1EStuiok@d~@fmR!SHJh{Qs#X$m=>@wU!LQxwaZm0$AZ)R`+#{{$o5tE6ib(aZn>MGAsI zJiv|xJfM4jn*y8{us%KPZo8dz@p3|}UU&6^nR2Q%B9%nl``FJ&ax%6z%C?qDYKqe; z-kx}vp;#)3^N!X{*N5f8?E+q885E>`0=}(aI4pyhDao$<-4Ngvn^S8RQS125iYY0 z-b+lcb67gAR z!UGxhIGhrA&9A&Tn8qGjMG(nSacJsT{liqKC^jVAyw-@~NK(Ia+$AB!kij-42%lzQ zIzXIUuDpRh2dn=_YSPtyXVlipDN{9j)s0Egt5cprNaI3+U#AvrGVib`oP?g!tj6p> zJ|43vmEbNC&(5uI-%zenb1gd_12b#rKGF4ODh_tBUrYgkHDSt7efeRE_+%yaP3t>G zx=b#YEB5TC%P!;3Qe3)6>K(HZrHl*ovXEbdckZ=F@*C~^GJm-?o&({2#dlD6;XS#4gjldq+V@oTNfo70l{Ux|*CI)0dzs=&jAs3)Q zHvt|~;ly;q;)9*;s25-=S+T}u%6u;(b+~2HLz)&+SY)#~Q#7ShKa$jBlQ|#Mr1bLM zBkNccVg2t2_KU^ZUaPQV&3ZSUn*S_#b&ln4N^AN8HuprsS;VPALmx(scIdOr7wP2b zv7Oy6twe!g|GnAmP9YN%QtS*l{uSVJd?%r#{&6Rv>$#(;ABYe25U@*A`L=%CRo{(-cay#aF&+&Ji zXL63bK1ZCQMHhL`uM^Y8^ssogo^{@}88$h26(v31`eHnA zz~M66`cXI|n^*S@R85$m4ARMM^^aGcT;othtZm`q%v`={=@87eY)`<$bx^R5FCy+O z*nuTM)F+X{T&ROLXHno-Lj7ii;I5sMdD6OSXm`qpJm<fYZ3~w{(lPPvSsslM2qO*EkOhz-uOB`t44WTcC=4(5 z?HSFs4s)8U#5I2$o&1#;ROh;YO;#grJtu}z)W59sblAM(R|$UnK5%-;vpe_v zt{gr)hx^;=)0brRy%K+!)*~5e&Q`DJqlF`df-QJD6ix1?uD|y2?}+DKvZ{_zeNB%ufxr)$t*Wk;rDg&VYB!K``)WJBzfeFM z2xNH?NCrE3_}N?w@^bRE`Wl^41A^ds8Xtg5i9jA1@?8ylqG&n+#D6jVerQiElr<>G z3Ii_L280}kgbP5+|MwmTPoMovG_`zta*~G($ld5cAofG@44~zIxt*8S&we!&qmFj~ zvfkX<)SXuW6R}@801*D}z}SB#_dxL-l?6KQ0lMNp)VVq^lzsVsBmWWd(^+5~!R7c$ z0im!d1BmSqsS=2e_es8fL2gd_9zv-!S$kYgG|9yaPj{d0| zrv^|P-2;F|%#VPd*L-WRKvHN z&!1*FL7Dd&hIoz8!Y@S&bVE|LpFrZNY7;ckh(^ORARy-1(Lf+X_ZRv0NYbM3O>I)k}0qK&UKBqNMll84cR(QL!_&zs`z!Sl2}9Qg`P*EH#@yLENq z)%|TwLZxwiAHsG3O(zCvZOb|=(+4tp4Sc%UgwAQyaEi86dxTu^YB(g+$y zj^NM`YE4gDaw10R4=4!JTjdc~^zRlo5}*zi9`<{@MtV6kf&7K@h!9-& zApP1?cHprVQe-AipGKqimP$~9+LvVV;`a<+Uj0d2aNu>heB(f9;(@g21w+9QMEO6e z=tC(d{0OYxCKw0^Jn*lPtA)KA6XTz2ZSu4%I15V9rJ%$(MY&^}hIq-Uq4=>viC}-m z)J2(NfOOff-X0Zk18uAU!lTPQGs~aZ#ds@pW%i+3raDU@h06olTQt`#T}JCLX|UbF zB;M+~VLJ9%F#s|oVGGI!PD@Q$S+KcFkYrrY@5E$|X5*_8J94l( zpSzT;^{THKk3+?j)C|4J@1Ul;(T2ILTKLQ7zU@gJGLVx^Yx&MtdTfTO1Uk(cwap_@sEqVd+t20SvIruNw>8I zE#dlF9r54%(Kt9Zshj}P=T?I;BeFvo912`JV^qL)u?3*=Yh5DN4zw<`G%i0p=^p%V ztY+{eI_`%XklL$T;f%y}J$uM-6uXNs{x7)RY2-%U@Z?{K2cS*PDEH2))Lm6O(K(BS zWsneCjK^=?^KSU$f@%ZXji>k%Ys)qVx{eum=B(4$hyd9?Rwu$oX%JT>i= zg}(l@=MMGNgki)Yx_#>Mb^i7}q`>peb5WvgC?|ge`G~4*h z2+GI7B_pEmsbb0KKZ|v@wuXHQES5b42nfo*7t6%i`L9~3PdctKqjbN}K^VAyv4Iv* z!ATDJDKaOvf!mOr)4n)CDw_n+!Ll3R{}hGE6Tt+C=4cDOcuMCt}n1zCxyIf827Vl+EZy9C7m>z=8lTsPrI+H5*?O$hDmrbIXE z{tHQXv&FBhOVRoBk5>y}i8If}$Kk6~<<^zW3>9XjFV*00_14us6|({yQhR_CowQU$ zBuOxnH#Ue#pK2TG)hJ191!6oFNODTFD<9d#A;2IT2VJ~=ZSsVB4Y(S9N=$a)Un^^$x>}i zoCfYt^R%j=NU6O6 z*9QG^v;T6K+kpO6*am;K6P4V5W=>F|ak8OWc9*e+M4)bziad?r#gqt-&Wz?l>*z>q zS=T^OFAg)G0~ghkVkbPH1N6X&3i7L+0FJVhr|vG%gEj6O3gfi{%hC3ehXDME7Zi8E zw`N$v^mADIx9_o&gu&nJx6)Gur?s}o`66?$HT>+AlG2CQWQ0nUqO)uVAyE zZt*^r0B1!1IWPt@H`-j`KtK?s|NazvIk}oM0YAzf4!{}f&&W8}UvpjKM)TjQ7k(P) z(r98#+vDDBEYC5|G3QE=U0waoCN4^DnrsZB{N)DVhr6v%bf#jTz^SK6NNN})+5B?% z+cELLDBvxP(w?#?c|$&TkDulug}2=e-7EEw4djwz8jqXWc*XcW2+z2xu_xvpTME3E8` z25+)bDIKn+FRilLT#6%C@8dKjHlm|c7vC3WM$)(u+~R|Ng(|-+ zlD83*DL|#4fl2Z^GA@sX!@kSz`Vh2zaC5+pHojf>q%?EC38zTU^$YJu50i+8fUq=- zf)+GD{v%(AbE`UOuQ1=vl~7pK!{@FhM_;kws2;wYbaF}Gx~wc#uiBjwi+$p`F`z>u z6>lz@ygFg5fFjUUPoAZ@+GJL}zKW%qoTd@4+qac};4Ml?4^*=Bz5n@P3R~DC6b*JWytAIZ!4x8MT?NGK7-f$wS(I=e+Di5 zCz`Z~rD)`_oZwhvR>xTBo!vwQ4*Gtb#H&G=wDIUe-+_Jt`It-$H%^^~dPt>xbyN3( zFH=z^vzt6FnN9Sls7g4Wz!C32XowI0yMlk*;>SS{f=G*C5B# zbY+PB!Pa=>zZ@;xDbrl*OQgWPs<=cJ z{F<2)cA>EQZK41kQe^QS;R{5*IcnL%N*Ddt$4p}y&*6Y0-l)~%k!JLJFFg1*^XRVN z_D$bvKX^~(>Ry6`u3m|4wlzb~EpNQnhDavfv{T=7%ug7DAt1F=u}krh zZKt;^))8x>@T^4!cxr(?AO6gK?>!9dnmKR?Fc}Qyn!%22bj_kDVqbo%L*TF&coILN z{w5lZW`&5=Ggst2EDK9hhDg?>2r4c)yoVFt!r=jR;@inn3*try#b4IFTRqY?mO$L- z!_duAh8o_cPi8Zz8bCQ_^rd(+b?cPyNG$f=eXkFPc=X#uF@iT)$DiQXKxo!T#0Q|N z()53da;FR1HYEx*Ej2N_xmC9|SPi7ncV4iwG*&T}nz`{dF()2?PrIR~X+nTSWIj#| zYv9>`0d*d|p+F2=xoA^Bo}!)qAtn*9t4>5Dy34 z|KuynPV*}j3>BSAOlE0buwtb14TL70CX4*6u><%qLxG2|Mp@B8S^~9lMdNx+dB~M` zSGFk&ID(E8T|}rWMe(VW%2@^mF+hN%TCWFpkY#`sNaF)c1i2=3(nFLxMC^&ZMjKK! zvVLasE)_1W*Vmh4@>PoG0o+7#4){`7Gz;p?P`Fm=X5?oCl8IQFURZ*!Y@g!|s?v$H zzpTUeHx|vnzER8?1Al5oZ?|x=b}jkv;RvwQ(sr{z(mv9R&^Dl zBz%!E{2_=cT9;>j(FOA008@E6KDyOLpXW7DrRABG27)<<-T|qKV7j$7?FPE9t%NG_%dRHidpS($hm?2km0=tPZQU)%==R27?hi;JmF89Pl8BgZZ@=y7%K;$zXt$H6`!gDUE) z&?!r83nuh@Up&K94xfAdINF}=J4jf5p6~tKAQM0Y`-YxO_kxw=_o@;5no`9%R-$OQ z4Nrz}-32v#m3@Z5joD0^`y&rRhcij#iSVmCIH)-YVhbZ8L0ak*oMGirCU>lSsieh8 zW^ptAs4?$Bp+tucVofkCY+?TCm<|&j%8`0+gm^wGhfP*|;WWgj5j9ns&2*)A4z%ZH zsUMCHb%!6<2629Td#sgI2qCK+_$Qv~veqdJFuWAqpLfth@dxs%8q{Ue_;Pi_AxmX` zJx^-K>r0+%w9=gpt&S8SucY>v_=@a#cWv_48c{6SpATqOQ|i-g4EAhUTmMG8iftp< z2tc>qs$l3_{p9P{$zU>sH1VeNoCT%;UD-DpWXI8xQu0b;p_DwUz0By^PyIRy3wzt# zgT1$R(@F=`eFQ&VFQst6h04j5+UasUH`=QzlVnMOzmZDE!K8$~xlW{sz3$+ZR@u&E z0(d?{4m>R5Ek;OSj40LlHPJW6S~3LFhjQ5}ginrf9o@PlHl1;&j&4YY5n(c0(>p-}OB$``^N7Rmv_ zV@==(NMD68sZUjHBBuyg18L2B5ATtbkxrgnwJZdYL<3zL7|1HupF2kN$um1A9r@^tUD(qT|26x>>r8xjr!T_+4BUGqksJ{ixw_jhnK* zbUk-{yz(Xz&ENTcfr(E*NXSEA_N<3Yh57jg2A9=}T3_f!Y~78cd=qJ!C$p9(1~PJW z_CAlObD>hT%>x(nXKvc$fLoahWt>a880HUX5#V!%N0nb7x@r0Hh_rU!#6GZDp{9v$ zcs`i^bAjp@gonlo+__8wcP{wAoy%Vf)W0|5|GrH9dt?4pL@AURC*txQZZJFrqs%ep z(^(N^7p-J{>F03CU&;4k2U~g@=JaB_!Fvy!{`pi63)A;%KR5Ys=vrh$xMRVPF&4s= zEztO_kke25o24~G>}rW&c^d{edsg^G73R(xQ>|-`WWOrB!ZmhK3^&1%w-!1IcVO1V zl^jc|B}X)I>4NEq&n0J=MCRwY`*x9DPuG3B>8Y{X7TU;i8`xHC)(ja$g=kz^GBzON zpfKLOJo*86G{+BogEoY-Y+1q=D?12&RnU(^GlI~3$N?W>do;G0*t7g;1|sg&CzdY8 zTH27KwgY}s&K4uWP0^I47G@XzY1)q4_HOR$u&*MlyB{=|(4JsyIsPsWfwrB>q?XjF zz~EH|S}-w{1D4yb3e7{~bvf=|cXRa+JWrwX|0CDXi}jgqOdud=8jv8!|K!@u-N)X- z?XRtXfkDcyWRrhiUs18q>%K!`7|7U+BS^9*zKLH?UgIU30^7pbk00393Fk(E6ePk# z8mbzavQ^1Ve1myd$}|UBxhd)vZ*Td5-S(FOMnv)1ZxiY!|D;Y=_AV?la894F*f8VumeR1kFxo6w_gd>Xx?Y$ai){}o8pZ#)` z3b^skBH)_5iwSUL@O|U)CA&?j-lOsG>Gk=wE+j6R5E#z%-n-u0)m}51I>Hj@(6#R0 ztFL$2`vwquwp2|g9CF#(U_#ys$dDlVES+~w@-bZh)Mq+SKe1AAiQAPf6yOu1-u0R99G#>-I`OUn8L~9;0*!YzvKcwDzE0?^!N^}n$e%l$skmrX!>GZ?j@G?N4F5hwa1AVwR zE%w{5zH`N+wFIZ#$Ni5+-9E`YyxvYdNzNyl=YCG#2Nnvd_s6wEPp%_)p33WykFF?u z%6n58A48^MoIX5FxU5~6ri?vVM3ZXdoeE?YRiZ%+BGVedVR8Q`n$J-S0Un;ccVp)^ zBUEsP>QB)j3+e$y+hnQv)ZfGpsNe{dflUK+LMjbT!2b^E--c~JeHc7WNv}W)Ix4Pt zSb*WumUWV!Z@qQ%(+d%$)z5m(t?MSLvJOVJkEN{CA}u)rhJH6}`PX5X>DZdSrY|2I zxVY6|l~g<@pk8&nESub_)7TDYT0Mq-bQ{$DoP9Z8cy@kpc^i|tzZE*a6wFbPY&BR~ zyc#;Zz5RVLyqNV5ZHv#rFZmU*PFe{}xi;O2?O(!|wRt_UU(VmJ zeml_R5RT5He#RuZhnW}oOkhMU6I)8gog#Q?S*QZp(r& z!hF<(;eK_~5W_oc*5npT!>E$cp@ldnw+9jD>>KbUB%ze#bytUPQ#gU@aDUemMp3-M zLJ3Rhh8$-Qy}?QhN$iF!DfrWA4eZ4FrxRj_+zC|VU-#Fw;!-d|F!{ib56WzU{eykZ zo(v2n2Hr>WSEw!!EgXpZAEAF!K~z#*+LCzx4}wb-GGGhbUnpnrg*iYbpN^QznV(i#9C%N zyL!svm#Jk%as4;*221;4^ECLcd-~K>mP+tmA^N)i`HX zhYZ;`doPl_ zM{#n@?B-+grHCDU)9mr+^!@Q-(}harLb~S&WvBR5udIzv`m6h=`i2C0&w6w0Ot-ht zu9(>3GmVOf1KN(BrM2sp!022uC&1VF8%pjzcP};T8Km!R;is}q-V0!3v>I4Dl}10a zL*Cwpuga8;CRsP#38TAZCbCs!C$qO-ldHHOX~ceBD#*YQvNkXH@Cm2No&OBX5l7P^ zFI!w4X1iMY-dnR=FBE=5#2EOBlJ)+W2uK+Nl)sTI2C+r=?4594uiZP9j~fI$`ukyi z_00G(-9Pz}W)Br_xbFxx^ZBOj6L;t5HH261_siW^@oE@w!;5)OsR4G+>BnLgL4-E} zsEGDSRET)X-{N+a(<3R9sf&lZGB^8?H_9{s@|aT@tTHMcu)N`MT>y_(6IbU%M}#DI zI=Xy>Tf&jPkCyAFx_#1b$Z z9(}a!h>+Dm$1w|7FCrql-dz{OrfbI&Y=~r*dlYMp>Ri>6FipXr{b_t}XI1skf;J&6HVQB8m^h@h=D?>yd$5Qq0W zK(tTAT@ht%SmFTZ(qu?yFE_AN#QUD(c{v3ezH)@gZDE*Y1Vav%{X!Tor_FC($lG-@kEry!o1ehu%IBY={5K&sOPGug|~{(Y(%k#hFi zk~6*{1f+qum!k_+aJ8%>MN3o{ejTH0Hh!Ixse2ru&bOz7Lu+{}O$GYOsO^`AJev{r zm}c~))@9qx?qzJ|`6Y{n`B+GiNs@f-53^>5fa|>O3aJp~r=Qi^^g}A3cH9UxJKKjL zv6w#ZMOmm#0b^e10x2P*nwDdM;+0Mtw>Og09Lv?RkP>#QNW>LfE>C2%hT@Y)&`WB4 zXJWGtN`a#cvA45$J511B6#J5rs`1Qzo}{D^Um$TPQeYxGH}M~_H2A`wlLKsbw;oS-Mw5xx0^GJnI> ztL}CG`1rI=rJq0d6pQEBu-xqCIeSK}+8MYqkYq{3$Yu?v$gi8VsFI%Z{JAsO@2+m! z&poH_U%K{NEti~2Lw1CE`+7OXAW!GPsSswEI0+H$HMFNILeMdzX|s@!+Gx)@HLlfO z1*r7UzuWO1n=Hl>a?iO0G;SU@l+_6UAk=p_i{TXA$*U?jc*8VU>hv&UoV%wlYPH>C zJzV@-bquchJyErnSo00@ve4<$)A16pSXviV%Ves<(Ydr**$H?|ZKHQ^It>H#>yZ2` z-%coeOUH}rh=fV15FD5}L8RcKE_gTB_;9IeVjd!PfV(rO2ZFuHWs+_$?fd>p@3F8( zc%ECNwND!BmFP2dG6DQT9wW=}DnbeELx=KJdm%UHvZCVkAc%ILhOl04vH!qz5H0q z?$vu3EWmo_W0ln{kxdP?(gFNP^S*?UcsVlr=F6%lPpNBrPknLrj9*)JD;B?yk<-O@ z8jn1;Tgv>JVQ9^bG3RTCZ?p4F$t|H6D^0^E({WvQk=u20PhsCHS&T&|XH)Dn9`)qR zmigtTYdcGmEKqLHP#%HTy8>a8l%ta0esXZ;OD$tcPmjxTUf$_;rTG(e-Nnr#mL4Ho zYCEwJ+8kyT+*Pm5OYlI@DP_M&Ka6E16|$hj#zKVX^p^zthw(D@S0V*lM1fFo(IILZ zvI#~m^Vx_IYQB<@R1KoD&U*~*3O~3NJil7lVnM#vz244i!LeJTrNUb1A^Me?MFn-L zg;+67?@0A4uBw?S(ZlnsY~dUKB#ql~lnz_2Px8qYgc@#wQ7Q-ivDM|GLRJT1bghuW zY+jv!&xel-=ZIClO(ed&5gmbeVC)f=SfO+MJJyFyf^6v>OB-M6>rJs8EW#^8Ff;&- zTrb4594Fv2o9-DWD0MZGcsgeta(}@ouE8&(5vUiXyhe%_Y@ThO`IsXeXG%uAjzNOmtKn(deOfP%P7Mh{7>iD>*~zBFBUIoN<+03ZMJ0+N zArUEh(fKZeznyg!C>{Za2d0)UCqo`N?(U=V2kCFiPp%JlCb!EPLPK_+M=xO_kNUvL z2C49^Y9Fz*KJRr67f?ctPOA$M)e$|n@Zc*Ix?E<~d6R6c8tQ`dnWsi1P8tM`AMfoHl}5IZ@K|mzey`eSw#Bd6G1$;r5ndEa zjPONRc)w}~>~y;}4!+8p?VNJ7T;6&N8aOOZiV<#x1qS@GH!H$^Gpr>X#%^vlcR$zn z>mVd1cWl)bxO|B0^Q|YOW`FTa`|2i3^6NmE<35-w%Zx)ffrV`@tAE)tagjk4ihQ_i zW1Vpu(Ym=Sh+fpeTu0P#R2Y5s9&Bq?B;_?{)#@ywidf=q^cUEeu+v$tb5?PCrCC?o z&`Vu~*s;R49E&tqJ$~Ac?0C2CAg+SxYp^Dmo+Jy{o=@P4uf$!)=9ESo9q3%xZrLrwTY#aVIjC zJ5E;Ap4Fjfw%lg)ABg;3AU!;{tEM|Qgcv#fbdeNyWqbHc+lb!P#Z?xlzGRbVzHHDr z+VAw^jN7y`nLUzD8Gm5vaD3eq#GQK28KH81tuZipfb>A_Rh~Mt*1{`orw*a{F1W;F zUqQ9-7z$$*ay8Kxa+&qZY(K@PZz*Tyv%SOuQ$!xO$qg+e`Mgc|K@GM*tMqO3vbvbB zD*4xHy%SsFJe1I)af!=zIvI_~Ys&K7o-5$s*5m`rQWlEgdx_l6aB8!SZ*{jl92dB{ z!1)%TQ{9DXhS*^D*mkTX0)xH`98>aiR@*0-LCrh)=vf_g#|+U7&DP7{O$9Y!L8V&l z&L{^r&FMC@W46xHAZ$2q(GK>=;~iB%kwFtd^0~S)Q~k0$E?^LEnpx%2RM6R=yuL!K z$tY}d+EUn1iNU@z%J)yW@iE)U-{C#~3OD>GTuSLr_#MiL#(8t0R|;X`vf61rW6@@g zMsqFQbF44Tb~sp`GSf7P?>F_Higf!(b3V+)$y0}~l08?Wl3~6YFDb_x>oEI(Mb^rg z3NEW9`^f~sVnaCZ!XTGfs>cS)J0|R_S!h*a1?U%ivB(1;KN7L)&`&9r|78R}qfobfj zOS636G1YR>a(*!vt564To@)A0cDX{4v;>Bor8u$&l8qtWY+IKqhgfxNEZ9lcayqdw zi==}-mb6-H$H#>dKW=yq?O17EvcsBuQEw-Xc;B+iLay_|a@KEWMM4uWkAx#B)@2~&?)#}~F0d|QhA`<*NE)-^8pNCXohxU&Tv2NXAomzDzb8EROOQfbF zXz-hEL+eIZXfIT*I6fgPs_N0L$zYi9ufc!*;B~#X4!YA&nDpPrt5p?<^RMfjCwj`E z6x5PJOoff0&} z{1OD2t;Q7)474N+v{d|65xJ-+^84I01K0`LrmaQ-FS2DJX@M!K@|bsla{*~V#KTs? zcj@Nsg*F)Jwv?7!d67|ozmq!wkDs&ob5)sy@J*a%W!|WumJw>=XGQe<>SCC2z8q$t zD z(t_pDvr8%v`?*Uwj9`Pawq>NoF7VvBOWFdoHB<%1-%s%YOHyr9Lw0G903 zA`4)@@4)o~OP>9`n@2=^H;*GDE>dQDK0{J^?3|WUrG_%e0}QwQoKkNh*y`l79YQ-tM1Qmmkr+RS5F6B z`ySzL!AySjm9JiOPe?^u-L1`1q4{e4{2&N(ji`wpQ|sF z@ZR5i?re8GM{GoZBB-i@BgOBJ5xJX0*uHDEr1lY$sgAb7Dhu=QX3hCZK8arS{kugH zSR;mb8f)Gx6#Rursx~^bIGp)O&^V|(Aqop|8!1!Nqpa2=oP;G`k`6Y+I01Z)0CVT; zR5<rgTT(C4~t~*pB zIL|WzF_dRmR|~Nzi-8FXM&`M=rGNO;R7b=rI@8OP>G&L?v%4wFGhxQMGT6e|xFLg% z{Pn&zVN7s7ksN(lBI41#Da(#KT+3V;N)*O823Qh!hA6SAFukN9N@GS{1J0KzPts;a zltjJ^8b5Qbd>sUyPSAKoddV$xqHI$Z-&~#wiWnL~|1mR`EAXqR5saoC_Kc~+oUHgz z&T!$9Afz0K@!%X`SQKzmmYa~M5%=&BW%D-lOpoB8_3vn_Y*Nqvrcq~U@e6AYlEOUD z_w2J%2PYqx4Uwfi^3vK<8q=AK`gb|a>=UVtI4TfR36hV_LnjP1z$ zXVic@h_pDcNif2s2&ml#L6o_W14O`Dm`(U~Jbf zFTNW1c2HE-%bXn;4nYo;T>X@7GWIDemdmQiQ>JvcSt#gR!>oeW3VZSqg!RMRb}9@m zin}=*P@jf*01G{=M>X%JYCQ%#e;S8*y3&%#Deb`ZB+-w6K`x4a2l4#xAbUoqO0IOO z8JV0EZYv$FvN_FIOK8CURfj~895o@}8|#^gP&|4-C|;D? zU+t}5$(y6UcOO0t^EAdP7i{ZY~`0_CFk=3YO=Lzl4Hu$Zt){9g(Y z&Y3SOz;|=3|5n)WzZIq*1#I*R0ZQcP#i%ofuU z!g<1IwwCdZo1*U#g}hz3$G%?tHAByST)bO6ZN2Th(Z>!-@A8I8H}6?y2FxkdNz@BT zIhhBzdhnMr&T=W?S+MJ+_y_X{Oy5_{?J+x-nI(7-VJW&UGuTe?B-TFgO7S^3vP1>A zG2eZT{B75o%dtEf2gV|$QT)nwuY8%TL=1r)w$FP`iJWTN3J-rVVFDY4gCrhCzwSUi zZ+Uk2DITZ7i(Ptm)o}Y|s;bPf00N-7WDMJ$?uwPxk4J+}KM+pQVHumL-Lg$$$U{GX zHe?c=7~X{__bWD6a0m??5Dz8yN}O}Pnl0O`wH8TH(Ov~5_wAgsxdCF+F2xv@k^Vd2 zbYi#&5&0TOorZ%X_lH{hFX}WPHNW;>tM3^=n_~mS?O)VndO+&6M%2Hk(-l#j3$arZ)b7ShuTo2t~vY8VS4? zURuj1KK}>`s1pL@w7*u_)896VE&m~}7Xy-K^P>Dk9#;q?-}!&~2AlQjemzY47da-B z+(dfkpw_p}xS~X?vJ^q#{E4{Fzu#`2FA%5B2g-BjG>*?6mnF0`(Q3!~u*S|fD7Yy$ ztOi__<()pql(Jt8K_RNn%`|2azS*6e+iN8BHFHFo^QZyxpniO@t+((!sp!Jn?5#TY ze@tEqtE;41I+qE@GvNa?p0&}+Q;ymNBgj`^hm>x@2UF`nQ)l*DT0ZI2*AmPVH!NHl zqC%;#>D=NWXP=N;CA%oy$&>j~8uO`fY;lXEU=Kb-x^XkoBOntlDBtRFLnyLoI^-hcU6NTK-^;Vkl4Mut_bjvRb;+U> z(C}D1KrFX8f3TU)DO;*1F0vuc(%ht*RJ}}qT^#jranDy;apAUb)S@nue6Y zrgPTM03xMn8{IcJ(k!SVn}Z&827qCe_yVu+0z?)G7}jh1q6uo-Lh*8sCE`CGJGm4X z{|zjAE1Qzw&*?X(BpK4bw=!2MxhB)kuT0w#_CL)H%|Qv~57 zA}|1JTT8_ki(yPCrXBp-qWPNtYPhFHp(V(-+SWUG&7VC6dwW9)mI=cz6LRkHKs4{*W-e>aypBP`pOl8eJsKR{ z{SKk&Bk)(8-eK5qCPMAlF|bzcWzz_LFor?W!P}5g_#lQsQJ_1J--u`|4dypApD2B$qtNaAnZWi z7X>A8-K=i;Hg?x60pK&*t0J(!@hvucgZ7H$UWvN&t+@;0xD9SvL84=J4i&SQ01Wp zNmq}D1tSS5mL-k1Jvs&Nw*I_UstOl0jnj+w1}C`ZOe@f;VNBdjg(WVJC24u$5}K=N zU$I#|=1tZRbgi-XfNi&5muLV-Bk+!@@Vl)gled|KuXt0{eD`OFd{A)+L9wHM`<64b z^ifz_m)^B811y_dP~a80Lm z;8l|3!Ay%=M@eRC7Q$I>syCWJ(FMG-xcG39QcG zw2ZCq#xf+$mrcYpfWSB)CHS<&Gux7MF_={-ScGIs$ygiY799t3_%&=S)jS{-n7XId z9~ljZhgMG_{dLYwdC3|1pxbE~M%Zz}Q^=7|aozn^;C`06PDxL#TP^CjPnAU&SKo}G zD~^jMJa>!sCOnbraoee;>ccH} z96lc%K_itz%=rF~QhC2Gnx%JU*z`yoF29R_{gXdF>2qDK)Th+T%TTA&Y)-Me*dhg; zJ<5yRX2vMqXheh}B~vjm>aQ!~4G&?NDo<0~{E2ccrRKmQZdbX9Ay}g*E&k zmVJ4#bWjuwG!%X_9<%!Is(6sr#a?xM1Op>;*SKIt_ zf%&TJuV1^Z)##=5X0J&5sPd&C#Aq2lz#%875`OcK;O$7}%A=Xg$x#r@*Z&s$D$nrv zw_vycpkN@+LW1jtc71cq_jX;Y+YP$M!sl&Sj)*j1M8&Ik*C=VeTEFp|7uNyiP0^Z4f|4%XhZ@Fp0o5p6F5mmwR0_GRm zzxotXNS!F1=Fw+zD4mFNOs#MNyT=*34~+gS4tg5nK6zE!htjEmb7cT*miYZU&(%|+ z&4DZu?tZ5syjwm5}qie-N&fPQ? z_h9jRv}&O7*vMb$`(3U~k|D-kAjZOH(@D4$ox5Y4*RMAUM;x1+nq7GZuCowXfzSFG zTw~z}uIpC*2S9=!Ef7FO>2H9pK6HF4?oA19N9cR!;rG!6lLPmVJq|o`4DN+%`E_E5 zvAA}rz-@1}i8Dz2F^w`Xd?vzC<)FRoP#l=}2iSP{2TO{7DZq`TyvRTYff&=%<`c}} zUg-EH0uzttlOYNwa2~&6davs;XAH%ti7NP{pTbEVQ|9SSvV}QZZ)6`;kg13KS z)XdJLak-!Qg$j)>MY^8n;1KN8wZl;JXHal->Th#IcFn_t!xSFBViG%1`;=6l0egNk zra{XM+D29%P0cf>Ni&Np&i3JnL>-+vFAYmD*Mb`Q<$O-IifP#%>me}h%>Ihu33p42 z;>7gEQ-{@vkX$nhutvF>n&KM!{RAn_m(RkMcnYQ#Xumni*Vn%~cjz2CG6@?zXq9P(2c_-C`Dw`_Un)U23C()#(Kz%LQx5uh>$V79${pvXXKi4i2R8VloO z5hSgk%>Jwb6fDyD-Qx$wS$$2JH%ypk2J^bQ>wa@5R`nrJ{zDaN1EezkLlvq+j|HCK z-}~v0&4H=VrypkMVn1u=A7pQ8YF0jFjZJJL7DcC$G&kulw@*Rrivs;NyIKA~r2`Zp z!moyE2mmt@r;~XJ>DZqa)>O;QH#Pxbny#%QrIX3wX=Uaqk+vF?rkyG7J59sIMWN5Q z+znkF0FF)vETp$zgZ}L8YId>?x@omvgXHG8Rf@sST3|t}EJmPq_%)#}(G_bDGE-rl z(R?&1L9qX$qL0CQ%~VroT-LzMHfg|_T5OnDxOwd(t^9~tjm-FW#>oj>X68oL+?wFg ziI9$?snuK-ZFJM@8u|<-%N)0Pop#_52}_lrV!f`^ex>~yVBqPA{&sQ)su0KZR_C!x zBhdTZ7P3dd-_NqS9Q`nDbEL?!Ca0?qVVCBNw{J6!)b)_IkM}1B2|0b#6zLaXcHjeF zA*(fdsJVLMvZia~wj`Z?jQ6O`3Ioq#WqIX|es2FXvTi??;IM$8utUCiGy9#d<@F6f^Xpg}#Yoco4kE%46 zX{SC5HHW2=akx_TG3dj2?&6R(CE$h9YLH1GE2qUNUixdtj zv3Er`O2YYo+2k-r?_9@V!lw2Wba_MM2_xbtSKKV7OtfJXx?bGO7R{HEuzO|xB;FVD z4t-o21OOE>`Jbg3Bne;E9+P{F{{tKuX z2vqoPb^#EB0|+%sN}HT1GU)A_bOrr|u%{yc{BH3x$(9BMXksRbepei4R}H!hBQFt_ zF4Qb3Xwj+=C{g}!nSUk;|Kp>C87}8{Mh{;2>fWGr$&db_U4l>AFTam{3HR%wFl0GJ z%aXu8laZ4^io$u2Lble5o&)0&xR;Rsb^z(e?NWADKa!EH5-SM@%)};d_%EfirCVnc zi4x@|WY##0?H6KL8WhzEbef8D3|nAa;8|V((1F!Y7@EhnuEgKYChV%#+a{>U-8C;5 zG6Klm-6Y|YeeoZUf&cki2xFg+1s)C%5Vv|L5ZwPfpRZ-(ZY^PHX<_F6_fdU+gY;s_ zrt_C4%+`|;rMxr{lJArLGiYF=hLd~OnfH%^)zymzS#`-Lvb62M)B3{sL1^O%(&J(0 z0U0#HpAtLR+{Tci9^V7sy?uIL{5v*kIZq$95I#5`4;?!KR`~0F=5GbwUK;H=QEjcC z-^T#Dh=Z7X8YZRl0F@VdMjkn5< zbVMUNrkKdv4-cqNnVSb&f1ADgc+UDs57(Oo%a^7Vsz65}s=k!~AHO$Fn_G`YBY%B? z`uo5#ho^MQfWW|yHiJMf+r70(|B}8Z#*X02fIuHXqwdPid+*}%51tRxNw<`-{IKlE z_Vfeaz;8WXYX@$wL)m}}8-VXQ*XqyY6s03cS|aY-1|y@$yk7;8Ya`41fpv&C>+gHl zQcSaBeD#hl+}k)wEzy%q-i{ezV=V{AzyJK^mC0~#pc^UB9y=jk4l%K*U>-)+mvHuqMvAki>#4q4TICxM# zvUVGgPRN75_TD5wz(>eS#Q)*(%C}kb(A8DH#o&oty;T4GjiR=%22D(8>Sx!IzpG`( z%DGk75~&|6FM-^wP$@QR;8Vle{~_!hz$@9-ZsAxRcWm2M2OXOocWm1o+qP}nww;b` z+jsTe=bU@?_uqT}XFaL)WYw(kjxlCo&RJFSHHL>Q8(Mg8OTS;&9{JHlpSiioJvmYG zTzR*5tM~rwVb}8Uu!&#$n8JC-$oTx>S@ZtTclNvY@serX=7o4kKEy{wd9wrU#pxv-&H7dPgHEf?Ifgw2j}rD|yXA3&=wZ7V zofMsh4{#Ursn+AIT2`jLi%(S+{^NB>yRmG7O>J#+Q?{tc4?mH<3|~1aYV7{?b^E#Z ze(uEf`7<-|)ax=aYH9Zq8~@<5WFTl_)y>NzTN3iyC0-sMUSid8Zn|czR({``{Y%;5 z9eG=LjUcvl*(oh90*Vwq{KseA`gTj+sMI%F^yA83?Bxw?r1b|3*yz?nq-I0JX2wGE zk+SoX$;BDa^4y98P7i58#0qtm&5D^>>->?I#_UkLuPbobPo7SG_?=nn-2s4~jT9?w%L>UNP89KNS|V zKX>qK+tTss_&lUl8k5EDj30?p?AZ|2pgV|Dm@Zhk4$n*YV2*7 z*pOPJrj6h=Dpa~g?WcnsRjbA+_NlbUB z9t)48$MpHyxUv*y5cIwE$+kng(s_)VH;xY6ed0VH#80-m07jK~Sp&Qr$)Xly5ye@< zQ&MQP(p$jyU8NhVM}$Tv!bbF9>z5KYQxIh-Zb~1@dE~fmw3DdSU=VV($d(keD)AZ! zM>&!e@WhiE$YOA#x0__Y22hYdFRgbU);3zVZiLO3t*gG-+&ba=YSQ_ zfECpu&EPiwfxuaWY65ys#qJ{A`BUiMTmLEKGGPgfQ&{{5rT!26Unu`w;J;D+&nz%l zo4{?7bAN+yNX=LQ<3N};0m1wE1Hf4S!S!F1euJFQB3T29K?91J|EpNHwbhp`2H}Q3 zIO`xBu(N+~{=2|`X|5Kj2cOp<+5i+o1{C`v4c^?YO#B7@;r1r-HDI4`UxVzeN<6L| z$kSuqi12YdE$zpUar%h7*U=%LP}9$LFE>?1}jUYsJtm`?|jmf52&i>li^PZe@j!$B}u(?05%<>d^p1Yh%;%-^{enC9Cwp6j1 z^lP>CNp_&A;&^1%TEo7p-E%b+Z_;ZuHB$c~%Jt$JfA)Rrji!<>vg6SR zko1ll$2!IwC+1AWC}j#FvRnF&}OG1$S|i!so!Y!;-wug5ZB zwEMLIhQA>Jy-&S1ylEENtQBki&xmNJS1{8e>*zwCA{97USz?}Vgf3d*rp5%Pzu{JX zTp%!S@stMMiV>%a{tBuy=1sS+cHm*>4V}X|bHSMdS*U_2twJjH-1OmM!Y^JkH!mZ= zQ}J^tN9y10^jp3}4DJwkse;=1H5;J<_1P+MD+JyTm8nGRl8e+6*GcSR!Rxya5Gb%M zBw~Uw{oPd^Zofo?LiQDYKjc|%u@%Ey_+1dBazKy?9@QsBF|uT==phJJar{{>Hlh@! zUC{PQnQw+yx<~1nB?v|OzNWCP&vD^a#%brAkM}S;?d^S+E5lV=wx@G9j_0Fh7|zjd z#pC(yElQX-ZQt`&{e5nCwDzr@Pm2vls+8`ndF7Iw9*5Qn!TMpIB_;C5b*Jo*%T|l0 z`*ZVJrd~{eZJ(f3Vr&f$nN<#+NJ_=t;RNTQ@=8VK)a@jC1Na-+mlNH3Sx&i*G5jV@ zIp)*q?wy;fMoAmTU?g-=2J{;Pr%8mv;M1SF>@;VM;{~O5?znc=6|5QNlU4?fP>P7m zGT)4F?8Z614NAQt2?+Q%-$U7sAnkwUC!a$7CDc8NOa`4?Ivb+#@k~|>UZ5vE;+lp(|B&dyI}T0qnysyxN7iZ zxkOxBX~GZo+17sBaL{}al38DLIc{zz^0bR-LZ{g{OHaz;DEM+PJkzM71+2webEG`F z!&8wWu~;653y$QD+qoBEM08-M+-K%t)#fQsc$vTDwpB-y+`rM*2jDC%NGj^1nrIky{*}Yi4fmia88NohTB1dx-&A9i*xy)e)%?iVI zeDTaB%wgR`aLkZKsK(ls;_H>r!sz0yAEw?dC*O0SZLa_i6~L`*JFeEu#XgBUy6I46 zXSs7Jk2!w2TGr`Gtc$m^%M}to>Pa;uySUzOEgE>SS0BE=at4(mJx_?OI8YMG-=?$2 z<;#R`ZnZe8Uo<2&Kg<~uN}MQcv*1XsUo`Gny}hl~v)jm^V<>yHa*W&?j!N20`hq** zV7##QO-pwTrSe$5Xdka4^>+~-t7YKQUGb`V)Q5+QVL`5-EV}AKMqXlDo!KaUm5Jvl zx8i0-;@6B+ynBs{nNmuxlbOMycJ-)8jnf&|**mj$Wn^$phvR+Zy4LzpM}yh>K$p3e zw$g+5XRKYLPPrns{7ED32D1}wu7&y$`uP&b(M|rU5B=-v_4u$2`r~6{=$#uyTwi%< z?F)2#=N3m6PMZ(g-#*98omNgGjeL_9iU}(4K`S=)_XWEa!>u<8L%1&ONiwy9TCOFh z5ia;+k}is#8Z8eQVoK~2qz{chalvZ@>(AXtst|r^g8Zs_R)Ml}RbmqO)$Gp!WSb!x z>$V2BC5;FBpl(-wJ9KulOZjo1{7M=s*w^qSIxp6giVtz^dZ%Zb?;(h(wcG1F;zmBZ z&%;*E{H23?J$+2B_w4u<@bvYwdyCdRNF3+b3u1rj5)<#i2Ln-EFaejy{Sih?p8h<7 z154^Izs=hFi|ymc(E8<2Gj`nLcf_x|Zc^bRjEoRusglHmP{HEg2*~!llkY$g0}Txk zg$C>KFcs|#q+Wes-Q#eoaW9^CqMuVE5`*T%iHg#$YcGo-(A{dCG!2z%5IQhb%k7xr zRbq6XMF#{2qC%@SLC%`eYOAhg4-HhH1DUfT6pBMANO7x{v6Enc#y^_VNzafH=t*1d z;0M5Wl<2d+Cv&TyOp{F;TIVPi*=xMq&Cfj@Q*s+dGgC&rORWnAL<%~^?rM*grEJHJ zz=@sINDTSd#rb1rpqyqf3dX%QpSh}aM5C3pPgfN6p&7i4=-9cn)kdC_jDiW*t32LF zt6}$AO76QWl==LsNK*_rJN@DC>tgo)nQp}*>+6a3M$T=I$yS1c$w%BtQ?I8O%vI_} zC#{jt^7`)a|`E13*zL3E@^tO;6+FE?OZHI-MM*}_`A>iNvfn8?-W+|=0 zxx=lKvkGn+yghz{UzeP=Yp*F*626HK<&-t{bFS{c0i5c~@Ats$tZ4gHr87ka)Qd(4Jd7(W5uLv*gDmgu)Q6} ztW}YK`3SYu)Dl$=qk21P95A5P(q^UnU>^&KwsqLo(rWk|*ErqTZQRc`r2Gglq{m)_ z*E_Z?@Gs!juMO|0Nf(Tw>Auz zqnu_tFb6gqc=RtJma=6&*V0D1AB|+xx{BM_)6e@leB1@$)hy@#lA35^{g>3o!libu z@IG-C-5WD{XOxSGfarD@%saD=y4Cs&sNYGMOW6ABj-vSsU?qeYzQDv$cL zriAbEZLX4y8SZRBTWh|} zNw*-sYfTvvnx-GqME&yrJb(Pnjg)l&PamEw_Tg3FnW zJ;@~9an#FQH_P3J!{7j4e_jBLK?^Flpiw5(opHZNQEQ##Zhor+=xaqm z2{Kua$$TSu2y9x+Z&L2o;KihDZy;-Dea>IvQc!14XGwFL31~7ZXbR-iIl?hn)c|Es zhX^cfQcjWOIjxW<`5M%e*inA1w7D}k6)m(=tBBw_x?RC~T!DI(I8^~-T>)(^%^TDd z3A+w=2vnryuw9{cT%ik(!3FvIVG$@i(I@Am;zyzAl$$w9%glC#A-y?|eaq7wGYNg` z*}ULyM;CKxH*@gzeZ_%Z4f9@&I9c&Z;%%2c%83&6pDyOgZsy6Lfs;Os9jrikhC0P= z<_RksbsJd1rOqzqD`Qx){dCtvwV0+GhDCU_!+jd&cWn(k8#gJ|0E*icBgU#rF?=1{ zu;=q(bnUFv9jxQPf`|O88(7{mObkjL4WoPaYSs}%_TSnY&I}_C6VS6f0}U#YGTg*M z5v(i3tt-5nJczUb6kW`3pm!bf{j52*dG2$+SWie;!`XLFb51ulLFm-O!ctnFU#>3b zCnPy>R9&PSevRwq6y*9yoV&)yqE|Yco_n*#N)3`U&6I43U_X7Qvvhx~R)0^0-*|mZ z8t`IU#W#XRoE=Pp{+WKR9CkKxZNJYdixa0DPi276bWQg}!Sg6*1v*zP z3{{je=OIo>BZu|Qawm5b{GdW4VIcBYsPuaIdU*NeTtndKo)9JK7?0q>zgO&5;N?<} zQiGqyeSA4Lz58dIQlUVm9&j}-5Oz1&W^X})N9Kvlv19x~2En;8y`UqxU@3P^yY){9 zuq62TqET9p=x58v`*TVL1*-crMO~pXi)9&L9HZ7fr9D;(2I`)Mg=M^F2^C~FKzA6yKbHyq?~sVV{|2vba^7t)zR8HBacg&-EvxhD35 zUv5iupb`SoF@Xrob~_Q3=K!cJEwZ{WasCd*pN*C}M-fy5vS%3x1!VUsP{}Q{bsY#* z`||BO*q}euQ&`8?xJ}HW;X#wwD{`GizH14hPcv}OfZrU@cQ7cV3`7XR%>sOZndd%G`m=7-90@9Ay2Zj2 z72|n{%^!-3DS*=)NtHQ((N80}2MS5`Z8#hBp^`KM1_=lw)rYV+lWT@%lH8yxbri5S z{CCwXsDlBu+!HL}5?#3pg7JO_BMXGuxt@QNnBGT!1G~ngn{6F`Lo?OZa|&ajM0GMMmLLLq z=U`&!otWZ?XmK?EsW8v~bA`#^5K(JLH)2K4_vS=!+aBHMV2DxdI2gFmw78J3qZ)Cq5B#Hey+8*6z*Ib! zeLj8OSv^SjAJp)#Lj!$DQ>zmiy+OB$tL}BOc3dp&^(>vuEFJA)qd@_ZP{p@fSK2;Y z_?Ox##Mr3F3_s$B!q48q=P8Gqh(hMD6O?FMl3Jb#-iI^~YKOv6d!&}B)Q<(fM-*g? zR2=kYX+7*Z986Wc^^~#Cmf=Iz^|a^P6+m=pjiiq&R~g9Zm8sN{@2`)k`+FgLb|QUh zc}lQ!4%b(Yvvf^k2yV2yFu_(vMZ?AP%vlf44XvF`toaXZwhrO9>f#8Ke$jfM@bJ8K zL(=m4-4~g>l=Uu#pw-2w^~r+=xf`peB(o1e+Hg#r61Q@b04rhfB=L~n?x_}b^|F1> zzH>h&8#0KVf|i(afR3eqwCcft|A zY0%PU@De>ADVDZ@x~fsCwn=!7^&A0|h{_>_)!uFX86?S$BhlvE4JS{o~qz7wX#s1Z(M9c0SuLQhNpb`x1=!=X}|L%r)otShVeX0ABxT zQWkOqJrgYr^8`K1;B?dXm|hP@DVKMEYsqOHDQQ=Y%^+9T;ozu-~*nD)7p&PGsy6YKvkdQRZ?zi9RR zQ;)S5rX+DA-03Na`qBNZ;dP$aM$-Mx_TCrH=e}AvN?8>%XN5_pErDJ?l6!)dYj;6j zHAxEK-Y-CxlIZDZYp7q*#TUhZRU+s-aei5tUb)lrKdBZ^&r;^UXcl&vN;fimv@DLz_hbWOnAB@++rKw(C?`nO|H=eH zp~&W!#{F7qb1bLa?5~;F0c(f3$24sCuDLycp%Wzm{hgr>y%2GnAbF+b%@If;BiiOJ zl0}HwzMnMM=pG0(CC~%{1%ouzwo_e1jQ}lI?SU!S2yucS_1-=JUElj#X+QMLxK{Fa z7C#fC!>y=uroC7m_p2dFkV`E z!x+2qP-_EBjOe8ZQw3P+?4M78^0^4Nk8n+cjShk(5hqT7A(-H1U?~_RSazvpFXR%* zgG>M-a{t2kmxvR7L6$!v0IdGsA|m>Q>Lfd~{uaQP4Lt)(-HvCpn#Xj4IB^z0`G=Mh zofodR5;+M@2Ql2tIF2o#jq$CUzmPB$7+fU8s59BbiP9Os=TCeH7Q$5BfTetq@S@J4 z2hul?al+J)9h6*5%)iCh{a?kHsIa`6Hk47Oq-Y5!kTJiit8<^I;?;6qp_i&>b!SnV zXMlE0sB2*U=6vziXQFI?|5+)OL`}E8>Qc9HIm=n&u3F(K-FEl>KDNV`c8*YIU@LBI zn!@n8V%0F587?z4dnb_DcuY{s~vnnWf!O71L3nGrMPEh9hQnl5Di%;o(wzS~(`LPi(;VjhfEgB?ip9|h{$D{L`_}_@w_9CvV^xue{=d@$ti)Y*@C+UtUYK3B1WVZ)=k*MN{k?(p!c4J-M`PHz+rsOF{j>NcoaK{Z)y#e$h=Wdk^|nS z)ilA{r!}aaq@~6K-ru*tk4KOHqvD2&3HNUmuE73AS{A{5;!1BHH*a!I3jflfn0*M) z@nQdO9UmiWwM<0+4-$^KYUy4B_b+mZ3R=JZvf^vz63n z<16p1n>8|QVh2BBO}H|#c&vfV%w%-mUAHhRCHySp4Z8ag^)0cEofJN?nWoG|Bk+{p zG3*0HSB9fcWiEqEwYc_%XS`NV8~21rV?YG6;vkxd zmv0vOrXbprOoOlGjZKb{#IH8(d z1c)#|43aw4VsOIu8ee6npXEr{On(&B1C`)f7Ic6b+=J z1p@2ALHesBn{!b0bH=0Y5G?YS6PC!3Uyn3lHe4Evuu7Z?ly3`}xK3i_5NRn-k5-1U z>Ip&sf=8g~-0ROzpqVR4FzA&g!E^Ov1xn~hFN|7+u}&a>_G7_AHIOXY%(FRCn5TUQ zm?o&8@+18eL!seEqGlciO+S{5e_|-B-adIncEV3|^Zr0sJ-B`>+U}E~;xyXDVibfy zC3;+kVAgQ0ros)o|4kMP#r&i5=Qm{)ABidg$`2N+pJ~VGx!Z8FUKT$NJ+3&M{Wu^Q zN^~OI8B3BB_a;XA(e^7)m2U#i-WUBlcuT(_JU58i{~YKorK@23MRT|2F{GRj?*Ofq z*D4f2_qYF%eP z=zSHyHI>H26Y>=qDHIjO2L6ef5B*P60{B2u{P}9KWH_aNS)f@Bups6O+8+xh+Ct-= z3GSA|wu;@5;@wo_2h!);La87OH-0G9kUv)DwhGTUqmur`cNhE*@_Jk7jXkW=j94iL zc0gY6dmyB_?+QOED{m~Oq@qxilp}uBx}=#s>of>I+YzE86#!+xSn#;Gp$mX= zT(0Q5Z&B(kWSyv8|Cp@y?u%tDcR`kZv4`01%=ginHM!zRww^*J!>vJA8?K>jSkzwb5_&Tvpwj3e4+^fnn@1hmRP z-WDcYfM}tD|5PEx68v|PxB>NayXJ;dSk38-i`k zzzv%r9Ax-t5a^=IE!$oK*r#W2aOB8uu07(UI95z_^xIdrzd$T&p8tv&GqRpDz6TEt zi7ewfI}GS(kW${Qf#^pMQ&Ua1Dq+e0Q5pyXP)ec%P%0zG3mR?16+^or25pQYE3u)j zcaKhjBgT=G!@kBCyrVADL(-0t5u^$6X0YVL{R zPP}cSq-sk+UC!U@{|Wscr|1N&oAm+o#eDqd?5~9K7$TQ5nc@&s3AUFNMZ=VO zx5;*Ikm30@RUnPNWq_Qw73;udosw`kJj*MbAvt0kLqBSbW1qT0LpSfLnO|WVOQKWcv zeEjug*G(0mgVO#L7btlmO7D;qClZGPrNp{0W$v7UZGDdO=7@8Ghs?vE0bbyXf?H5F z9Oi`JwEvm{wV{wh%|1KGdmc4wvx=dn>wtJ3xP0FFK6y%!G>vIPSn6&}%1lS4xEnf` zvZ^DIsts%%^CDPDg*|g3;-RIi8W(kPP2jjH%*^Z}1;5AF_KqzdV}#K z(TDew9~_798}K#c9AoGcAj-$u@-^V0_oPdMmi1shKp?)q1M!3;PF_PAXnqHj4dU*{ zZ^iPu%A*)p`N=^)|2#_=%;%j&_@M*Vk_sWfpl2LFDh3qx1iFKeT<0{Q_J>G(**LuaZ2713bitD#Mo^qd zuV(SL!S4}Ipih`P2()o!fZHGvp~8 zLY)2{1{@oJXa6OI*ypJa>Tk#R-*U>XA<0t*p5t8(1Gqkafc@a)87o}tmbN-P6=QTGj4-Wqypo|`qohV6VFpi0LyVUjV z{xpa;9e~K6MJ;eN0JK^DUeiWn0@RFy*pqa28D;@P8IB2?)HKIbSBa!<05RC27JBY$ z2f+JdhSVVG2cDc0F{e|IXQ~iiVQo^|PFb3Kz&R#fHvQ>;8R?cVM)i zRczAgfnLC_D$f^Hh`e`dA!4^v*N4gWrRq~(e!s(?yPU-1UWCDsA)KG47kQ}m?nUA8 zE%1Pt6=Ti)QM>PNVCgcJH$bi?&@gHV%f_Bj8bu@NLJ41?D`*U{sj|5}1&NuL!fYya zjtkSySdZI1F&{CI`rY*H_ro@yjMo0E5CRFEJCp}S>c_4J0Zf5shZH6KH-=JdZ@@Q2ahUr~y9&i45~6cS3R0p8LF(faW#mDbaT#)m z27ha1L0TVmy)5;e@s8)@j2U#d5L0-XO?Wko*L)&bsqzDGz-?RIzw4cwAAMoqLqgi-AA8uTLGsaJo8yxbF z75gN2#OzxoJSI_gObCE;{HGYvD0GB~oPt7)6mfQOIaEZpO^z*=PcSx3B~ma2A`_Mm z;3f=UaXSs=R*1wQk5dbck zqv9xop5q%!w+-3Z=}^?H&$1r|1 z^tU=>f`76=ONawFvoG`?XY7z6bI_~pb80aX;YP)A#*LX9?RwovD99}-$@warUa?yC zY(iRvVx`kzLj0A9e8$YgOBD5&g%aAh9AK~c3uCzLSJJ*Q|61~}%5+w93B=j&6wg)h z06On9I+nX#jA;~L*s`T^6(&C&+(#N0CR%>^;s}P*;4}LjM(wdtknkAv7}&&o?C|YW7zV_HySbh z$r(w*Sx`IxY^UCgxK|(5fW$8&EW)IQo*TkHN%3{WbQ{xd?~cqKdzf;#Bvw9J5qEDs zD%YmJBWIakCQpqAb_E3f_AdgiTXlQ!M9@6{B0#ne!HJ4FV#|`@4k9coMN$-CJRCEZ zHY-R_L{KClA|T`Ff4^&^5Gx+kkfSOfBdV(zE9CtwHW545s??@tClOVSIU>=37hw}Y`OH2}CIdNaasBO#Gd!-j%A zYqRNOHcV_zPWXS9Ek(Em`Wt+9WwF(Yf7wd(!yK3=nf- z4)+^AJucu}YY{3HJWr#*lah7+(?oq%>mrjX{D+!s289=YHyZ31tGSKru}mu$X8tFK zF|@F!!n%MaY4LzMmP2sbYJ#|0CoxT>FNwlh`47SJ>wY&XO`x#6^2oN zo)qR`hI^|?=$xG(_-lluj((+^kd+r1IIMiEGu|);WhJSb2!J8{5FjjJ+*+L>ERv^# z9NvPDLo*vKQ=cd-^KHp_B|I&qqHKz$HUfY{)aSyMS<{7QIDv|-nm;H?q!Z(48e2;f zOC*h~YOwvidbqP%>^L5!v2J)eYR&sykNUKyhX@WZ`?%WPauyezthSGET-vrPaXQjs&uI#z@wRpk*kjp zi2{bf>l9%b;_4n!gd1l7NCt?IeX&!?d z6u3tG&S{vg`l?CNaQ;ImpF7<}@u7`)TodS*QHhOL)LH!Sy!QP8~j6>1iKvrm&khd%0(k=fc#YA&+;}w_j_W3`Oz~=FZ zXm^1E0UbjBS9U~uLk9;_E91Z3vuaRYxBJb3(z#ir)4Xj2-DoSdOLww+8RAq+=IY-ccWLi9 zZ8hQwXw2bz*^tPwFb)U;Rul}|**~;92%GO^oGwvocE?Z8`?kXE9u-uo&<^ttVdE1? zG5pZ`Q)HtfNKTe@$(0LmGyN9E$Z4dCip-_;I}Mcx%^v7H;%Bn@<&&5qHztL+$u#97+Du~wcLrwe*H z+%yQN?R6akE}zmof~Z+gO53F65|_EAK?KpFBe?t)l8uBl7r@% z42*oyGDDn5zm-X2b$^Yh>`Sk8JIx_Ve0skcY)%RB+TYm#j8k~sg3`Yn?&s2m$@_&# zZA%&yJOG-OXoIQ_u0eT0MIv|u0?6RvPcTga7aS|y3Nu=^SYYekFI^@}o525kz4AzX zUl9n9YN`?N_a~kAU#X^?4fPcN>wu4#?lJ3QfCZX%@9f=+RopcQXZ#+^gn`=@FVKi! zgG0*L0C*W{;^mQ9jSZvij*Re_uw=svYl3b|Jjb$@iJl2Uc_;g9-T?nHXkCAh0*V_d z0xk6nCjNXiU#k@S1^#<*L_d*U2~(~3A)@90=LlwaHV*ExD$WcxlHLiql|Ns`x=gMu zQL&aOYUNuc1>S7LbrS_gq(m?CXdN*2qU(i#C=%_&=uA}OS}yGoeEZOm^%Rt$ zSIW%?zCrw*7)KxvUY%vVf^9p^;_y*Crdo##TE`J9i>d!Cn=Xx~oJqvk>UDtiv$rU0 zMsxR9M_V{J$O1w%^N+(-%Ny{2I`+z$ZLbf=|NIKbAdCEO$BY2!&XsiaEdELYy`I=( z^_u}_@bXtEMN+ziMO*_464hW}{TMv9JJ460YP8jubN-gu*GX=RD2DTD@wPk5Xt#6f zaS~u(C`<=IF;GIzm<&%1YA^(JSUS~4Ljs^-gXEE}3$P{w?ZJ#PG$j2=m7? zX&A{L3^DARV;`MkFk*@22o;+fF+YbYP}_5Z9R~|H4)`9VA)zB95bdlE4qIufDo;+P zvT+&}es~T-5J?m$g)^du^5@Cg?5$?+>9?J(^k7H|-?_0-nGhPWq*jomo5&;Tm-<~x zr0Iw!mC+o9Muw*r1232NOj1oZZ99RVkC#}h_|6yA{PYRnJoeMD8Z3lW%wozdZ!$3J z_zHXb-X8s|<-_Fy%yTPfwD;o*q-g8@_ZgIRcbf>Gf^V5WV)NJUZLMT&$@J zI~95>{ny#70Rx1-=#3a%NBv$n{%N2k!wBe4FvM?7jBOmbWTrm^P75M}XNNP$O{!^3 zGOWa>;Z26SV1Da;*fV}MGt0dPqo5rgg?^8pj-*1kheP+kp4{yIYji#&p2-r(-{F|dfW1@Zz-VZW%k+{IDWsSxa@fGbVC+PR$ z`V7=66f8=C<2eYB{RdtQe&?1M4Ne2}YC~T}oEUW~f(nOw!n5Mjcu~6~#&S@;<;Xj( z8v}n(F9?_&A^C4*&bK-VMp1F%7!)CzT&d~%KE4b(0YnMah2v!xjN2bC_xEFIrlzH- zEZ|pKRZHhTX4}ZOryC1|sh?2*Q((`Ei${YDuz<%pj78Y3i^U}rCd$@r96Mm0lBKE# z7G}!MQL&$5|M)8XO>mB)TG}{o)iTut3p3S-SNikDt$g_7ZP7#G3m5^9U5gBQV%N|sh+g*3p5^+2Z=y4 zOizF8eEP)gVZ5sMDlBBK7;h9*iyx9Se@)~ZpSCKeyIq*(hd>{K+qh%*!bTgXU+ za);CJO87*VW=6Fh+h8kTU>Q?VB$(Xo9uU16JxgK`cj5T+2zt;xHT$9wf6gZpZ*d~xS3Hej6JG+N1)UQRBpdhDWQ>D+KKZrpK{l& zL7U{NMZ&vGW6aVf?|V1bdSJb-Zf())>UaLuGDk}LtDu>Aw(b5+5hkB0_usCv+?S@t z#YME`5@nYLZcCvTUbk)QGmcXl4y)L@-<<~HvsX2(y){z@kH=Q>W=weY!sv5-sJqO5 z$A_zi;4ovrgNO`}t2VdM!RAY$QaAJ^o7eW|5oBv_WYPYj(k{MhJ$gq;OoT^gy?Yep z!bj1V4_RCoJqXCjz5S)QlNjILLN!0XNM0aveq&l9AA%t(7A3Kp?6&uGu6>LYk>4wo z@v@g9n+m;Wf!humyU*B&0)vSud!jHn2koKFWWL=4_DOl9;5|dr2;9oX0y?vXLiQy41DM@ z#_HA4KDP+8!a_aK{2{&a>nucgnDp>^Fo$MBBi%JzVJX|mC}~;457PzpdG&)w-nGh;IHHh?^k?+uO)+=N#r|I7Lt2E zq|UY&byV4Ox0(DK2gWx(t(GJfT(Y6QEL+4$1&sgxu13L-Y#jW6gjuY&p=4F-&225S z(pt7{55mS;zO=aA{MprkytuhvBBz++85^|aHuHnZ7Eyzb)>3w7JWDR$O{(b!Hn{Lb z7v2FVg=RMIo+}J<8Ub})Cs!51QRFx`qV^B(ax!#{x>-B~_<{&^wTk%-v#`fYA~U(c zqujdZL?R?}&c{uQ@(h0>>fnYz!g<)|U+rRw`h}iIS~4B|V`8k)&xXldz;O5y(Cjl% zGGaT2dOXpBA({PtmdX-52qnx5qEJz8YIfnq@TUEn0>s`Kf`UZ(xw;Q?C1?8vhhnwM z(~RbpoQs%2+x(c!DoOg0Y7Qy;B~(W6nhbuy`55dh~&f+AXq4^)nz@ zi7E$kr21F>9#rs2SEgqJS8rJ(S_Qi~I@R2t+bJ~Ac2OMfRX?8~|9Rod^yXSB0$lJQ z0Ik=rfNyI@BZA##dDuSWkhix$GeZQ6t_=Z`iB)XF|uwaSO zADEJMt#~QpV#p!A&{aZk=$=>5X0DS4R5in-8QA$Rto#pMyrcUI9RNM#09|Sk)!&646^dwhOjJpsxi3WYpbN#MrW=fC??Ye z?&qVur58e3vjADtOmSgpg7=Zloh{pm@v@gy0zso~J+%nK32^?)Mc%g_vlauB>kuIU zBFq+sBI}9~T11*3$pndG{+z0G`k9Wk7kb9iX{{f#&s~IVgej08%lj5{YVy}gT-g?N~&l8e_AoU_G`bcl$< z!D-WWsA3L+4xZ!&Z$D+?`e?1)c)XEV%!=I&V!uxddnAKzqT69J42^1?4mn=m7K5z( zXS<~5Ff{Qc8sM!`ySE?gwyB2l-%sslrw>bvL29ZneXK(Xm;?A0wjK5E91%#!{clulzGYgt*}rhzz6pBx{(FVeLZl zTz`VPJcF+|lvxVdX)NNa*CywOhs+!J)7zC_>;`-YiapzfpGMK9AkBf~_~P!zW6}}X z*`osV3NrHKbh8(~>F4%G)YL1|>MS@iRaFs%D%byN>&(NU+~PPs_GM&g42h&GBU>1| zTSy953`sQZOEF`6!*=oEAupe%sg|R z_xm|>&hwn}{=UE8Gv^%_U4J#MlbzuDD^@9{Z7XxuY%Lv6a_cGUHI)z$M)ebh?wpu* zFpZs-{mqyl7wO7mTyg)wXSgEkn~fpAJVT=Wb+1NzjA%^RbR3wqv>xgb$%&sJUG{<* z&N9Hv1m#T)2bViWu0Jtq^)h~0Br9yv;-syc-9z@?)Qm6EiqVVfIVC6PA!Y}c=;w*~ z9iyVBq^zH5ub$Y*xbnZd=8p*w_g9yZ17oLFsyO9#VE zPLq_YjgJa-$X3;&f9aH>t3KD4HYxcdx~twgh)ELkpc5D1HO0$v_snY&2c%MVz~iSj zymbt-AMnD?gIxoxY932S6uisN)}*U# zC)&TEQJ(Sl^tCKpvu?oXRvU{;)4`za|+QlfRm0efBR(;}G555mJfG=p> z{&b60uB@;MdMyr!b|by~VQ*f~t@Pv~ipPS-v0kf)0ehbFQ@yGi)s{*W=4M8ib<

z7vr3fJb6JQC;0pnU&DH}3Gy>Dj{OQQ3Tv6KlGYN^LwNWvw7RZB^^wESK7pf|2{_$T-6kv2F!p5c~ra?3cz?RWod zbIpymyz6;#;fSP6d*ks}Eo?q6Hbs1l&Ndh2dQ>9EME8m&_rQmDtm4iQWD^4j6d?ox z@d7&*@PO|9Z3=K+!1{Ezz3qC|+0zlZa^1xfV#2M~fKn23>*YKn#m&^#0Brj1(r;}$ahA`XIZfFst7>Q;m$Ml zUW?2!+#HCLulMl6y}&LAf-wYk3fJ+c)PmM%{LRv7e31iI1Cr4x53I-D1Iq|J4K7q-#rwXrUIp7hq&g@;CXP_9&f`6`)w18bFnS)t(aq?M6 z`~z9eSiBPBs&83Q5RDysB~dg}#lEq9H+14;eTap(AC1cOadT$ilTVgo-nP7F zqRW6fU$NsnU3!^tmg3wsT<4IPAZ?Vdmx=l!vVE^vO2BaYm$}O|aa>5ZD?S4ve-7BreGDj(R?}f(>V6s?7f)LWf5-Ex2($nH8SJovbO7@{yzlPvU-1o!rBB zkE~-}i1WK6)F&QebFJKhE%W_YO5U@em08xmDJ^O9xSSL9XOX805Z+80ZS2o7UZjzy zMz?o6wGsq}{Pt$HLfj1f{@mR_?u0fo6*hf-9)VGBEqqqN8z)nXPBjvq3>D!J6ALFD zuV2q>jqFz&j!gA6tz0;Q>o*8+5{;iBLbKV_$Z9nNc>Abt1bCzRV#2r!_ zPC-vRUzt^7g4E`c=hR5sz}e94JS-ttzr=lBW8dp^XB3_Oz@4a$`N0E+F z6dG#K@815UiF^Si$F-E?mP-u3UFs4`ntzmwY1RxL$Bp~FF-g%1vi)=HE+4lqax_X*(?_91+l_b^b0v}6JzwN16>^|B#Ux%UHRjc2Xu^G5oc4dFg=@RQZfj!DcuuvcNq|>Fp<_b|bFS-vn-y1R!U2^LdRv_d2?Tz%jhKGHPsF7t% z8|pEkCUppo3iQ)onHAXyA_}%ztZw4dNjkeYEG7~=%Yj;goDg^+ZB!PrmVg3_G^#hS+uN(O6s>r zOSK()5{UYc%<{3%M|kPk#j&p=T`KP~A4ZXY5E9DYB%>z9`_XER2l!=m{LS z?E8e{A%^j|)BU?YecO|Z*=|!e)T5#0ru&*6X9hKuvRGDLEltS)BGfJ*=l0che1D;V zHW0|-B9IJrboaHs7U=2dWBD~YqXq=R^fY#WODBOmGWfe1_(aom0Equ${Qc0LS_n7D z&kO}F*#d-IhlKM$%m4Qtdk^pZOfjyc_6B;85oVz)<$(|Bd`d$WLd1X&9gF zBMpSYCJZ2sL!^2jI^HMw_y)QezP|2yZ0 zA_}#fy6|!!uS0#4Qwsb%`)yVZ882!*bv$^W|CI2< b5<46n8tO3sE9mR0;sV(M%VRn9>$iUaXw+cv literal 0 HcmV?d00001 diff --git a/use-cases/preetham1930/poa-generator/evidence/screenshot.png b/use-cases/preetham1930/poa-generator/evidence/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..87d6364df0dc85964c79bc542724ccbbf7576ca1 GIT binary patch literal 219728 zcmeFZXH-*bw>FI1N7+_X6qITMM4Etrv>@9;6Ht&|!Um}!MClNch-^i=^iEU+q(*vA zL@Cmxh90Fu2%#mAgoJlR_kNyz-f_-%p7)GzeB=A^W-K9D#FhKL=RN1Vu4~Sfu=@sD z#}4xy=HTEsrlWn^h=b!-CI`nqGJZV>oS~m|F#&$<^D@%h0M7oo4T-$}N&YiL;|-FQ2FnHPN80&ec!Q=_p+WA#nS4-E}X z4t>7Raskct^wo&5u??kUIqP>nACvJYex(9EJw5uj5Uz@figzt7cSuu>R^|jGeE_{| zjHV|vo&zStkrahfQhXur-1EM?+*nXhaCvd@J{(>@Ha;$*1BF5>z32BzSsChZ(;Oom zt7;FE{D=Ly`soj^CIZLc)RYjBf9rt8kekB#-eR2~p3J@^$5ZhdKF%M{I5^DZ*&jHT z&VKn?dHFD@=l>ejW;A7JvrL2JQDNl6to zHSZ_u{l?i&4m{j?GL*A>dJKHLz29eN-y)GnZZNJaDK`kDFh_nbhO)3STF#zVolhY! zDMjemJEMH|L8Vg739n^#@=XiF_O$S%8l)d_cPme$QmME`FIjpiR!gW~MJ{wEXvF`OOy0GS_8{`~Q}1z3{5pKy^qO5gYQ6P8Cqkbi&u zl;`tn;lH2wznA#eYW)9gE$l)HObhAVFl6@Lx&|v$+PdcT5nky<)Nah)8^Z3q67W}F z&!59Dy6~6bN1n3CdrVv)vACMOnNfS&D13i^9I&^y$a^fZrM2~-6|6C5kAd35Fn>53 z2S?G#fBtW~f}w(|iQy~~fz^%7pC%dNJvgu6-J@C`L7E zfI`HRx_=JEeE(m8`$N;|b6<=vOZ_}=%JtV)R7}|>pa@e51=^N>j$Y`m(C&T>$W7GT zhj;pa`SB+^%Ku9{{KI_1W_kP=EfEG;FvAvWfg9a$8LrCYI}rGSvlwO3SaY1b+fiM) znIECzAolaa*&`S}1PtwFVc_;#p{RpOmyx!E*@l^e+elwy@(3An0Efg-37xp;#yA*o zEh2qX=NJfnCmH4H&X_yH+_jnU`#D44r=1`>JyjI=5e6LLR^9Nr-A)M=>dLmlP9iS_ zuTc)>98Q*pS?4oRj9W2_yBf$_3w!H&^bIZhcGHh02#jL=SDj29!AG;{$NzcFHMTdK z^Hwb_Y9KEOEUq2XS8vq1RPVtBh8xQFBGZF4%-cMc7jWS$R6S@fzk5B|Y{VYw`(}G< zb4O1W`)8D6Urrd<0Z)Av9fWI*!9w)7=;4DbZTa4;!Bq3B|NJyy9o+uwcnRt5HE>%f z?~A)T?!6@}e1BsFVS-{Na;F_a@%!)c(=n*iyL*{t;K6U5WS#Gx+A_OgvZ@{Ryp+ZG zg=cf0t5*95mHZYRzUxu8Cre`UaPKr&OFGQtWXxjOyWlqAJ`^*1M^b{^05{PHnDb$- zWz-6clE$!*bS(Ybagwhj?opC{J#}$6q#wLjjaS(0d^|o3Dm_TSGYij}}70zSdYti10bbq-S!7-spkh@k%Bad5ECqp9gYEDD10>_gID- zBEW6P{6J+AUGW5e{b~T>qFJzcM(S==)(x{gbnX2FAa~sy!3D9nV)`ouM5CV#Ia0zf7;)_Nt9RSpvpMo%Vei_&>GpZakgA=5_~#u0iQ*^oz}URb1p}9_;R9_TOLWuH z#`D6!JXYKk&9zZ|t5bp=c8=hOSq`lf$@Rdu3GUZQ;!ke?#pbz4rsaz>D-R{{hg^hy zV2>|o`0QHt2aMB7wu=VM@Ep@54!KSY1o*TzDBCJ4JZllR?1>wONp}d zLZb_myu0#F2yxYgkqM?%`GS06N0;_-{K%vqRjL^Su#Gj|5;)F$ zP<`<2-rj~f^CBIm@btGp!-!H%jn?yVHk0e4j%la(dqJ_e!R9MMhZ%#ziFsTx%3`Xe{rR+#Ww2ZP zQTGAoPpMyixT`tuRO5TC?51O3Xu!dY#VCzvh0Vapt8yipHa-5|zOWf`Q$C|o=bUY^Pm%=_Up}e;|fMraxUe* zT&W>qC5vD+Cv;dwfQU6!xBufztK4nj(KopKV_o7&dE>x>j9uA8Po!HqmERi|URFxl zD_f>_deQU77)Zm+N=<1e^9Al6DDaA28df17F^&&TGDm&JukI+wcogbZz8~sQ> ziBn8%SgkDMfs`L<%tN<ZFZF{fiKy)3m z?nW5O7OD!^GJ&)b z0LScG)Xd0}ZcVy`!IAGE$TmxvW`@K4EJ|;jJSUD@{U45?iA^BMF3#TB6N#Q`EWjZP zY^O5Wd0c?8;k2-w2qX68B~{PH2_`p#Hab?q2aJJ>wigDrT{X*wicMZmN;%x=THZ-I z`5)rHFLI8d;?(ul7@p!TCkqQGxyV!)*$$&CydMd1r)c_Om+Ye zxAz9WhZj~N-1ipAEgRUN z?{4mE_)OvM0)ktcXho6v_0PH#{4X8>N5ij78Tz7Bu+8}GKTY_w*>kGWaU%WEdLob# zHWJ4^+nJ#RQA~EKw%a=Ka3%52^zf+TBlkcp9>~{X*EjWM>RMzlizDR(mK!8`B}&l1c~fjM@Dd%!+b%3YR!*BRtxXoe^Z6d8-+PMUc2re9}IS z>R$jnX(tF@7{U_`#oQ-zH1sS$ZMNYHT@njL8fkoX(k2x?$*h$azFmiJ6U1R*nLW3n zV(UlD1%mOy@ZuiXRd$Z+ZSI=1mu+g7rd4}gE9Zj-A=?u#`_J)4A_MO1-RJk7wY)2i z5h%QXIXYTuUgbo=JJzo2x6T@+7I>QS!a7Ia@VLU- z&ZV>h^{<;R??#4p1MpDk#L|D7QW^vjm4Bv`k};$0=bQKZ4drFPeRGzqn8n3g=Q@j8 zI=t)ex^YFM{zxe4vJk@+l`nAmovpp*TQ&0Syg!=jwg+5r^72WQ+$ciZR?4i?uuUxL8j>nQ?e(d zeD&jq{v028er(4#F5v^3n=vuCk&{;a;?z4>MeVuV9smVoU321eOL3ryi)Wa6kC}T) z;^8V(!dq7@*mBo9+3w|GDBXeRn(UoUnQNpRxgy_-J&P{co=jKST-H%fQ-pe4z&iRS z>n=Yga>NWrV)2#%T9Dk%Z(p4V#lCI~_ogH`e?U^4`U)d?gH)NSy8*Ao)hA zK@(_({mB;cofXVCNJK65Ly8F=oHd{^H05l2L~b*pw@Vu0E9O<@0e6~pY85RseB>(0 z(>;*Es#EI@5BB2+c4Izl_5Jn`Co#m&+v9@lm%=y5jaXGmz|7bjS2rYC!|Gazj;A0O z$M8jVtgL%{$M7s<^R97*ZqxaA0Q@EhoL`F-q!-X0evBs!_h5p+h7G$}6%=162GI_C z7(~k-X{&-3bna4LhM5QW~w zX8F^?rB!pgp)X;nB)OsCaK))=pXA&O!^RX|uHWW`ZG~$aS)6uXu;%w2k{v`G;#;L} zy)l%&LNUe;B76xRN6#Ia46MfIh{!llFVt^`E19~a==_(5u(4y z6)N-C2%Rf1Tu3Y4Y?5(ZUbo@9)(;~9aL;CcY62KLdFug^;%#(8YULLEI;BFv26D-l z)JMx>c4k=Q{`jPUgG$h0VSZGkPCqqoooUOHBd?}p~82$(2hNi zkrDRgk>=NjidQfYk%P8}sqrI48-4c{*~zo`HK{du~l#MULn*9OfGT&(c@r%tWm{guXiFs8;5F15Pah^sg$9$pS$9 z(oa{+e|VLBG&ZUaD=i*D77V#f@AdhpmJs&cjG&F`%!o1+1({mgA#%>5I2-hL{EpS= zE`K1K_a5*%^qQT~#XdU*eVRkP4gDj0m&EJ=sO2@-$3GHrYSiC{+oj=jIA$4xvzLTr zHT4W=(UJRmQPSGm2p=M>w0Q0(|1`+NOVy?~;9&V*#HJ zFrMR`(>tY{5|UuuR0lor=kgoo0I{K0M%V0kZfNW-4UK`SP+Qyf0buN7g{sK@e`z^+aOP$=~HyH8vPblxiN-2+muc~uPjsn@b-b2*b*xSgMb&E8``wqt=eAPR^Rf=~bocWfdIymh#FXTU#M8FO#`b@3ycnUE{CT*>6{{&OME z{{4J-Q}2h6-)&bk)WFOxHRI5Z^H>(+jkG|n*2*hN-T1L&{Ozh_+o5gID3Kr_yRj}} z{}=|ijQ)5w*e&Ft$8r|{?~?ah9rqfJ0~n~`+kL+P?*(SA?>jV{`%?M;&}B&1lP>92`lR1wP48Ra2$qj0_;Rg{2G&<;?Z5LwIw9_V z0XXs5dlitF9snbE#;4)hJHVv~AQ0O0cPZoJ?gII5Nk(KDKriy~GRudYk+Dz}dJNZT zp+8QH!>;PVOb_Zv-T>vZL)=6lTaNhi@zov3GazHb zPk?p-X?Zk&oxTd`Bg(<(n4IgUQ(^d|g$OzOE-?W$$NM_ePRoGi%XNolAs_92jL7otuzP;-?@?Rl=-PXL zo+&!n#=av7GaKnLfYODIX;Frs%jxob)FLW2f9Fxs1$mxMybFpsf({a2$C9jIc$6H{ zEZF;4%WG}^(>lag7T6->+iAopj>kmVfuHrNE0L*4OMZD~kqvv6A3MHesUNuchr-#yYslaF}oYC87e$_LM@( zuEw+K|95vX7LI5<&S4%g<}p$8z)}D4j}^Y(oGR};I69gSlw;Usi^fJ95~->D&R<14 z-waj1o~Z;^e2EQJRHuKm+}Y%6VucUUG*MRsI#!_r)boG?sC1NKHNk7f#irbQF(u>Yp zkIm22V+$hWd&kn*F7$fI_o!OWYqU?Krbcm;rUr!fPs#YHOw#thG+E%C|C71~RLNuF zy|k3nQV195D?w^hr?pX^aAM~H3*Dd@O_#)F1uEPM6L?2beoI)_i50J zntQL_msF;t6W5caUI=8sVykL)(Q6=AA1Dq1hZ_tQLper&hIXq=+d+9xMic$`KezcI zhNga?SBV!yKlXI_Bh?odwt~mE*4E8Wg|=<=cRLtKks4KYTVt*dy5<$uQzw{Xw0E8Z zw1Q-L6yZI@SM|D^$rx-sg&IVEEqxb9P9LT?V^0{C_w2L|DcNNGMsxp2z2A4Xc%SQB zWv`&?l4Xy|aWAAlZ28AbY#L8bZ8gxaC4>z8clhN= zuiFbSEQY8=!0RzCPi`La(WS4*_mR1lHdA@G0Woj@1An)5XV>U}a5bx|*MVHPf=Hgzi`tW!hP{ zc;q0#;ifL}mJKkC;_4cn?;asXGcT#gQc@}+_i5xq8(Pmq4;{9Um;;9V<8f`#*}+PZ z$f@#nvrm#U!jbvyS%m`(xB6?mw+9Ztqb_jS+su#TQ?07ty@9aNP>zc)`IUgcpz^pT za-}$J%av4%ZUaj%GEOcKjRDD!(uezc4_JX1U5sUo@BDeD^Za<1r-TrC5!2`bh~ZVO zwY5jVSd`jIm2A}Y2!f!vq~dDil3Vsr|1zR!9(A;a@k#=EY1qm-;&G*B6(mtfWU^3( zgd*okzEvTCA694t(gbsfENW{w|!esW{xd#Q#Q-2F;-IK^#U z3ZpfSapcE>6^$ddpZ}xR!LEhzH828lG7D;F-mn|!nUreETG^I{ubUS~RvP`Y1QCL~ z&#kV|C4_?CAog|5MCnCvpJHd5()`^(kz*_ZlbPtJN)wsGghksy1v@lVubR86IQ znF3JqqqgoMEz2S=>Kt6UUyfn@%w%e-s+|m>!bt>TfMeSyB$&A!qr6$zBAt`v$}DRw zB#b&f+2&^WFt+*3_=h@v&uuJd=bWbKbsLM*uG+Tk(7r3Ef2S9X23e8J^}Y~~^aRdU z$0xWSsfV3bHs~U`(hj(8!~|D>W_7LNhj~ucyDKl?`BcGh!a1hJ0ef?d6!0L{G^sO(7ZSLf2{oyt9lFvG)oh->QoN3@7aJgYlnFE%QwcJ~YQ{8WJRcKwzVO0<+h$RA!*zhpLhKLY|KAZT3h~>3sbC zHH-C74dytzIqE#c)xRX$=q(@K9dmv5cW)>HA z$5hGwwjM?9J@ip1`CDZAC~Uw!vs{MY)#u5`=$6K8PJAE52aS!2@hs8Q-Y4e@#c$P> zoK-?=FuE#)OoCi>jV*Br&N#2+mPx~){=BN#=;tKYVY0!o%t$;CGy6BxMV7oCOeFyh zaR6d0m1QXWSb98uoY#$Ub=V*uaf?D0nQGH9MGniZdL4a7xnO2FS9!;B)UyYp+gbfF z;Nby#mn^SxZ2I+?j7Qa)(rumZsRdb0-G0y{@&naw1FS`xCsgg3? zQ}tP}iIWx>tlpa3N~u{Mq#Yt%i@P1*Ja5dOaz-Y3{Ay)L@gl>1efL@PUD9-BjD%Ws zmR>JXEGMBQXfi2j>dgM~sD|YQm+Xch8=RgBVmd&lzoj#g-r91&rX!{4bD@m?(C2Sg zQG-hM1ijtp_^#cJDPlOxQrI)chg5%V0pGA0h(513Ov6QfDKE7I^KvX0r_L#*Y zvP(uuU*DU!YQU2^DlEqFCkZw5+)m>w=wl;7SNXCP&KEaOYcEOJbXsUJ`#rLU$~@@5 zT`30TulsDh8T?z8UTNnGQplb(wfb&e{0fN2MS$jh{zJ`cy*OWYavAtgWA4Ti9Y$^x z|GqaP$7=1?UeUHFf;Q-ct-9j~^|X^0nJ<=>xO?@s1^U3P)RXfT65}TNLOwZqhdo?! zCmtmT1Qgoh%^R&LF&=z^eX#9453K5jg&5*=;TXMFB|MQBMvU zsBRr(Kl2_{@F-wn#ax;C{bgx=?}0AlH-qFU@J1(4|40*~m&gkCwDNWZ7Q&+!gKikx zU7K;acBNXe-l5ffDtOld7qLN5T23PzkFs2+sw$jH&)LX9^k>7dc6pY5K{IKJ@{kAr z6Kr#`M-2x!=vL^wTu#M0pVEzSpluc#9qfJm0B0 zZ?m#Qbjgo7!ddCH2@SmHn3_76{%f9X-;vU?oJoO0Rd|@?9VN$p7)xx^ps)~P*5VH)s)5@qrdS3QNOOS7a9$1jX#*K?~RYwjt^ja)vuLs#vw!)2r z5szA=C<-yYX1W3mbdwJFoIf3OGP|Md%*!aLQp}ETebK(&nNha!0sls~(JnvO`-R~N z^%@GOMf4&%OzHdQc~WWraFZX96dI_OP7T2dSo>HS;5y?{r*Khn#N_ue z&e-9b8IWFP=2r7d5RPfP)c@PHufMZF&#r9aR%GGs5=C$8`F$sKx5Hb<@Kv`{56>pQ zbPR^}kOH6&Uc1W%EC~0qiV_ea_9MvXc1R(P#0A#$WVseS1rBv}0uRE*lX+2kMzoLC(OXaU-{Es4pCdx4ZRr7xDo`Ods+T0wA z572xYvu23IRrpLpeJ>KC-uTzGz<803!&E+@Ht_?e(HtCTdC1T?} z@XnW8^jACO<|9h7PxI~;-H<&A$ zlM0E7^*X`WZx3S95~?0kj$vlrX(OZQmpAlA=O?o8JO?^>;{8}U?Mi)Ugv0v;Y=$q= zrd7Fp=-{Y8h?5&-qqEHdoHO{jMNIMe_+{Kmm^rQaMvg!jXd%7F1i-r=HdM5{FDgE! zlbSvA{DMeYt=xl6exmRXtmT?ShV3n$m?f(=dF*G5g~SN5&#rQ0KeU)H?`_bGo5sDb zS7Mm8V-B}mO^eJTU;*l5NU8BPW?8CbgUn@rq5Tq5p=|KK=Dt5<6YC{`qQa9K{fkUn zEyX!aj{pc;n6%)pt8q%LMHp3}Vt(*Ew%Y*MT(pWUUT^PZSsmDN0hu10cNgG|D`j_{ z@pHGQp9teW%ehSC6?GCbSIdnRixV1@0^64E8mS^+3J_baVg#1R~}t6HL+(1Zw*Zl7vUMD zP2{VRO9@K0X1+tf|@BEe>Pc`5p_h7q2=IDkqNZiE6Qb^sErRjL}RnJh9CsWB-gsh4I!q>&pShexy z@|FD3j)>B@xyM3W*rooCV4nz0jrUQ=N_1L@m%c@EX*ZquoSPD#O}wk4U$dnx#hjKU zDX-gon15X>tZL)`3At0V^z(KQR$D^j<>(WU}kAelU-e z%t^!Kk@+j1D;HMrN!(m74DD$1g5e!}$XIVgO0kRTqxL{_A^P314_`XOS{3SVUy(7e z{Rvs4cezO=2{&rSrW#4hBIQ|4p}Rd|V0?65H;Z(`V6(#smo*k$RY83`G+;;W4N$A! zc-$sZ+ig$o9i<+pGEOaU$CxjWBsQ%FBSFrd2}(~^uT{~g3Itz+s5=Ir!@0%y=OKaq z^fw7}Z|{jS`=w+2fFu_Wazy*`I|2s1n?)NwI4wXRo}Jii5E6PwFS9iY#-8|6651ra z_hT&%31Zty2VZSO$5-si(JfbHU-gGtyOI#&(BE;ZaAYOH8I2Oah%w49$h_#mx}(8B zsYJI*^YMev>!I#fQXEBOc_HRyhOu+z(MVquC_6hl<(bpwrLwN@x36q86egf)EAy(7Hs5F zrp{*u+#Bv6MX2Su_l(?#z7=1n9*yk5G;T&O%(ZsR7ojJq$9rm0nTk@w%8U(lUa9Kd z(UlFAUcoFxcU~sKJ~pd2B+wOz${1q!dNtD-eaLCp$ClW>n7py#w^~-i2s5M)DjL#i)>_)iHgokZEiAw}jQHH>N zSHzb@(*S6!X;sE%VgH8IV} z_f|Pv=!l2!U54r?rf>km>GM9S%o}jNVFllT&+k#B#&)^L#riti`qH#mKS|-jqVzp$ zdbADUi(6$#_co$LC0%V(Ct!>qW01wTZ!lp5?u7W|G&hxlrIIN71^Z~?&nZPH@vzBR zKcTz;M~M^s#=7ttqN{F_7~AXCB29h^p?D?8`+{^LDi^T3J$E`uUI(261Q(7I0C;gJ z9wxc55jIiy*|qadsu9Cc&KAj(*0ZJ0f(FK~6eFV*uO?ugY253=t7yCt)IImoZe#UG zZbe{$~ZoT zWRQVwh3571ZOpbscEADRMe^mwYiEVe&Tfxl5_~%gLc5g|+v)I19m`Sf(l{^Su(RWc z)#i{oAtCU3{(E&sZaJX#+4*9^*TL_{w@Pbjx*@<&KYMW@|ag7{sAXQa7M{=H6Rk2laHU&Cs zqM^k?x_0K6O{R-M6zSG{n$7ydn$+toBA&sl5K$E8CU7 z*t*>$Afm~z(o%m{qt2s^QXh4JLbusj%gQoflrYn> zMjFgyR?-y@?&l=*a!opV9O<`_+f-I~D;4sC@YCW)L$(ctYxKFmWdoxDFBplL(zVR3 z;D#o=?-T{PURPHovYi|mc1eHh^&=^va{&u2iiww?1LGIk_W&QopXnAuS;cL$K_WfP zuwFvsc=Wc@7N_cqrd!9**Xa#5Vp=TIQ67Xt{Hvr^o8hO~YCWV9wUz6wRpv$04dYY& zUuexJ@Y1-wkF76SK8HL|+3kU=z&hncoQ`^~ihe|AYl_uXdDZDe^&WraYUjij0QzTE z7e>W7>JRrC%*11eUMvOm^^ec0CVlRQK9nJ|Gym^1+c*3G>iCVy(voqT1mR!8(ksHgf+)gMr zuWIHc8LdbJTS;Aoj@BUYbfEHeQ?J7oxxX-q`qAwM;{1-$D&tNfG5~TKDmD&?DVWr} z6N|T8^f60eO$&kpLExd;z6JT@{Xk_4V)bFdl@1W{dx@nM%ul2A>G5aYrFod_91!4+ zJ>4ZDT9=wm)r93}uytyC$0N=<6$2tp5{HuqyMsKZdQ^)`N}%m?B60FJ_{x=Dlwa|n zZ2|0vpG*uS9gMp4|2-WSY`0}in&i!G)k~%>F~_fzBg5$a^ZeZ_C+nya1RcZ3#m#YY zqO{Z>g&YC{)zMb+0qI7|$%{wcbOjEcUgR%| z%6k{#kv8b@>5nBqD$(jxZYGk7Lb6Nxs=D~&`l8@3*fl4n&qW4dA~4Tc}~vICqje#W@PsJubzF~_5!O@;!euecB0)oimCtum>=i|-kslexiP;mg0lVSFlU!@ zqSUF06z!=$v?o5^c#ISM%oO{J74LtYp`>oD)FUvSVG!lg>t&YnOMqBM+V3r67krX} zPm7Zl3cPX?nB$Zp*k*)rq=T@b?#6GsJSqPMstSJq^c3(O$S@<~y4->8HSGI7B z&~epDFH}!3>Q$qzuXKDq!;7!7j;=B^UJRO!E5kIy1sC2rZ7Cbuqs5_%x(myY62*>$ zQxPjctAm+-I&~GV2YQ*c$Pe4~wW!d#<%HHef}?nvZP_?ryD3V$3uB;HXwb&+ANY~g zMfyAM?@*~MzP`_CJq@OFk)5Ni)O7#obQvJ)zaXzdk zrHpcGAA_(rg)-v{ctF!WHP;?5$5EgS@~uBp9BSy^?_a5NnBeUj$sNS74xTwk6b4H0 z716zdg;6iM>GlyOzrpQte4-a`kF+%;hrb1vVDFBJN5v+?Z8l793V#Z;6VmJJ^*lJc zkK?J84BvUQibO7CxGNGmOqR)b&sFC)c_sf)vQT~wSS?pwOlVM#QN$csy3D5Av+ilp z-s}o13|j30vIPkL;q7goB@GP(#z+2Ak=aVFRz<^YH5>c%?4$Usda#v&+F4fAOuFBd ztB@P~1#nu}8TU|03$hm_zG5Gb1+BTpDf!pAmUhSwqjkrk-u@|PRNFK@8kVn7uHJ0N zSI)SWqg0~@8p=j7c~#j`#p>3S>mv*=q23a;i6=T9<-qf*#t%JE@w-xUTq2hmj?6%#27Te2Ea4{ z^H%&Xy@D_fk9#)*{qHYz2I?H3|8~A!=wSUWTUT!u^WG6^GrHBGYS6#Yxd^FkyQvEl zA}Y6`)z6uAWZDoCjbJ@#$Xe-r(QsCUQHDT5Tb0E}5B?}hrw?$-A1Lp-%z0Bs2+fSk zp7dOf{l=ZXMPI1WaT|><)g9N8Z1qhuzBG192%H*lM#ba9$$#BhH(UV78JmDi@jlW* z_OqsDXy~OpFASkeqWe)cD%8kl>CjTJ3&%{b3)vpX}mIvlqUi(=3SZCn1?Rs8^O{d;j3Z_xX z;>Ae>-v7jfOJsA9sfE&B?UrCYHg?F3Go1{mjEVvC@f#I5aE99Un~a(qY!aUBa?Uu* zpH+L~!^D8}s-xKa&oFbYt{X9c{Q!MgJV$+ka$Te|UMyOuF8 zFmmDT9921_ydl~v6bAOHifvco3839pOHMW*CAa(2fCw3|6wkN^%dVg+kG;Q-joUI0 zdTh8LR&h&w|H*q(h^7QnuJ}TjH?iaS?8oD{ndU@vHYPxr&B$UEtEJRBlU{Wxu@IuP zz%mWaUHir5PVogZ2lVq^oiN3h2WM+IC#(S$ObXiY(dj=DvL397L^E|=tBV3hf*j(D zaAxW664g&iC~wEUES+31PZk$Q9|TXun%oxt4HBqtcoc*YG!*`Dum_OO`W67~p?_g%;L20Y ze|`nP>i`3GAycTc&(7!Tf4~9X6xD(9P*0w;mE#+5$y>(? zCt$U_u@x*NSof`bMd-xeI;r;LXsPG;=HLV7VOTg$vP9DZ7W?7YLx$weZb-HBG^~n-G_xk&3oBI zUu)d>*%~Cfd)2P;@qaV|aE{}Da_{`V^KA>SfBzYt*!>{vJ`kkBIKYQtiwH;0orJC1-RRj-2l-T zyj?7Xq=1EeY-HS^xrWO<$EJi<&5O{im@0$( zX+J>xChWw%NUI<#Jb>jnkumGLyAFdv)t&^1-VCYQGH9$rtlP~pb@OM7D=-iupxu8s zYUV?UdskoQ6UsEo{i~tKXbl00GGtErs3X29vbRd9BMH(XC-xj#Mb< zPIslyV+)bU{?3|>b)5w-Y+rcty5*cc$)EmGC~@o4A&I*YFygeFM^F6>R&hCVaD`dB z>DAUe&!3(6SYo>|sb(AoU3Zm0UoXsG|JS(q53t8|#-{eXdfmbi>Jk3iArv4V!vvFm z)Q&CcElax$G}9}Q>S^D3n(a*(4?-wbLvlQQ-)On*JTkbF`Z{_gNl2#+9458yQ@r6c zqqJbm9sok`U2C(igoC%lc9i|k^>ABL`>R*z<3ZpSHSM1cRf!h+p@Rc^LRM@t*lg?B z>QI%zrotw|Oo4VYg%#{jC%QD|wXNF!98juaVo?{Pn1LzISL+*08`tOOPt>7y$Co_G8xbO0n3pPwA5A;a; zV&~yS=8xtf3p0{n-}(T`B`xI@0z|IkvIxYgiJzV{eXz zX0CfXp}iCPrjn?(8bFPD*A_}_Fh&#LF#9k}ODCot*5ll)6@YLUrS!zqCrD;91Acew zU>z;lm2*qXRjUBeUz|aqd2Gg4sLjP= z1Pe2I6I~oEv7+_GUf6PNA<|N$R>wkGw{%Q4S>*a-A=r(oJH({^gTBJ$sGm0p1O1kx zXSd3m{uF0zV+?OAFXJ6pE2tyDzy6ICQ+5*LCW&`xsFq%LV0B4+=qg*BF9j^M_d{ovlH98h zk~*v8JQ-;|m#FL|3~VF-z!3us)kSkAye0dEengrO*zy|x?^vKcCVB=g4`b-iq<*O%5)cgh2K=tnyH{5To?o* zCE;T(f+*b*@dA{>Mw#s5@{SZ9bOUygAKlZvugmG3>ve1=2z{e>W}U2vT5-|X z3z7;zO9aqfWI!`FGw*=d_TKR7o|lZ7+!o+dj!_aA|HY@%Z9f|Hm$h%o17D&7bV#iV zGOa60CCsZ@tlzdM*{~5#e@?jU+QlHZT@92}-ARFov%~7>`=lwYl)dP@1tJZ#no>eg zN~{DXz}F z1AGnjC5F4 z&(&;I5D3vH1`24?P0&&om5!FnZKwu{^l?*zLl@-@C9%a352k)shv)?XvU13grlS`W zvew(f>kC(O06z?`F9&-AH^=GJ6^<)E{oR7$L_*`9+z2NMV@?|x&N0aa&LDc8j>CXj+~%WFi^G{ z6^<+Qt$kdl+KLR*?#`r}+(k3zGu!4|Bzl8xvCtAo8okQL$h7Ea?YcI$Dr4icMhu?T zra1A+N(rsF7I_Kyrk<-n|8mpO=(;xbbl1AY-~V_^wzAq?#(|dWYzN0AXD%c}eG-hI z41gyoRwY)v!nn;S)2hhQbB{k0eHH!`Fjj}YD^1AjD%tA~j^_hNMonYytO5bB-dMl? zKdomvuXg-3@P#a+AL5jZ-z|nY&Ez|iy?rY4-tz(-2FcNjDavbhfF%A%dLQ?sfKI(D z??hMM^N_J~QNQi|DGJitPquY`{blZ0=f+}|%-KVvbgcs$`jWV_WFfg*U?(Yy@ z|6kO-cUY6#wmz!MC2k9{6h-MO0wPT*(qSPAO7EdZK|=2}Kwv2%(v{w$BE3lo9U@X9 zU1}gSAwYm6gceFd!u`;-cDd*5`#bkMcmHv}M<1mm-PohVQaXY2TJGej~MxCq4(on#M`=!=UZx{g&6LV8*qrx?-CAmheISe1hBa znws49+jl3mZ%K09?Ge?P+p1%WaIpxOmd9MeDe!Tpn8`=8kEwo%i;pg)86VqLl;H^+Jmk zvxyg`uMISGu~dia#U(ZoD5qYuz&1P|ngLXimGFyIl9|Cf@7B^Q?b^Re^95mXkx_<+ z@s3Dz4(X=#+-iA`0DRijl=gM0yL=}doOY4C<%};8%j-b(iw|D0Bzv^0sVf;@q$bOO zXT7aXv|YU2;Mv;EZ#It~dJzl&?Stpip6&cRYc1prt(4b+>a4`zr%wJM0v7wZ9qmg2 zZXbjm;?sthTq3ZVMZ3a*-^|DE;*;`P_ZntEfqgbe{T*PNWjMJ-rdw=u)!Li0H(?CA zW7IUo0TyNXi;B{&!VG{VwJig1FwOa{Hf8$sJ%c%YqDP^&OR*DZ&{(FiP*nm-g^RsY z_;p9oP7VXUw#ZiKL+h4S0uTkZ^z$K~3c+%P@cO#o%oh7zpnqj7^Z>JyTX^+ttQ(W= zHY8D1@BYLblKxz$zImUcC{Gt&P#}2mNUNv#Z&J0Vp&<7Yd)rpUN5z1`mv zQ{S$Yc|r!S1VrnyaUX2iJwJ~0_~hKG8+4qNulzWs)Nh2Y=3g3q*3n@^w}5(hwHMi^ z<-jr(pY&hcILVn6pgEM$kUmsaM)60e5u11Vx~0nlfMkW-5%cMCf4Zj{njc`(I&F-+ z=1O)Fh&!;k(dx(pvIu%c<^%yw&1hFnP?@h6akwK1EK;m#MfI%q8KVYaZFV1mV1&H| zPQfx59N<}6X9#9Bixw)H%&%il((g!R=wuNB3)HN)oD z7CeI8R_Xei8c`8_WGhhgMGl%$IXA|__9!a4PcCg5xY02Tc&litCvSH$jLQ%4+~CG+ zgEd?*IXZ|JsaSfSn8q1r5!-^Bzpe8^&8owC^2xL(1yCIN9UvXtg;1NdbTS(N7Ci@g zfZ%=V1@d^UPkMP8NgQ%9_?pS;?!N^Hr@7P%4udnOji0i9x|T=juA}mjE4-FLmHZnG z36`Wg%~?oV;40P9qY4jc?=J3TrQX4s4b1uj@pl{ITP5~aL)@ER3y}L}O;hD7r;DBQ znI1r+m#eo@?+m39zh6dX`ubLENzU4I+zIn4s1Wb{Xoo&Dy zv2m2)fTqI+r4%=+A^Tshv{9V#(fQ)kkHyhDHPyQmsMaEO*U{Y0f4bjn-f3eKzm~DZ zx}j$^YvS9yg%#oLu@A9IO|Mi66-4%?j>}sc>STZidV}rdfSLJQIcjN5j+EN>XxKi8 zIF3Q0KN8Tm#y*{)%|x9R-x5zn(1#d@{d7W2|9<}sXm!3gmjK-9UZZ?$OUk#;(5AU@ zmQr(d_HkG*ozvb3IC2WUv(5)H`n(&R?hE7To7Sl?EO&E@*yFaxl# z=T1}1C)?PCW6=O_30afrugiaNUv4OWv`1#qqB<|%BSAn=NsWf*@QJbTH*dwMu6MNU zFLuwAb6&CAXN=wDpNHf>jxkbCBkrPht^?uNB&99Y)M7<0;)S)XBQAOb|BhCy028sP za+Q1fZ}Kf@kDPU=g8VJq#p~U&d<0q}<|}IoM{H0QjDIYIlQ#M`4!qAnWbq*9Tm=Qc zx?$^BU7BUP9jDVI+OTSa;(wG7Tq3=ifDq=JN|ABL9wljyqDg2s$SH7S74jQ{t$Jkg zo;t^*)yCbM^Vxc8=$fnWI=OCtpe9K>=zch^*ycVC@rv*%eB}sf?4gDzH7g+Cbzm}6qprB2}@FiM*JUaXRw!cYb zcLE^#!Xa?gTm>>s5W9)F);O@A=>+(LLq`$9A3PZ!cQZD^;QO;<-~B-i*iJvH{bO)* znIMA1d&C*JGig_yxeA~vy*1A$L|@k2db&0pS2&o8OV6*^|&Sqnp(q~=`p@NVfTej z-#!i-gKA!N_+oQIJQ$*MQ|}mQiQ* z-U9jI?9s-A21+^ZrU6A|KL|z-8oNzqk^3`G4o$1>$O(%q>wD8rmrsr!#xl}fwG@TY zIk^|NV;0eJ{A*>w8=V&{fpNTE&)sgeMt3edsI<1;ig;^5t{(8|CWY5LnT3BFJz+fS zN&L%-QecW6*(mI+3w?Yrn7(hesR-KKWR#hd#mPklVTM`b7643b%GF4ck?=gi)F)Sy zM;R1h=j|UJd2CXAV1Wf%k{!x1^`XI>HFc}8##l? ziwY!msm}QFIDit9?OD}{Y$_ALMQ@I(zN9SKd5s!T2-&C??AkwSsX|0xI2bBn$#YDO z>k4RJt814Zy9BD;77HY_`4i``aVxnQu^BXdb(lV;4TJ0FS1L?#&Fi>ZZ8F9rGVZbj zI;=yLWPE~uJD+>b`n3lqb63O7d0ezc_Oq6X7Hl+tr1-Wr1mQohR96zbD@U`RvXAfy zV5Q9`5TeQ`b=ZV}l9%L&#krE4;^@{IwOyEgxFY8QCu~CdTzvQ5{Xk)k5*zry&C!%S z^>Z!j5jvk)cl&`^P+BuaN^iU?QyS)M7xD=hStJYqCy=@(0o&2&wnq*T$l=DY^iZD1 zE*~U*5gQXd-*KwuCRKU<@20d!L@&w4HDb!`TUy|Piu)|~P6>X2=@uKGgmdJ3R#1{H z6^PtX;c~`GiUqx%<=C-nNgiJ1bU)=fi9(t0<5HG;&C3Ac#Q%b#BppsfUhYGZ9Z$6V z32N0sr0RCK*Lu2=W{j-D#Fee23*1&3S7!CjE?mJC6r)2K-Z#}f08;cx7;>8Evi;k- z`k|Yv$$AhpH3s&p6d{_u6CQYHt24zd@K+}KRjJQyPJg?S-y;x$I(bn%q@f#LfaG!Q zRM7;ot*h);u6TE4CS4{k*1zn;6;R z=(WAQ?T7HA^{>Z8POX#fPTiaVl1z~PFNEs78WB|}AS(*!FacxNdeW&TXkhPI$F$MT zM?!#I$Ml5gms4It+@3%8tXv=6 zM9?*`!5S_SDEX3zt?}J(9GLPqNjA@Js%zM+w3%l%hmQ3bF`(%BRE}>SeiR&A0umCZ zxP#sSiW0&iB96Oj)4sAL%tw{KmYLA8nR8DbS^z{>1xoA8{yf<8`J$}k#hE%krKgqe zvj9G!GVdwGZ{pWb0dx!V0iQ+$Q21N+qf%IWEaJaW9S*gN!eB6tE4_y{L}|aZ%w~b% zGRsDSR{Y2R7Xq_StdMd*y!A&i04{Ng51Ge*k2IQ_kDs*+8m_N7DgG-i|8a=)wLKA^ zN1gO5aSi|#Z*^Y5H|>{)J=8BP4mO{C>X41AlLuQ>^+ROxt5yZS_3J0s;@|50G`A_d${5VIho5DKwrjtOlRNcy6--BrR zDI#HM(yx-5$CEd+ST)&~6v74yers)8iOrrJ+ad8$CDlxCqI^L|uu0xne(O=<;3K=F zEx_?{!X@1H&gxc#pMt)WLIUI-HKnJ7wr23IFQ^_r$zEJL8%U#s^^qH9=~F{n8lo!g zZpayOFCqLMx+N!gqig*DYfoywi%;kT7%>V77Nn>>LTz?9OtFyr><&JApM`D#(D>1f zrOZv;vN>}F`T{?JrXD`U5VYHz*U7ph99T#Z!Oh09(ozp#p@owIUjFKf2}hmk54v(#K9i)*uHd#@6lk-1W+x{!L*2%Ve&|tj0JJd zG=x<=d( zr{!#}y-zqYR2D!x!P@rr)KS!TVP)b<7Jzy9@g^RN7y6&E&q7iLAMHLk2&xCL&I|NT ze2*U>Tw__>ReKy&Tn?Gk&DZ5yunTcCwkezkhzjJ8_q;mzXMb^V z(TFgzuF2AMl($X>XMe$YNM4+*+irkXE30!nH$F;X*{7qo+m|k6oj-gRvNv=g%P;8q zabp!s3hYb}^iYnmkNz}4fC7(KorkDjGogR3yFkd$E)uP{cJjo<7J6li zJ&n`*-(gTleMPNz9in(A)l9w5;2^Gl|+BVts7%3!7b zaDX9@`{ZfW|6K$CGBs-6q4Ib>^KV?L3i_Lsc<#SZb~<`4J8Yz?=5Y^!%{G3NbtIW= zN?}viW8i`!@gIQ%%Fu?@pTR$o{2<>(TQX0EAQIygY^y#pQ1v59%PlwX0PH=Ay1*?! zGkHufUkic_LmAI$6fAe26olJHlxv)&%q^m~PR)P40FSPtj-jM1Z>$N}R#U@D=V-a^ zizzJ38TjUi%1fno>PpqTvEqd@wFG51Q&b5kodJsv023wSXzCNdjIdwoMN^Mv7B>P2_dXy48 zfwB%YUq8tS18C5xIW(zosRN?Yu|Hy56++5NaSZ^Ef()<~ELNo0IOKte+uK)JflV#)xUJjP<3jE};5KCPJ$ez=;Hq$5WA?O}u4J@$Nd_alD zCayawgU9$`4+jyuecUVgwdnHlh|N)CG||^*6jiL?NJ6+7cS1+o^g;T8oeJt^x z3o!D0Bq|R7ci{4ErXD7#eKqejxSzCKGqc7+Ds&?de-vhCh1I8N+6C_nV+K`CpM3Y-AA}Vd+ zC`*^`iC=5W#&MCnJm9W!onqZU2#O~qhg4tJo$&oFsABw{No{7!nUYgS3Q~|2j=>1` z?K?f8-^VCGtt!t%{oAZ(9K{Ik`Co6A;sy|83n?I-82nO$yUBeX@qHIyNqEgGH&>Vc zE{oDv&zcNn|IDPLv|zG&-Gqy;vtOxcW3QAr7sGlA|wlgDE|0P*o^ zpMOP03L{S-k8qm;JHf;{=^i)tCZ!8fa$1fh`rXb*r5dJ{Ro>!*G^sBZoZx-HaG+`sn)`~L@Uy0&2f>{C zdr1R>t`|T`ksgb7xAgN=Co!g~hkNbR55R|38AJejIBT(DKpZ2P8UAf9kN`_fu2A9!1;2lw zN%)m@`Ccgy3>H7h|HY90cw|_D)Fb6 zon{$F8cjYi$jCVBomagWBiP`1JyT|^_b`SBb72v=e=lh_H8elrpWJL^(|L_+tc%5c z&iD*?pAl%~2W?&21t@d82k=m`b~$y@drV3G78Z+AytDA=4Dj?25GZBBBEsX8@={!7 zU3WAD$d4VEoZvqg@C|7!sBcG!|0lI+U>cUI%Joa|eYr%j*Vj9JG;iYLU&F;kpz-^i zlvr`V+e`07l>?y|;27Zp1@py;$>4G5u@&3xD4j|Gtzu(!Rx2)!@h68@#g>UNOma={ zKIWu&zu8T0#gUg2p&}6s!XFve3Yf+!2JGTjHG+5;I(7S)0Hk+-m^?HrikrGhE^7ZE^dhh;C&rw)L6XpI=t_3@Hq@Fy z`p-S$N&P>G_1yd8=dn8m@L}9DxLUj);0#i{j~kE8FQPenT(5f`(M?!Zf6z_d)RZuN z();sW-u?5tj3NJVTzn-boa6djx|!aM; zmHiAM{K@u`pF7vPu77%}F=YRRthyt0mLT||vU?Q3Noi~O8smHlYf?)4jsKd%pQnx1 z(OLCR*Z(Jy_|?;O9ytv_vk@l6z25JKEPv5`bgza2I!>X%VJwHW0{~DG5Mca{o-)iH>o4#B6 zPqDO)iWi7lM>qLo{X@`5%l_|;0>bVt=OciOF!8Ugb-iQL>aP!Z^zZ+mhsgh*Te4i* zk!zOU5XDgYLI%a%Cd+~N+uyGec|c;vA33})z9qX3{CvhO8{H#CKyPIo;+`DSB-qV2 zWkacczWU+#&SPxw?H1$P;J?54DDe;nWM3yHk6ye*ndG)aOHw7>ki+G{PHqX?8M7-W z1DwxyP5K`h;DgSYAr8T$e{SpXxwI2d#v&Y;qjR&hX`@f_*FJ%rKXdMqMz#&$gWCpj z}!b zARfS|NsE*>Tn&Ly2%_Jdr}>ubVNHFeNi_|;-8*UAz^;TApK}UCLahdFLKwwT_|6WG zByV4+8A(uZZf#Vh_N&~(4KQ;mqmqI%A|9RA=WY1Ar&+KJc(LJQ!EVUgfhg=OL zHqd>$?PMzOlu44%Dhk2d5S-6_2uLOT!S4>lV{*HIFLf6CguE?7s98`R@CxRjsp8y( zj}~pYq0Cex)G`B(2AhS3{?q6lbG^(!d*@I_TMlyR%p!$_Le?5dSjq{xxo|l$2+Wjx zJn)uN@^zl+6eSL4%7*#e=M3Oe9l9y*kzk>7mDXU=Ana*_rh)>6YR_7EhlZ#PX7699 zdKelCe5d@BMyQq&nUzMP&22tG(v*640AzG@l|Xldz90SPlOiCHD>xs^#D;;rJx z@U^ok*QtF-JZKIdvQ9lzGZ0lfkxG9UvdV`SG%@&x|ln@S)D?4+C@01c{ zLcg)llA8Z&&+{p=AL(Pcx-tT0^kqPQNiL*+o6&~V^N;%y)zF@0`7VRc*JUT3=P54K z{(0k0KyQq(BApPE_qrH?ocG#%>Km&xHu&Kiw%S`t%cEXX;98PR&hhWGW~EKJ_C=;{fyFf zc=aF<6=LC}Ga2-ViIFst5*Y0X+f!+9TR%^fNRXNG3yE7hd+OX{?jaap(9d;VBI$G# zx&1o}wyTh7rG+Zz11A32GvwW<4CHfJeHk)q(^Q+#K;Myz>Qn(XrMM7pMUKzR(uV^( zL(hOO^EV+Z4P{6K*IMH}CAZ!O!IlO}4P{PaZ6W9-964p|`?aHKHzEDosNK5`nzpFU zuj14xsl7DHwI2?^?<)T(;k)q|IR3LAi;oufPztiefa!AjetxiJZ*FjD@1xy0V8c@x zJg2hbBMFkVG1rO9JM@mygRHc)X0=|lk}AWerIZ)@)v3u*2QfUS;qXL!$j`0Bah(JuitG0wkn52>`Z zo@7K1Yy5oS?}D<21Fg(mAcc!v^6#X{G=k49y+V9_akKWDxmw!AD+a<>332IiylXp=^_QXHF z(>vKpLQ-7b>|or`n0N5QfLp<$FJTA4*%4I(&F^9?YH{p7onW zh4LaDLnF%YZl*Iw=gen@zx*fQc6uRu+@T?)y*MYAnhd6u_j6ek&p5p@rjSRwj5F`E zAY$RvHDL3d`{VkGN&|_WMV@F3Im8B7yKCH8&)*5iUn~A8Gs&*@?-0dLE}O2`e}*Cr zh&=#4!G&Ie*X(z@)zF|HGgA1EmGk54|3>oiPnAmG$F<|(+uzoKIluH%8vYN_zt6yr z7a#i{d5=|kN#UboW69KEDXWNFPLWAg8N<%eE6rE5boHQ3IxxM)?U<;qEDzw@x?`Wz zxQnBA!*HRBrJB-$Y$Wl16;;z!>b-uQzNJY}WcHP_71mGa+hhm-rc8_?ekQULmU~$` z1=P*9Ca9}68(e&i=w1>+E_9R55fT(|9xzde8tLHOr8yDzHsy9ZZAadhYQbP_x2d*2 zmDYieVa5>}e6QLnM%d7()YvGCHU1`J-%F%ubfJ{`n37%~XK}gwRO$0VWzO2kllDcy zT8397l80Lxb_N`;iw;(LI~+Qj#LnCm?p*_cw!7)3C-XmC?-MeVY*Y$ZWHA}s`f%X{ zqeC8Bc1rQt^p&@KYNtks3T)4^Ob6@Ah0494xg{HOiU=Pjd(ZyhyIK#iRMo|#Ci#+@ zHNBN@_nUQM9(yGi$W}aMh$ul|dq`94c3J#RJ_U|XA&=5CMtMnP=a`tC|ic=##!@Kl!eL4(?+My%u>I!u)8a zM{}|zwq#Nf>VzqCGLLI^@Gr_1_~<5$gg_Y*@*!t?@zwRd?# zKk8`2OzXUdyKC}$8wt#f0)Y-6?9p8GhfB>OF2D6u{LB4%*8#YBIjeA8N#7LV!jKPd zGa&Xw-Ix0&C)td$$38Y|u})U(xD?a~I@JXlDu`FDgkA=9r$>6#uC*AQkT9N1kEDw2 zXZAGiIQd$KR2+9n@*uJk2j7SK;c(@VWzx((4u=F`>9VnpWRD=9@^E#D$pBgSU&HtW zP<%d9oESce9?Pq#xrZQ|_NWEwXD z`{`M-%xe{}-=}9iQ}%vM^5>H_>yp6D*^9uEVEnbVk?+(h`RY}7w{jw1c{J3g){3dy zK5^i3_J~P7A_p-Y`_{6Pa+VGgGh~&bd?1S-1ZfL>kQc*ri8vWI4Phi^d{w7=71O>kHb1x+E0=Pj|d8PPpCf*SjB4qvb+n^xJUhaKcuiW!3iPWyHJMrJLlL zQdfLZFKpaLoMKlxecLt$Ju{n9kaK2@r}%JbakQlFl*7=jL3OWo1lAsk^9*UNv{Ddf zR+kYHvc!z>IJg?BdEPd@330&5!s~8gM*Al#gsWmKNTN4sd)=iv!ltT()re%+T3_Y* zSmwv;M1@@w0;G7aj(F*I6ck7 z4Zgf9WMefQgU03Utb9#UNKY+gAj(MRq;gyb& zDq)`dfc4T;0S0coHoC^Mr~ugF2D8%=(@>l9=|;({jA1A=fi7YP?uz!hMQut2wfq7s zfn7%XO20?qsJ`6yfN(vzbdK407K6^8;~C1HnD}l{ETeTk6^EIpJ|ZFap~6aQo0zze zG=rP5JSgZ8)xt{CHVCdNnHPGpauuFBsOzTN(lY0PdY5N?*9(q6z+Qxnp`~M6@4GAn zAj?OvJynW|6<$k5+fy02A2$8r419r-&zKmt_AI{AGO41>vAH;kq#k2`?%f_>mFy|p zDRouE{SawBS}A6i0imhh@5)*)o#MR7+~-_3`@j)O0y79Lg(cEp!j{6MV^6X-Q7K#D z7N0di$C8$w${H_G=zw+A+5cLwB=IpVg1*jY`E$b?T9~8smC&12;+K3nZaG5(tW;E* zho%y>N7Pi7yMxL*zsMEp(ZhGb_x;HD@2TYMPyJq);)sY~A0M*Qp)+OEzQuNDIF}w# zR8&RINVKE{O5Su>D?KS#szef78dX?jQV5sOo2*d_jcrNb#)}Y7&s=pIvLxASxTGIzL;5Io3b=cHODjC+2BO zs-x3mrO&%c8mR3E4Js4w0B38whKlSnuKb9W`RiLv%LXk%32d(+b<14eM!rY0K=?~| z-^uU39j#posFqqis}~aC@I^75B|#T5wTR>{7i;8=%SqgNR%O(Qa5H49I+ee$if$*o zfV>Kjl#(fWzIKnyn8F^LMzyHzt*eV|y#dT&$Za^br&ckzLSxD3V2WK_s%Wj?yvsD( z)uc{HEI&i_LWqceaKB8hP*n1$YdtPg_c`hLiVuKJ?VolEy*5>XVUyaUBkI3Qu(D_H zGCyv;c0dsVxY^We5sE9VsVf+bH2#wxCyZM9qIxO1)5QhW&zb>xIG(*>Cw%t*<{}7(jGjJr zD-zgeKH8aAOh!FlU+P)-{gaSN{f$CkW3WpSqBpY*W#&2h{Q-QRd!#iqbagb-ad+W1zeKJwaplagraBvjJu7K~yk5+i+Z`d$$H>`D)bU~5c}!jMjBGL) z$*naAZW`Kazgr56T28X;d%{h|dQG@o?iZ=}?K~eQg`)OH-v#vg67{1;eqGQrLpHhP zdI|l8cIfStoS?#263l%@; zdjcFa5#e&%zYQb|5+u)xE2Oh6*CfB$+PC-{G`Ror9D%?8>dgU}2&_CY@SV=|k+B*B zCS)4PEPgdz+;XQ80%>;?VDfW%;;4%v`W{a&0yB@=tKz*asdk}w2f@x%(?&ndvr8xL z3<^=$hI;Q)R4R(C9}YzA5atJGm*xncb55-8a4}Z)AI*F+y1q$&Y|kBzX*eaZqTuD_ zt}q}7g_^ib=M24f=UZBR0qOi$-xag%K5Qlji0m|bj6ubGX(E}3ne0BcW695zl}s)J zcShV1&%3VW=>5(dSUPl`cbo@yp?BP_os^n&)PN@CODff=SncGgj)wBn@h~mV~mGdgZ8n5q)5L z(nczhar)L6f19b(Ql9q>wqQ#E{MTiRc~zTiBd7c+>9SW4gwdU|!&A#GBl!uNShksW z@XWF#+km0;MZ1%WDWW={PZ9>5ngTtuYAukpoHVGdX({by={IO)(1RSWA;8U`?X8^` zVe5_YJ#~eHuv_O1Y*7#VFMPVE0(xR=Q*+Do76_z~Q~rqPc4hqLx%i^X3kz1$4}lI{|Lq3KdRBNR0At z9lutczS%O6?+R8MbAdNWx?EAntX;j0m6e|;t_WMuY|z3I1O+@qJguA~DTL2t{2Tm) zSwx_?e`b#$c2&;v;pF+Tl#9E1th4!r+3(+h7jEh3s5jfln#ldpFaobvZ#{#DKN3k; zlV><gsl=(eNk|%v@z;bOj9{q!wsc$K+hGV3UMmPo7C+(EbNrr}+~S>5Xv`eN$@UTiumr z)W=o#ab4?7#BrUy1wE3<-mh<9$~RQd*~-`(y^ih9Ve^&F4k%D|fnxCmIi;NvCrKIj z*`C8pbA<@fbD1C8s40!PXeo#r5C0xGAPlPi01Y(H=^kD?B{NiGL!8ug0gbuu76d>% zAqL>nPPhAadXf0?|C8ekEz$qrAkHX~_f@La`t*Q5{T`R%M`-$! zoil-H)sI6ONi4Bl=+Uo+UZLwFoE6*JT8nP!`knf)%0>$JIcKs{x; z*<8KOIiFJkk4JTwjN~Ae!I*wcs?OLH>JE_9Z;EQ{t;$~&l z7a91552p06hma^8o9rjDdqoI8DlR^^zwS_%Nt-H&T`(+p)H~Ggqv-Ltx;U%W#vH<4 zKLdV5*+?>nn^i`Sk1`8Wr!s8kN@X6as85M(0G{AMCY4<+BTRg`8evMqxJKQ;Wxhh5 zYQQfADjt6`chn*3sI&B_-z=<_QVgaQQ4#=+w@5~NNWtwwj9kqV1h7Y&7Hi_yVfMshMB9J%+{j_koKf) z=iRq_x$+`~u{xmooRl-wMhLTVw;M3p9%rO4fgP<|DirH!TKC z2b40lH2SHWjY^&luX0^8IYnL8%ETrk;tE7spu+rXqrXr$1iaE;;JSLf>bp!)=dAYS z6jiQ3*zo23z=L*nAcT`^oy^VHWri{OGkDpS7cu+fjTp=4UZh@KPxGvt0Z=IRBPh0k zcgGx|ctu_bcadiQUiRJyJ7HL)xJr0xQCkjoX`d_As@8FJ|E65$v7mzmfXYi>1tE)C zsB4X*cyu|)4e0jL4Bdi|m-;eBLulxbQrf{$%kO>DP^CrB`fqVG`Z8`51TM{%hgMRS zGTe+s!7Ub+78WJ+`76i*It3`mqn96&kwr)v(XE)&yvqf9|2YnN@BnoQP0}qLLsCBm zyF|65?3^E^yIS388IT=jL#uuuIg*v?1F^Hi&at%7mfDHg$1$YvLBFoF_Y1S9`|;wP zKCjz%zb^M7C@=RLTa?CHYMG3}UqOn_&dok34AFol%dl#stYZ@NvI|@+LPEy-k{X)r zS%+7-7KbOzIV)=?Y|h1wl4FWz^``WPsqK=bLdq5v8@sZ;AF9)E58<2j(%lw~W7Y=>?0IC2 zn(^!^k&)uO4nft|oTlDlOR()|UOn>E^B-}j$pABYPwM07^rt&x=H(fFbn(&mGwYO* zHy*U+Jd@rjT*JJFhl>|+pcp{pI=4K7_Q&!~2yC*uopoiQ57Czc6JYr)Dcb`l%Lioa zW=p{AQC*I;OV#-~{@XTiy&UGCJ>btn?z%Z0Vmn9Hx4vp| zGW&hRc9$~8-OelV<1OJ++y!UFZ4jdOp+Mg`5Breu3>HY|3wBRE8Ef$Ec^~a|(TA>S zrFC|TjdzUW3=C_&--w`vT~oIU`oqp;K+wi@+LG3KD>_zt$(>l|HrgakqZzs{94&3S+k`f1I?g`2LgMqCS0XWcENGK&x- zXL9wNU!BR)+0lxb1<7hQ9DQo>Tnl@C(BZwFd*X?8u2SM7=A3Mm<+zTXrEVoC<2NIe zcbR;at#*`(h#Himq6f2Psfuo~wkU6;#yY`$ONg^=K>yZ2^IlKtGZU-gSY^}R&4$gs zd36$IDHim$rV6{Hp3uN;-W{BSZ2dsoQEZEm0R}gvZ7)N(R`YJ`)rRh-JH|w<_RnjA z0;JMBPPpP-??lno3nv8Vy6SvrMT^M~v^q)@CwqpV61s-LW@x((G)?tz-Rc>DafIy) z<1Dg`cVELeE_WjppF9-f7Sq5+#yOc=4%W3-d2odG({d4S@w6IiREi($V*H{nF3C7W zXifGjN41SRQ7Slg=OEK-M{76GlDw;k`$%>`(YNqR!Q96Hz0g+7!oiK>SC4l4u$Cvn zYIhnGHx_f>iw3ZNE2_EtM{=tjEHCXkm1!-hx>FHnT%J^1M-n>Oomo&WiejvLkXBW5mtqvtkt?QSrmD^{T$0sGj@l>z?jhz3L=x@hwSEwu46mdN}FFu~0wUd0D3A z%m{yLtfiS%3k|&xJaAvapf;!ZA}-TJNuz6kel!J**%v_O&xrgoZ-n34&YNr&W?=n} zg;h`Xz>a^$7!LY_!7jqzet@R92EsGJcSnA)mx^rl?2LKJ&bb|RFE@wQ09%_zAD5Hw z!i1V~Wyq^*?Y~UD0pK-ZbX+;`DJiMqt`g2zsuO^crSF^$OMuNq;^X6(^WMd!r8;f4 za!JVg+92e^e!RLrqw?z5G!VNPIc;>aP`-4(Rbh1%lhX?Lma@0rY?-w!`~eX2SCvjA z#DClmSkiiyJOR#?FPocFB$E+^rKRRMUcQkK$C79(Pn+rTW|g6-pX>5&f&nz6WFXhprVf(>#95km9oF zgU!Vi{loU?S2wk|xS0B%CNGkBz~Z}uy-k%RW5Myp%krj*9|mO{T&o*{gZ$zgwdZQq zj-ONuj4cZ$!c#}@ySuNv_Mn?jq?YPhB=a8bsb47{=&4$moo!U=gaCv4Qppg9R!b+e)zD_24oW6?YIX#Dv0JzhCmQz`Tdf+GPP%BPk?}v z9+-&_#UV&ONO6%!uYG}^cESmActGSTnQLw~#|1&M1#nlSVRrVv0`iTuXx_^ck*1;RhdObZFfES z-en>Edx4hAbhxCFL{8Z0S){#jeU|)u9b5Xau_Y!XNV7S8cv+G9swu>xuhOuS$;s2m z^%*rq6pTDC%V00z$L)|_9|u`VvPJA9K*IVHXv`7VhK4(NR*#!)&bE@OsLkB*M2vVr zz$0TGh~T9ZTMfCE%?D)DnDPD7WgRkQu*Dz|p+hfMGJuhnMi@VD{#&Nl=hedKVo?rq z$-2gG{IFlEP%m@J0>dT5l4+i8N!y$SGSE5lpeO2z5d1mJ@{8 zQQNZ&hpr1(OMLX$5g%e^%FXJD&g{*?87>NB#_udLUQ5xka&n_U%SNDQ9w4t;+^pbV zQ&3u3n%sY&G#2ZH>z~LFBVrIeGdCa0{*U`My0|YsMKEeQ+UMQIHD|4-o9Ph9>u%mL z4%dA{D@x{Sj`yjdr*C6dC;2(0hW*!cb`WwXYImCpTPg<-LvK=jqhDjzVzZhSe_pj5 zNC)1#Q(jTQrPYi8RUz)@E+jk|l?nGt8=imSS)-+!-SwW;C?}@hdu6h@C)M0<7Y{52 z#@>wmx=n57JRxd95k%}i?3giTd*VvWU^VhJ6eks@x1@l{jET;VwKpIw|XccEfJ z8cX$4iJsB9o83rYG};>Ee1puWt5#OMHy&c=F<^N&bWQa=&-@rccfi}?&k-Sy=MtpU zQ~cIqJle$rc;!K3RGk2`1T-$Qs(gYRU5EK%nOyCq{XvI6meDWxbpOur?@?*NpLT#O zhft3E+i$tFs;9_x&IdyefEZ1BR#r^h{p>8;&vC=0E`osd9XmEw)ow8N_J%m0ahd&$ zRlrkML#m@efgxaXi4q^1WkSXWVz~6GyILMmdQQ;`tT)vK|Pl#&AZ79Aj1 z^zAyHLANduJ+*BAsT!!bVsy~X$w^p^xLgDTl^Mepr6A=ciIP)N7VyiRM!0if-2{hV z%vYzqPfLNkec-~BdZ&zlOF}lrdh5So++|>meym%oYeCH)1Y1>Y>cImh;SQk_X{wS% z{vqz3N$+*xCClw2nTf`z*G@{wQ@)-{bCDpk$^G`bn#>tVqqpMnyV|gw8?3!cMeUl3 zCns;Kz|zWH8PUERUt@|5;axVKhB*qpGAdN;u?qf+Z4DI8&%R);7Bc2+n&c^M?t3uqECliCiHs8bIb(U`M9-kh zg*v{3$gIGgHWx>Hm7P?Nu>jL4-26B$?I-s4w-`%SH+qO|3zrl{$&~(X{xQpI6M2d< zB1{V!dKCSfapm7Y(KA1$>dhj*(*9#I2uU#_Py5Z5S|Nj*1CI3Z`ZA=7rQBwKMTew2 z5pf}hq{F@ewgZ8bp0HrTu9{ML3&oMV(3LPMH5*hFu)n#uIzMkszJlC)NJb;+KI9Nu z`JoYjibBj`04`cSdos8e%P*T(7aR_F8ATr{1K*;v-eH`YX5?ygOO3A-*)(?s2B3=0 zjyfXDvI@`h4)q(NUIo}d3;YUXS(;jfr|d}Ibc1G927^l$#BE03n?Q%Qm{un<{9%x< zC2CV&dQA}MugF^9w+%F|mV4a@7VzwG?obe%4XX&q{7~6;*6>_HO&#u67;)sxA$E58C00YA!4`S^W&weMD_IG?Ms zDVFFIQpOTvY6kPf>X)W8XNMIlSWFIKzGmGk+uJZAm*@l&ELMgFnpb8u2(qb4E4cZl zfv|&t`a4;s^y|e3h|LDf7g&kxfDB_0Q*V*Er12UBXB!ZGJk{g}@8sCAOP!lmr7?@m zVGU#v8{=v;dFJ7=6w~o}Bpl2a{)tqPfQwrrw8Jq06n0Y&AdPT^-1U6f%?&yyWhJ1!MeT$nG^1NoQ}L0%XaMM zW3P1CA7OQdC6u?khnJ_Vw+sHQL}44&Hcg>Yp9|eGC_|3ryi|M6r8ckhSaoD%Cle63 z4aKyrTz?OEcZ`=jZ|||`%lRRllHm=3KR;6p?^1Nq;*wm-OkZKw{p zN4U}w`Y64iIOKj+RTUzbHPI&dxRQ2lO)2n%-{k*l|9i$J4?fnZ3G{XS>#e|l{piOh z0m=gZLyY3jX1c7PS@(&VGsjHM_a8mg&&vb_D4;zxl9)jQ%!T!QH;W{UlP#m#|l`>ZE=N*mekd=xg!O)z`TVzNXhV@tF7VY0A^yH=!)2Txeo;|s$_^%Iotc2&NKpmJX zUuB!-_yAP(okp-F0u5w@vj*SXba*5_dYa5ma@Iuh70uC&P0@5y;gaRpD0Hcxv+}5; zBwhS%v?sLA`~;8$3M(oaJ=wS2>)`!P&V#vrV&&ua&b>bqMrn0Uu4fqpyt#ul{J3Sy z91*wGe8jdLov%l?X^w9}zRX_fG`v==fA~O{X*?R+;()4QlVxB-Y@)W*3+;p8Hpe0p zCbR**%yeiKkSI8JC?{dCoofCH!eOI`OX;qo06D1%uSaL+pq@`P(>3Mn_A|{np~hDj za=VWE*fGw({qp?b5tZRw|NDvoEe&z&g&ZKQ(nf1vRTRIeb^g-TJ%hoBLYYE8-9UZ8 z(SoF2xo=8(M!&t{f^Fn}-y-S`G%+-QT;E;zYuG-U0VKVT%K!q34O!0?OB{h|FZq*t-K;8h=l?W4@=C(NL0OcxgMR=VBr7#)@NteOfCIRP&NH+R{hf zKp?x}xNnc{^ypd#^?6zw{=V4V^wEkjoyU>F#1PuKRPNt-l%Vxu5R-J~-HJ8zwzV4i zHsgNH2FT;bI+gfo=TO=3Ki#9FAt?n4BR;bk$=uDAp{lw{Ty6Of5aQI;BRP3?hJh3uZ4j{I^?%Fq5zBth+bv4y4j#3znUtf5$ zvlip#dfEz?Qklo!r_AwIgvi><{SM1={+!3(7~=a@O2;FKVDrbOFg_K?q!~>3QO1nUi`uKOE=t7tuso8u zAW~IQH+IVAv!ZWbO1epS6IRBwUy(5*g5Zh4rK(ypGatqBhnMvoX|mE|87A|^p4RH! z(raQinIK#MADh&cy_3PLyNpMPQMWO>NqKp6$#GeSp#i^EXBibt28%B4-Ni!rsr;Jf zPreQ;{H~zAQ=(*b*KxTCGT_d9=F|=H`8BcYwd zh5CqSuJNbUX7RB~J=TYgf34MOTa4_ame@7|R;PK+F^0jb-md%aEw@dWfKyG!5~I}k zo6r@2t!J3SKHj?@1BYugHcIu1wR;ajgLl`SrGq$Kv zv}s}j0j>fVHx~umI0x5(#`FF`d_v(jjSRV54R)Wdd?z@6f~Ns|8~t!*DL$M8!p(&L z|6=bgqvF`MwowuSM6iSew}b$}-K8NUxQ7rN0>Rz2ApwF*fZ*=#4viB?aBtkbai_a! zzG9!f&pGcqzCU-2d;i@Uqp0qty1HtvwdQ>0oX=dVj$#5B7rUJa0h@F@%G&TQF1Dp6 z<3vHjdFuUfyR4EOE>$P252yZokmS^|gxNjuIYMqxaw>x#1*+BS=Opxzbv*Fd`3X^q z#wsa-YH`p`YVt&T)Rpd9pIds9)D{nab%w=6`s+dmnBa{1{R zL{P3H`r`t65-GPK-|N9aeGUTiJKohBGe0vB`cM=Z&WTQvNd<6cK-EK^ea1OXLKAu1UD%vge}qY^-v#)^8IFR}xMETaK7gfX6EQCP99f$A>B z__19;P1#L#5=r!g&>IT#aZ!p-wSw6%&kGLiu97(3)Z7JidOUI$n{$s_*}**9=-E=M z=p#Ldi7Anyv;q{DL@mSu;k;=bNBi5;fm)Qhh?5e#sHoRPl5~4&TgMB{lHIJIa-zvY zuH=qlI86;Ws@1n?=xEG%Tj~H-0@ecm0gz@WJm0n{SOXjt1#H7d@qHDhG6g9k6FTKR zVa>Auj7s|8^(Vg!heYumUpAG3(StbrTDX}%Pps0#8{Up#l0kVIi!3wM3NO*Nh{(+g7 z`cl51 zi&<+PYKSmLqg>$<@E<(-TSrTPMhe7t%)B#DjZl${>&owYv=4|Dqbv~zhUH8L^2_yZ|c0YW!5nyyYI+yZ$>vkWf9oqj4M~tUc-`^c?Fu|{&la`@0F`UrXJi!pbJ~;P5=&ngwx{^K% zT^NXIc2!!s4>sT$ttVdMzm1ux_T|-&YMQAmy=~b*%~fPMSkbMDh709m+PDZx@OzdF zgB=}R4ARe3kNC7+QASW5!jdJu7mcUc$4ut+%YGVBr?fzu_k~N4#}Hr0T2$rzxAI7@ z=%kXFB=@k5RQUm;RfZO4JzX!-&MoAo%caGF9*&08lGqZMyWAVqG*YL+0uj4J%)`z-gE)- zByLT5NZ5$2IGPp0kTAj|6l$Qa$@gF%;suM#yJIGWlNIRhF8SzmwCrE)J+jp2_8hr0 znoOjgnPuJz09CRwQ6?!@BmIv_y|pf+{Jq?Lf#;O-BmUr?oGoYp(t-JsxPnLPPQ90= z+{}`NeSL&wI=PW*bYf)mIPQFG+J0@`tW{^qZi*_EW94gqBSUI|8n?Hbsze|;x}a)q z@3ijLPugtqy+qrjLd#qGLG1G>teUfPU^UxaW%k^-NmSa_R9`~?;L^0xr>Nv>O?D0> zaL=ZKpBj*y7)@NcvuMRx^}w-}aL0l#I0%xzzrknKC1k}QEXXh4irpjHppL?&y&$sR zpul<1mv*mMm|{Tvxzd`J3CkNxue(Bzc8|{k6$?Un-=~*R08`u4S2NJ4+JQ}~jZU@f zQCZDwNd{A%+k!`L=S=Eg`a0D|#ygsknqUpSsQ(yOzKUbq$_rU~3@A}g;GJs8_|vWH zsLd{&i8#`3LMXGtin0YsJb3Exdpgq2c{%1BBT&Iwloh#y4Q)EVBELzN0=-1l7v|m= z82zO-Cg+d&W$lgKBbV>Ja+O9lBuVyR*D3nXxJp0OvYQOn*!&Tx7d$Cp#Rj6Y*VTr_ zK-9=4d|9h_X7%%JZIxZP5Gz&s{?z`~BNKmNl%((Yq^ zk4j36Pd7XID&VKO_|&YHt}=pq+K(&H^`@r<~rzXI0`|-{mjM?N8a+ zzsGmbAzg5TrZ} z5JA^SVX5QWtv&F^Xc-Xz&#=U&{Ft^*xb#m6=*kOw@U@hS=WV8n3gF}~1m?D2neqsc zh2h)miTj#tP6ln8m{M0x$h`43s=3}lZs5Gu690~nKb_;fTBCIvVL7UGcQ~dI$z4OFKF6I(sIawajty+)5j0(R$Cl;ODbODU zI2tfp4TJFFFv}&gNL&ht_UVPC`^5nc=H^Vc_g#|;S9Q|OIYW>{-{MA`ROFyTIpO?InSX_sg*Gii^JfdK;UK&rreAQC0e5veD6a z4MzqZDAA5dKGQ2=>7QO9FB`<7AfKDPa4ty7>sa#sA|Gw4n=gXQ$^Y+HU&AqD*!Fyt9?DS6)i z)hFN{b=Y@K2P{Y(_5oSrpF~ptf8sk{P$Oz%e@>cmeyJ;+g!W*Hxhvwd&GjC}wt$|9+y z$J({8Vx#r=Rgig_B+{kuhCJxG$h-zge$E{Ol1{v0>>-WxJe?DI?yJ&PD?RgjA13$EVCS)5CkZnsZ#Z$yV zdpHV|gk&1FMqdS7E8@Xq*YuNNIua0so?EpXQp^!i(h1(DN14BY+TB zdHV6F*X9Io8$#R+d(IJA4Mzb1v-9^pley$Z;eCVos;JxQOAv<*+4bV~&Bx|ycj4mD zu$F{B23FEN*WEmS^3*UnezOqHvJVT2jd{p)z zgF2cGxALpkgrBj#aYf8g(1)(g2%NKCN&Pa6VwP2NEk+>j`SYt) zYMd9p%B7S0Xh+IZ(}OyL^feIZAJjU}y5@sl{cfI&)UAHz`%PrlL47!r5}DNhWOcpa zz2O%(-4FhLOsBc?8b6lCtwVF6%(QfqLhFJrtkT+^o~eKr`Rb*DfXNvs)U?1FsdgZR zMLnYMRHlxmrK)9Ofoci`EV}Ct0?9i1L3dl+hZnFeySh(g=ubu4uH|A!{Wr}WWm-o;1}ALY}tI+CZ8zJM^&fxFPd;BW{pa4 ze5no;1W{uO>R1FXM=ayO@_tAYz|jlX`s#Hc*;gX5cwNG@^}tVX#C9?d@bk(5KQH^J z>C=Za$An@%JwFTqN!GeqX>7YAl5YO;p|3va*;AbtNzaAIh{F&2gV`6IDlpn`I@fAy zX!uNW+;L+Vwq|D`i{HLKoz*@@e9_)lxmf=7Gch~4W#^gkg@Ismov?eNN|;Gg*}c($ zjT3{{<)M;xAWo#cku!LE2~jomwe)e>DyBbiyCg;U;IfFhvY9hDNT&CG$*EZV(;Omi zDl^+(SQ*Y_1rxIcf#+1yDte>#weJ1K$EOGNtLRyfGEjj9>D;}MqD&D^g!l>)RPg%M zE422-#Wc5*^HVo-#&0*op&+=^po-9eJ0`X?O2IV4wQ zRj^`GG>>oR_x1Li_WF5ZhOOiwM#rG-oSXiW%g`}2*|yp>V;p@|&p7XadE|**ZyPEVNg3Q5Nfo-Vpj%mAPoE~@Nq4sclZtL_pZBx19XUVLFM|dfwn<9|-!f#A^#}O0OpP-~P%vXI z%lC=26OMJykYliXd1+^xBttqln7b&phO|#v zDILeoy{E=oEb+7|`qnz<(8UQ))GROctE?>k?LZSQKrEN{6mmE!V)Reql5eG?PC^S% zPfolAA6JS#cFxF_yzOOt@x>{oSooUHAvnlPkNm89p3PZVX;E$gbec3WS~g0TCY#NK&%S}+?raZEw~ zd+O0={sGmzkIw?GU#3kTqxh~t^<8jelz`QeL_aw-L?*Sz z^`&oacTIoN)4PqrSwSb5<#2_Kiwn9mdo$&vSs;rMt4_tPHRjgm*ld@ANgDo`F_v`p zecY_-&&2pd#-Yil`eppHaXr3mNpdqgvQ^u(A5HeSHfpWi(rw82x;EPVXniNzUi1s) zmG2>OJ68()yjoelHh;f(NL*_%bM&Et!pc> zl3lqn{Cy$3-xNd6BzOhCg$|kfxRl526JMx|p7ug%ozGzlG%OADF&Wk{TKQFlOrW@y zMSTUPW#`i;7&+hXS|OZR3gd6e?FS&AH;1n5z%NCrU`xF)URxg-PER9|3v4?F+t&+o zr>AujTmA>Q@QPKrxt$1cK&-17RE!4x`z%Dd`QN78nx@{U5SBMT!Vch3qdg6_6QfvQxJXr<*Xqsi)$^Y6Al9DI0XjxY9~V&Ojn zL_B&t2{A<9HtNNpY@Z=Jy(zWE%foZdg^s-WfLHcG+?t@l0UWfVEEe=gkSm;@fva4z z+vm5kkgYiuT{l%&T>6mCqF5FNVUWB-o$-1NGI{SE;KI(Q zdaur(M@E70BZ7iVdc{F%zYCV$dg}CMjKu2-4i&*r+uo&2hs;~Wmp)?1;oI;PqI#9s zx=)@U!z<7&Uw}SR${`)>_Ob2i@=Sv*cmVa*DI;lH0dsqzzEZO|V{E$z^F}3Yxu?A# zeD=Q4mLRHpjknhsn~q#e`W>+KYMcrR5{@kGWHrw~2gsGX+@B=PgiOxR_&Tj2s(m6a zZ49dqhlxkRTCKkpPlz&1T9B&u=_^PJ&d*r(u*82(>p1> zllpk{Ljz&Qt&`Uf&MPP_nq6!N?n=io6O5_Eg3fgFUwq+~=CfEU5?-s^>NMvcmFPO^ z*pvo$vXLB`MP$2won88AqH&JYjLjTS`E{fhB#`OpJEjw~ZfPSN(YdEhSRklXh6uEJ}gT#bQIbpP@OI@-TYH{vrF$BfcT zY*38H$1gEgVut+8ZWqWHtcD1#rw*@_d|{Ci)cfSvn){P03#?C+eDVu!SB>>@74-H5 zOBq;8bK{qWe*O=yG=<2+MqJXB_N1SVR_cw{ZmcPiIHi@TsO;l;OIXdJBI4Z-*J7$> z6WRypo;4dKX%ZA-prFPrLA&`^9Gt+c-klw>*S@Vq6!RpI{5e9oa!pzJBG=!}jMKtC zG~3y5h^zZkI9xgTQNlU!(N_7jBA#_0GZsT>ytmSKVO= zVfR;*Aa7<$5s%k4ORX$_!E@kG)qE>hTiD}FTG$(@v3GC~($XSwyf?%3*M2$-k}B8t zzW!b=apj;}KXPZ=!0?^?)#_nU0)3yG+N+B>+h2ZIc-@yvK5VGtNS`1oAEb&f7_Npq z8&}$Uf}wYEO_25h;^V5L@UGVv%{PAXY0P%Zw~DU)uV_{rLHkVp2~PeP4~33+#wZ0@o^64_&+cAn2R*d-Hp88W&I}UxiBoOMP^{#Z)xoNp?LDKT z5TF8khC)OM&IY$zs2i@W$qp@7yj4iWcIsUqB`b0k!()+&$@+RFv8SSa%^CvlvY49y z3%iAMW(TFeANjO5U8^Xv1Ymx+$CqD{Ou72IzvdWNNE;NOZ@3_yoAmN29ZC>FY5&JdON|ELLuYViC)u+>|;Pg+Q7BT{BHm?2( zCAvC?>L0KEdLrz0ghs@sr`C)4m*E8dyg!(4^aCcmUhNJh;J06Wpsuc7QMIb{MyF6M z%ju+T=jdv;xO_q(Q4f6n(U|~k89-YTU&H*hV zg0~<^qh->)V1aHKkM~6xorR1@2wY9u9rqo&@XqIp5zX<0pa$HgLryeT=x=03uQTO< zHU6ul;Ej(!lVH|l9U-HJtbB6`$2yts3%PS03Nfl=U7`+tezdP!F7~w|zLSmK)Od-JZ%ZQIQf!+Q0jNOo&Z3rit;SUT zX?bdpDy0C>fk6e#DLa9GfBW&8Jg7gM>xNwKKcC6l=TZHA>;FDv(RTf}@ctH}T!7zy z-NxUSjKjvd!T(Py{_7PB2`>EQzpwW93oTCaGl_rNQ%!F0E$@2z6--1}d`rywOib4=OaUtLo=P{i_p zxDr7(N%kr)Q-4B-dJu8rZ#$y>Y0&u0<1J~xI$cwM(X@Zl@OKP~!*Gvfc%`&gdh!UcvD(>t{-i7HMHVAJl+5ppXR z#|PNeXTrTci=Md4&lf%RpQA0WwB%P0ld2_sXt=mL?b2BXF)nki@EBOi#twVre@Gg5 z!xejHECIEPgu}|+j#n`#h26e}60sHSZ2$I*i1c(luBS({dwdAd?#1AdiEBBu+t0>w zbAvOBI-#)YmdMr!6<15^*_H?6yWXvED6XW|D}6pYCa(9@bQ_w0T`lAIRQzxkTn-d6 zLY^7a!jKuzGv5jWOE!?3rkx&A9#bv;(XlQD7 z^o88cWy$6~%Z<5J2<-i&56-peI|D3BQ8F4D03c`q19IHQRBqtx{DQ6J?Ae_?X=)0I zjg8ewu77ul*ML@f?k1{>S?paj{gukxyqs&xCv<7$d5IvJaBuu= zZnC%u(V@?CD&iA6;hx%V2ZNcN1Md?76!N_n{^K;^-qQW%B#2tG+ikWT`y=2hrECYQ zezvQHV>_3(jul_DwpZN9`?(lTod|$@^zv}a;ZMz|v+LyeVLtt^FU`~Hr;m2|ba99D zIS-}?eC&pV%2PWN-5i$D4F4s9fA?Ug=k_Cb_s|Z!IeS>z(>L;utQeZ7QL6%9W3K9b zj!gp_?LY$4)U+WbS$u;KVWBcI1HmvG#9S;_bti6RNZuzXaiJMujaOOpK zp|s0V&t^2mRfR2QuPZI~afOzHt@0k)S$!>1oP5=j2VZr=dLEjz9LJN-xz``$d!9VF zZ6q>Y9gjO~)SOu!ZvFBs`-!deb*gA6-V#`ZqaV-iv8AQ67jdli?A8wGM&s~?UHo}9s{I4(*JsBr*+_gfmt(M z{WenXdhZif7zWzX9vnSDF}bFLBs;b*`CZ*#>0z9_u@y(jK+QuaUPa(E14zGN3K`6Q5@0P!C042WlnziEu<-KQYeBh`+@`p`on> zUM&x?9xRez`9&zM;(czLKK!>*V5x4=^+Ntx*b$4oJXoC-_7k~ijX5D*D}$rHP5CHh1hE5JJ0!SMf1{P%;-KF71*HQ_Kfb$%@0!sFX(Su^a=LyG zMZU;=wJm;zwe6HgrlsY%v@$D&X9ts+3XhMu>N0=OZx?=yB5DRk|HQye$lr3_b~}w% zy`8&itdWO18aJLBdS8{(tLimhd1h`b5p8)I%ea34%|G(a;58B^EIg}{0UaTZ7NC`D zNRF$@^_7nv|7x%|-~7BIDx1^2A})Jp(7g}5SMgRG<2=x*b;=mJ-1v@*Gqo=}^gjx`odJ4 zKCC=nREfWQ3_iv8@w}fB9QVwVLP$|$vHU^^^PgeRzjwv}sU@7Zbl280)N^cOS#m^f zwQQj&u9QNNB(+@|OL$WZI-v(R_IX>}AYp}OklbHi?bS54wN-++9p7(5!I|_RZr`x* zD3<|1Yau9T;hU(P*R~hjTyr5lvy3Z+t^inbYD@> zM5PC$W1`T~=bh-x@%!Z#Y?FF!C;z0kxlxvzYSb&YM5OX5T3lXYDPN=Oi9U=Q8`~hs zlN0>zx>&mQHVcAk4u;eif0*m#mQ+PGxx7>Kd334L|D4|u{u~8OJ~v1#yNURh{bT7% z6>_x$)=y92x4%!Xn9|l{2>p}fFBngpk1Bxv)w7W zPkQbc#l^*LJGt={z}v~mk0m7~-Ed5VFUiYyt4k|jp~W_#gLO}l74Xmdod}`);Z=z( z9|J@ShYhVaHZkSe^F?>Wb4jX5d!~kZr#!u0tkD*tai?}X*Pbg_i>@&Cp-SrCrh)2Q z??=j|Ej#>5{Kut?jz?*PC6Q9z(q+g(+xTJI1>5j(dOg*ut&tCYxR##3aa`P(;0I7> z8|3LCS3eUKXJ1~MYb+7X!VUMe=$AK9O3H0*5PJ)cHd|XFD(QWm7l%&}6zG2$!XzA1 zc&2>YZRzb2SKH)mADzxDY~Xr{4l4wL)JD#k-=^cTR2#65*C_FnMoT zpcTosfX#`zA$K?U-F2+NMds$xbT?8!K2NFqed&koEfHmOY?G{uw{OVh@1qUd!$O;{ zmXqCzoZvUNW)*rtxgi>s%0UspI~1NyH(h*N=m zUiwB)qBkzv(3ymJUUkcWL%J~$RRI^m%=9SEWTX&!yh4_LB;>CBbBcN)xm~|Yvt7R# ze{jU{GN##EgxkH4%X#fg6LPvqWxuj4=U*g5C|4~7TwwuOE;ndpAheAU3$ zvy|pP=X6bfIu@_+a`4R-7 zT~$Mb-M?B74lu_L1H(Q$5;z_1I z@gMKY6Hw*N@!8^hdYiJzV~=Z}Wjr)??;yN`z14$M4ix2CG06YM@E;K4c;`AW{`$_x zx?I{_rHfMQpgM^c{zsv4H6mo#wPmd~V+{`$pTxO(oS^yO`josFU`I!*@4Pg3$Ed!V z_lT~2JuQP=;r2Cd=yAIZ)fE;7TWos2|LY=Hk$&E0RKW8N0XUM+_7Cg0lT$-Vce1Tc zJJuoGdaI3fdG^;4RPUc(RP%2;Q@!|_-kg5r=T^2LL$+5LI;yddM|`+iR<$$Lq6>(& z-+19MCPpd>$D2wiE=@5_<_+VV<9rJRHt*OvPj&t+b{~6w#Q=YB{bJmq{h)n0XR(!W z&zmHk?dOY+`VcFYyYiBF03>ZsgtE%RHYV0?EdGZy{(-kyT_%Uk20OHGmAL61viMJuF*$Ze>I<{j3Oqo5dBd_$G&DOvhE%t^6YJ}NU03p^4ZIpFU zNqdp$h+R;f-Jpc8XTfgf-)eX8`+vKcaTA9=u-i*{{`R*|S@)eck+w1D((B^%eg9TB zZT^V=RyQhfejq@XFM4?PkQk}?2>R9Uj(em(GUSMSV*9#Zm;kr~;_Ud_pth3K=o0|| zNT>dy6&Orc{B;XDI{E{s1~b77Q=h-l?`gr_{(dkJWLbvl`v9RAwxi_^bRCGBih|VN zZTY+lrgsc8v*ZrPgI}YNo(?Mcro$E1v{?SmgvyQUs zUk#ci0eJWSJd*}9y!+3LAHw|t( z_dA2(u0$&So8g)VDEF;IA(npo>YA-otok`I326GdTH2HRmzS6Z*CZK zu_XzoaR0PY_3K{?hYe-UJ6fc>0l^`NH7O33ZoNSxA>mS$4D>e%VxoqP@5h{%h0vj8 zO4Se*E$+$IPnksLxlLXpchk5N_eQ+O=b4G*It%~C(m~s1D;eoVgKte=QTu--o<>9S z#7Exdpfkx&?Y%!_lLROdj|2(Piv5Bg_EFOjPe^UoTVmagIJYqiPxS^*l%t*Tt>H}; z!6f8?v&hg#B|doARsvBS^Tvxvay&A>X?M)CIhKh&PfmzV+!@3%l~ilavLREinpUs= zn^x)f<5@{N>r?{+Q=L!y;v;N%H#87leQ4V2zH`rph@zzKiO$)~kn>Ejgjx~&?LW>@ z5teul0$^Ow46nba8vDnyXVvy&_awnJu5=c=R!>50XMkV7XEm^xp9j4p+51Zl47l#; zADTE}b%UijC!|6J!~Ft?A5nhAWdG?`6fAckgBdbuaGj=ZZve8Sz(o%s=3ZH3c7eo_ z&c2AdX6q@25r<%iH0XX=%tQ}%{<9eobNl3~swZQ*4kv83p$OZdAgvgE%6-C68}B!S zBia3yF{uUTR@~aB7b&c*#Q?V484?0QgzZ|*dmyLsGNS}u{?6Uy9uF?J@{mnupRXfk z2oo#n`E^PIeHF@aPrgc#=!j~wCsu)*FK#8Qz4W=JAE6o0sNW5WU029dz<^yRfgp9zhYcj=y&eI z$7-yn$-C5rGeW-uQ!|(EJ)>X)5_-FJ?mCST5g+Z2rYjx7hN`O&Q{u-5MkO}m1!Z_I zCpyF?9Y;j^Y%T(hN$k)Xw5`JojMmygPh>|Rt9Y+k;!|GsF6?ye>acw_iGGFw{>Zmn z)Fe7^gH8XB4hN1|*?|lKDNqAOl(IQLT>LrE^PlDvs*TvK$}uG-FE2{NCmW#Q z5ewTMUx5Pw%4ZKeD4Hg;kUwU(T!e9|<3tu-u;+cP>(&I(rI%f6_%pq&K71U>wvkTQ zMV@&5oZ(Yc&BHl$Q5*CK@^`u^k88fcxRuSXyEx&c=44k~)o$B@vUA(B>_7*@Arqr2 z?S9TClUT{3954OKM0+}-1*AVPQS*yoD=|l!dHs}w(P+=h;j&HNt0O~v}L&ehQfx$%CwfThXmq%FG8$(&E!*mZ2vgUrDIoI-NepRWO zXN<O>t|f(hmO{uFlD);6=rHbHP} zftOxD)oMwP^i3JD@n>%l*?ox>1f6^0RQP{Je*qf61bEw%j9X_%A@PzOU@%E@1$Dx* zR4hM3c-!AS4Elq1?oC!J*@2~>7HX6IT@=leCZRB^DPpBtKMC`)ZrLie>zoYz9aHM> z!@wNt?ET{DCLfZP%&$NKH5_hc{b$2|+$9rMEA4C;jUlsNt1)yaac@xmPvND$TmJUsC#SN9+MnI! z%j~avIoPTtv)astDoxn>mUiWl9?uibe>FFTKV^0*Gd)}b2m^f5raja)f}^VHgn}_I z17PBsCdicI_^VwbdY73c@g*$@D=URrfmru>AP?EuHq_oDc9!RtOLvJB*BXnqm^{x+ zpQ1FI9qt1Ad?6f447=vo;WL-N;MpDjSb|62u~n(#W8Pd>7Le4kjf~~ZqDLBwol8!) z?k;aO9yykxdrnBfEJl>ZW;N-n_n+h>ev0CxAGj9K(&!M4+73+`mSfPhNZBCXX=Ng8 zqTD!pISw$%C0PULR7+zz&S1W^n?M=+c!%Z8q%i>W80qGgDeB1Hz0Sh2>jF?YG` zTa4~yThFVd=N3o;G&p@RsMd5qc*kp$vZzjyi9gf!c~XITvIdo`T+~in*`xYNzLC%| z-JT>>Z>V`DTRg=_ZcoD|;Rx2lAJ56ulDDsETchymx ztzTKSQ_0bYOicXxQ-L#vQ9U{_{E~4Mj^=STLXu~onFM>;8eo9nuGUM1e_K6^wKI?N4wx> z5;SJzyjhj5NiI_y=AJR!h3ki)(c*wQ8|9$Si7}hE9lNL>B6F?9GsP*xAyH zP~FHX#1BoqJZBm}Rm%P_fed)?H0ky2B^axkI{efN$4u!)+a>S74zC_GNHmIar&(kT z4L%pubV>>NVH#PchMe*4PZOwR=BoEu6j-Mmf*%AX8hMNeH!K8jb6F{-@i zp0TG&hQ3d7QU%NP-*N6%j3g#S643Tk)AAllXIe#!BgZNHjM+-MpYSxo&TL}5g?7YS(gPve7)9nNk0;ss)hsVKBbohor z(H2sQEy(O@j) z9I`3rS4jeL?YXfL{?PVk^lX=YcFMfUAYO~zQsFBFc98%X^08Cd7=Em<9IAYc#BsP> zr6Y6dll&=6!I&#CSqj%D`%&Eevu|^HaUQm5*sdaR&dJl)93?4oR-G#gS@&c6jv5A; zDmXe_xZ&-;hTL$V<>ejG$M9gp!wgqde00R6>&=UdFFw+AsL6y<7dliN1&jUsiTpW zqzf^fN~^7rx8!xF#ld=g;AkiF^y;y&v&vZ70)E<_vB-v1t`Mf`hGDBXZ{>(xIDX7` z$_BTEtdCFD<2S{>!zM;7(8*$UE(hvNBoS5P_uiL7QPfqln<6Po4firj7{zOrfXo?T z_EJdXwAJ~y0j$@7;zv)eq6^zdFXvgw^S?=EN1qhCah+~v$1#O!&O$oKPm9a&I7plf zvLjuJ17veEZw-c)L04z z&Hvbx#G!unt4%;=g8Qs6w-ROb9t@9|w6@;YvLv*Wmyo-EShU~Tvx^(6AZ&Pam2iY! zHmG{%{P(*=_d6&_`{ia(^$tCz2wxpfp^_H06TG*$ly!TRrMX<=&7<4{abHi-CJQN| z*ehZ62qn|`_W)ib*8&Kic@qf-ShrY|qkE$O>(~hW z;e7z}mLm9F!5U@;G0>Kf{U!=*vEMG+LzyOQ*GHa>f~N$H2r-F2?|k zZhQb*7kmYpf9%sxrv_o=MSPuXq?CUcb%I^I=OnIm3absY$qsN?(0@;pM4PgW2Dn=+ z0V2{7(Uip}u=W5nv*?iCO zWA0g5`g<2|pZSj)_w(}g_IYk}yEOXuXBAXa?MpHD2}=S~5zmybZ4Y=y0R7O~iJgPQ zg4O4#`V!M`7_YCs2VrQk$kFg|1kYQceIjNq?;$ld1lNJp(pfxvb!dcDpNNf@K~ysr zuF12LAVjMkp5nzrxjZDw} z6}w2k8RA$8Q>o)Ty@R1+W*0SWU*mW@^OpPwWm5NlL}0jLp^gyl54g(d2l$zG#snp| zlo@U|@sAE9=A1tQ^7n)l7L%B{>*`oTEA4L}9UN}B8$zU8pZ4FVWywozXuaiF9JCJ0 z7#g5T=^86JGtNg3(Jj}_54iZxu&W9*E8l zSAt})Pa}}y?!m5p@?wC#UMG65#=O7ffse*d0l4O6KW=$w0EE}9Hh3{WWKQCE)7gpH zfLqm9oxSc_wN-c^*7O=Fj=%eGyUHk2Ws^hxbnLwJm3$;u3`VL^nVaDt>l`}PU)(&K5n8%NIHkN_}&v+r*Ow{fC_6>ewJ{j=>BX&jr+ z^@f-HhPnOP_mFpG315#-&?Oso4>%}Z-Bw=mAlwd|(q+586%!6Pj#_gYa*6Sh6Q!}* zLW9TnZ8&+G#E($NxyW(D1RAct03_k(xxQZO_9(xC&=22u?NTMV*G$@YtkG|kJ`U@X z(g?fc`UaHY+@sdv^_b|16BW-o=#b?9j5^`QvuK%UCCcrGAvT?r=M3Y#qxl10CxYK( zx2h!n8hUYKRsKFAYLe~br0;<510BbppyDVe{dy{>%ujMq-^21>@PA-Hf2vrNU03V( zGWp}@!I~G1F%-$~nv2{m>x}E7Y;NC<4VTZu-~Hh<<)GuZtdc!_`y=RnZQ7KKf|%q_&Z~OpBW;94S$H?2U1;%RI6+S3?y%v zi|&p@-}h|9vN|ngklBOm?GN!OV=d)#grXB+5e)IVsAZ>-pw{W_ljHDN?QuNW;qT** z^C!dd$qxPPkCz3LiHq^dv5iubmD+1`*rmx(s%;z6y$5>lJ=Z};A)!i#idtF{ zD^AO@OT7b?BcYgI-bG|T9Jm|gOXFPN9vZr*^QT%~!RZU2Rht=_lr zr?dN_yY56~yMnw{Pqynvenf0F>gs%)!sR^R3%(+p=6i| zV>*%qVOn9x<#$LxXU=@i^RAH+>gFCt;FlfvsZi@y=oObG#MV23Jy=eHi(4xVONjOkeA~H;;UlwJpotp{mz$e z49UTnt<#TR{eT58ojvmPBG{H-e?jxwi7g zOg@p>)qU;n%3S)y{@#Q+OXR~nesk6tll9z} zZl`6YU!m3+QOG6VB(=|Dd=uHtc6Np*n%*&EtE_Bg{Dw*Go*647 zHv$YMMx?zSK#2|Z<_6n5qoNi*6*0()zKhM$E2Rb9RZ-kkQvg*JPXzVoDCE!*TKB#s ziD`@>)J>%?i)FGzgsjOc^Y@t3eXIx>4Q{Gd|7@zlg8{EVk|ys)mLBOeTi9z zN{IQ!G6{VaC-QJNZ-a zg}R!x?eU(CR8v(-ae0%_@2FY)wjm9b=0O*1FyJt&Mv|Pd?7QceOU)Qh{SoN}W*IH_ z`w$FkMykSF@!tl0V?hfFV}4=LCfnKx-|A7A!?mEuY5lP6D9KS_4(i$hU4FR;~mFia8(y_-!*fWh;th4#dk-J&fgrWR(zS6^5aBX_LcoSgZ zjYrAZFFjt%BYymgq)DU2TW`BW7)dGeERqUD*xcM~2&8|7Jul?%b|mt2coY_|@4W)U zZ`FaaoT*;o=>z2}g;#sk!`(ozou&2WLi-qqwjbYIE`|Ca*Qi!|seGO*rHj4*-JB61 z-hPYsLiuk!KU|8!Us>WLSt;7^k&lEaaYb$I+M*mhF%q82HF(4Kd|5*#{!TR-Ws#wr&|R-ujU5*CCNkHxiyGv;Jh7(t}JHDUB1Jh7k^jq09%mUQTm) zAw6_ygDFt5eN>U*R=oY)1iVv!O~4BmL{neY%a9hZL3-r1Dbkf;%(fQJ@o8SZg(laC zS|16-T}5QER0r1(%dzDSKI^^U>ludrbb!IXvFe@>H))uT<|qZLxV7p2&&nRvW~FZG zH+~mM>HHB|n^oiHIo(4(ma|G(;||ZdyN({k7Iaet3qDAcCC%B84^$RvA{O)0ellRO_owsC;ntC!@aJjW_)DrF9eH_bIb zgB;c7vz7+*bJVkGvGqxSmJ3lHhSpej2yx+Tawz0SnMEplq+Q$7YB94O{*_pCJQ=a( z&SH>XDCMPQ`#Fpy3~>W*rVG|@4wPRQVrxO8`FoI=+1m8Q10+`B##`E%GQw1)eJ80 zEidyq)aZ-tV%wnzbDT$?I;C-ekhR7Zd5@0r`;)F$$vSWX?hPHl?NInSVfD+{>Do6L z#@7%~o(ag*aI7B|q7vg1&I8=1VcZj{|Lc6oF;o18{0_9zcoIs%2q>xIHTq*+2-yX! z@WKcB4~9zI_b=E4JkP%k&^fxZWh^Keh4GYFx4CWsxi9>wfI|cODup)zkD6aiGZG_c z`Up>=M>xna`@Yb7|j4d@uGb zP%d2MuM5#=BU%d3$N^Smz>@%qC~aEF+Tt6g@^Uselx*zSa=a1Dc;w{goJw|4T9&CU zh4*ESqk?CP1f3Rem)gbzl+IIo6kw-hIE{ z%?B&=10f-x(;ev8%DNlgN?T=T!?w!VvWTA8P)h**(Olfv;my_&w*GbIaD zN*|?2k;(0}r!-$0OEbAjdDZZfmOEsWSPMKum1*!L)?3 zMd2lse^TXSEkUnR@dT^m!jgM4K6BYRej%Q%(6E?l z)lyGT91v!INyt)y?CjyzM3e@+jEoFRl`yM#8Jk*s3&Nnmg`u)IH>-BFD_= zfI}``RQnvRT<`0L?04^e)&h!uzbY(*5N}d}snxy6J=i_;X_$D^pCFk|B0ODLYax(B$ z&Cr^}+-9+$NajZ=d~Kqw^eW=@)bA+PkEeDCN16Q>-RHd2577Zk%Z~x&DW};*?g4SN zK%Tzvp>*Md$&r*cVLrG1yw#5p98N&|BQToBAL9zXM!^Pc-MN?!FJ{#g2?dfPpKneT-WR%}_ekF?C;IEbv%`tR((>TN(?VD??Y!y+)Nzs{gB8f4g3Az%1 z4hKjiqkE6CQ>4LI?O3ouv#|ketZDVMcMv!rQ(mwOjXi7iI9k&asOk?S=?XhSsVcfStmQNAoO|*JdgW! zU^QLY3Q?U)JyBh|yT#Km7|BqY`1O}0>eB7#VkO@AOyn)+lm}kZ_*KVLemeN61Y(cA9T8}J= z#&5QNnFsbNARuvP1JQqHVm{yqjx5bNW$z=y5|$4CLT;ZKBL$d0wRB@ougW*8Z*!L( z_oU$47c|F}ZcD!q43IGOXVaf2YA;{n-j7xrd&?c#YYIfg)LS{T{+F`dK!wN?@ZpF~ zo>YGze@V_VSA8TvTqdH|PI$0BtySp%)4b>0G8R#jTM|v94#bEg1?utvq9yS2as*h7 zwsI#upcq?pHAO~qCiv5CTBp^GSC%vNS`xgHpR`?SC zDaDeG$O_H#BSymoHJ%OJ2Q}dHwQP+Rnrm0O*;IywF;6e+XO1Sv+tJHw-*Sbg@-DZm zUO`l02(oB34r@~0V_(|wCvozjcAmx$pBn!UO2*A;2J~bGo~$4LQ9F419c!#oj2cpq zmd-ffIJW-D*!Pm=Z;m%G{Lfe7bqo!=SWAE!z{Kmuqfuq_OSBtDOUo=EIG`<@_`yDW zubCFUaChJbx}caXL5!z6>1p+6bs3Qc4sEepuZG((%oc7ewSy|TjFKO-ih2L#$^moq zG!q4wXL6rh+jzFE`)xq%m7MoL&OV%fKJs7RlZF7tWvO#>UbD+=-SkE>`;%9I=`!?-Hsa8ILA+Pu+PK3#R#WdTfBps%1rAXr9>wOy zArINk-0Zoz=}i6lk`NARwXn)kBRs%Al(Dt z_n*%QBVdT?Ny5w5l$HyafKn7FVq(zE0vWc1;t{E-<8fK+(Kex?HP7FCRGPxn&h-`f z$}1v2y={yL#>8?jg(8(!GGU*VHZxL)6|d??Z=1o6BYIlEt^nx zSU>efnP=*9^-`bA6zFWq?tE4=x6}1E*_^^E=}E49N8qaY?wcF~&Qs-)W=(nO_|Lfi z_>kA1@#c5lK;j3;P5ucBXF%QJU}FA&p%Y!9+0kO(&lu!O1?iQ*q>3hR1RQU!B%7E- z4LePl5$&jqtyByr-gNnwmOZS0}nV z18ERPhw&bp2`V9){7Fn>PNJNWe+aMc($WG`!RoK_K%~`u3_$pTQ;G?wdnyVja#@om zH-FWq|4{duqxX;YuC+_z2x|&FuFRjA@USNhkkA!1w$C7Dm-j%62vi(qgI9-YKU2_n zFuazsM)S9!VEd4R?ijiK5S8D!rp#bwHE$=$0@Uhn8OsiiY0k$G~S)9orf%U-#a z8cf~zr5&AN3wkM6;RVE$Z%-z$5_H$3Vu!VRYYAgME9Axa(&_dTHKXIYzn&00!03Ay zzA7MJTI#6_ey-Kw1Cb?YXXMY@_?S~vOjKbVdn+NYZ+shME&PSwFU()(c~4wJaC7~R z6!zL8w>B`L3lD`{Nv=pYo#-4Lqc9Lkt^&o+&e=ZV<@3w0U05DR1m7pLS5+Z--L=Bc zx}WI1O#899oG4ORT;{)h62M{h>GRK~&sNo8fY%?>BplJ^j-9vhf~)8$4cB;Gn)gjw z^CU+OKtB4@S<(qQk#Y-ACysRrHU?*sgl1mh{ZQKbz&+`W3W%BYJwkU--L z?uvut3H+5km=s43S+zO5=P@)H|l-n{mMLB#U!sSfIINL3@)IEG+I$ zon%>^qjO=I7T2})erJ8k`~tOa7Y4*e1=xKNRd&)z8)e_tHZd-yM{t;CT7n~-+j+XC zTKOFPBm)MId15A*H-!s~h)8pf?jI+N3HA>u7}Cit#k(|)`yv(88dYHW>@6|suO_EG zDnEZHOONt_S#>d0HP@T}W7!ya2H5a0Vfv$3gFt~COO)uer#DY(AUzHjxv6{I{+)-v zLp zDL0j)9gMpsulm*rusd_G?8X)BSdp>=TLLfaqRIpkq;5&35zz@RVqEZCqh~--q1^)!o)kjc4(fL#9L=iIiZ0=pwI!vhw!M7A=Tir8Q zl1;=n4su>Dsp9I_MrOJ<#$8x=F zjK^`L=;6WaOhwiHLyFgCY8exY*}>}Db666sAnKCe26m%NV8e-fJ|G-(JeGe@b~abmceYWe3Xy6B@veatlnw1>mn-{{iphRz!gx^)5P z`+^K__YmJINsTJjPVrj189sy&k6LTYf6*E+BGm^m`L-K-a^^|Fs0}%D-3uJPc!|G= z8P#^53VgO^u0$ud81IJeWn#C!oO3(5cbYnS8-N*F{ZFpba&6>GyP-r zjr{;PORA^_^~p-f#MsD5@I|Cr49Ad~C=pSZcF+xt`=R&cylODu+cCM#FoyR`2GS){vQ^N|ix+yk9CytRtKETv zzU$0{(AEK=Vwp5>z--mB(%=U1zLbMbF4r=zm6*b)y6a1SihnhK7S3L~P_OTh;U=!* z$(Hg&?YGF+Duq&9O-Q7fhoeYXG)JnH^WgG4fD0)LrKVIC$Jt4$;F}e)QxYwTu{_0x z%^&%K#+iMlg0t{1lnz8*Ec@d>Jm892Q0c)NAmvo`vySyZ3uqiV1YVMY^mn+&1<=aD ztS(I}HdQ~$g`TlN9>wNgX=bETF^RT#(N_C0klB0v<)B&5khXS26clIldnO{&_Ptz> z*T{K(C)DE$ERqw#JpbO;u|U(T&N*zC$?XkavDynN*C=;dz0v&Dw@#o!(-v+^Zs2 zz(;g)WB-wdCY`N$9IneF8eV4c7TuM2CY#_C+^tn$($yR95mzl7#3#8nt^4=+Hz!M{ zT0Q~|A}RWxk_?V>(VS$3K%aUfvXIO;Q1)Nu#>~BXAkZciv5k4(HaRh=#^GgTvJaBX z4z{fWFOh{kB>k=*z6Dek$}(|`sMxSEcGc)hMTUzGgd$=?cJsarqC;MDQkWncT2h}#NO0~?Pdp8ALPMbaw?8|m(ku4X&v`_zs5K()w; z&;c%#o=l`oB|0>bY$B2E(8fCPp)n90C~|#W07PU~^$MTIW!2xFbJV>%r7s{bJ1syf zbVH6pzTthjX9s!jk5?~wJBplKtt&m$%lsay&G4BL8+o71scq?Y!$r>t+429INBIL1Sl?Zkc^GClf9_a0{UOC$|ubF)pR}!0Mg(ozZABx_p zm}GQn`IE@=phO=eX!1xZjO@7q$_ZAN696!0QG&hoTj(4Win%}GO61KC1k ze+vjx$PM-Q?40hCnJGf!(8u5#C-N5zhd`b0qeS|6zl{=ZMtx-FkL0UEhI3xWhsb&1 zPe>r@SX)@)M`mtjEj6rPe!jmY5_j$yY@`HHM)QF}BHEk{&qvIZBnrw5hylHj@7gb% z5}R5z>{uNWAZl{VFpn5+u5Yzw3DLS8kQ=@wMd@RqRIv8hJ!Ml#HU^O0#ymjw1fy7Q z_%M8OS}bw9o6D~n5(J74n&F9|r2V2YHqb^VTao>v5>IXF%7 z#^s8fd>ht}J5Q@r!*?*5N2vHM=_}Bfph6F>g%YTNg46nth^0Q$kv?LtFSPw?fBlQK z3%3LBODhLE&)LgELFX7)waHxL?(53C>8VP$0u>d`4Q}IXd<}L2*ff`J%ryNZNIk7v zw=xr(EM1A1JYLwEVch+7{u0F?AYd~?Y9AeJ;VBVk-4dl;|HxJ!y)KxS%mlwc3B2O- zyW~*7Bfd;n*?XQSe4bE{_B|iCGjRFNagB+h$8nGADcE00GHMt=dabT;97>JM>GG?3o6;=urFMsh-lgTyqc z__L#Uh8FDALF>5sb6&<7YRZ>_)-JRVd4}jA?5R*y+B6C_Qi8our{uGjsO7bExds)( za#R9ulhNQPGtTNRE0x?}4^IM()1+_Q{JaRB4NQ0I+GQBFROd`JmPkt-IfGAl9R#Xa zzqJ<{qkP@SL1hXy17gSJl=A4b;Jpmu16nw8pK=MC?lv1{*M0W?<@PmG-x8B|l>V>-nIJ#beESJ^`1<@5pp3{hH~~qEPf0Uf zFg=TpavGmcLbd^;;yN|j6uw-Y2r9%wuZOCG#vRoY-OnRD7>ZgpY;c)wjvjDFA}eEyuWRgU?`Y>P{YkH~cn|a~SE5 zSLx~ruILjt@nyNC6Xjuo+0COK0^ILcsVv7&@}`tK1V-q1J7xnOtt4O%DLBy8JA|HL zA4JA$sqb(<%-czgkLr}9w>YeHP!g*=CUCqtaB!}UK8PIx(3NpLiV}j$L=Bc}VkS>Z zvZ{4K6QJ$>JC=TLd=D^EsEL$fpyfte$Q6^5KEgtPl38>{FNQXS_=Q(V?_tF&;tF8s zAj6BAtY&)nemH5p>*aO`1_v-DrFxcQhA)?_X{Nob7;m$n2RJMQJNr~P(HUa2c-bQq zr?Ioo2Ih&)ew9$O41Hi#7vTqpmhZMGlzQw7AZLT)^!#GvbMmaXqX-GRjkJ0VFGyIf*aL5QxKtVTFjVbHlxz{b_|Pl7VKVzIGlKs)i6Uad z)?vKE9Sh}j!A_1mSfa%%9mJZ$LDMmb!IH6YZFVQ^iDeG>IrKF0H4Q4@{;_SjvOk*d z(+Aj4TCPSTr43Ek!!6$o4JG3dL)<&5QBQnGKboHGYi^xGRyysvPdould(+>Vb~{G- z^%}n5e0-PyD_w!dfW-aK50Pw?Ji1 zK}SpMy#_6Fs<(e|yNZZsrMQY@X$OxNP?3IHG{dQcb*q)kG?W=j)M#?=(L~FU`RZys z`C91ubC={*!$O(|8gY>nD$9)y+|PfGi6#P6FY&O%G;0(tL|MoMT>PGsvmMk!int{r)$tRQ5bv^uI5IINK zrg;pRUFMb(s9h*OM-iyuT2fvKG_-VY*{Yhe^i0vomMOWLI@DPr!TUR!vnwK9&{t2x z4!~LXA9YY=v3C0Ijb;w%kS6?5%WB2>V=1TR=B)45Wry7CQOnhpXNk0&8tNV1D>XI6 zg%{pkEghW*lh4%ykqV!T^#XV$6hhb6(R5NYr4&soot~S!kIvN3R9Ihst))XEjan`b z@Qg$DR_Yxy(KYD{08!OtogTp4jJ&Yh->F&n2)NHbz*|s8fV2QbEQvhJ_P#hOINQii zpC-T4xO~VWmJd*m-u^)8x1;9sz{VAe3%7&ezd=4{L2&mn*?Nbz(~)Vf7L#OGj20T4 zQ(7yLyhz6dvEQe5RVnq2M=Qfsff7aO)(1}NiMzEv;{oF+a%mqv$oJmSi=-2}z+&)) z&ch4ypo^N8U3lE5V#oc{N=OHbnW@%^kSY>7ZPCdi|FSx9=ymW(#8*UeYD@juLzvcp zeoi-xO+K)&5(EdqNfyTCO3{J4Kp6P9O`<7ppVAb<1r$*V*jXC`EALq%*Rh}nT9e>$S zBHLixb~0J5>kr#@_#PTk-4C$eaB;7O%lJwsIB)maGuSI%nLR!-2B@SXA9-m-g(bIvxyOq*e_Vo$thvCWD zgrgE8Q8(E1-M=tdAB}@aMo-d@E*|x!#W~5wYbO;_Y|f84j8&QQyjM(WtSn1()Q+Og z7q-#^e@ui5X3=Q_<(6|hXmt_=XsHM$D5C|fJgqDnpO~n0q)I}(sK+FLUa*+uKG8qx zD6mN`F0}Ov9sDqhfq^Pk_DFsJ4-uRt7kCB>{-5 zn7W| zrOcEa4_sXp9*#aZ0$g!PE5mVW|ED9xdg)RIy@)58&h=@s$k27d%>#%AJm;qC+tXlO z6DQ9Phy5TlRYGHoe03osUr?CNIzz^LR|D@kWK7D%zWW7S(2Fc6sxLE=QZ!pI7f0ZqAp zNW$X{7FxVkijkr|lZ;3a48YBY^_2-qL%2@)AwAHD&z%9Ue|Vjjv0%3dOjucvIG#5&&mgI z372vzC~yqA*8&ctAPrQuCqYE+g|fWs^PzdV^rLexo`6mjD& z!vMA_Ktv-sX8tJwK-~bW>NVUF_a)Bta1!aY6>`F)Sl~Q4O+ss`TjzBYcrQdPn-kT5 z^NxLqsu|Jukfzn69`frmlNDK|wArLlaCyHbZgc<;a33t}O)&dX3%t1-6KcI^yT;u^ z5i|4Gu|QG{4_rw`02~XhH~;^9EZ{}9_Iq4X1{NNE>VezTsksyJald#>^^0UdU_T%E zb4XO380L*qJE3>Wo9fercDBw2$YuXH*TIQi8qgaWh3V%nSEp7L0|3gKfc)WIabrbg zi7gz}x>tiD+?Pb19v#CV3MHrXp_Pb>2YNA^BJUL}mLXL$9nM1Gs3cP%StnoIRp+k) zW%JY*E=2kFB>H0{7JH6=_0DZ$2Y5_JI~M=RpWTnAc!hxhRD?Q!Y@R`^b@Z`;5J1fM zj*8KB>k<7eL1zFI&X1)HxDI9gLmv>GUPX+`z)Td)gIQ9p*^}v8{bx*MORhH^h?7UO zpN6Ro~u zcZ-v=2l@nUom8zAPcBz_Og%Ex&XQq{d1V~a%`-jx!Vm(O7B!P$Bkx6Wq$k%i26p-} zouoBL%}UkwcRV8YV5k5=Li26Jt()Z&n8 zjk&@)z^%JJEw!+aKQeuzn5azSl%9E#%Fx^S_c)XSQ>ukO71Umkx#J}JeX*5O<9@uL zefUL@&E&-TeThz1M1uVJ%Kor}%>jod;Z29(vOrIrxa8JjY8{yuaUZd?<_vWVyCrrGG2xHpn4^B+B&C zf4Eb-fjyrVx0_r#y~vGb2mpE;lnzB6?L?{Ey>D%h$U!MMmp7U3(?zc{_6=D40czt1Z~I_rj`N^toC;gP zlw={VE1bzf$`Tk?lTlKo;^Z;IKx2U1%c%!zOc_Hm4eXEJ%lC|pSAg!7=)&~rIZI%l z2JeH;%Yqz|DTy2mgO`Z!(qjFOIJE0U7WnqSYj?H{2+3R)J8z>2I;^a$I=cH1{a61B zFiJv6a<%Mt`$_c{9(xSLy%-+?fJ+N1xjxaQki`B~9DjlHnk^O`kYHODaL&|ii+rto z6Zit>O)cTmB5o1fO43HOc)6IR53RIQMsrX8D-}L63v53ZOw`n&`BSeAj4Hk;M{lE9 zlz6lr5sw~xq?td|CX2~!iRLC5H(la9wDKrs#j^Xmu5J6{RIc=40y`%O+7#Lc(I>d3w3x`p;d>IsWl%_D*tH?S(R_?)t3I zhnZ;f=^S-NgRck(v5tU1>R-QOb!yv`r!;U!bsSIw25CFEQ6C4^TMQ$C4L;uc=Od9L zU|lYfzyLhenInc-pC6aY_#&D4N0RY}3!Ka=w4`%KA_($?{Q(BDNU8jej)DFXA+s3@ zm#H4e$0-pcFNxsbRu)nKZ58_+jhd}x7W%8*?z^lF(x!;}k)csZp(HG&<>f)K_fa)+ zu}Y7WSWwA?QNZQON^>zA6WYeF1u-$2FAtQ_)X`XNqc${z)OX_%@X`!`#sMRxthue( zME=(2GcWIjNPuMjnHBz3-^NH3{$2mie>INI`adBi|G!j-M<3w-`TYCx_E(Sn|E$Y@ z&AYz_(*FiMR3j8z3!s-yC3<4fU+SLw!AVkE09WiB;lCGgK^JRU?fDOS13KCfX#|pA zLNHiYPx|aJ?z5XEcIp1w-cp@&>N!_%v8blgj#LJex~DlTDyIp)H^u3>&vl8)M}@aY z))<1EUx*FHnEu{*_RkIn<|YdGnbXiNT>6Op8W;aPfJ%jRSvQQ4y5@c4egPqB@&3?o z0OQr`ZF}a2+96^CNO}K7w^aJ#vvqt$;(@oqV?+KDv=d*Q`mb4z{*#Rc`U8ssT&*I* z*1?7n^EH4i_?^xkz;3ozTw$~(jMa&(DS36LN2#>oJJ4rBPoOqq1gV;`#G=jUV2*-oFPs) z=oTN&`yy`4`wI&-SI&+lUg%z~L~PF(-?NkmxEIeck=-U}N@+26z(AItaE-!D>E5dn zadryVSI7AEkfTYfhhLS>^a^seDtdW58JuBY>zha#jb?AUc;ic+&_CXB&|A=yVRuf7docF3d8Nm_@KMpNCv#v?)txPHRZcur2z_S_8-+1hvHIp{75X+M{RY&Mg zQ$2GHUsGz?%k6pn)NgxM?c^HEk@es|qL@+DUo{J4H03p|vPIriO&wSvX1Y$u_XF=^>^X~Z)D|X$8aB3w)7}g;A^wq` z8+i9tJl|G8yhwvaWt73{H5`qjICs)bpr$#2up2XNX_DM)zfEwT?=yP1?>M&-1s$3D zHf_$sahY*n^wXbC0A-zWGg{LZWW5i}@(XJYH@g(7`bylKGxfs-?i>ume< zjaE1Rb7L_59o!qK!YKVq4kdF6ln(3(oHCBL#qe3hNuRc6bvh4+xr$yYFXElF0m}VO ze0yb^NhX?gy8zkDcV_$%`q#u>|BCoM=zSscL)|m_)VeZlX(_EmeUs?OhNGei@48`t zEQ-s?0sX!-SF7pTj>)GtBlg_7K{Mbqu~yGs`qmb|^Ym%tSuqNC)-N5=2Vwt_s|@LU z=e*Y`*ieg&@rSI5d2UpiLz%YZXPBBD$ZuUnMnEPUiiZLVP#hgBc$30RCB_cY&^$H* zwa=Fuq#UU@dluyImQ&&4W4gstr^#q63)1+(^gD5*U##BT-ZIr&h=+th@Gkn|QO7jX z2$p7C=hY*{6wJN7jl*e^7ziRh`rQ(}ooFmb81HeD@!$(!Bv_x|W!I(!-NzR=z{L{> zu)K#`UQZIL{%#1u%xi4t5x5Jzd{?QDQ$`=oUCBa?&)XJ=yn-&*5_Y^n+X3YIbhPRy zN(jWU0Vr(3TFqRLF=x(S*5X7ILe;x3MTzc-eDVSe6(6kuuJ6~q}a$O$UU*9-F(Si4!>n~4f z;#^BV9WfOFm{wZRLHc0&9@PQ;5_s&&($6>24GBtiEtNahn@3+HAj9|$@Pms^X?Nv= z{n8YaxiMMxLrgx&pRqC=lS|FyRa}LqJ+1iiFfQjyocyTIh5AOkGWuVDvf=4CD>JaJ z=k=h^PYk|1t;Qcc@=z5#W7O;346NLq0RVDW3}1Vk_7KyA@6C>rMg+|b5X-_~NLeIN z%GAEvT$K5yt4Sb05Z!P|Us}?ahL+M{Zz38X!nExB$ESWn!I%$O_7Mv=S~-x&GD+ee z#!$QNv-~G7)R^3kl|vhwwt4cnBkcG@$2jH}PWsRsKLcpkpV7U}b-h)o#uGqT6MhOF z&&x*g=Brvd%rl;!XZl%#BT&>CR~Y%xMQp3k9@TeSdV zvtv)2W+B|gQlF~v-ZtcVztMpzxUN>=o&8$;8P0PaoN$ClZx@A6@?$aNP}ux9M>($* zw>*q7Ad0OvVyRk*I2fz!$*r1lAy6_31kqUJ9doIMSU^it%0$vS^_`4A#L@IcL409d ziSc7!4Oh40FLuT}Cq}x&*qfmj&(|!#23V91*Tl3}AviT?_uH>u0JY{HP`eW!2Iq{`89Yyo|_W-dp{oC?HdQEg;*ov(&u1#Z}dB+p~cj}#5+Uq#_8tna3G!m}+Wl zvIrnx-$PSClbEAQk&^z0v`F7A=@%UQO)4LM!t%K;gv8#{BZy*71d5yrfnpKkIau4> zSY?+)cYnwcy*^l8Ad-_B0p~S=lX$94we^u#Zuo{rF&2G@!wGe`1o+b^r~igaY%iT? zP4w0EV)48y5tuE1cz#{ECaW|IXXdL_A`&skH%HbEn1h!B=L%ZYFKeqkpC$y#LQyM{ zi=3mLD58vtTGA`XFLmoZEiN|#jFj8GE&vB_f9JU3%z5AlC!_@>dK^8ve+W<<)m+no zf%?0s#`;W#HP?vud@Rw%lYT+bKbTMj6gLm7K_)lb9}V~}hBNy2U-M$w|JCE1LYj4jPff%~UpP)N?uRC(d+Kf2bL73qIR2yIXSm1-QYa7@Sx> zR~ruUm~@yr#)}50ig{I9GNadXO=sXJaz zHHDmAC3_6KNv~C2Q^&?K&Y?7E_f42QU0GOM2tppLK*VuRkfCtQ6ycpAva7ufH;e{~ zh^s5qjfo(;jyG#(Lw?{Z4(tkn>n`0u z-@!1#eOL8&e{R!X!!~ZHFFy}UiVutHW<=xZc^BoLhtMb zflF+mPf~;l#Qe*F(bKC#SomtJjfDs<-Zh8v4GO_PIv4B`;ZVUB9l3~Yp6uSI4F*#Z z2mr5l1^?PC`hnj)Qs#Wm3Ak>ly|!{{-{W`0ulPX9v4_`aZUD~BSO&zs$ITscHqJwb z8$`a&_aL`$K(kk!>B7s<)Ca+gfA5j973nGMqHSz4^v!YH*#l#c#gKIfKnc&kf&`%W z9uLiP$g5}HoH0U1NUj6B>?Eu$0DmL5spS4H!1=DurU1qotgYN89}>LKZw2uh$@c-2q>9q-_xk@w`ovW{*ra=fxG~Y*lVEw8Pc!AtCR>K9Q|AL>U;bJ{k zTRd&Seq(rfWe#B&PCRTmhTk8!hvLKT1z8k>pM0)~H|*xibBVCBhIS!fU`hamjzhlO zc-`!z|MXP2Q;7S#In7>qv%7fZQrZM&)_JfgOoxqSd%2R-F9*a0v6zoXq;5eJzg1es zfKOp2tzFT$XNP_OX_KV^qaE{6YX@hLWdd;d!esPx@rDtS38_un%kz9}<RB{X$9CNh2P;k^47M9MXK+R)jPDrd@$86rY5u)8D*fM3zq zA5_3LN9{Zc=B@(rjvs3-Y}_;c1UpmD&)X?r+j%X91bvJdYfZV*Y-w8NvEquflZRbr zD>h!8oSbx`aj?LBjOZ(Qfa@4BIgA>==KU$dJ$UeOw$2ULdMK?6NQ5~92~dClZerPm za?r@t3?2Q6%IjDHj6a%!mcGXm+7qf6Z#Y{n@1dFDE3w*WW_Y}j?`_~E`Xt!ft5oPz z%9(KY*+V4le9h98Xjb+%I+4MPFwkT5YQ4=K4U!Zx=aLG)9xeaOV*;1yRXWk(DA&7y zu01|H|Ls;6-0j^aLlpgAlMesixv+sEp@OLZfSn`$L}ZYo@%L#U?ON1X>IE};b@+_) zfBV*#wOUu&S5ISSwX?UnMn#H~KEI!mW198SvRu}nB3Gk`mM6#6cu)en1Z(VbH&Y)o zAWG^bIy-YPdYX&KIy`J`97=uz3f$rFZ#wkv*J2_9*E0*4GifZ1;(PnUB0pAc89`4v zOjgU;k1Af)&+F2v$nJ?Nt8jfq8&=2!f2@{rM^u37u`Di|{&d}S|Jk&mlR6cGrADs> zalUO$e@d?zA54}M`(AAR_~!e9MC!*(QA~Nh#l?=JtfN{Mt$#|d*&j2U2&b+f^9VKOX1LC3HIt#)h5#X`gt;TnG2ceJ*zWUYC=rii(c_ zYlOP>Ym=n}EU|j`k*>u&@}=i!)T6hsj}bKsXiE7dOUvoc-&Mn2i+s_rt{U@?6Ah3C$9qq?dSDuQwr@n7dKLZ1#BK?u9aIuGI zVc(~dmwkmxb-fb$(^_M-YDdT;@)aa!KWprrr@!T^B#g+_m~>kntQ^%VQ7?C5+}e8B zHMsG>LonCTHrY=5fnsMNCd<==kYe|3CAn|<#OXS0^_mhIG`zh|6iaXWc@Qo(=n@ie zKji+w<8tVb2i4cg%brhHla+);abQ394~+*a6Z$?2oY}7=3~e-*QYPQ`p2VCbJP&*M zK)GsuOz@*2llwX82VF;>m*%?LLZ4+J!v^u6+-7yCf*WsGc?_(72Z@mz`m`uv6ZFI` z#zZ>>3gu1ymR#M!-8|-*PPk;dl~@^NKM4L7`}#e4BzZ=QZ%4z(HKDCzE8-nqfrUfuJQgeIux4F{ zyh-DTfXG?6rEX8$s0~csx8=w2i(;8qdDGlfq0R8MXzj_M^s0X)F)LpHyU)9DL0bFY zBJZD>0EGn&diL{BX%E^CMFrh2ZGRI(Wu*Y&>ovIMN~azW=we`q0RrAC)n6CR>u{17 zAKop@NVGTPSxa+Z=&i^q%y4<7dMhF`hLS)W4ArBzEd~m*#Y2Cq$1qaA2Fdjbx1YSm z6&yNuU|^=5&X&;Ap1|e#n4z{*6=yMZ$k8=Bd*vy}+nuBna z0G&JbrSlBn`^xgk1Nfl0Re&n+1IlKaQ~k}~R<7wm%sbXt*)GFo%;#hu%x!6r zCFd7``_`EFFa>^VN!1^$GEfgrhX%zoDJwr?B|GZlSp_Ar@?J9KI7rGYT$roKiEv1+ zX5<wj@g-}mL;XSEK<4mk!9JEK{s0_AD@?>(S8M4PW1a1ahlR`V4? z1p^umS%QFRu8fJ6ld=L$f1bv;NFEE(8(QKc-w;RRoUW`yTi|9?K zTzWXhv&K3J&I|90*M{#Id~?bmcW4ObAE-;FUwg`d5bcH%1KQ@Oyqzjer8@cbgC}Ga zeCK6so+%cPVdHp}NV}x?0t@hyfY-l)n)pij)3q-y&De_xwv1N{=ej1N5_*r~oK|@( zxPJ1MG~tbOWwYTdxT$P@=eNx!8;sPnGo=>5A+dCh-rzU1nkILJNyU3TK5dXH7w$GL$3L^c?_fvcbZEnL8k@rZcL znjWCN>QT_&7&mST$HmobzTS%hVyL1h`>KbB)Q){wny3hbLPeIJ7x~>LOBB4*Im0)H z$pb9(?y^AaeP2^D{_2+ur?<(l(|MnCE!`V{v@5eLrtmrDGj1i3>5{?rQEsk)l2xkU z0Dn;m**F`TNb0^c{TN^O1q-6r-80pu?3HS=R9fx;@()9!0*xpp$b_i{zLmb zdOPr6w9mf9xlVye=Q^sKCojMi6y%Q&RYsvhul9jyCyE5$m#+Do&{bF8oLR~EYi_pt z$NXb&j<}*iyUthO{?UXko)}0 zq*&*iOamuc>=26Bd+xvbC}CXi-9)I>)V*gWLfdj!EU$c~+*7xkxs0hFE`B-V$l!%r zo6?P4hE?g7O|J!T@(Ozz!TSBg!v$=M5eE!L(dBU{U9{kw832mvp4{W5?ycarwPqAo z17PXS?-zqL4C&3xG~bvnXodY2;jgToa+VPnw2=o@fTQcKqZK&?IrRKSm~y$zkX_6? zF@&3)!vEsIhf!O&O(%5!TbJyF{e#x>^%d1S1;}7h2w>M0BH*LC{|XQ%@Wz!2KgZ*= zZn;mNIlGBfN3=bfcv(KrBKhKgeppV~r%sfj#8969?7pvL0#EDpEsIMPk}(EI`(70R z9I|;ZcUPY^o8TZ)rdz|0^~@s*0Cb*s#if&07%`DC3&!EOzUY}y+WOvRf0tLe&+9hL z`x9MRN}j`%nkFKE{Xi!l2EsaSXw8zuTkAzJz}OC_535ywPIdH`)3u|Ab=DHYmVyA_ zZ~ZZ(AJjG@55U$wUM3Ro#(wU(OG0>K=ygufk2jJZ=I+2*v;cFUrIqekMZv?Cr9218 zV*DkINq)0kfBZhbhcxf%^Tthg!KEe$qdxAf(L zuIy|?P<)43{oZY*KPi3=0~@gGi^UZ+9tz#@5WnLwO$3^1s@$7sf#Iy~ty*_@<3se3q(StN5iTmtW4vyuLKG82rleRFu_ zM&Fac5i;t(GI+&da-l24I(AvLBWgTX@kr8qaiLEoGBw^xdaC1XqbtoXbW`3P=tP54 zW)M_rms1-?g1(&12Jb?O$36^PPT2@Kdl(b<{*YM&{8t1Z$*O*lwC~#$?;Q9ewq)Vz z5RF|_dm|8?-gA7lwdC=e@5?c(rdrAON@+FxH>GNt+cX=9c%_15=qfaDCNzQnmn`<- zaBQSu!E+D1MUpT%?Ov9T=JMwR<|&_att1H}^r)FIvOU?2=;X8zb!tpW8|4d0lbI8v zPEmxPwVNA#S>XuFnBIImRoj#?=!==Od-NnA7bS~=NxuvEGP}1N={LCh*qv?|-E)nkRyO~n-C9^o8NeU0mcgMB z#kU9a?$RG~?j53WCe5J~({ZKlmT__({4-7r_CH`OS?2oSnUzZ00E@g8{9f$uOgI`8 zeCM0cry4AbfEeGtj|`+rw?d#VlBTyHr6W*B2!H~Ke?G;%l4m!kP@a=uUYY885C%#l z2~anMf=#c2+yQPL4U45;+d=N6J_0GBOe?SHoWAe=nNn;b4Bs4ztf{m6slDeSI@ctNLxsl1(pp*Gq9u zl}&2%VGc&aIg9#J514z;_&E0@PQ8D-gUzw=W3Li_=6zlitRqi=+|Bu`Wt?*1mvgBn;`P=lVIQH#d z%Du7|9GR-D0o5T3)a`IVg_muxE}5{a>_mDp z_f58;-!)=*Rzz})~yf@CwZ|477@A}sFz3W}ySu1NnxSZsk zbI&<@?`QAlLAm@$!%t7L6@E#6(4Z?ok^6r5s-5gOYF5}3amlKT+3DBy!KT8c#cU$0 z&*aq`s|TycU+28K7w-0m`O?C&HgbD{>U@DKW^OMC?q#W^3F zoU+zx;c&ob{<85sY{#&w?t3?X&@LnS9_$Vn5uFRGhC$1#>_YSJ;QJL z32z^j*L*jkv6OYGaVMNxJoWQttgRi`6~b3BdXX8S617tbElBpYV;tu`x@MM?!>7!k z?Oyh+;vObKu8>P0dE+~KA8rf-`N90kRlf@dQn|(EJbG}=~rg_7e_mG-ghHns?c&J%JH{r`#^9oCq`Vl6Y4o*1Oq%iwK=z8@rbh>6zi$?wWJzp2EwmC>(vo zw1n35OZ*HlM7B81vLkw2S057QpDbn@$(YQ@O=1V@R^{*UoZj4YvR$2V1_m63*nYJk z^M!2mqvh_3ORmfyhv0(GdRzd0JAauz;IDhOYwm6#JEdG9%c<1&Yk8|oxVn$qQ9DMz z4j~h^y$XRNMI%K&^T_%!Ej-K64D$6w9Wr(o6N@Xr^SJME3r|)yRppf#O)A&DK@Duq zewpPRy$754pvUEF2l|8>t?43|rbN^nq^CN4nJp3Vwd9g;?ahk0z-gr#<{(wufNSc`h@F+`1ImMIasW`+a!YHmxJHJmT z_pbG3n6zF8G3hYOx_!=Dr>xUq?;f@Gu9-l6*v6c2PFL|cz?Mh?_M^q&t|#DBb>_IF z8omNYYS&3q9=_Ms|6{yaN`kh3sw#3)qN`y6#w_M;dFUFJ13OFCj#kcR@=!qW zbK5%|*aJCL8VntbzZqP(uxGK7qEIZbidf-omzys-U~czzUulM!G=~m9S!4mGyFr)2cpCD)PlJ;c7vv60gXi?#%HofJD5j#)3J)z#qdsm+ zB!b-+FEangT~Kw#KL|TAI>VEe>_3LJv`?B+Xm9hw(3+QtEn^!%tvoE+tyz7Ve7Ef-^DQFB+t&d0MhOrpD?pGp1Rv+A| zZI2vsn7m69t)9nzYND!smg!U4YK0h4H}BMzNB&U@-#P`vspF>s8mj$Gdo68;-zg%j z){uk>u}#Yan-1s`o}kgDx z`-N_m&>Y?-Rb724CtC%?F~+@V&p}}!Cm3B{vtMpe7f!t=%sTTPR#so!-dr5I{n7qO zj#`raMJSImz=tDPNIbGK)(>9h zoV?8TCx_!XynG)3;TfP;kJAHsm}npXH?-*zaO{~IbK7~&55J%jyZaL&|5Mn}JoIkc z-7SKj0}uwuhV+I5f-RR`P8bar3K|bOhPCa38BFg4sS6b2b`GV&Pu)Ddc;>{;(hxI$ z#9WGFztWDHA`>*|@g|i+IHo$RHacsK(qeCvJl@OH9t1;xkcqgr!Vk^yTY-lHh)HBUZW~&w?H!UXw%y}L?po&O0Mw0gl!2kXo>rgW> zx?BW=$+(AoO#b*nF9P;CXB~FAaCX@%;Jdp0{ES=3$|bop#<=5n&)%C ziC57z_nl@zwtqyObFKZG?|0+d z#h;l@>ms^t+%u~JM!6Z_99geGY!Tx+Nf>*~CCLX2Vh!IC^I~di&YvBiL8fzO1Mr$~I zRixPEpdPY*kRFg|sjC2IxDz&ULi#ztvsf|s@E7rm+}e|v#WzuO2JUKR*E4|6fmE`a z$Rn-3H&Ewk*TlfKkiwvI{P)j!)c0$y(G(o+#4ous%HK!Up%TV`e>BW_7&@vn+$&#w zY8l4XHDmW{ACJybE`)AoX_y;kyuxT+`QbEyt9j)=LXlzj`<;7mo%mo_ywUlsFX}1n@1?_PkLIDITWb4`HPfjqFxjK;}NfH*=04K^7{DvbH);Z1W z<-)J=@fbA+4**6jIq@r;xi*RFjPc1PIohVw#DRpxOiZn7_e-Z8uiDxsEu|T>uXl-; zzZ7MflEH=IR2X0Qou)7s2}OH?0(KqKoF28TARtVEeZ7)eiMZ(~)X>m~a*;=Amv>!G zmMhWsh0?jjrM~v*?g1ZNpdNpfaRKrkTl6{b`B&_HNT#&m@a!BfrE*nc0s?0ZIiI-d z^n4i{={h{g9~uE)%5CSxbnIg~;+#_a;aE$Bv$yGmy#T_x1CVB}&2%#YoS6(j7ML;S zG#!VeH?{w5rmg-htN#miCT|>oYKeN8#S_nYBHDCsS4E>uzM=0x_<Cf0TLB<|9(o4If3=|mx|HsJwLHH60+xSvs{Q(Z z-1wip!T~b3{|^cO`@H}e7*Oq0BK2i+B#^I?xuEQW_o!^#^RTK8Mgch+#E_aPuD=W5 z1}b?UKQaH-VoToVY+awyTDlAIF*}s&&2jNT6K9*gk2~q@Vt`!NX^tovAX=9CV>|0a z^k#bmk0wi~wcur=dSO>@1Dr@^r00!!#mVO1R~6q#+ddwA+JQczMJz5Eq3Rm{H4sBO zz;JB=MW=5xF@~}N02X=zG!-1W974;OkNQ<7{q+_z!D+2G#ybrva)#u-r$Xm31XMb& zo$M-jS#9h}U`jb?_q9V#Z)5Xml4d$gx>j@uISg~(MWQR%B$xn*o?T>!@wVkTXKanR zBgb1lc(U_|d^DA_$Cdu?FT#Bun9TjkpJ;Gdme|_&a@FWhhP2Ywkv-mOpqTN#9b3qy zG%!32CJAXhx8D{(d@(oJSzVs=QR;Q!LO+`=jup^Lq}T2O0J7=@yn*hN&1)ah6NfdV zUWw5{j)ow{U?9x8$A!zt+QQF!2q9Uq+F#n(>U@mt`}9MoJ3wdC_u|z>l-KQACfyZ# zfDp{y!hbVOTdPf5^};^`HnaxRd;2LhD$bV?-oToj7PtG1$2xoI-vJ&K$P4!8lt86? zzlHYhSgf;^{EF*ST=rT^67j9j5XsZh~;MyxYN*hSR4r%U|4DdsY+`NzUV(op^NE z+h&*fY{y$D`80=hQ|#OBlV3(d7zu9a?=%mQ3Xuy|L2j*uitm?hE#&VYDEUGS!#1b( z-JRvnIikSdYvvbIhD5LFw)*Ucq1!)l&Up{b8oXa=avE5SI0a)CkM$n&2D_f4CL|lq z1$AI!b6uM=Nk4kp3NyNlZ>%c;hcTd1$`CUteH4b#C8`WabqEXM>yPfd1p@*u`>=<+ z?o8GTjmcVU0!}TH;yQckAAFNZXyS$8f`*g@?~(O~4Ff;E_S}xv;>G>&HgX+CjN%nk zX@T(pS|-Y5rsaut!A)s;p&Qvg74!3^ntBoX}JnD_!+ zL1pr7whx_=5^@|L`QVqIGMUA316`dd7g1FTjnEZ^*PpO`Jexz!rO-RK*Z-nGxpVQj zgT&o7qIyMqyKSv(%%U69$>0C5ZJU$o2oLMT>I0b-CujggsNLQ6n)xS$wPOUdk5U5a;vTDZIfdmT9U&pDOQZI#$>)2a3P z-Z)Ra^?19#W^S|NIOf>dF1gi4!}PDM#=a@?6RVQoTRl21Pc!{a{BQ&h6;qxoO6vNur-vd zh-j;s6p+C^zb9|Vj>`A8@Y~e(N;#?98pF4hbxkx%>Fua;tmcuB4;PLtg9|SuR?*a( zIlM}iTJ@$@F7S)%N+@B^N(gGN_=~(Un2s`MomXr^UN{|O7c;JL#gF2~BIX8;#w)8VsLATBWFxvB5zmcqfL@2J9s)hYe zfKbn)+QZk7cPQGOo>j#&EK6ZWN(sU{&4#xM-7JP@@(Mw6{s95?qTly>Jx1=ychWeH zwvS4x^e2XA)E`E20HKv@W4q}a5hj^^VKvDTZ+>BMuux+vc;>ylkkStv8_%fkw8Gvw ziGIc^#042hb-?v6#%a_U^f2{A2LI~neHQR*(ENkMH%vQeBK)qKW8yu;PMWubF6!Gy zYk;Pr^B#Va!5FDxydZ@N9Zy~0 zx-g0zs9A5NAW|Y$<_nVfR12dY2Pm3|Rej`@B^~=nwKm8&^%C@HPiF-oMoA_P^3#-T zx4hqSKiCiJe`NV{cT<3C%@0qJYbvH-jf|-{$8Hz7gTM{Qq|9vl7{!W(_XlG z0$-9_-}ajGqfVcF$uNpnbT^Fr!J%z6M`PcmGLG?|5XY;~a1QS)<2*J<{|31ZP+sNZR;zth~l~?3m3uyt9rwWuVnt(8T!&^!}xS z=e63+{TVHV!5DGt&rrApe4^4lzQ1+*rK-96$n@E8?iq)&*fi(3wC?+p?&^^x!kT;y zMsiC7ZJJ*qXJ=auK8gU9$bWouD9YiQHy#h)SU<%@db&;5Fv#n?!Szc{SwMSNWbL8w z*)su0_AH38qi2ARec4sgr4V>pNjjWjo#9ne4ey)Q?+6Py-2o0b1xYl1;}tdH8~9Z1 zrF=jL`6>742*v4^ER3IpcPU9B`4F1q!!`O9pB#!7A5B(cWwlno?F|ZNZYu7AWdLe9 zM~mtt-ZSc}AJOXOrq*F|35Kh~aSt9w19Tb2*Z-oP^$C0P{qfx&F`?N}%Fh5I_1j0< zYTRk6_j4?eauX+xiL=u=1}g<|`>$BI6Hm?B(m6ghmR#tpe~Wdm)%vGxYLoNL6-DzW zW@;E17bBW3EH=LCpI(g?T#$POZvL=d{?y{Ex#RTfbhG6JLw$hVL~@YFsQ);jth&<} z?l+4vx+7^8ai$UwQuKglenWqn=ivt~8siYfhGu>MM$s@1$rVPI7MM5p4gAKm&aa>j zFCmHOyPSt@{!Vv#h&N*aLF;Sy{GaggR&>d! zI=s&8Fa5)?w6}H9*B~9Tcl52C5h6?H!h{lCV|0Oh8hq>7jS$E=>AHopTpB_XvNQD6 z>y{Lr{jpm3v}>KW)VSA5dm`)7-E{>8mM_GGyp{=l>s~>1P|c!-e;b>`n|=%+hW@uL7{BVmM~7r5 zki_&AR+Ay$WU9ZxK~RK?zZY3zYDMRa&NO^PAh&TIrQb}yP1m^`5d^Hq|G4corwQG!t{ZPOw6c`@(j=!zLsxJ!5Opr-wN;FB43(-lO zEvzZfcK6!cTPCXQ37Q0@tmL=EBUz^730v0V4+G;~mk?Q-H+;wB^xXqnHJfG~lj7e5 z{#Z9RRlUN_%y3*lKwu6ao0wa`70m&P9dpxdA*WN+ut5n1^-gFs8yHkhlOA;w?#OnzoHWEJJ4t04HUY(_>omM_0{&5%WTMQh z3%^_^T})^svsbZ}Xx9x^l*a#9)d!b(vBjA$2WQ6^XN^|bWqME=FeP_&E@28rKBkW) zbRa58*)FB_%A~ck;5&?3zZKaDPr$@a?eztCgtn>=JB{X(EgT%qZsfaL$B2vxLMP4p zG}*Vk*TCE}3BO@)KtPrR++zBhC`}lU$$FEj9p!yJ3-R?~dQz(gOR?`tqkgpgeJusD zkHaTYCU${cJXS~!J2#kOL9kv-2(;;o$_Gu$Q{MWUVX0-co#cPPmKNUe zMJPOUXYY?rrU4D!Fh>>IhSBWg#;!VGlllG!x?MBXXGz$CVJF3*h@_Xd0x6tmc z)aQu98^M1}2ptH^1{_$qLzY50^Kxd(UGr;Z3@SLp`o+qLUx_4`cTFAfD28|cYuh88 zOO))8tvE<#H zHncLX)N-yhXB?u#@W^fm%O~pzJ>;x#EVb#|K!49L-zeZX{LWdxs8@cNVYCC3{?1X$ zf(vC~1<~POsFsJX;0tS`pVd+)GVZqOfMeLVEe}tno6AF;{i8#_{6by7zALzNyiQzj z?M;`+@Yuqu*7sj0$QhY(h&om2j#Ca2wvpyUjLM_NAF3+@)QnYqP-}4QDd+Ef0l9a^ z!aFis4{LbcIjeRze}QT5{+2_Cd$s7$C)r}_ygZv4iZ;xyTThzT4=@u@8%-o*)}mC)-g_VqOw)|<0P{Mh^7SZE$wLhS$| zc|}zv6gls*DaJyxgKawKOm-K3lQdj@VYx{DAlTpq5z zF+{bDbiq zwY6`0b0xeXem4!5V2jnZ`H}7!mlWTa3iq)`Pr}&T(>J0gP>j0k9&3+mswKu4jb-3+ zY6Ml3*p7uTzbgebO5}^so?=c|J1eGlJ2>!_{2F+85wcXZgN*kZJM1adxQH@u9OJ79 zLxm%eNs00o@)9sCaoQ40`!b+ESG)59wa+^isohiwMOgeC%QxC*-J6fkcFAYqtQ{76 zC9vhuzYNny(NI0Jenk{o)IDjxEZ04$)Ay)Uh3CTBOc~0^EZsuH&?85$?qiDrb6VY4QEVz#_dZX!l z2v+Vd1-+#n-X+;+XE(d8m>S<69ht`%T|t^hh=7>z;a{ct&`t;m43Q(tr0JEg%t|!K zK%NopC$cVFD;k}JFO*nUIqyHUQDDCgJ8&i7vkLVnmR**6?)S(gqMAZ0MO10ZBMud= zGvIODfUD^h>ng3z{Fd~S_`*uFIVQ1XDfBK!uN)D4TrP+H&Gjez;O0)Gc@G=#}A)Vk(Q%D&3sU0cJQUDkG-n< zEAx}3wMl0&JxlCq-ue}bj|a4FG~QvWT}dq$Jn{*%&3aB$sZuZw4BWc$dWL1Ss(aC( zoL6?p!Q0}cbZm!}i;3s8=(v3cQ)9jFsg>T@S1PrSE%g%&XoUhLWu*3kLuB*xN1&?R z92{{aAbT;vv!3=t?7HE$IgS1o-2pgLvOl=$_IX~nud1dE%|)DyDa>-Q-tEH@GGHAB z^2O+IhO27Oi1&jp7*WT*m7i;Aaf1bna4!39r~1Z(9|vwQvDe>(xdU!9vvNAIMPXTM z`eXYdjw#P|_x$Elr4A?Rlba=_Z6jrm535F`g9|<_U3h^SWE6z~pV)qzdDo<=(kbGk zm&q-?=RVf^FSC{W>p|`slO2zAUVtwFi{v9;M2eJnBFb$jAZcllD`?DHy!Q2>Wiiil zwLNj8_;7DW?(GZksJ^^%e``ArC!ar;gGkwj#s>74PwAC+|}Y$;H)`d@S{&YyTw8So@=XLoG45$Y>mz-#M?%F z3(d^|-g|s^hwV*%%g0m6QIoL`VjY`4OF#?aKWV7z$nLSe1ha~2U}&!4G<2@IASodpkFoJ z+4Nfh@v%sGk%*1&g1{ngAfFnKnu9zV;Bmne#^PIpH#qr{=U6NU`Y0OY=F1DLj39YI zlFG*b4?U|(9?)@=1tHtiEM?8CfD6yolC+d=c9yKb%CToOxm^j)rA@HIICu*y5MrR?1f-pl(5M3{mZc6Kq{)yC&wMAfVo_@!HnM zo5bsr-J_#Lter#F_e!eN1^2_A=*>2la=6LW37eM^lTuLnf)u0=JDHslK;MEut-g)0 zUl}g`?~HI@4j34&_O*p<_a_SI_X7%d1M6h$Ku=E;bZzDp{k6MA@G7cWZQqCC}0>r%o0CBG~k7~F_`_2sGIv}f;4p52p(y?5?SZuc!z8s`R z)|V^VIb8AKY)~>vj=5>HX;~=ClVLh~6k!5#_J|QLbZNaBd(#MV3e+|s(<9jy6mEju zK*#&phbv=0yQgKFWfdlu7A|hSpvH)kOdW~kf0{gn9An~!df66JrReBcvKkez&*G<8 zP(cr8SX`GHO=7!>6$WHw^#m#U176Ql`|;Ptw(mvg;S~O5^Z*C;(OM$`_G@3ylGr{H zwU%xh_pNckDmCT*md&FLz4v1n6}hW0!bfXmS;oErdrn8kWC4OS&MYs@aguzKj$x`b z9pnMtoMGE2KUeUmjJ^k9m<%(r&UA1S#B%^A%|mNp%D{5bRcz9BZozx6@&(B)3qs;F zw(t)*+owuSaB<$%9j*p%4rYc+TJ&k2?MvDF9?82woz+$jKQLcyUDpeR{PuIQKa9}! zxBWL4O{|-I)XXZahQ`L=DAWPx*%U2=Yx8`EclZIJg9ZvUD(u*A>4^49xEV(65ty8w z9s?$`ghGL|0VGKDU$?eUK$}CK27ind=Lgj9LKNFOPCP3J&o8jvbX15Uh%tQFm2oW# z(B6B7>B#ZgV1DKc!-O3B`7MC0@%#whyP?#S3Ke2_dO>%jafpPxv-?%Day ze|n|AVb@tbe}?|U-);Pk@ZR5d^z%KLbN_s4`Wyd;U5M6(KR_62#)gMt-s()2CTo?} z3#=)h+8Di+43y;>m)1)Uxbi{In5N-Nv&hYAr@*ciB*B5gu3H05l}8V00lM{FTsnks z14L$|>8vmr+mv)^6_+hkWIgRFA4gG$^H-=Elrm-A`I-~JqZix2$5P@NttEd3II?~= zJ76?3ef|CW%`4nO6$g`pH44ey4Gx`~oL4A3l!X&uN3_T*+#Se%cm3S@Z|efi18b+N z-!X=*hz0T3cnQn>r=zdPd9p-Vpks_d5f2f0eKzcT%#d9N7qPbW6?H2@KC3YieIJ^Y z;A_#4BxhP#wUcM2CGV|``Haj&Ad+hRXm~^wsnU3=CAa29@P#cCy{Y!Ig5u5R2Hg%l z5`g;|ql(_Os-P%<$w)mHQK#}Oi}Eq(imHGI+1|a?$iZHh@vj^D5R9BZlcI5CKD8rr zIndI(AhT8)T3`^GA%Uv;RMOWrFuiWHc;M*h>*2(6H(58Fj}{`qt^W0%ujZcH$aPn@ zK)=fqS=kK{9*V&pB?XrGRS}R8B7s2 z0pN6vgXD6;L&}{a;KK|bgva%J<+uYElo`?csMvLr-_UrdDgr|)Uewu(4;cLvjKtJ^Kt@$wy44%!Aq6skUaIX!<( zML%9To8_&^^+V+P#`HU&QZ*zwHLw8sX$*2TOUH zNOQq9o}?F4SUQ+I|B>x4Tiu0;f6zE-=rVP5gg zIyrToi?RwH^Ds|$yBM^xK1V>kVuZ9cO(2XZ1Y zec-dQ9ZT3M3Ib}Z>?Z0LdiY!3>Nse!uU}+y-wi^*>T83sPvy2-;8_ajyh~6~f&e4z zJYifX`jtq975ctb2#I|6uLa0p2<)5}ZZ~H)zP(lws;ijry17KJsM4pYlJ-1JMB&#h zY|Q4c6k?89_rDM@7f59C)xG(Jw;i1!UgeO zy@^txa*(E$hcWPll(d7Lib5_B4X=EdIcfS%JDU+Zq|>8RPAa)up0K7b&tx&;6`Y_R ze7UDOLMegVO;Q}nH!!InAg|mgQi_0=cuD9@kfbPW*VFX1T19-j+ibb9O$I3H+ov9Z zbD79)XW}K!vgK`00;+qd`NzIyo?i5ihVifF8Zmf|s)ywskk8rCyX)Q7Lp3P+ zY2Oivyf#6~>P?V|5H6_02V4NWs}!@RDG&Mv&Y6J|S_$sn@hzfgRynb(K)YmU#U!*6 zl(Y#;D{?e)NSd?4N92XK$h7Uxdt4}lbQBv7J}C2nuam8`qPAVJ$vDx~x=c(!7Dge< z%!Q+jF?2;=k*NqxxD-87r-sNp@m#Vik5{-Nxa|@#>3ZXrp{Y$?+?Sls9fYpChtfIi zu$F{U{A@a@|6!bZLTg5Ci;1vs1rOk6R{h2&`WqJWzTADSOvRO@=`)K}@uIpZ4VZew z808V+rA*h1LHnGo&w7>j>?Xz`pi;!AnsR-L%Cf>*v@;-V0~=_@*dkF)-aWp_zEb*o zLA`M%mIvIUUcI=-8n{u3Fz&c5zx}Q3(xDx_mBmLP%@;F7i&P?5D!nW-tuU^)L~fGx#NFS2i)EP9oWt9LMnzKtfPPr}p4 z!Q}Z}cKyRwKNj+ldl(MN)MNR^4E09ys)!;%%2ev~Xm0UneW3NG^^lx(n$uvDdy>4% zU|ME#aD8OR@mV+hRad{lU`93_{cS$g5Kp;wrgrtNj@`hSUgx>TU*d&VXrFs=)nuOoh)6b?aPnty(I** zYkcY{A1wxQbbTg$yDg_ry(z1=c3D;$Tj)o`)JsN>Dqv~k-ziESv4L8`<*v`8pR1|L z`OogPE1b9?xzba1VRWj(bwRNttCU;BPZGv{oyeb?l8f|@@3s? zT^{ri&32kFa8Z)sLep)l>w6e#iY8dF=!!zU+DA2R>j`^)s4k4j%0VsRhob)|o`B5UQ=r$RDDWX08T}+@JCOO?37Q##y{R}0eq`=wwtbCk+4<|`O5NHxvr{P?(#Cg5##Kx=nZ8zUiT!_W3f2*5XLSa|toafzyT&k>fAF}+qH zVo(%F-@-UciK)}#Pyb)pUA}?B<)a*Dy)qXC7Rn5w3rePj3>*W4rw@^oc_d(w#kL>T z8)QtgBU=;+_#OOm>)euo(GANXb7g*-#W}4t8ANIwCr)Xm%5jR{Epj(S3(#BmnAJ>f zz}AC@b4CICuj1lv$VdHmMBafGW2MGT|` z*)|Z)Cx2RvfJ6T}r>FG`XJ)Yh!b!GcUmw-?vPHt*m@S$smTjLa^6dzjC;hN9&kKf07cw)&OA_C}PT)UK~Bf!J>eYz;al5;kJZ zNEfA^;z7d#3+lv@(TiEt>)e}lk!8mG&&8Tr1 zl1hSNxfRQ18_8*lQo3Jcu-K5V>jyvu-jzaWXOG~DQUfI$6nJ2~t`r%-x8v&UuWdh| z`gInML1EcI!m;9{?6uNt*I4fT&f9!2_>MlWnA2&I>OC4ouKR-Rei#AVoE45!qL5D?lZ0L zLRG!fFgPkVRr_f+=la+s(v>bD|MG6I6Fe&#wd)XH>}yw2LouBes04Qg@SsK&MOaXE zQ1Hieenj{OdH?_a&^h+?i1eMDYjdhL11b*&)53YEG33qups(vkD(STE@jIa$ha!_1 z{3RSE={tjVTk>Cy`b+Du4!${A2f>38hf!QE5RjHlRPas?;u0ou0YLb=C$rv|_195q z?YdMVCl}#c@`@fIHRo|bF(Ns>>wBI2VlWeI(7o5V_GntgZ%2xvItD_w~Kd=NqMwdfzjZ?ys4aTI8LP3KM) z^}wRB9nDg-?47=C6VK^I7#I3drAPjH3j~FEOQYMS(tOb@8E`(CvEbR(K+p^zvBDav z`YYdJ-J@`r?+woiahvNlV^j}hP5reH(dPb?lc1(}N~Kc~3|agzyKgIsH2zC({TKm2 zmI4V|Uy<*IZ3yze$vqI>YwGt9z@y?+jgW8PZi^M1vPG4ICB|)SgqM59@?2v=3Ym1@EY?CTgJO`$htRwK9ebr zVw*rYZEIV~L)SpJj#%No0f&)!HiwFQ}oi)_VLO}(MmoiSFPkVXwyTQp3JiYLp z6IbmZeQ5(ZY4S9?ogcGe{^COOQri!TnukG?m_M{+6$53rP7g?wyEm9?Fd%iNDn;#Qq@ z9t}(A7n6}GC);cH8-ndMIpJCIEH9**$alXTI1FK(J7%JTsWxk*PS)sf(<6r=rn&L< zBc#pUUsWF5RPj2MV{(b`;P7z5g$=ZabTx5_klf2b%;Xq(s$f4_!h{@gRqRO!Q--Ja>;z{ zXO^HQBzRa5!Z&gx8aA+l zTBhb=5Mg7%69f+ke%QaBJBLehVaDH5rsJJR4itqF;RJ)SUy+>@GaoZ*Zzh!D~`!8b}%U*!cCS4S;mk0tS~dlpd-DP`R=QAo$_yK_p&jewNVM zC@$7S?D%pm;$C2Acg^PYc+K5@7OY|g?BW=TUE9WWv)Vc}ikDN1*?1x|gc&fGjGJTG1U#=CBl;q>ca9%XdvX>g#V;w`GA*b*=IhW-SLs^()5J&sG8$ zR>7M~Gz@b9YqF}YvV*G{ zq*>4jG6)eAH2Y01qU{6wh;QHsl$W6soIcyIlk;bD(#0xbxfk&&}hh_1v1)+^GG-Gn*d@h+`FVk4MKS zwfny@X5L8L${peSyp_fF?XB<1gagkBU_dg8tcYeVc8+Le=xA+32i;j_XT@rM%qRFL{CRaotw11p21U%g?+9|81Seu$P&{ z7Acar`#E&7JhcaTzc@$i%VxG`cZ8`R}z0~z~!Eiy3N zy$?V$QMSBQ^B1`3Kpoa6nF;0 z*(gU(Jqk1^;Mif7#aVBN*K>1GQ=&;MZk9xGEAjO#zH3!0-yb<51s^Y8Z&R%2xftD^ zQ($pZ72hN-PyvCHGQpW}m*2JbGcl8;b2Hb+3<4|XSQc{Z(At=SI@n@{ci?{&2|jU0UoP&3V;QrV)eX-2-g@tGPg! z=&jPC8go5oBu9_WkOTG64b9Fvp{FQXXQ8KV1X{)5s8Of~H_&{rdWGpgalC(}P{E+X zyV&5M*X;W0xACR}1ONhB?zG0dhfIN0S2a7nt7_ed$1m$Cw8TDAo@^N5+GC9^%;6Nl z=!pgJiGF*i>$m-+c%v-knEPRZF`i;-vM4k9RlfQwO^PG#bC#<>n6z8$MAk`({TphQ zTo@*49jj1n*|V^mCDJ^b_FvX$9ep=s>p%dp1LX7?c^q}$ifR=K+X65{pYVcs#q3P5 z$v34W{vfHb>WK~BX5Ax=9aA*;i;#m*AK9&{(LnM!xh8Awx+(bK%hDt3XrMJEmd{M5JT6`gL6L!DMWmd@7@kf z8E62%i)?rfRoXe z0>*Y`_U`mO0hOy3tt&!8g3L{`aZ+mJCtnIbH@@ln+xU|4G>J9V?H6zd4SF2*UoQ1} zIONU2E17!`MA-X+5^1wt2SR>Zt+BpV+ysh*%Ox6j7yp=8bZa1CrqSGbov1&INodo9 z?oQkUJLPbC_mJRG-tPi>g4mbLYrAhh0zDO-8%UAAKt86~K@MArcdr*8wPy2ITk!?@|!}R$Cr84HamY`Q_(K+btf__M3w@ zODnCK@`TJPpSZhggFuJpUuh$TrK~rIa;k?E;-N^pAdX0zB6iDaCaCQGV1{^kEe*&L zM*tkq20D8M_|-im1Ozl+@Jr-3G+W|M;7%Jik!XD%*!PkP1A?7j@??YXCD-0D~( zgf|{iEY)=DrRMaKo?ZTW&CyJmUC-EjTup)O>5wH0;qh{g5tG?FDW|P8{Wlmhn7{Vm zyvgXMR_K{A$gqlW;QYeXHLJ=$kSx*4&L|ZbZoP@WH5l8KnHf^QA1kCp) z3r$W=4v)c)2!JUerOQf_9gK_EZ-|KY>fH#8*bZFNO(@V;*?mYL5Q5hSWb517D$H!4 zYQV2_XclN+$L)k0#nCqpxx>9M+krnn;_6#PdLqU@jq~-t@lXba53hlk&up2EX;u<@k2&GNlY@+O)NM2_Va4B$^1Yv_U7B7v<(+bidL!gWNq-jZ{Nxe z@Ciru+>8VK>jK{+#U-4}vjxs;#2rl0A8~8{!0Y>g1uLFTs1HTc@e9x)9lp^~apmrY z($=bvjfm2M??WO}P`?LR&jCow_$QXs?+0Xo<;^|OZ$rk5tCLJkI=?^qU+leiSW{Ws zH|nFK%*d!?1Ci#Sf^<=;l#F9Rq$x;mB2q&SA@o?rf|LkIZz`Ptp@vRGN{}iop+^WE z0zznkly3!{dFFY`Iq&zJ^PRs=u1h2$JA1Fa*IM^||H^%}BNNU`S*KTp`47#>8{`yy z{X67r9stgvaXZWcJ~ekAS5SN|C(jSx^7Dp4Odqbd?@o$e@+@!+m1US) z)V;b)evKieJNBe5gDKu2^zGBC2UqzP#_#lW%Jny#+Y!t@`AIr&xO540V=m9_`Faac zB7Wf8V1CInSw6G-d|m+1Ifw^$EBLv1ghnKoYqxVPjci^0EfiEW8eW|4a*@vTQ%!)H z5WR(1===ZBMHpq6UZXIt>zV94eN2b{es%N#T-s_lLbk8LR_yx++-Kw=e%Zae6ZP*? zHN`6uJ3rp^D{WSu|JP_bhTf!$Kx{*Sh%~*&)hJTd)V^u=6f5?CMGN2#xwlOBTuAq{ zz$QN1y<;N)ZsHv3F8k7LwxCzAjJ|yF5;>)2{?6!V!ch0RiluIITqE=sHTj=+k7CS# zu)0{sza3`xUy=m480MyIhTCb?NJgEaBWmTK04G=AqS4ooB%1P~gonsG4YM*+e?yF? zMe;uB)!BlYs@+e1BmsJ%_yy?a3OW8GuRa~)oiC5TFPNr5b}XHHjLuf!4E56lX@opd z+*0DGMm6SUoSL6M1stm|bmKZ$h{8wOTO2F{_PLmwwnpmy9H)8D+BDDm_PDRLkW?MfhJywO zkz%C!{F0#^XTUsvd(1#=v)tRK1x9<4)fJv7d;I5lfZU{bpmWFF z)8E?lR)uZ&1&ubVQoIs#zmD>;&n1sNzf1ho$tfao;`JC8rz&`WzM$891Vhp{&BEX| zJ5K@}g={(bgHb>H1h1aTx#FCwCzeB~KtszRpDp{XYA)|wtHay`CE0UugaN1F%o^`W zIY!u!q+Fl0Rd0$>SReZRXhasFBF(Yr>ZsQVLVo&CzRf88Z5qS#YsN?R)8}lMY5Q#z z%DE?&V=iScMW?YxCA!Tv`gX>a)Sj$#(tv?c%HfTL`XgUka}lBKA6Qo{SvvaHeCOfs zcaBVI%nM9Q7W}l7$X7*THE~|W7)sHKwKAOHXvUL9{AGt0|LL{+8OktLKYxxq|d_*rm;kLZ^)>D|3CT{ zy3O0Fu*Ydu`#F88s>S+GJ8gsnM5ivi41rzPPp?0fv^87Nq=zuEI8 zQY`wmA3026&5GHE>_##$ECbd$j%mb*WvyDRHS@Z;zYzS=z8}$RGH|OmYYcN0pxmVM z=HKzO67^Tls;cXgYn~dp+b?xr`L?db{`k-R{NrGEnH*`!&oB0?hpr_&d&yOsGV3RR z;jU~z&=O(`@FghP5@F~bDsTA+CAGJS3Z3FFT5oFk%6jKrP8L$oV zRnbP31)Yz%eZ7nwuWdky~;wb-ts({|qjkmbe9#&luL0d8#_qcf%Z~x@~?$D6x=->B)MG zb6?o(B3J+Iw?8O-0Q{w;$*%1gNRV{xO4V54*}C42rzFT{x2H{N%S*NSWF(z~pGAhu zT5S|aC5im)m-qBUv-9n?Sm|}ZwVefoPCsC^H0C>#>~qEZ&XFJ%2g7etnn2SP+zCO9 zjqY&NZkDgSaZFiYAWzTPXt*&ZU20jK=#?K}OiyGwGU!z0+0VR~`B4O7ZH)9BPeUBq zrj!uZ(N^?Z4J6gWOTVw>jxj!kZ!oEw+oVl3qMJI?reSPouzLp*7#K{p+dT@C3=0&X zv&@u@)>+Io{5G$s0&6pq!n8cHge+jmb<7ylR&%?MJf|khY~pJe@_#sHnrwS}d+~*E zgG+nVM)@!EOt%oAZeZt`6+ZtvdFAe^_UOVAsEack)2&eS{rpQI0S|PT7FM?+E&EBV znZ^B(-GREa@?+uh`bzh{S_H|e5mZfaOznybHzMnZcx1p^E2f>S#aAJXRMbsBmToW0 zVynV&)Gl$2Hd_=lNGo9;8;prT#u1;AI8aK7nJh~|bpa2ovyutLSbmwN__y5U5fx}+ zVhNkuqxi1#wYjOR&GSoNJXeu3T=D5wz=4P@teI-aPlEs0zy_6(-FE2X#G+@Skz*CP zR*VTjFFH!u4^g$Uet&p)+xP2iAxcm!l|Su9xPQU;tsse@aY(}AmLP)e)s-@bOx3!) zqD@7CtlMTQuEc8bIyfZIuE zOfg%KfAn^mZ4W|MYN5YK9rh#VZe*f9`Wd*fHqm~Xktqx-)p+IK{5D4g7bwJYb8v&F zO;^EFm4AO2z3oP%Ah^T=Mp9E2Y&3c{w<&JSw~4GZj!}Bn8MSRot46N&_SJ7^r-&h1 z68*#YYx8YeJsj{Kea$9zZ`__ouxT(A{~*!m*7eHrNDRlOx}S7pS4Zb9ptq+V{ddG2AHQ5%=@#^&ZWdL~r2A6TneX`nF}lnnR^Qmod-k9=Vs}#2 z1^Vk=F0WVXd^O&7Q0q!rD34I@wSm#6nEP1%7Q|%__Boi}lGwhQ$${HMu2aVtZ}zKV z7{;u#0oRFw>VA;bwzs$>72-}Z$M&ekLyylT_$vw1>$L(S{(K|+GwR!@-m|)cUgLUe z-Nq$Eenk*FSEpo8B$~}esA0L$f)}!V;CAFFRc};#Q2l5N9dh^H?=G+2!UZXG! zm(&-C1I2M_?=?va*kDL7EU( zd<(3<@{gE7e9e}hp)z9E6p%aQ!L+%3NDY;}i}fQ%81(7``$3}?`}HAjN&Zj3PA4#N*rj zc%;9U6~RiYOipgXL6m8Tz+W}|43MnU%XMVVrENxA*ZmfhT!gcL-D|C%!S`Uhw5%*6UlO!x z2lb2usckoNmDGUMU~GB$I?z`*ftnkpX7v?#%Ps`sGxiw~#O|>a6cqkihc&rf{uR9}n8wb-2_ z^lHX+EPGB?1XT3?S%FGoKR5@u{A!}>?$}4`FfmAu1cd^p<>as7zhUMnte}2e$yx~u zEdt}X1@tL4wG8k?6wHU?DEX;1C!PHbSd=P8=kL!@JH6EVSGCu=H?9V()z&G->#DUu zVvI_|#%($+{R1~%mON|w`_lA}ze-#ml&F&nUyj~QyIc6%xr*VCbQB6KbU371tXe-5 za!`PSRjWpWtf+hKE@|>Sf29@ouc>a;B8hREOIZ@LzHIEj-oinRMZUGna*SD$7U^0K znt2?AIA+4PwpbLN=^F@^KRVueE=+I^qJvVg?R}<+nV-RJv4aD88||(=d(JBQ7Hle? zReiZ+g$2OQ6k{E?Aid(kewd{W{r!|Tbjh68z>Ow--K_n;SUNcBlbF!^m1JGvAm*4+cLUhh%Z4sGB(n0RW12r2qwXBfxb#$`P(BP+hTp&JDk?{p(~A%u$j$RMWJj2S!3M957I;5pM$Hp(Dt?K%YKy$aue*WW>ZA`{#T1WG<0#7_<>)9#mAT zJf~g(#Y$<}6lnB)PfWY_1}iLNCQ4VW8VGK~+l{g>z1sPnjUig|NhbwwQxcrU zeR9g+YG#8*XS2eiyd_WZ$|XA)o#c-~pVCFQ?5l8o)!DBNAEKB~29&nQaQFMZY8t|K zf~xx}ESEKAsh{TXGl8cXKAqqD^7|GAKtcFkE)>?ZL$BWzh+j?~J#)j}Zpzimj zwg#w~e@HdzFs@C0SJlzF%{7>$07d=F2VSBcF0~4G`}xf#{FmC4JC#A<-0*=d1VV$p zce`}>?ilgIsYBCyll98OdWasNem{E0-T|^Jq_+uB@Z_!`zgmJ7J^d+5(OzNY9RxoO*4hSC}gRM)$eu}Ca>h_BPMeOa8 zIBw?`B<_fT>&lqFTtPGllIZ1-M)4}??UwL7`b^LDVSjb?6m5V%^M%2`G*JQ3ICmt!go^%<1 z=dn*ZR>9KJp@~}RJ{+rRi4GZkqpzjcNA`F*jYZ6M&`U*dJec6cd!`!;% zIUSk!*xo7g+0T$(xgjU+r7cKctNe^^7BGc<;GFs5O(*wa7+F@ZG&RW{NKgj6z z;Va#^f-SmboKRtbhp&VOpTYSUKQ3|qpRphx|C_k-?ydgtT$%AU4rn?080taM3+4Tw z_N`&d&3j`7I@V$$FAFI|UsANQO523Ou?E4us9>Y5R9pcje-a>FV9LwU*``j13mN zx6=mLSWVhWcvFdkaw+Vs&X#fc>LFTx0!sUIRvqGHEIieQ`Dx?k_CS5-RrQ_vfjhpC z7F>>Upk%*pC13MHPnG=$g@MXUlW0hbD^v(26o~uH_gzw5nPxo6WSxI$)^1>PeCwq< z&$W0Oey6cJr@c1`yW_|W3F=^3Zv5H#)W*Jkdl9(}rTrX0oL>(sMqBWI(sy%~-Rv7msxuMxZo)fUU*a?O)B>4J zh=?cgL!Oc3x>ObKti+IY<-WxxnY%cPkP#QpZe`~Jv&rx-QFzj5bh%xv&GgWTKXXo5Q*p8f3!J41y0le% zy0&{nY4rj`C>;ViIxz1S-mzHS38Jko23DVfS`zAxa0^$2zKf^(RG4n6B)hgd?9@!) zF;~39Wj;x`Jzg=!@g(B@sZ{X>PYzJ3Sbq}nFR1GC7l!CajgX{|!!hQuxRhU&ZXI*9 zS?zkpxu&~R;T|mz)!!J90#`f9$9Z`UF8#4GIYi2*^|gXG7fSmr*mzIHe~~-omC4yQ zu^1R7sqEbr)_vXi()5>hr)ChW4Lmd`Yp$P3O%c&p^{Z#0%o2j`Wg{|o6ozg%;+49; z3t1+}QzJQOo91}Pr0DVy+0NUAQlT1OJQdd~Q;=iI`cRZQ*yrCeV5IfX{$ZZn;XLz{ z{Q^+bx{?>q@XvW5CN2~h9i>HAR$GY9yl?&w-WDDy1Jdczc-#kw^H>C~uneQDJ-93@ zb8*U(gcQx57KOi`D+taw1F`CL}b1i2gM zqoz`|Ani5#jSTn2RbcdsH}9^^M)R5u@3W~se-m^N&V2E(5N+n;1cR(7C1J=P7430~ z(_Vk`i>r3S*-;7yJf5w(G55KuX??gz3CSgxW-dYQD#q#2yhxf*aSJ(VB3bTD+ zTL`6F^knxT%7ta<{RzZbot)vDE8HVS4LG;9ozvHs#43Y73eM!~-o|nBL=jf*eHL47 z0sKkMh2k=9nqKquh{~X8y`jR_Z)!Tv3E1{4tFj94z03UkdDNQ9&YElJSL=>_WrZh0 zF|a|YF99IFCNwN)BOmhz4(hi2UAjW=ubJ_FR&vf!eMJz8dBU2F{~~jQVFNFz8!!o$ zD^0sYlDb5cE5z zt`14qb64`d;_G_Wv~7QMA^214!|-P%`PHn7GV8)K=yc%faTb%c zG6E|mT(~SB+va>Yg~Hx`XYSOVjiS=>#sDb6cT&`VqEm4qoRc@B#eL$MAtlJw2*luaOqgJ zz4ehg`;A_jW~ds9_~boZmr)Eao;Sqh0~VJ+dB56XYjr%MNhhd=I1v6vO_fC3dDa>2 z_qC$rbHCDzBZ399?_2-&)*Ujnw(}nF0^WD+ND}*%zUEPke~Mk@a;8zi;*q_1$s9NZ zo?Ci*DJc6LCE{8^apyD6^{%}uW|xF@lBRu=u9=xH)ZL-cgehiMhIrMvMt+u^?V}>s ztGxpG9>82BjW$P=W`U>Io;`e8AdLK2Ek-!KVb!V&#Km?9t2{ZO)Ny?+HiMyTke@fr zcayi5Qep+$!7WJvbmvCU#Hd?eCc=MlH-bD_@hyTpI@YaMPZF`_9X{^ZlY*`GcE5;| zPIK*FC_%>Fwzd#5KbY8L|E8ih%quNjI^km#1%Ru{2W^dYdaHmj`GU1rjTJk!R45kWX>C*sAC1H%OOlFk3W;f}sigrOBtQ z+b*&Gs*g0s|0^BLvH4wLOY@4V+5(du&fRl~uT0By^e>_8YE+8sAg=Mbz`-jn$k`7_ zUyl23hGxxTi@u9xG?Vq!vqjUR15smhrs^Y?fgGx%tY8W_ z&BuijMY@jw1&##cLv)CH;GcEz&)7knz^CH~=xnmIut)yECa zJSs&g^>!TxDwTQ;l*i<!n_NMz5qC2m(e-a^o zH&{hTrj6qNVs&*Wo={F8>rEpJ1JMQ$q>UZ9VKwk^@2TP`K}<;5n0(enUFwj4uG!j_ z0lrg4cy3B56klP`sMH-ES1NN9oAudT7wWI|-okOF)^^N|J3H2+1igVvdODfoK`h`^w{oK9f!hdRRN{U+Dxe=`K3RCGyvC8o+LSeM zf3f3YLMIT=DC(hn_%j`!D&&CGq=SO83Oz__d>T9T^kaZ;-SyG;Nn;~3Vg4l*xB6R} z-n%19Sk{1IFze_^FeSl9R><*KNdgqz>B&ZWQBROR`FR zexRKnf|jTFnx=zg2KRhGsqq|IC3eLjYJ=l9`jNt%-(hmNu^Z$QQ`eoZ^P0sxx1P%`T<-@b~oGXw`&ftq=UT;2{c@5TmW~WS4+;>|nEcvF_zn)Mjwm zmRVVFrj?*?_g^gs!(hv{23 zPLsf&pVqG_*noM_M)(>sZ&OrrYyo4nwdntthLE4rdLaZcS6)$R{6VFpB479N>I=6SSV%*fbO8DGr z``Oap{4WDzUG7YHu1(%VkPW@Qy3occ58W*Y&fXu5I zSy8kfR2&|%j(Dl@!MC8gOttuIh6T39MIz8z675jC*Kvo)g?cTCk=n)c<^Ncb}#1Nw4NyQ-J(_L#QhYJqXh;RO%N z`F|vF0|II1v%lHXJ#D<9m~;eq3LTOS${xSX`Y>%?G1p4;J_m;nEQwzc1D6FX$a1l+ z(rfAvy}W?K00Yi@NsIy}NT-bT<(}ieMMq;JX*>>LTsBA6#%w?9&plO}<=9%nvNntH|aP$+}aFBYnAtpFWd0A1(Ic zw6AKIatLzAsPyoHC;M_a*q0k@jWFuy4)=Q+xD@9e&UQX(1LDXEeDu|(5{KQ1!&k8li{LcQd5#cokCQ9pgYb! zleIA&?VmvPy6fw5K};G~!w4sPbc!|&I%+-iaM(%OJ5&2)xx6TB|H8?_X@kZmQz@Kn z{i-YbFkfC+DUZ*~5}96i0r}w15d6U*5Q1~>=pZ7lNT}9HaOH5g*Ml;yB8=@|r!ai= zg%u&yjQxOsB=CL)E^pl2=kA*Y{zC_z(yXiw9hh&gd;CG{+k;^&b@5r!@Sim<*wM>rL+jcT2h@4u%>(+z#q>`%yKYhGT@c~X9q zdM+X*Jgm5}8#(IGI2@;!)K2s09tK8w&vZq*8*w%{#}KQcL%KW+%llfUHnUUelY`BWTI7MvXp8^W|-)Znr_s3pi4eOO||{6i6E%KC+7$ zzA_#X(rzlv65RG?$`U=ze^AX4&O7&3s3{9OlmOmW1o8VF(Bzeuz-29C9r1??&c*CO z=wukRG$5d1I=N-%u2tA*!9DG|naPbTXdZQ1|9!#nQPtC8i1S(#iY5G#v%=SF;YtwOmtV)tqwkbVZulxI`(WF2eaY zYi=dF;D=BpcrnajP=S&*V{oB4+#~)we$&>_8L*pxa$(NS=JLXPKbno(v6U(L`I0U! zG=&o&ukH2fMv^CJlTo^*0Q6QM1?aPd5L3}c?6<(94!kRxR|Mjbn z^zONUFM8iB>MqRfTGUnC@@8Aq?fK*TZg>opctqea57LYKQCQPoWY0a94#B;$Fy3jI zqjxn0&q=)<4|topXAe{!lmgPb)9y#lDH+sZ&*q>Tysf$ubhceaoemqgj1nsA?jGG^ z|4(7V8GKCW z5~S_FAH^QzpSyneMl857fH@On+Po&=-h2yJ7zfbBIQGla>@-6fJ5@Sbh) z2W-CCIudbA4f`!y+5=v$&KJ2xPT3q|9mbWDca(Pr=9i}Azq5<}UAwzK2bul%|BNVE z|9l>OSB%ZRz6XUKBUvc?XIpTZC+K42<+4G2AK%5qmIiblJHcaons!7{aYgQLD8 z4=XEwv-uRyWkWanY}XP`U#ZsxsswvX4dnB{)H$!89z-}=P^^sVOkTY)IY+rU&M2>F z@qenauBE>2bIkm3TM9RDVg5cq?&Cn#z^*ce9@v@MN})EH>axN)gRT6{dJD{MTZj?M zhM*0iQ|Hpt&3psR-~5SjR*=T6lNlADmMB%E7Uq|CmkpanC2L9~rH3w1U2Q2S6Y%+Y zwwY@oa*~%MJ#~yH<6viP7n68J*N120FxMU{Dy4Gkt$L~bbs{|ALS` zsf>^4m6iE^!91g(xO24P`E}g=pJoCVlQy@LxffpcURh<61K2NfMWA2F0~NMiJe1{e zEedqCACL^ zCbVd~@ANA!CC$^~!^JMZpe5@~3^d%Rwi_n0PpgOOjr)8 zm(TqYq2V2y&;Dldc&r-At&M^&t#oXwCdGy5tH@)sB5l{BkDIB|`IlhEpQ<=``+r2C zyaVb_aXFGll8Pp64ekd_g)cNJUGgp{3*Nl?kWXj$qC&V+uy6VkkI3mZ$!MqCvb4ZS zfvD`QxU!h#*NATHAgcdZVii%)?mNS z%V7QNk<1ol!G{E!e-zl*>zFPI?HKh>_ian=D5bS1nMv8?{)RgtI$WgvY+~|)T-SAn zkxuc!l|LU|=Sx85VhC&*n%uq<_M!~Q1~6y94S1Nhb5?TR|5tJ-^4bwrN6K8|;fI*x!O0FqCP zn(L$5+|RMEz5IHG^9?Z5a{@^-Qn;7Pr8nI|KSQa<(rs}@+4WOYI&k8%>5G@|j5UqD zRn={M4dG?B&xS4EE<}%3qK&>+%ZHynCLyj?0eh%;O{Idl)dlhu4p3ZmB6{~nPx3FK zC0=;A%~2mP;$#$De2L0IYDEKa9=cLZhCL#bM?moe?pxd~FGVu<9b8k=z^)^(vWDTQ*m( z!4XCmxmtI&-x@^UGk~gmz7L5@+HZ5rv?IVWPcoiy%stnNkOa>GeIjm1$rOIO{Y3iZ zp~@DedcV;8u^6TC0JS?os$5?#c~9Fm>Y&|VPK+2v(%s3VWaZ3*8=4Wt{KNMy=!EN0 zCHyQuh5ac)xZlgRHIYesG(5>B`F)(5E?r{7@Yniys96*eZUSuLrrj55=O3xFe~MUcN+~K5Y-3RO3P>rqIMI%!%wm}K`Wb6q);`Uu zmv5aHu*v)2Pp)JKT=O^3NK?<@zC91*VrNxfotqUzP5M$jqJ;fqb93R!p6kLQ2MB(d zMHz6{-oHHietBAl%wCbTxawct|I=#X#;AEE)dsJN&`(JpZ+NDu9cw07uJqbz;NE%q zdLR%Zt@iQarKY8TdoCkRK_TDwg;3Gm$^xCPsJ{M{HPeZMJ7RZH`={M=->By*H5D53 zA`V}Pq4m6hhuy!QvvM*@MLjbK>QX!*Gt%90v8yl$_NvJ-6QiuI2UWRaoE{VW>Gm}B zy^F^eJzTref^m5g>~_%4k&!myJ@xK#A$T!~b0fb)|Oo>2={#C1k6z4~jrm9e*+Rcdj;ltpC@ zTeTjZX{5J(1z5svP*e@FJfdVP(Pt@h&ogv3PUVHe63wOTP8PyCdIrozxyKF9;MAmG zXRf&$rGF*To?f;0yL!nrLal2FUsD+`c{H&V&N}>Ccfa#b=y`O1=eWe6OsNLc#$ZKi z4XZq^>*x9aWE z=2Y1+7~R+L>jBMA?WFu<4>UwXLSDqs3MIr9GTQ#YMUDFszRzT{?LZ5AOa}IVT|#TJ z;Dh)Kt9W}g;JGQ>-zb?RmeJ=zv2p4>@lyY5Lq&k7?-fDZZLm%-&xXC|P+mgh6qg|l z2=Rzntp~Mx?G4*w?<2(pj1 zl%_I~QaVvF?`fDUxF_i-7vs#0uP=U^KAW=Ildh^KWDM86p=YbAj|#a0>2Ko=@&xJZj0D?}-p7L&=%;^TP{asw&Pxe)<3$IP2oKlQze) z=@LA#RkR`Z!wyEDDdqTp>xxL8wT{HSBOIMJBGTSahmu!)m@`>0Z_7mPMeeL(PE+-y zn9kl`K??DOo>*$Yxip{LN5y+U#xdWZ*`A?T?2J-T$*XV^{fA#t^r-yw--Ez^IT&jb2I1{#T0@Hp5S~x;tpM&w%uX{Zzwl=iMiAauZFL?fM8JSYa6OF_RXoxVB%SU zr;b&nYH`{8gO*~?rn=m8r=w-?E<)=P?11m7VI~3U_jYIG75@DP#h67rzI-Y?Hm9_r zb2YTtt0%m)2t=;RL<|Q`t|KxRG^Y3+Jg z2c@ap)riG&>~$XlUIcOWT`BQdx~T~wx+!t~n@c?A$Jji$o0igbcp_lN>AS4AD~`x7 z@n8UkRbicAm+&#vKPBh6FGxn7`G#s^4CEr)&5NEbT#{GCIOQ7ffIeN@G+O)oWqDQI zo2y#4J2#45hTDPBpOP=naSa7QZX3VmDBS3f7Bv1SfpInEY5-ofs|(bfupL9f$l=l> z$r*i3mu_ZG4Q)$ZF?0@0GSmSbwTRoNhAS?AQhI$q#PJ|B_4>fZFm|xDv=rv*I<)#z zIcOg+cjDkg#Vsq7`PaW-@ItVGI1{99n_yy$(K*EEZ^JFq%4dC<|k zK%NNheTj`SqPAwermI~}Sw*GwQ%S!-noqJg(7rXi>-?^|RB zPYlz*q)Hwc#?}N-I|qGj#LBIl`P$@f4Bb1U6BAUb`Ki}0V9fXY`JfL6+0Kh>2gY3| zMkI423EUshT7CDA8;;?|^nT_(CwqL2Y~XEOjm`HK9vwr(Gq8J`k|d#68_6 zjgz$sT#u%0FJOREM?iz|g|DwVQ{8n|7^CN7w%U)=lGtsxMqGCmkJ6HUVRl?`HJD)) zaH?HCr}jdJb$I2wRUl6K;E_@Rg`J--i=gZJb5n6Ysi7}WN_-^3C~n`0g_MsQN`<$A zf@)`2*jJ@z^+6jyVbv4hgne()2 zI9<|SJMlDxw~ch5enguMNxFpFmqHXW!^xvD2w8ao>~|H+t)E?JO$}G@N(USRUwc}{ zVId}hL?tyy^aJY0GRqs&G5NYZ%|&4ppP7}&hm2vhB=I7p&s4omRUjMbznq_pE!{sP z8%E1J&NuC@RqtWrQW93yG4s{jNm)KWNs%~^F~uEIn@ff32ui97frX`wGKIEYiLG>)Hv^^d1ZG{A_^ZMxMSmD zst()%UvPw6uX?gB6ArsC9Ah-CXp-LLGF-gVt)@VsU5EH|Hht0z9~`MGZoblKhjHv3_+7hH!DN`QPCd@_bs;nl z7JdrWzrCYRg@pynS^1AZ8C@leONVKYgka`V&qMM+FCmmV$_cG{!tCu9=MchaVkDTOLL}h z7Vqc$$9hF6=Xq?+$1a(buHgPk7v_w#S;Fk5D-R71Ni$oZe6+M8(w!+Ui3^R=mgv4(c5%Kf7F;y_0le&}IG?*REkfUs_6m_{4SuiI-$-@jN#XQd<< zD6D9RN0MrHV>ZlC8U&Kzrf*D5e$l6wQ+%4WN!zWS=6~e#2dfqKyNz7xAMn7|F!_5w8qU$9!b&%6~py z`p^$?bB9x3u+=2M_^pQimrG-l^mMulr_|fVy7GAD7olzFO!gbjvyQe}CaAW$`?H?o z60DD9e?ph=lYI(7k6T4?r4@fjaR1t%8XWQf?SN)4ctW)1;aLKrl6<){XYsX7VxI5m zos|Moa>kUS>XlVpy1V~D zY)oodI5r5L7)8Lg2QMS|sVn5wX&f{#%^ckJCf%50CD&Q!$cdvdCsd^?$Cp_V!gI)m8sOh9!GWAk-w4$0*N_%-|wYfO0_ADYqY} z_{E$G-f4ZxI5J-aAVz^U!Ky%hgbOL(UZZp6dzUarN^1^rx+@!lrJm8C&y`ViOR~U8M1S39_1QW6A@lrSy_H~>Nb}7# zy>zSex)8pSb6Hr4b_nuFA|i8USy?*=?k{)wgrW<0ur6l(>952Z7rHoC4o559D}(#r z5WMZN6e{RcF@3~wcPPGi{R`?@p1C2+m|Na8=RWXED%Y);-(t8g^UB(2GpY;j`I2ON zx7P9}rrdw(8#eF{^u%WlIILqPb|j{eIg(4hJoUJ6C-!}EYjJ*T1Fk;IezhU+;$ z?Ot*cV5k7+n2odowsnkd&mCYtcCP^X4mwruL51*{uJfzWt!klb4ofc_HBCxu9g|ku zpz7{fuNz0pMEqnU@6{H#S6=&72X6LApEOf8=n>GY6m9YO%0#JG%Hb8|@))%L1pZ|` z>ICAW_}V|)j$v|Q`Suy^AG(VZW!XVoMpcVP&p)VFvIMoC&rw+g-dV7wca|aTl8G7k z70yx-gmz?JbYcEv4p_bYh$2S;9 zpj$sH94X9r&Kmdu%5${dqZfb2;k`Hc&RG+wgHux{Wm2k#^Z^>nUHYr1`aCEuo7!xM zyFrl|98W*Hl;$^14 zTIkaDNuhd~e@DuVeGB;AUGyUqE62_&OX?EI7UWprF?)IJ#*IM`pq_t_02!muww`w= zA549o4n7yaoi3tv;dUn)4HL3SihUP?fW0n(XZ-?!13v0%km0J|!M|f6lr-b>ehd!x z3sMyetMjjY)hTHP;8k{{GmLHY`Sgnl!C}C88>Mux95kuuz0+Z5q>Mda($+B$OV9xw zYUQDc!d3pV_a5|L)xt$E#>gtJdF!o(qBDBKMWC;kB3?=oLcCJdsbOX|!No(x23(%O z*#yog3fQ1ST=M>)b*Orr^OQohI%{H=td&=TCLo`#2P@P5K-OeGURd0oy5%FS|Y_Li`et71i zEUJC0$rjb?>kAv{30lC0#$|>h{sc>LD6)16@4l;And{lu_JPw26*Z@}l6KBqxO|@1 z#(di6cCE`y^P=C$WHZsg2fTPgEZJqa2d{Y=M=9BSdZ$aXGxomjP2AAJ!#qmqw@?u{ z9AeciBkFFs;kA}3J~_jxs7R?YKHgg~HnD=$XDn18Q2tmYtLn|WMKW$75ySMZ0*u9m z47MsAb@I62)Bb@|Z|wcTT{M7+yJg$jbhviVQi7)%x_}&D^LUjlxde2} z^-ksGM&f~QvN^5M{|a8jy>tKf;KdKmWC7#D(J9NmZ(N6Szy}ois!vnGY!H2^*#KC` ziD`Zs7{+^^k}J~imZP&XJ%31Y?&;OZ!*@O$m{_sgU1)c|6sCXQh#O1Bo~Zu>S}W#( zmLvvcJ}JMrw*pwusTUu)4)(rTa^Isvf5UF~B!vODLE!bX-^lv$A;Z6L+M}v}Zf0iD zY}?xlMN&V05Ir?Nt7ApEsa>5Gqjq0tV(;`4^z{rU4z3psV=`4wYn^IzE3jSZLUcL(^lqnpblwi6Q^V7KhAjaIEdm14I{`uu5CVh*Ncm>e^}cJLYro%h_Wp7HoOuaB z$efw;sbh?LjQh^eX{XorJlC3KdIrRUgrgq|`z8f@EQqR7+RE1eHMGTBoNK-qj-&>Lj(2#De0CAp?eS7fe@DU9_qwS5mUdANOji|y}6Gd&H2f7Oi$np!Dz zLDeKU1LRjJ>rN+Lq0_`>Dy@Ssr~FOhMGT>GCm6mMIw2=yh)~GR_t3D-mJ;n_`;gvg zSj7{o-=+f~uQ?c>CjciekS=fz{U#@>1XhT81cdQ(YQ2=K7(IAa_i15cQMRGFVkY}M!TaVfhU)jR5dIdM=HqFF;f`%|^SioSf)5=y71+j4l zF;>@l!GMCB7N#$T)osh!SAEP!uu?(s=;yP9u&#E-so|g&X&_$F0E8}fe9t%jPNm9yvG=6Mk^9yYIvyi~>&Z)u#f|5Ia=SI2=MEtNE%u^n zb1%Q&6d^1Xk?}Y3NhjZKE=-7CV%!Pb3yrw;zHjZ@Uu~Q~V&BcxmFut)Nj38mtft#} zt9EpWGKpUjWtl@m_kcXNwmZIbFm|%XaQ_g^#cpuOd*@5k#iDgh0YSPnT7;qL_3Zj0 z7GphCpor|y$sLMLDK=}`!dkhjH^tWygUZnxd?DMHzI)a`2A1P>L{dR3VvazT50O|o ztQS{$!Nj5d9w3r-eKR<;O|OrX2A?4=t#9A;{w-1_nd}Qg8m}abb<^V2P0Gz8JvE{i z9{4qsq~fYCYenRWhB}s%yKu?*tc3weXpUhHk5ME`uiW3=8{YUZI9+MgbTcF+wrQ)& z$$xgCtNn-K9!*Sfon|0(MksqX)c}%MZ+Sc<_BpJKDsdk4pn1mhGr7_bNY}|rs~c`2 zM#j`XHK?oFq=$Mok}M*>c)6Og`9Xx)$LC1l{eG4!hFXc~VrwE}i`rtl!#0?@vE$w8 zwv>pE9FdpN&j0G+A;6jb_)bHw36gH$=^>!i;Ii7}y_?`EaMD9JS<`GDQ1v}@!zAPE zYU(+sae$#SVg21sE|6B4#nJXR!{@rVzQ}$B*J|dc|4C2;7wtb4{GVZ!q)SRA*oQ&0=GK4g&9?UXsze=rKRlmt~>=mxW8ypQv`9)-O2fg$pteJU(>I* zJbm~UGhHc_V1$KZPio{Yw0xsltK}-3DmN(8whToGo5^#SK$KCh>t4gBMeV_pA`Htc1kP zn*+VucJ>fdy24~*L$=*bD&VN~r8pUmJ;MzTx;PoMDdYTGQQfs;YZFs$>E5e-0^MrI z#N1p3D-inE*P%XJNJA*JHLM*ZZc8~AI~;RUueEt~*kcE#GX?(8YLYOa(DzgB`**p5 zsb$PH3D{YjeP@ewrz9@4bvz!usNK2+IjN?uFa4{*l57A1zd9Aay^Gb{s}re5hUEC@ z2_j!d0V}54@P)xUwqSJ5lYdH!)x49uyvvc$3}FC@%+@?JYl17jF?OuGY3Cb93M)gJx7!o z--8B{NSAlVA!Ri`R^34ccw&WK%g(?QH*>igfbfFBf_aH_4&<+`!v@WsGD(0`S=Q3z zJFQ4sbATNwzOlaO&RpTAzE!64^l|cH2q?c`w)JFs^=HxS8b5UM$o;mEWL`q)=^~j1 zOPy^t%GqZ4%@4FMF?8#)iTSoCEAq^n;Tr(Utnpx4LiGI?cW$?(IsK9+1B=u1xvR3= zpxN^|zu?L*(f^D+m{Bc1y!&?d?cZt$Zarfr_gz=N4xYDIye?E$;5o6_O;cW}`z|5; zaXf}mxd*)h%1iW#1py-1*IZC3nHGK>xKPHinmD~V-1*4CZhCIs^j$2dljhU(4j_%Z z(s+{oT-{dzZsib0M7QBa`29e+^FSDe6Dr^FM?C=K^KA6?y4d4SJiq0_zI(=gC`Uyf zM%)D$xCI{l9VJNb;quBE2eR$9NX_d?9G$3-fO=#1$c92!JF2qPEslZ^fNG{*YND;`)!!sPUeMp=64h+|mBSlVwCo{JaN0rShj}+;*()k`R$V~5*9fi$A zc-KGpUWo=m7=Ou#mi^H$w_jHacxN{QDj)il5IqXo2JWrSEN4u|pQmf78szT+RLaw( z4PM*6(3Wy3k-C=7yR~+-^EmRs2Y8Ln-&zGF=*xWdi7ih2IygEDFqN@=lUW>I`c=m0;g+AYc@uXrqpgaH zswWrDSxz>2ni_MlcbBMj_GgAg>?f612swqjICsjpQRRlb!Z3gXYKT(%m|A-~X@TQb zs)|+RWiw(ij}O=u8-vD|xVd}F#AYQtY#bhuMdbZO-5--INsis0ijy7^^^vRUcg=6; zS-iD~yJpq6&~}fxyKcJVh|wZ_BmQa!k*+DS$Fp>tAVc!4t>aJhOiQ>0==h=ZW&R zBZE&jR8)biv}GS1@AoUyOq$6wu6DVO%;a$2lL)8f?sV>NX67cjr|QNUA~%`f-!nse zZH(_%u#vBKE}#10BXJc-yPA97qTAzc4Q4$`Rg8x&&Vp-e7a6^LJg(6lv#*&RfY+U( z;Aj0yg#l1wpsO;x;0n>myT<=kyk~n>HOsdxF)K2hL+ch2Id(qV;YQZtXy*l@yl%2n zQ9|zUjc;d${D06T7IJ)hF+g#uu&k5yL_%f`kf+YAvZC!6@AA>$bI2_pi4CCKMmMxY zN<{q9ukgd``ugV}J%t+v6;ZwjlfhAmTunrKp-E49{fa3paC0GJyVJBjudU4biQEv7 z174qjA6K+@`?AOgRY2a-b}EIe5hRB^$NB)ZsVe)4ExvL7q6d(SDl0*+)w#-XCnqr< zK;pIpm{(&$0)xaT6z9{~6NkwSt1hMU2 zZOE}=T$6~f)N&|S>f(Hlnh3w@V=ik8Li~dud zV*H{p+L~}~1NQvN481hE>B(7wW2J(}FBCGG zy}^^ueN9uQ_k)ukGzQ8m;p>^pZ@4%JOkVLJtG|*wD^d0!n0P@={VV~Or|p+94th`p zs2O3MwSalUz;OyeJowV>$=@mwRyrH9X){WpgTsL6d`#z!w+#8;0V*uVy^(xsauzEz z;~f30r1)^x)ayJCisg(i5ah(e3QRKM@jO_7&Pp6*@vp2!tK2l+^m5Kzq!?Q4^G z>z||#bk`41L^5F`dWt!XN}5`prB-7fcnSRz-~e)>?DwB?(w|TOsAEdB>C+b_5qgV_ zv_^-K2>6b7B*i&?05Yb-% zPe4GgNVqaeXjkd=G}95eel}5Rvh_{ho5b|=yMO2zpo7`)i=l+;FasdL@A6Q+A)B*} zi@jL0lwibv#TY(L(e8{qK#ZZz`^W^K;_1WsFF*|9$yO9m>I#`zV45sEEO-9Iku78? zHvkPlO%6{F03PJgMN1n6NG)K|0ViBkrep!K&Xf1OZKeT=G@L``Le%NX3A>2=ga<%i z!)|-+*G9AIKBH-Ir$>HK(lleDKuSh1f>pl5o*shV`9}y|1_Oug3|@(ToLj*G$!7#^ zZTc?2Oar{qk0cd9Iiw+`zSrr6&2*3_!gJs}H+J%6UW&WnvGt3lql4e=Z7(1#Z9is* zHOZ^xPTn5sV!8c|zjfd4al-kBI)JdkeXwsiP<+NDs#HW2K+`k{Ot-^dJ5{{y&pK`TGNZz5egiRQ~^T2?Y`bVE;{}ej={JL1?|N z#62@q&mslfN-O}RM+*PYKK^Qv5fQplO4D23z+qe}D$=@iq2K2dkyifW;Msq+SO!3- z6SLir;?IK?TplIvk(Rzic!k|bKu*Ame;zCd4vAxpWG^aT`}a97x85p)f7bcYK4IV? z^{_o(`M}63*rw-4)cD>mKtH+#h7~g5-~9UoV9k8E3^0n+17xpY@?9H7`2az{n`N;4LfNi(|EkCpvPX6`&U)H9A!}0>tFutB0dLQ^CEpLL~zf$Vl#GOfgj# zW)AifO;4hov#-mj^7PyW7{Ce7c;I;clW?N!6(Qqnqx!|wRdo(~E7To;syG0R*v5tf zYGqZPg)2CMAu<%zP;4|wCmG+q9uldL+m6!RB;mCdu8Vg77n`wURi(#faEs9kbDXnP7r%V!e8vnoT2(9jF==a=Z@ ztw&5iWGc(vJZO=FuLX2yXU7BX@@MsB0 z4c=O`Rcu_`BWMduR>6nBptr$i1eX1vWK=lJCiI(I11&LJ5w;+1Od8ZOA2z`Y=$5!{ zo2uX|)%N^v_KBZEX!{mIXo9A5X`eJ%09N7!-O=innQu6dItTg9K3pz=ihJl!MlCgc zc}Cg78VQ&?ZDP0oLZ;;P1LOTly{=yPUd#SeW`pgj0cMWf z`W=4wr7rhZJ#HtnnROb6g=i?q(rK~7zSuno);Y`8e6R<8Mls7l1q|n}d~{{IO^>p| z0ow)paf$Kznjw#aG+&>;+GFhLzM4}zdj-s(%JjhTna2DMllx72z zYf7r%w@p}IkP*&UgPB&U4Z@>*4s_QUPzQiD5_eWhp7zUanGQ~EmjE>G0L8JC;T%8( zbuWTW?M>lM5ZyS&K2}d?0~p%-79ntO6~hdUww2dMCt}}4k#}Uu-d?3PrQ-<74tJXB za8yI*_ss58U=Thws*Ou2*eP#lJnM>Sm!AOsqAXua@=jNV0nw%bji8sJ(Es?Vi8 zQw1A@Wgval&>q`$qX!cpR0#^p_Gn4b3ewEW|3rk^g(lcf>>lVT7-;z1Z?lb?nvF3heI4NsHmatB0O{15CbApk18O#?e3t&Q}m$o(E&BvsX0u4;(K$Mt;fk z@h|I3W>0)wUNwmdQOkG_M+9{TvODvajALK*SeuVUAfm37>>d0Tjs3(!Tb=jomU@TE(83d0a+WWqbb?5uh=3vrT zDVlQ4gagQC3;FQ-7$ctRhU9cI)RT_EWp{I<+goRVqxYKZ@?vEzNrnuoZ7~?-xeef26X1NX?DK{{4xFRg7vYxGDl5Zh3uQu7E z&ut=%v!p+ldjUNWoHQN4@?x=@XQIZ9Nls2~WHb^lHe-Fh;q%ANr8}K+XVVaxIdoPzW3T88Wk9x9YqzOFT!T zww|SJ;nn;TVOpD=UU)ppZ}b`SB=MY)(WV#6s28ixF7TGpjpsR#HRwy)PHg+0Q4-J` zOkcq%^(|HHUN33Ov*-;lZR2L(vY9^_t3YnsZfI&%BU4jjNJ`6oDck!kLB3Q(pC+RnMdDu@TDH&SLn1NAZ) z`!FFfmJZ@=>9vsPHnmGOJM#?SnLP2vRzdQK=iq z9^lWgwqp}H4hsPn4g8N75Lv8X(eSeQ5AsF0@qb(frC#%_x zilbXmOs(Mam^v&2axdM3_LnyYd>;A%z20Er{br;Hw#!T1a~=TKZmRg-DFlp}X4LZC z`CfAn)vCrp7*5Sw?dC7>!j~+w*wnyO$K5(K5<Ls=^lW{jISDjj1c8x~c#~C)(Hm_2#+vupe)5sQ_W(EO9 zmLc~W3it;?+8zy2KJL?h=okGxUe7we`*h@u_7&Bq$4WRx=!G^ht5=Uey8Iw$gMm zO!~Fg-lh2UZjqO+GHBy>bzuUfb)O!1u~13FAZxFJK%q|$Jgl*qUfn){Xd2=&^jXLo&49Iw@XnL6{xvxW_8B@@-3?X|GWz%-f>;f0@} zZ3U2=Zi!RjvP>@_-;!7&e*(Nki`}}tw4?~ z2f^^XNeVehYLACC$CLD8_z?|r60W3HKT>FRYW15Vgt70Iom=EQ2(9CSb0^_YjHOcP zrs7@tD3LiOL^*A=w;_%}f!UmoT|i$WIcGxbPSfW;{iwj1`IA6!BYbtPFLICYepKKy z8kmTA?yr^kcLhE;gOE)Iv_-~7#*J(ct~K&ij5uCT^2|{r`7*VC>>SHfvvWNQM#Y12 zmb3#bck4KHM4vAQJVE`zUNcezgHon!DlB(jWs9imBG0rYM9LI?{bd4>CADH~2leJx zb5}IXJSdkn{vPVG%kRIB)sga$v*jBG!OqI^R`nS{_?@Xe&2Nspe`IDnG ziosHxn0GN)Ij7Q z0k+i45(@&7Yk!coCrX`Bc5&^`vGGAgt~%WSc7ygxc;b|iEl&|uoT@;n(%>!iFt@-L zse(;2yfnuo9p}|~{d5xHV6$0aE4@#xz6OKkFrO3O6T(gcTmSFHST<=}iQa3!j`FuD z4c>uhE5zRJaikjWrR_XO_BkV1({7&kB&o#M{yXnDBl(hFJ;i`zCOJBXLC{2KGl|@c2+mOWl@O!)I!J8_awnCl{8qLJ;kVpc&O5Jrs zWkHo4ZH*T?_xX5a_KDI!Av}M?5cF9;C>!74eI7;a5ntQ9NNiT^3qzU+B4G5#`7yA- zu@V$mxndS=MH!QLV(fH7`r6G&6}lz2v@o3k`g=<9eRkaXdxJ?*f5*T;-Zb;~-?#*$ zX~QJIjdhyb93*rr#0NB6)qz=q6FP|-!|?PeTLE|r$6PIqtl*y{_Lz5#FawF{tlV5g zcC4G8_2U51YU2VzOpyYx3GJ?yKnR(OI{wX1#~(L~fxthB_H?Auj>AP;DR~*GD_f_D zN>9fD19$C?e41Tec}$sWBBq2YOtI`R&ht{PBya;(qz`4}`two*eQNZE_SaNbpQhVT zJz!c$l{0#Fxj&l($bt_L{#dFXBq9EIMn%%}A-YB4wLbB!yZe-<=Pa7t<94!x!Y?^e9v72uL=Hpd;pe_B`Yt^WAiMU4}O^I*L3dH zXj1*>8`hwtSUmsdAcTr9&|tnS@cmd5lDEjOp7|_=$D9AT*Z?O8r#y{mkbaFI{@raR z_5CcrF8Ysey;9Bpy#AlFhfe^D%s<}s>qoO2N!xBFjJ&s#HP`aMfyc4FuZ$mY^QrI6 z*UBZIx=m56SIuyGB%1IkwQZ_WBPP@W=^!{fJis=0dE@qy``*Z71Bjo&Pa$qJtuRl6 z*qYTjY^!4v>0w(Jzqi>zq%fF!noGXA8uYwRYu0(DtT`%}yIl-fo@FAH^>+___~68% zxmPoIAw=oGrNeJ<2N_JkyKvcek%iK6);Q<;9L+u-N)CPH;%8Mo!S@DP_0?gZu8A2M z>wu|g%o0-&KcH#DQ2ez$-9&OHp>|vGQu=?n4M#`Fsq1X*J`dlnzX&UKx@=;S7_3p; zDPFLwAA0rgHst#}9uNt0xaxX*lf^jh>7T%)S_b{A zX_ne%GE&FwuD^{tHIT^|VB62^kodD%!92N|7x=sZE1=>jN$PO+jcS&6)$nPC;ByXL zU9iGyG6s4@p^jmW=bTLZ^r{*tSjqVwfG)2t(2=jS6|eQLq3Le_qK`$2osoAB=xX2} zR140Oa^>Z)*3h#1Pokjkl<4Ia3?suLuP?)Cjndlv$fF+wjTDw?%sBhXz}NnXnrNj@ zN7xzTw)c%zUC0Oc%tl|u7GN0XCzetkvJZ<#J9j({ea<(SSznJVYS%TsuP1VHlieHY zzJuvhD}!DQV_@J@m%p4^TR$8Vsk@cmh0e9So-qAWf(CIp#hGO-Wejt$wPghDHdK2BfTu}5r;job9|Tuqueh2 zNao=%&puy>0jt=>=@1Of{;kF873>>Wy?UsyDB9yKc8^be$gQnC#)zWh*e)p>_9B`P zgh!QFmjpi|6|x#AkGeXQN;k9lEI%~TIpr~*V;lX}QiUOm(}ob*d8_b;ZkgfAmi2ih zALK%sV2Row>+{au#hOV8PG(ftWAoBvV_3i9mmtfv3wa{@`X(`H4Pzb#*mKpJjq_2R z1?Q`3DO%hnlIzF!Q_D?5HLTgPbI9AIORd9CPCOsa@2m8xX#5hQI%u~WV|nvx_9K7p zwIca$nc%Jwn~GaMAD~j!4X0&Y6z*QLkM7~nrK)#2j%mjSvnd{Or=+xoR}?l*u#ZK7 zm@@E>2jBA*4ZBPXDT3T2dpuCw`~j#j2?FZZ0|6y4O@A7NI141o&jPj4(}vGCB8aK(sk20{ zKXWVtScq9~m|b{a1tE7rW&zP%PO+QOVr|hG9JWE?D3>y3-&@TsZ$cKD!aNgQ$+aWF zH?8d5EW>p#ZM$5akFV<2GX3Btf-1aP7vTh*EOniFbG>4?9NKF5Gsd!`Op)CTJcPIt z9xXInAlzBW;yxaOxjz48Ld??1&UWQzOr~=zY?81q5S&+BD5yWLkf?fM9$?>WvK(KQD&#yQB5Z+kO_j)Yo$qrVbQKn;=X?3wcoxu zAHQj1=rnvvLxF43Pb&35ElhlEpMActS8(!n7XGot7nwqSNMW8*s8THRr00U=V~L(d z?9pMp{h};zx%=+0X#b)*`Us^~lH!?F3HgRMI@-K9U%9+8RH^*5Az6%x4@-8{^#xi|VfI1+Xx(!X%HA<4zi(CY@MXBjP&5G%ht8nZKzaXC@MgmvHQ2Cx$p zUKGds*!LrCox+8eB13WT;lWIT@)dFkf6^*Ro_)q+B~s6#p?qTMbe4ZX#J>Jzv_u9* zA1W6PLk)G5p#QNkJs;n|n-6@Snk=k+aAw;5$XDpgSLHXM>V23;#$PZYXO15TuV2`@ zdgR@;WIqKD?Sffpvx&(_YVc95nE4UMXxIKaCZ?~Os)PKee5)6~bc%?k8Y&!VQPf&6 z%-zzrdimaxDQ+#jpLqQkAA{M6MYj_tDaP4RixE<7PqvQ=1(SNsbG{wo_~T*aWy${dVr@|VX%%j< zbmwth8K0+Yc;%n1OPnq}j30`2rAL(>yC$_O4~^RDQmJ+q_Xr5?zG1kOAUoCRvuHLamPXqm^JEmPzKAdQe4c0 z-D@tET5!@X=(;Nd&Y^TOp{$PZy~tYVgp3juyLj}~@EzPy?~NB>yYf`2fonJH>zcJB zJlWgdcAvW4z%OD3ni=rCtK?TQvUKWQTsPyrrDhNIxHV!49t4--4Zvq4uMWHRi4V1f z`Qxu!PcnsK42S1;9OZT%`SrhIV_olKO|sA@DCb05B)RVP&K0uln)icp9wd6Q_FB%+{#C%!4t>Z&zn?CIGYGZyW3_5?a}$H@}d( zQc==X;@G5DA>;cxvo=I5HHW6rSyYxdBFJZQfmUt>G?8%(IUKS(lI~bxzM2V9c#kLn z0??-wntD&XV0qui-?_SeJ1s~sb$ae%l-SoF5~-)`;5n(b5-}l8J9ankAi?chqdZT1 zm+U|%!+WFqzXwq`lPuoNrZ-r$GwRH9Y!jfx78mp!TrJ0|#!x-BLjhI|(dr0ZRo;rn zzK>N8>6M)1wOC~+>pv!_a-({_TkmGb4Zk-(4kI+(z0TBrmUv`)NP^8U%31Edhx8=X z!=iIwpyXJkOX6m>+0l@D)hF-^VbYq{VRuxMdOc*+oIs-4C3i-sGS$j_g&k+7nZ}K<&;s2Xvgeq6qnQDE!@`UY3uHNhJ5N7u#e-0I$YU0 zVQJ(h{$t!B2l^X>f_8E>N^s0d>bYN)kTOjNzgk3*9}F^gl%!9jHV08`QPH8M#=xaZ540qxWR)y}=Qk{V$&2fWnh8yP}RZ{X1PChzGVPhH2hSBilN0 z44-PaZ-~ALcCR!0PgbZb<`GmV3kqhnWIEVw$XeMkMA({76E{V%WzCkAP9KLr)!4$` z9y(h6PRav^jagn9D8I37Hj^>F+@Tu#jSW_7w8YHu&?$m&L8eRXivRGLummNI6Ur_V zM(TpKhS5Yj%ifr@_Qcb7KV)>N#;4(q6KRP9krkI&8}r8Xv#z3K<}(W-Aple>PAb9W5jmy zc|@RNQsCyuqkQ=z>L=FB(!{S>>Qoptu5k_yjau(-WsbT`Nmte?=5@Xbt5k<_&tksl zo>!^>hP#ciQ~xQ)iB{ry=#l4p*^l4r)!3Kkl!skSJ-;l`!lkx&IN?I{%9kLeTnSP1 z3THGd=F9&52<|oM75etOpipdXb{fV-K_2nuDS zxt~filcCfPLu2(m-|{+2S*wOJtK&z@?9o3xxC5CHwXcTRd{P& zZPnAROiIk}dh4d2DRT9F?dCvv$E=%d`Ntc!wGrsLHPcoU|EbNRGBv#B<{><_R$Lze%brIY2W)v>tu$UNyI`V+F%Yf$?W*-xP>v&tEGFfb#?p%?vz*9P9{J7*pxO#xx!w%sTkd=j za$n>w0|RYa_1>MwKULA0dJeU2Aw@2Ust=r0G{jY)Cfve27CSPqJ+ zfj@S7ylWJ$7!DV6U+xTNseUV8GtbifBB>~HK_Ix9EBH|&wsiH-U8og@#meThGr?&M zS@mq4>R<;M9 zz6Pu3sx}&Lv*)%IC>q@5NE8XwEo0^!L|rYpf^6sfxt5o!<7cqS?4}-cG72JhzrrJT zys#hE?=rEEng9%7Tr|`ITZ1B6(17nQ?^KFF_gJV(o zy5F;1{qt{Y;mrPaIQ62h=e{`*QoWxQ%$&0=m)msgF05D6 zJh$$V)ou8X9Q4d`_c@*R-PaN$B#K3Ttz!osYa!kjRnQfxtoimQO3$w`zNH}l^rB&y znXc#M$-P>u`TAo|?TT#ogGxS$qM~e|BDJu5PynB>R-uVOj3a2NQWW*HL%YVTZDOEr zAWc*aEk>!u5>?p^o|CJ5dt7mDFaUa3miyFvuPDHr*jlxCj_4iEOpLt$R@I zFx50s$kP73abBp1Rjs2(XUF{Vm|jM(US>|-PFzx-Ln5?tdDDz7?`N#d3cT6XOtS-8 zkRz1tSG2%c*cF@L7wkIxJkp70GOky0AQ9%AAT~Uh)|S@!)=6$lA}h`Y?ePYiOEKsN z8P}45_0%oPIRIuB50ld!FPbJ@d-D_u9d{?#kMkq#>q39IO=~<@kG0F!Fd3lF9=4jt zr8_waJ=wFVN&(WZpf4QXSk4rK=|Lsw*O4%iFW{4M`?&P6aK(#>Wt5|}mmBl0o63I? zhM2?z`r;pD`a!BhYA!|$#3Fpzf zj-YIghnLV&X+dchpyuv1h8`!fJg#tH%Ey;i^!C3>hY{RNwfd{ZN^fG1_16k-2actb zCl6utw^FW3ch$}qHU>v(ZpqGX`WuAS)oa!phSprj)3zPI*WdVv`FeDsByW#<2{wkS z%{|cR#8_1JxMrq!_58jhcn;y`QyuvGjcF!#&2$7yOju7l@x0K>L`z^h~^vVgH zq^44hEQ%qT<8osjE-@pl`lPhWlIzo;F8`2rw*}_mcKA=@FG{-RQe7k024h7;*Q&Cl z2j+*%lWW#uVg77Q)s0YI_qFz^5_Hx;*qbpL{;tNlOf;yrz;gfvJ=5zMET|GEYI@;R zU5A|61o*|vR?9aXasy&J-=u@DFB8KRyKl`PLwJ*)yyDuC8t>Aa|HQrh?YK+%jO1RWf?9cM z*a6<^uNLkh)r$u&AYS5|1^qre47(u)D(lZD_s6fxP3CbQimtqp<=M8fg2H*OIHT@c z7?l;tc$|RbO-PLoBn)9bA|w06dF>XKluqG zTs*rtc5QrMWQ_dNgC2aw9tT;v1TsypPbx6nxS;WxbdT>0<2ipx{On$3G8KB&}na;?XZ_jEQ@hHjBk!g zct)KRaetp`u__Y5oXO+vt72HI*p#zYb$_kZB3+Kt;x@k9d2 zia}2pjs(RO`sSN^P)bj>_1%RyuW&a0+jFqYXwUo)oayMhcG7I~(VMGl)rUv^_6>I)2kay)6EI9MJUuthTGcZhFb z_n)-N=8nv@RXRtO24VEectx-UQ4mT5{dOkBFj6bsb^kNz;z#xR5+IH`Dv{+!V>rbw>vQuP5N04omYkuz@DCJ<)D_W) ze5zAv**TWG>o~L{e5+BquUO%)GRJZZGBB^3W%4Gld6b8(pWYIV^BLt)mq_giz5!lz zQ{TFAaP0cdx5^a@S*?9bW2mIYpAmL4TCgx9A9sJUb>k-^JHJ04x>ff0G@)S+vJorx z=J`FX;(NO#MqHEowN}f@G5MOecI1Cv+4!lIdh4^OP$ef1KeX@MXND>2i5G2;igZP_ z`s?o+gR43?~@3ENnD_NtFfRZlrTXUwqN>Whz>JI;ay&#&@Xz9^pP|nca0c+fnw4t@4yI$a&B4;pCtRr7Thj8Uvgtihs)D-M)FRh(HX-;; zmT$qn0Mfp>QK(gF_i(zhzr^kRA>Bko5K+CP)mhN1h4bl~&Bia5JT~>xykU`>M8KId zwmHEWH)cP{5hF3$*IH;|eZF7Xaxk}k5P_~Bj4up5YSerl$D_?1oI|VV6y2{CThBth zFl%)87>KTOO^on3uBH2N)7IAWX1HQMCj0uuI+Hk!3wx$FZ?_2`HjP8>V5uB!BT~FFbvrEqjzrPN27lky~65P?B>sM1qm$BT@Y~AwsaJzwn0oxtR# zr*(I?MpjPl+@FByJ-zPk?iS#8-c)jp_wHt9HGuFHL6Ge9FEEv4x#PRFa1tUzB8=Lk z1_H33sXv7QZHYe}g?ude{nhQ;C{5iVZL5 z;Zypd^{2V+RFKu}CJk4xXY$>|P0p@|^Ut^XhBzO)>e)rYXkTyv&-OcH$G*E5LD6aI;abe|pQ zmR1!IG^6|E-*8*Uf1fGld}ZQ)lPSj28}#|uUc;Fk|5rJaEbGIpU$Hn#pHGTM6q_>8 z%||WY_zUd5D`6=Neg2v%^fh~x1-OmR(Ljjz?-2*QiU$7P%wtkK!sR<2Y>_V5aqK@y zR0fc((R4K+@^%r-{+@0?ouBy!V9r-t(}Xf)FjQ2v=5jD3uJgX+3KKjqcRu~eYpwgt zvY7YPQQW5{Emi{@tG>3oybK&hQC&l{)FR91SNhKWPc*i-i&f(%I`sepdphsl@|v== z?RDj)y07hLd;wUlJ!RmN;{|Q#qdY@C2IK5T!>j~_a0pQ}O;<+UHMS&xM zG#f0w@r?;I-JL8~?Y0P@-JZo|=V+B*`j?+!+i6*eivU?`3jl9V!D-Od_4QexG~gQ@ z?e_92DJ#?ED~nZjsH*R`G1H~ z{|_!&r8k|{zx=6x`785)j7)}Vvc^!R$(5I}AqY+Q>j%)f4z`ejuUO#kmVH+q)v^P_ zC;8Og>I7k@6Wi^1p&UxKt*ehX8VT#mUdcnhHyAdLc1+PPX)k@+?cwha;itWGRZX`A z-KhHWbij){GvLKIO2yqJ71p|!B@B!{Gpl_IM5s?&ssIe*eJK5x{ag$pFE+n#-o4yg zQQijgG5PZ`Kny%XD_P?1rHgM~p8g#NPoW$R!FGSdPAM?G4DeGR@Tk3W&0+i@`tJsu zJP4rniG=l;zM?KWYzXd@_8wcMr`fd|b3N5$X^FXMNtFJJpm@2qkpt{Ph{~mPp?4Dw z1)U*Pcg!X)E!=&gxSEUm>G+w4>77gYCO&&@bEvoV$x1V0F(;4G$@T|{*)a~Kt5Fie ze${oF6>Y9LPofpAodSOkFyQ;rJHXMX_5F6$dPTC-rX$sM*RhoG)=^EH7oZa^w6DNt zjwB>f9PT)suleCEUM5ysGsdgp7+Y1zyY3>*SnEXuAZ7iA-hYmW-rInc#K53Eorb1& z+^2rxHgXJc(3|osS(6FstW#^R%Bnt1*dM*!R(kf;u!;@1G33^GPp3Za*%NN`y~4Ww zJ80%oPGQSnL8D!Y>fw8l`2}xPVh!gm>S|kO<1T#|h}+NfOc!+0rS1?3on@POW7=oD zB2m?kTMLHH6y?p`XJdTmgOb%A4v%w9Ul{Rp;M&r={0EG#xo(R8Q1qOyHLYHMxhgkl zY~j9W8J_c$yv8Vg^5Jb@ioT8JSC_Y8qh*vlUvj;HNDUzy)f;_Z>$!I_wZ9U7BKN_ zISR>|Ad0F_(&$`AJ)%l(!QS$KanTK%RC|vTQG1o1=;ET_fA!^urM~|TeU3SbYC7-+ zQ{6;p_72A#XE<0`1o2u8zHY8v{qp6FFq-(?nYvBdR%wz;t$;WHMy5`#dT(ASfL{K}AYHI!FRSr1uhfQ32^e=~AT& zfgl}1Qv@PJdI=C9(jkE$y$0@%dd~NpJMaDPzQ4JD+{}bgh6(Jo*Iw&+K5fz3wIzTW z&o`HH#wXh4CuAV0l_Fye9!UyeO)o3OrY5^&7xI<6pHCrZx&piR0*NG$M4oe@|LVvgv3 zeK~bUMwcXXv4k2B*xX`t3sGi^esA%E4}MyzQcu@nnOR(ugC=fFA+N~vhOG8)DxW`usWb=p>3n<_ru+xB^a;Cb?2M4`<*J*Y`sy*^iz^@$J!=YgQFohaQqgCEH}}S zR1)@hP$lid>00E7JYhTPG>mc|>>mZa=u0GlCEH1!hV_@H5}tGEEsqEOSvH@xD$55o z)U7darBUVIr6QfUQnK8dxb*}3ZGdJUQ4lTM9BVGsXfrpQ3UJE3*Q7$NF=`xPTF>e) z>RG^X>gDcM)5c*^Br`V1m>{GQWHvNsdw&y(Fl;=Rr6g(C61}C;$|N&O(-{*Ukpr6k z)NSoLTKaK079~;?3kIhxqoc?F(9IpER^mTF9pk54ZAKSE@>a18j-wed%GQ04%Hyh5 zqGpG+aPf%l6mu5A?Zh8HkBv2R?i^1QqWkr0@))aE-2%P3)2n@3c<0`<#@)@?;-tj5 zyH9vd0%ss}=a0-#$NT@#L6CruQL}{&8-Rm{Z+KniuU^6V6EtK zO0r*lu!sp;#1k6W+>$Hg+KE%*`)~aqlnpiIqUZgk4A!68H?B8}=t=7~IGol#gV#;G z^n0$q2N1096L#|Z!Jz#9hn4}Sd6+u0UL5ZxX5&|YeZ#ckT@^lQ28$JY9@cr2+TNDS zVUVi=%IRzp&#=aDU;69uyGyoJDGU~5H|L}4wJ8tY#W9_dWEWInx~?BKjas0cKSLG) z|F|={0u&G>2da&PQcI8e8Wm!z$+mx*?rr;#0tfxe3HEmNO)asqz}`A_*7GZG0`+z; zh-4f?ss?y>$(oFD`ngf-)I|FiBl8yuJXWo3Baz;+Da&XN&E#T(5;iZ6rf?CwLmFc4 zIl=;ORVhTNNS{n8cc^>&53rfZab)2DrJ3DIhi|K1l-P&QO>Duz_4gy@qrSQ^hT zeUzQz_kZ;LW^L&3!+ejRTq37{tb}E}Br=ioB6Jb_AV>~u`Fg~k14KITBcmt8Tbe>T zSj>uM!bb-^1K!{M`FNExgqGy7K3lGR@{E(TtFx8B^fjy0#1#G}RnR`vEq?j~5lxIv zJN4wghS!ysMl*f~d~Lq_Fl3CaV1!?GC5p1~B!{NIC=klVFs0s1XGGo+{DX9$ZUKEf zV~i45=e|FWcTo1#pX{``Xg`BP(ST)><$;!Jb-NGA(&AsmfjzteBb>=r|Rp{Q)+2Zs>L^VL?)-eZD=-l0bIp#{K zblpYTJE{ACl%Np|ue8|rL_jCBY+=Y0Vr1)%P4VxW|MoZ?dEyKWRfOi6;tk=@ZjBh3 zD_i}B-#f|xSdhr=A+qH2)KN$v0=qcxA+w|!+ntiYbSmk)IZS%N2aw$rjofC~#A#uo z?>RG?_2WxR`4YYm=RF)Jx^dM_B_`?d9H+blfD$*|P2G^bI{D9VXYo;LNG_%Z6{c@0 zC}wul7qcLZ+pjm!)>I`c4E~e#vLJl)MNcJzLMBZ&nEQ&Eu-A;{fkzF~k#muezA9YU zMB<$6MsCxTEmYNCDp!ugX^|25rb9GB;oD&AXbot$U#Qwn2{Z;!|M_!fFYU)hrDGyu zf+OOUeTgwhl^7q;Kkv-43CmnJ5`wUA{d$3Eb&y6z3s;b^)Cocf9|q=?4UC$ zx>*60Ea3Kuqq(V8pS`0WzUIlZ)I|TL|LS#()Uv2*_a#GC;cElAozV^-3OMc!MKqE8 zUi`W~k+F5ZAibTNS#V03ZX}i3UPh~P8$)paKMn)ygk$2&N$N{<; zn#d=2m*Oqg7oieSoW$v9GoUQ~PBS2Pr)TzI9)O7#4XnWY9QY^z&^*(vzxa>mCpYtD z=E_dk_^O&NpU0~Lj_PV|4&MrU9oNAZ97-6d@#P4;6xh%#D$a-v7p*=C51pGsZyQSpqY!;{lR)=vYdiOslIQfxwm8MuD~Y+EQY%qLms$vvEY~2CVgpd zoCIJ)#=ES!^RiJLG(R`DoL!tkpS5^&Z9ra>`esEB=J^|`q)u>hb!qJVg1_Eh_wq$Q z5x9d(Ae@FvQrb$=wI-a zsmc7q=N_zFl=d&nh%hy!Q17{_K#|I&W@JQ^m*0I@{XmkD_WHZ}RA-)`Gp)nziC+OG zYodjNM#&rR44zcw4}wbY-}USE$Stb?s3z%Vwh;cdL@b?Tvom>#JUSdC3@(tXY1DB+ z3Q<396-A6*gs1slWqqHQC+7fbxD^1SkWU#jc)@uSJ&tN2i9YF9RNxy}uz;(;!Lu%N zXNVy$HGfobp>KQKSp4YqZP#>Mk4FTiTyo4#poOU0TB@!dG0mCJubsi)&&+6puT{Zw zO`9%B2m5Q{j0MnFN~G1@pu&bFQ=XavXmFyOJj#5KeBD;?aC95pEVz-Kys|g_il^0f zLYRmflywxhzqcZ6v1%X$H`VkS>qiT@O&bZ!tQ{~{-H#%0D3&v}Qu_Wz-LlUEEx9wnT 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

I=#`5+V%WW8 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 int: + parser = argparse.ArgumentParser(prog="poa", description=__doc__) + parser.add_argument("--config", default=str(APP_ROOT / "config")) + parser.add_argument( + "--intake", + action="append", + default=None, + help="path to an intake JSON; repeatable. Default: both required drafts.", + ) + parser.add_argument("--out", default=str(REPO_ROOT / "var" / "poa")) + parser.add_argument("--live", action="store_true", help="drive SuperDocs (needs a key)") + parser.add_argument( + "--sample", type=int, default=None, help="only issue the first N chunk replacements" + ) + parser.add_argument("--actor", default="K. Latha (reviewing paralegal)") + parser.add_argument("--format", default="docx", help="export format for a successful run") + args = parser.parse_args(argv) + + library = Library(Path(args.config)) + paths = [ + Path(p) + for p in (args.intake or [str(APP_ROOT / "intakes" / f"{n}.json") for n in REQUIRED]) + ] + out_dir = Path(args.out) + + drafts = [] + plans = [] + for path in paths: + intake = load_intake(path, library) + draft = assemble(intake, library) + plan = build_plan(draft).sample(args.sample) + drafts.append(draft) + plans.append(plan) + print(render(intake, draft, library, len(plan))) + for written in write_artifacts(out_dir, plan): + print(f" wrote {written}") + print() + print(" EDIT PLAN (one single-target chunk replacement per step, one change per job)") + for step in plan.steps: + print(f" {step.describe()}") + print() + + if len(drafts) == 2: + print(compare(drafts[0], drafts[1], library)) + print() + + if not args.live: + print( + "offline: nothing was sent. The intake, the clause selection, the numbering, the " + "cross-references and the notice check all ran with no key and no network. " + "Add --live to drive SuperDocs." + ) + return 0 + + # Read from .env only, never from a command line and never printed + # (hard rule 1). `override=False` so an already-exported value wins. + try: + from dotenv import load_dotenv + + load_dotenv(REPO_ROOT / ".env", override=False) + except ImportError: + pass + + print(f"live run: key {redacted_key()}") + ledger = DecisionLedger(out_dir / "decisions.jsonl") + failed = 0 + for plan in plans: + client = PoaClient(HttpTransport()) + run_id = f"poa-{plan.draft.intake_id}-{uuid.uuid4().hex[:6]}" + orchestrator = Orchestrator( + client, + ledger, + args.actor, + library, + export_formats=(args.format,), + export_dir=out_dir, + ) + result = orchestrator.run(run_id, plan) + print() + print("-" * 78) + print( + f"{plan.draft.intake_id}: document {result.document_id} in session {result.session_id}" + ) + for line in orchestrator.log: + print(line) + print(result.sentence(ledger)) + if result.reverted: + print(result.reverted) + print(f"exported: {', '.join(result.exported)}" if result.exported else "no export") + # Read, not derived. If no response carried a usage block we say so + # rather than printing our own call count as if it were the balance. + print( + f"billable calls issued: {client.billable_calls}; " + + ( + f"operations balance reported by the product: {client.usage[-1]}" + if client.usage + else "no response carried an operations balance, so we do not know it" + ) + ) + failed += 0 if result.ok else 1 + return 0 if failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/use-cases/preetham1930/poa-generator/poa/assemble.py b/use-cases/preetham1930/poa-generator/poa/assemble.py new file mode 100644 index 000000000..56fccc13e --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/assemble.py @@ -0,0 +1,368 @@ +"""Assembly: the intake picks the clauses, we number them, we render both fills. + +Decision 29, stated as code for this build. Two renderings come out of one +structure: + +- `skeleton` - the **blank form** for this intake's POA type, jurisdiction and + granted categories. Every clause that will exist is already there, at its + final number, with every cross-reference already pointing at the right one, + and both notices already in place. Party names, the specific powers, the + limitation and the commencement are blank-form markers. +- `target` - the same document with the intake's answers filled in. + +The skeleton is what gets uploaded, verbatim. So SuperDocs never inserts a +clause, never renumbers one, and never writes one: every instruction it receives +is "replace this chunk with exactly these bytes", and the bytes came from the +library. An invented clause has no channel to arrive through, which is the +structural argument rather than the checking one - the same shape as Decision 13 +in `system/`. + +Numbering is one pass over the selected clause list. It genuinely differs +between the two required drafts: the healthcare draft carries a clause the +financial one does not (`healthcare-records-access`, selected by +`category granted healthcare`), so every clause after it is numbered one higher +and the cross-reference in the reliance clause moves with it. Nothing about that +is asked of the product. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +from . import style +from .chunks import strip_markup, tidy +from .errors import ( + CrossReferenceError, + ForbiddenPhraseError, + UnresolvedPlaceholderError, +) +from .intake import Intake +from .library import PLACEHOLDER, Clause, Library + +CLAUSE_REFERENCE = re.compile(r"\bclause (\d+)\b") + + +@dataclass +class Block: + """One top-level element, which becomes one chunk on upload.""" + + role: str + clause_id: str + tag: str + skeleton: str + target: str + clause_number: int | None = None + is_notice: bool = False + is_heading: bool = False + + def __post_init__(self) -> None: + self.skeleton = tidy(self.skeleton) + self.target = tidy(self.target) + + @property + def changed(self) -> bool: + return self.skeleton != self.target + + @property + def editable(self) -> bool: + """A heading carries the numbering and a notice carries the guarantee. + Neither is ever the target of an instruction.""" + return not self.is_heading and not self.is_notice + + +@dataclass +class Draft: + intake_id: str + poa_type: str + jurisdiction: str + blocks: list[Block] = field(default_factory=list) + numbering: dict[str, int] = field(default_factory=dict) + granted: dict[str, tuple[str, ...]] = field(default_factory=dict) + receipts: list[str] = field(default_factory=list) + + def html(self, which: str = "target") -> str: + return "\n".join(getattr(b, which) for b in self.blocks) + + def editable_changed(self) -> list[Block]: + return [b for b in self.blocks if b.editable and b.changed] + + def block(self, role: str) -> Block: + return next(b for b in self.blocks if b.role == role) + + @property + def filename(self) -> str: + return f"poa-{self.intake_id}.html" + + +def assemble(intake: Intake, library: Library) -> Draft: + categories = intake.categories(library) + selected = library.select(intake.poa_type, categories) + + numbering = _number(selected) + draft = Draft( + intake_id=intake.intake_id, + poa_type=intake.poa_type, + jurisdiction=intake.jurisdiction, + numbering=numbering, + granted={c: intake.powers_in(library, c) for c in categories}, + ) + + structural = _structural_values(intake, library) + blank = dict(structural) + filled = dict(structural) + for placeholder, text in library.blank_form.items(): + blank[placeholder] = text + filled.update(_instance_values(intake)) + + running: int | None = None + for clause in selected: + if clause.numbered: + number = numbering[clause.clause_id] + running = number + heading = style.wrap("h2", "heading", f"{number}. {clause.heading}") + draft.blocks.append( + Block( + role=f"{clause.clause_id}-heading", + clause_id=clause.clause_id, + tag="h2", + skeleton=heading, + target=heading, + clause_number=number, + is_heading=True, + ) + ) + draft.blocks.append( + Block( + role=clause.clause_id, + clause_id=clause.clause_id, + tag=clause.tag, + skeleton=_render(clause, blank, numbering, library, intake, filled=False), + target=_render(clause, filled, numbering, library, intake, filled=True), + # An unnumbered clause (a list, a description) belongs to the + # numbered clause above it, which is how a reader reads it and + # how the edit plan should describe it. + clause_number=running, + is_notice=clause.style_role == "notice", + ) + ) + + _assert_cross_references_resolve(draft, numbering) + _assert_no_reference_moves_between_the_two_fills(draft) + draft.receipts.append(assert_no_forbidden_phrase(draft, library)) + draft.receipts.append( + f"{len(selected)} clause(s) selected from {len(library.clauses)} in the library by " + f"type={intake.poa_type!r} and categories={list(categories)}; " + f"{len(numbering)} numbered; {len(draft.editable_changed())} chunk(s) differ between " + f"the blank form and the filled draft and become the edit plan" + ) + return draft + + +# -- numbering and cross-references --------------------------------------- + + +def _number(selected: list[Clause]) -> dict[str, int]: + """One pass over the selected clauses, in library order. Nothing is asked.""" + numbering: dict[str, int] = {} + for clause in selected: + if clause.numbered: + numbering[clause.clause_id] = len(numbering) + 1 + return numbering + + +def _assert_cross_references_resolve(draft: Draft, numbering: dict[str, int]) -> None: + numbers = set(numbering.values()) + for block in draft.blocks: + if block.is_heading: + continue + for which in ("skeleton", "target"): + for match in CLAUSE_REFERENCE.finditer(strip_markup(getattr(block, which))): + referenced = int(match.group(1)) + if referenced not in numbers: + raise CrossReferenceError( + f"block {block.role!r} ({which}) points at clause {referenced}, which " + f"this draft does not have. It runs 1..{max(numbers) if numbers else 0}. " + f"Every reference is resolved through the same numbering pass that " + f"numbered the headings, so this is a bug in that pass, not a caveat." + ) + + +def _assert_no_reference_moves_between_the_two_fills(draft: Draft) -> None: + """A cross-reference is structure, so filling in a name may not move one. + + If it did, an edit would be renumbering, and renumbering delegated to the + product is the failure this whole design exists to avoid (measured twice: + the insert vanishes and the renumbers land around it). + """ + for block in draft.blocks: + before = CLAUSE_REFERENCE.findall(strip_markup(block.skeleton)) + after = CLAUSE_REFERENCE.findall(strip_markup(block.target)) + if before != after: + raise CrossReferenceError( + f"block {block.role!r} references clauses {before} in the blank form and " + f"{after} once filled. A cross-reference is structure and is uploaded at its " + f"final value; an edit that moved one would be a renumbering instruction." + ) + + +# -- the legal-adjacency scan --------------------------------------------- + + +def assert_no_forbidden_phrase(draft: Draft, library: Library) -> str: + """INVARIANTS.md A2. Scanned over both fills, because both go to the wire. + + The blank form is the document that is actually uploaded, so a phrase that + only appears there would reach the live document and never be looked at. + """ + checked = 0 + for block in draft.blocks: + for which in ("skeleton", "target"): + text = strip_markup(getattr(block, which)).lower() + checked += 1 + for forbidden in library.forbidden: + if forbidden.phrase in text: + raise ForbiddenPhraseError( + f"block {block.role!r} ({which}) contains {forbidden.phrase!r}: " + f"{forbidden.why}. This is legal-adjacent output and the product has no " + f"standing to say it. The phrase list is config/forbidden-phrases.csv; " + f"if the phrase is genuinely acceptable, that is a data edit and a " + f"decision somebody signs their name to." + ) + return ( + f"scanned {checked} rendering(s) of {len(draft.blocks)} block(s) against " + f"{len(library.forbidden)} forbidden phrase(s): none present" + ) + + +# -- rendering ------------------------------------------------------------- + + +def _structural_values(intake: Intake, library: Library) -> dict[str, str]: + """Values that are identical in the blank form and in the filled draft. + + They are structure or they are the type's own language: which POA type this + is, which jurisdiction, both notices, and every clause number. Uploading + them means the blank form is already a durable-POA-for-this-jurisdiction + form, and no edit ever has to change one. + """ + poa_type = library.types[intake.poa_type] + jurisdiction = library.jurisdictions[intake.jurisdiction] + return { + "poa_type_label": poa_type.label, + "durability_language": poa_type.durability_language, + "jurisdiction_label": jurisdiction.label, + "governing_law": jurisdiction.governing_law, + "witness_requirement": jurisdiction.witness_requirement, + "notarisation_requirement": jurisdiction.notarisation_requirement, + } + + +def _instance_values(intake: Intake) -> dict[str, str]: + return { + "principal_name": intake.principal.name, + "principal_address": intake.principal.address, + "principal_id": intake.principal.identifier, + "agent_name": intake.agent.name, + "agent_address": intake.agent.address, + "successor_name": intake.successor_agent.name, + "successor_address": intake.successor_agent.address, + "limitation": intake.limitation, + "effective_date": intake.effective_date, + "real_property_description": intake.real_property_description, + } + + +def _render( + clause: Clause, + values: dict[str, str], + numbering: dict[str, int], + library: Library, + intake: Intake, + *, + filled: bool, +) -> str: + def substitute(match: re.Match[str]) -> str: + name, argument = match.group(1), match.group(2) + if name == "notice": + return library.notices[argument].text + if name == "clause_ref": + if argument not in numbering: + raise CrossReferenceError( + f"clause {clause.clause_id!r} refers to clause {argument!r}, which this " + f"draft does not select. A reference to a clause that is not in the " + f"document is never rendered as prose and never dropped." + ) + return str(numbering[argument]) + if name == "powers": + return _powers_list(argument, library, intake, values, filled=filled) + if name == "signature_table": + return _signature_table(library, values) + if name not in values: + raise UnresolvedPlaceholderError( + f"clause {clause.clause_id!r} uses placeholder {{{name}}}, which has no " + f"resolver. Known: {sorted(values)}. " + f"An unresolved placeholder is never left in the document and never emptied - " + f"a clause reading 'of , born' is a draft that went out wrong quietly." + ) + return str(values[name]) + + inner = PLACEHOLDER.sub(substitute, clause.body) + if clause.tag == "ul" or clause.tag == "table": + # The list items and the table rows are already complete elements. + return f'<{clause.tag} style="{style.BY_ROLE[clause.style_role]}">{inner}' + return style.wrap(clause.tag, clause.style_role, inner) + + +def _powers_list( + category: str, library: Library, intake: Intake, values: dict, *, filled: bool +) -> str: + if not filled: + return f'
  • {values["powers"]}
  • ' + items = [] + for power_id in intake.powers_in(library, category): + power = library.powers[power_id] + # The whole sentence is the catalogue's, not ours: the label and the + # language are both data, and nothing here writes prose about a power. + items.append( + f'
  • {power.label}. {power.language}
  • ' + ) + return "".join(items) + + +def _signature_table(library: Library, values: dict[str, str]) -> str: + head = "".join( + f'{c.column_label}' for c in library.signature_columns + ) + rows = [f'{head}'] + for row in library.signature_rows: + cells = [] + for column in library.signature_columns: + if column.blank: + # Empty, and it stays empty, in both fills and after every edit. + # INVARIANTS.md A3: this product never implies a signature or a + # notarisation happened, and the cheapest way to never imply it + # is to have nowhere to put one. + cells.append(f'') + elif column.fill == "label": + cells.append(f'{row.role_label}') + else: + cells.append( + f'{_fill_name(row.name_placeholder, values)}' + ) + rows.append(f"{''.join(cells)}") + return "".join(rows) + + +def _fill_name(placeholder: str, values: dict[str, str]) -> str: + def substitute(match: re.Match[str]) -> str: + name = match.group(1) + if name not in values: + raise UnresolvedPlaceholderError( + f"config/signature-rows.csv uses placeholder {{{name}}}, which has no resolver. " + f"Known: {sorted(values)}. A signature block naming nobody is not a blank to be " + f"filled in later; it is a row that lost its party." + ) + return values[name] + + return PLACEHOLDER.sub(substitute, placeholder) diff --git a/use-cases/preetham1930/poa-generator/poa/chunks.py b/use-cases/preetham1930/poa-generator/poa/chunks.py new file mode 100644 index 000000000..dcad6fa5a --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/chunks.py @@ -0,0 +1,207 @@ +"""Chunks: the unit SuperDocs actually addresses, and the unit we verify. + +Measured rather than assumed (PROGRESS.md 2026-08-09): an HTML upload chunks at +the top-level element - every paragraph, every heading, and **the entire table as +one chunk**. Rows and cells carry no id. So the blank form is built to match that +granularity: whatever we intend to replace later is its own top-level element, +so a list of granted powers is one `
      ` and therefore one chunk, and the +signature block is one table and therefore one chunk (Decision 37). + +Copied as an idea from `builds/statutory-statements/` and rebuilt here rather +than imported (Decision 31, and hard rule: a shared idea gets copied). The +divergence is real: this package reads clause headings, not note headings, and +it carries `strip_markup`, which the notice gate needs and the statutory build +has no use for. + +`canonical()` is what "the expected bytes" means in practice. A document editor +is entitled to re-serialise whitespace, attribute order and character entities, +and treating that as a failure would make the verifier cry wolf on every step. +Everything that carries meaning - the tag sequence, every attribute value, and +every character of text including every digit - is compared exactly. The chunk +id is dropped because it is the address, not the content. + +The entity and void-element rules were measured, not assumed: we uploaded +`·` and got back the character it stands for, and we uploaded `
      ` and +got back `
      `. The first live run +stopped on it, which is the verbatim check doing exactly its job; the honest +resolution is to define "verbatim" at the level of content rather than of bytes, +and to say so here. +""" + +from __future__ import annotations + +import html as _html +import re +from dataclasses import dataclass +from html.parser import HTMLParser + +CHUNK_ID = re.compile(r'\sdata-chunk-id="[^"]*"') +CLAUSE_HEADING = re.compile(r"^(?P\d+)\.\s+(?P.+)$") +MARKUP = re.compile(r"<[^>]+>") +TAG = re.compile(r"<(/?)([a-zA-Z0-9]+)((?:\s+[a-zA-Z0-9:-]+\s*=\s*\"[^\"]*\")*)\s*(/?)>") +ATTR = re.compile(r"([a-zA-Z0-9:-]+)\s*=\s*\"([^\"]*)\"") +WS = re.compile(r"\s+") +TOP_LEVEL = ("h1", "h2", "h3", "h4", "p", "table", "ul", "ol", "div", "section", "blockquote") +VOID = {"br", "meta", "hr", "img", "link", "input", "col"} +# HTML parsers insert these whether or not the source had them. Measured: we +# uploaded a table with no <tbody> and it came back wrapped in one. +IMPLICIT = {"tbody", "thead", "tfoot"} + + +def canonical(fragment: str) -> str: + """Whitespace-collapsed, attribute-sorted, chunk-id-free form of an HTML fragment.""" + out: list[str] = [] + cursor = 0 + for match in TAG.finditer(fragment): + text = fragment[cursor : match.start()] + if text: + out.append(WS.sub(" ", _html.unescape(text))) + closing, tag, attrs, selfclose = match.groups() + pairs = sorted( + (k.lower(), WS.sub(" ", v).strip()) + for k, v in ATTR.findall(attrs) + if k.lower() != "data-chunk-id" + ) + if tag.lower() in IMPLICIT: + cursor = match.end() + continue + rendered = "".join(f' {k}="{v}"' for k, v in pairs) + # `<br>` and `<br/>` are the same element; the product re-serialises the + # one we send as the other, and that is punctuation, not content. + marker = "/" if selfclose and tag.lower() not in VOID else "" + out.append(f"<{closing}{tag.lower()}{rendered}{marker}>") + cursor = match.end() + tail = fragment[cursor:] + if tail: + out.append(WS.sub(" ", _html.unescape(tail))) + return WS.sub(" ", "".join(out)).replace("> <", "><").strip() + + +def strip_markup(fragment: str) -> str: + """The text a reader sees: tags removed, entities resolved, spaces collapsed. + + The notice check compares against this rather than against raw HTML, because + a document editor is entitled to re-serialise an entity and that is + punctuation, not content - the same argument `canonical()` makes one level + up. What it may not do is change a word, and that is what this preserves. + """ + return WS.sub(" ", _html.unescape(MARKUP.sub(" ", fragment))).strip() + + +@dataclass(frozen=True) +class Chunk: + chunk_id: str + tag: str + html: str + + @property + def canonical(self) -> str: + return canonical(self.html) + + +class _Scanner(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=False) + self.spans: list[tuple[str, str, int, int]] = [] + self._depth = 0 + self._tag: str | None = None + self._start = 0 + self._chunk_id = "" + self._text = "" + + def _offset(self) -> int: + line, col = self.getpos() + return self._line_start[line - 1] + col + + def feed_text(self, text: str) -> None: + self._text = text + self._line_start = [0] + offset = 0 + for line in text.splitlines(keepends=True): + offset += len(line) + self._line_start.append(offset) + self.feed(text) + self.close() + + def handle_starttag(self, tag: str, attrs) -> None: + if tag in VOID: + return + if self._tag is None: + if tag in TOP_LEVEL: + self._tag = tag + self._depth = 1 + self._start = self._offset() + self._chunk_id = dict(attrs).get("data-chunk-id") or "" + return + if tag == self._tag: + self._depth += 1 + + def handle_endtag(self, tag: str) -> None: + if self._tag is None or tag != self._tag: + return + self._depth -= 1 + if self._depth == 0: + end = self._offset() + len(f"</{tag}>") + self.spans.append((self._chunk_id, self._tag, self._start, end)) + self._tag = None + + +class ChunkMap: + """An ordered map of chunk id -> chunk, as the document currently stands.""" + + def __init__(self, html: str) -> None: + scanner = _Scanner() + scanner.feed_text(html) + self.chunks: list[Chunk] = [ + Chunk(chunk_id, tag, html[start:end]) for chunk_id, tag, start, end in scanner.spans + ] + self.by_id = {c.chunk_id: c for c in self.chunks if c.chunk_id} + + def __len__(self) -> int: + return len(self.chunks) + + @property + def ids(self) -> list[str]: + return [c.chunk_id for c in self.chunks] + + def headings(self) -> dict[int, str]: + """Clause number -> heading text, read back from the document itself. + + Read from the live document rather than from our own tree, because the + point of reading it back is to find out whether the numbering we + computed is the numbering the document actually carries. + """ + found: dict[int, str] = {} + for chunk in self.chunks: + if chunk.tag != "h2": + continue + text = strip_markup(chunk.html).strip() + match = CLAUSE_HEADING.match(text) + if match: + found[int(match.group("number"))] = text + return found + + def text_of(self, chunk_id: str) -> str: + return strip_markup(self.by_id[chunk_id].html).strip() + + def text(self) -> str: + """The whole document as text, whitespace collapsed. What a reader reads.""" + return strip_markup(" ".join(c.html for c in self.chunks)) + + +def strip_chunk_ids(html: str) -> str: + return CHUNK_ID.sub("", html) + + +def tidy(fragment: str) -> str: + """Drop the whitespace an HTML serialiser drops, before we ever send it. + + Measured: we sent `<br>\nPrepared in accordance...` and the edited chunk came + back as `<br>Prepared in accordance...`. Rather than keep widening + `canonical()` until it forgives everything, the document we compute is + written the way a serialiser would write it, so there is less for the + comparison to forgive. Whitespace *inside* a line is left alone - that is + where the meaningful spaces between words are. + """ + fragment = re.sub(r">[ \t]*\n[ \t]*", ">", fragment) + return re.sub(r"[ \t]*\n[ \t]*<", "<", fragment) diff --git a/use-cases/preetham1930/poa-generator/poa/editplan.py b/use-cases/preetham1930/poa-generator/poa/editplan.py new file mode 100644 index 000000000..5b879461d --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/editplan.py @@ -0,0 +1,146 @@ +"""The edit plan: one single-target chunk replacement per step, and nothing else. + +Every step is a `replace`. There is no other verb (Decision 28), no step targets +a heading (Decision 29 put the clause numbers in before upload), no step targets +a **notice** (INVARIANTS.md B4), and no step carries more than one chunk +(Decision 35). All four are checked here, before anything is sent, because a plan +that breaks a wire rule should never reach the wire. + +The expected post-state is the whole chunk's HTML, not the value inside it +(Decision 37). That is what we compare on read-back and it is also what we send: +the instruction hands SuperDocs the finished clause rather than describing an +edit to make. Which matters more here than it did for a set of accounts - a +described edit is an invitation to draft, and drafting is what produced four +invented sections on 2026-08-09. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .assemble import Block, Draft +from .chunks import ChunkMap, canonical +from .errors import PlanRefusedError + +# Ordered so that a halt loses the least verified work. Measured over Phase 4's +# live runs: a whole-table chunk replacement is the one unit the product does not +# apply reliably. A <ul> has not been measured, so it is treated as being between +# the two. This changes nothing about what is sent or checked; it changes which +# failure a reviewer is left holding (Decision 43, carried over). +TAG_RISK = {"p": 0, "ul": 1, "table": 2} + + +@dataclass +class Step: + index: int + role: str + tag: str + clause_number: int | None + expected_html: str # what the chunk must hold afterwards (no chunk id) + was_html: str # what it holds before + chunk_id: str = "" # filled in once the document is uploaded and read back + + @property + def verb(self) -> str: + return "replace" + + def instruction(self) -> str: + """One instruction, one chunk, fully resolved. Nothing is left to plan.""" + return ( + f"Replace the entire contents of the chunk whose data-chunk-id is " + f"{self.chunk_id} with exactly the following HTML, character for character. " + f"The chunk must remain a single <{self.tag}> element and keep the same " + f"data-chunk-id; do not wrap it in a <div> or in any other element. " + f"Do not add any clause. Do not change any heading. Do not reword anything. " + f"Do not touch any other chunk anywhere in the document." + f"\n\n{self.expected_html}" + ) + + def describe(self) -> str: + where = f"clause {self.clause_number}" if self.clause_number else "front matter" + return f"[{self.index:02d}] replace {self.tag:5s} {self.role:28s} ({where})" + + +@dataclass +class Plan: + steps: list[Step] + draft: Draft + + def __len__(self) -> int: + return len(self.steps) + + def sample(self, limit: int | None) -> Plan: + if limit is None or limit >= len(self.steps): + return self + return Plan(self.steps[:limit], self.draft) + + +def build_plan(draft: Draft) -> Plan: + blocks = sorted(draft.editable_changed(), key=lambda b: TAG_RISK.get(b.tag, 1)) + steps = [ + Step( + index=index, + role=block.role, + tag=block.tag, + clause_number=block.clause_number, + expected_html=block.target, + was_html=block.skeleton, + ) + for index, block in enumerate(blocks, start=1) + ] + plan = Plan(steps, draft) + assert_plan_is_legal(plan, draft) + return plan + + +def assert_plan_is_legal(plan: Plan, draft: Draft) -> None: + by_role: dict[str, Block] = {b.role: b for b in draft.blocks} + for step in plan.steps: + block = by_role.get(step.role) + if step.verb != "replace": + raise PlanRefusedError( + f"step {step.index} uses the verb {step.verb!r}. There is one verb and it is " + f"replace (Decision 28): the only operation that inserts is also the one " + f"measured to fabricate clauses nobody asked for, and an invented clause in a " + f"power of attorney is a power nobody granted." + ) + if block is not None and block.is_notice: + raise PlanRefusedError( + f"step {step.index} targets {step.role!r}, which is a notice. The counsel-review " + f"notice and the external-execution notice are uploaded verbatim and are never " + f"edited: an instruction that touches one is an instruction that could " + f"paraphrase it (INVARIANTS.md B4)." + ) + if (block is not None and block.is_heading) or step.tag in ("h1", "h2", "h3"): + raise PlanRefusedError( + f"step {step.index} targets {step.role!r}, which is a heading. Headings carry " + f"the clause numbering and are uploaded at their final values (Decision 29); " + f"there is no renumbering step to get wrong." + ) + if step.expected_html == step.was_html: + raise PlanRefusedError( + f"step {step.index} ({step.role}) would send a chunk that is already correct. " + f"An edit that changes nothing cannot be verified by read-back and reads as " + f"'not applied'." + ) + + +def bind_chunk_ids(plan: Plan, uploaded: ChunkMap) -> None: + """Match each step to the chunk it will address, by the bytes we uploaded. + + Matched on canonical content rather than on position, so a document that came + back re-ordered fails here rather than quietly editing the wrong clause. + """ + by_canonical: dict[str, list[str]] = {} + for chunk in uploaded.chunks: + by_canonical.setdefault(chunk.canonical, []).append(chunk.chunk_id) + for step in plan.steps: + key = canonical(step.was_html) + candidates = by_canonical.get(key, []) + if len(candidates) != 1: + raise PlanRefusedError( + f"step {step.index} ({step.role}) matches {len(candidates)} uploaded chunk(s). " + f"A single-target edit needs exactly one target; {len(uploaded)} chunks came " + f"back. Content we uploaded:\n {step.was_html[:200]}" + ) + step.chunk_id = candidates[0] diff --git a/use-cases/preetham1930/poa-generator/poa/errors.py b/use-cases/preetham1930/poa-generator/poa/errors.py new file mode 100644 index 000000000..a256c3920 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/errors.py @@ -0,0 +1,90 @@ +"""Every failure this app can have, and the message it must carry. + +Hard rule 13's argument, applied to a document: a bare failure tells the +reviewer something is wrong but not what to do about it. Every error below names +what could not be done and where we looked. + +Non-ASCII characters are kept out of these messages on purpose: they are read in +a terminal, and Windows consoles mangled exactly the message that carried Phase +1's thesis (PROGRESS.md 2026-08-08, Assumption 19). +""" + +from __future__ import annotations + + +class PoaError(Exception): + """Base for everything raised by this package.""" + + +# -- the intake ------------------------------------------------------------ + + +class UnknownPowerError(PoaError): + """The intake names a power id the catalogue does not have.""" + + +class UnknownPoaTypeError(PoaError): + """The intake names a POA type the library does not define.""" + + +class UnknownJurisdictionError(PoaError): + """The intake names a jurisdiction the library does not define.""" + + +class IncompleteIntakeError(PoaError): + """The intake cannot support a draft - a limited POA with no stated limit, + a missing successor agent, a party with no name.""" + + +# -- the library ----------------------------------------------------------- + + +class UnknownConditionError(PoaError): + """A clause condition could not be parsed. Never read as 'does not apply'.""" + + +class UnresolvedPlaceholderError(PoaError): + """A clause placeholder has no resolver. Never left in and never emptied.""" + + +class LibraryRefusedError(PoaError): + """A library row is not fit to be used - a jurisdiction row that asserts an + execution requirement instead of deferring it, a duplicate clause id.""" + + +class CrossReferenceError(PoaError): + """A clause reference does not resolve to a clause the draft has.""" + + +# -- the legal-adjacency guards ------------------------------------------- + + +class ForbiddenPhraseError(PoaError): + """The draft says something this product has no standing to say.""" + + +class NoticeMissingError(PoaError): + """A required notice is not present in the document.""" + + +class NoticeAlteredError(PoaError): + """A required notice is present but not verbatim.""" + + +class ExportRefusedError(PoaError): + """An export was attempted without a receipt from the notice gate.""" + + +# -- the wire -------------------------------------------------------------- + + +class PlanRefusedError(PoaError): + """An edit plan step broke one of the wire rules before it was sent.""" + + +class DecisionAlreadyRecordedError(PoaError): + """A second decision on an item that already has one, without supersede.""" + + +class NotConfiguredError(PoaError): + """A live call was attempted with no SUPERDOCS_API_KEY set.""" diff --git a/use-cases/preetham1930/poa-generator/poa/intake.py b/use-cases/preetham1930/poa-generator/poa/intake.py new file mode 100644 index 000000000..e7250922e --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/intake.py @@ -0,0 +1,188 @@ +"""The guided intake: who, which type, which powers. + +Non-interactive by construction. An intake is a JSON file, so a test and a demo +drive exactly the same code path a person filling the form would - there is no +second, easier route into the assembler that only the tests use. + +The intake is the **only** channel by which a power reaches the document. That +is the structural half of INVARIANTS.md A6, and it is the same argument as +Decision 13 in `system/`: the defence against a fabricated power is not a check +on the output, it is that there is no path for one to arrive through. The +assembler takes power ids, resolves them against the catalogue, and has no way +to accept a power the catalogue does not hold. + +Every refusal below is a hard error naming what is missing. A limited power of +attorney with no stated limit is not drafted around, and a missing successor +agent is not quietly omitted - the card names the successor-agent clause, and a +clause that is sometimes there is worse than one that is always there. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path + +from .errors import ( + IncompleteIntakeError, + UnknownJurisdictionError, + UnknownPoaTypeError, + UnknownPowerError, +) +from .library import Library + +REQUIRED_PARTIES = ("principal", "agent", "successor_agent") + + +@dataclass(frozen=True) +class Party: + name: str + address: str + identifier: str = "" + + +@dataclass(frozen=True) +class Intake: + intake_id: str + poa_type: str + jurisdiction: str + principal: Party + agent: Party + successor_agent: Party + granted_powers: tuple[str, ...] + effective_date: str + limitation: str = "" + real_property_description: str = "" + + def categories(self, library: Library) -> tuple[str, ...]: + """The granted categories, in the library's own category order. + + Ordered by the catalogue rather than by the order the ids happened to be + typed in, so two intakes that grant the same powers produce the same + document whatever order the form was filled in. + """ + granted = {library.powers[p].category for p in self.granted_powers} + return tuple(c for c in library.category_order if c in granted) + + def powers_in(self, library: Library, category: str) -> tuple[str, ...]: + return tuple( + p + for p in library.power_order + if p in self.granted_powers and library.powers[p].category == category + ) + + +def load_intake(path: Path, library: Library) -> Intake: + raw = json.loads(path.read_text(encoding="utf-8")) + intake = Intake( + intake_id=str(raw.get("intake_id") or path.stem), + poa_type=str(raw.get("poa_type") or ""), + jurisdiction=str(raw.get("jurisdiction") or ""), + principal=_party(raw, "principal", path), + agent=_party(raw, "agent", path), + successor_agent=_party(raw, "successor_agent", path), + granted_powers=tuple(raw.get("granted_powers") or ()), + effective_date=str(raw.get("effective_date") or "").strip(), + limitation=str(raw.get("limitation") or "").strip(), + real_property_description=str(raw.get("real_property_description") or "").strip(), + ) + validate(intake, library, source=path) + return intake + + +def _party(raw: dict, key: str, path: Path) -> Party: + block = raw.get(key) + if not isinstance(block, dict): + raise IncompleteIntakeError( + f"the intake at {path} has no '{key}' block. A power of attorney names three " + f"parties - {', '.join(REQUIRED_PARTIES)} - and this build refuses to draft one " + f"that names fewer. The successor-agent clause is not optional." + ) + return Party( + name=str(block.get("name") or "").strip(), + address=str(block.get("address") or "").strip(), + identifier=str(block.get("identifier") or "").strip(), + ) + + +def validate(intake: Intake, library: Library, source: Path | None = None) -> None: + where = f" (from {source})" if source else "" + + if intake.poa_type not in library.types: + raise UnknownPoaTypeError( + f"the intake{where} asks for POA type {intake.poa_type!r}. The library defines " + f"{sorted(library.types)}. A type this build does not know is never approximated to " + f"the nearest one it does: the general/limited/durable distinction is the thing the " + f"card says people need to get right." + ) + if intake.jurisdiction not in library.jurisdictions: + raise UnknownJurisdictionError( + f"the intake{where} names jurisdiction {intake.jurisdiction!r}. The library defines " + f"{sorted(library.jurisdictions)}. Adding one is a row in config/jurisdictions.csv, " + f"not a code change." + ) + + for key in REQUIRED_PARTIES: + party: Party = getattr(intake, key) + if not party.name or not party.address: + raise IncompleteIntakeError( + f"the intake{where} gives the {key.replace('_', ' ')} " + f"name={party.name!r} address={party.address!r}. Both are required: an " + f"instrument that cannot say who it appoints is not a draft, it is a gap." + ) + + if not intake.granted_powers: + raise IncompleteIntakeError( + f"the intake{where} grants no powers. A power of attorney granting nothing is not a " + f"shorter document, it is a wrong one." + ) + unknown = [p for p in intake.granted_powers if p not in library.powers] + if unknown: + raise UnknownPowerError( + f"the intake{where} grants {unknown}, which the catalogue does not have. The " + f"catalogue is config/powers.csv and holds {len(library.powers)} power(s) across " + f"{list(library.category_order)}. An unknown power is never dropped and never " + f"approximated - a power nobody granted is the exact failure this build is built " + f"against." + ) + duplicated = sorted({p for p in intake.granted_powers if intake.granted_powers.count(p) > 1}) + if duplicated: + raise IncompleteIntakeError( + f"the intake{where} lists {duplicated} more than once. A power granted twice would " + f"be stated twice in the instrument." + ) + + if not intake.effective_date: + raise IncompleteIntakeError( + f"the intake{where} does not say when the authority commences. A power of attorney " + f"with no commencement is a question, not a draft." + ) + + poa_type = library.types[intake.poa_type] + if poa_type.requires_limitation and not intake.limitation: + raise IncompleteIntakeError( + f"the intake{where} asks for {poa_type.label} and states no limitation. " + f"config/poa-types.csv marks this type requires_limitation=yes. A limited power of " + f"attorney whose limit is blank is a general one wearing the wrong heading, which is " + f"the single most consequential thing this build could get wrong." + ) + if not poa_type.requires_limitation and intake.limitation: + raise IncompleteIntakeError( + f"the intake{where} states a limitation but asks for {poa_type.label}, which carries " + f"no clause to put it in. The limitation would be silently dropped, and a granted " + f"power that was meant to be limited and is not is the worst direction to fail in." + ) + + real_property = "real_property" + grants_real_property = real_property in intake.categories(library) + if grants_real_property and not intake.real_property_description: + raise IncompleteIntakeError( + f"the intake{where} grants real property powers and describes no property. The " + f"clause that names the property has nothing to name, and a real property power over " + f"an unnamed property is unbounded." + ) + if not grants_real_property and intake.real_property_description: + raise IncompleteIntakeError( + f"the intake{where} describes a property but grants no real property power, so the " + f"description has no clause to appear in and would be silently dropped." + ) diff --git a/use-cases/preetham1930/poa-generator/poa/library.py b/use-cases/preetham1930/poa-generator/poa/library.py new file mode 100644 index 000000000..105b276dc --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/library.py @@ -0,0 +1,383 @@ +"""The clause library, and it is data. + +Hard constraint of this build: adding a jurisdiction or a power type is a **data +edit**. So every clause, every power, every POA type, every jurisdiction, both +notices, the forbidden-phrase list and the signature block's rows live in +`config/*.csv`, and there is a test that greps this package's own `.py` files +and fails if any of that text appears as a string literal. + +Two loud failures where the tempting default is a quiet one, both copied as an +argument from `system/`'s Decision 17 and rebuilt here: + +- an **unparseable clause condition** raises. A silent `False` would drop a + clause out of a power of attorney, and a missing clause has no symptom - the + document reads perfectly well without it. +- an **unresolved placeholder** raises. It is never left in the text and never + emptied. `of , born` is a document that went out wrong quietly. + +The condition grammar is small on purpose: + + condition := "always" + | "type is" <type_id> + | "type in" <type_id>[|<type_id>...] + | "category granted" <category> +""" + +from __future__ import annotations + +import csv +import re +from dataclasses import dataclass +from pathlib import Path + +from .errors import LibraryRefusedError, UnknownConditionError + +GRAMMAR = "always | type is <type_id> | type in <type_id>|<type_id> | category granted <category>" +PLACEHOLDER = re.compile(r"\{([a-z_]+)(?::([a-z_-]+))?\}") + + +@dataclass(frozen=True) +class Clause: + clause_id: str + sort_order: int + section: str + heading: str + tag: str + style_role: str + applies_when: str + body: str + + @property + def numbered(self) -> bool: + return bool(self.heading) + + +@dataclass(frozen=True) +class Power: + power_id: str + category: str + label: str + language: str + + +@dataclass(frozen=True) +class PoaType: + type_id: str + label: str + requires_limitation: bool + durability_language: str + + +@dataclass(frozen=True) +class Jurisdiction: + jurisdiction_id: str + label: str + governing_law: str + witness_requirement: str + notarisation_requirement: str + + +@dataclass(frozen=True) +class Notice: + notice_id: str + label: str + text: str + + +@dataclass(frozen=True) +class ForbiddenPhrase: + phrase: str + why: str + + +@dataclass(frozen=True) +class SignatureRow: + role_key: str + role_label: str + name_placeholder: str + + +@dataclass(frozen=True) +class SignatureColumn: + column_key: str + column_label: str + fill: str # label | placeholder | blank + + @property + def blank(self) -> bool: + return self.fill == "blank" + + +class Library: + """Everything the draft is made of, loaded from `config/`.""" + + def __init__(self, config_root: Path) -> None: + self.root = config_root + self.clauses = _clauses(config_root / "clause-library.csv") + self.powers = _powers(config_root / "powers.csv") + self.types = _types(config_root / "poa-types.csv") + self.notices = _notices(config_root / "notices.csv") + self.forbidden = _forbidden(config_root / "forbidden-phrases.csv") + self.signature_rows = _signature_rows(config_root / "signature-rows.csv") + self.signature_columns = _signature_columns(config_root / "signature-table.csv") + self.blank_form = _blank_form(config_root / "blank-form.csv") + self.deferral_markers = _markers(config_root / "deferral-markers.txt") + self.jurisdictions = _jurisdictions( + config_root / "jurisdictions.csv", self.deferral_markers + ) + self.power_order = tuple(self.powers) + seen: list[str] = [] + for power in self.powers.values(): + if power.category not in seen: + seen.append(power.category) + self.category_order = tuple(seen) + self._assert_clause_ids_resolve() + + # -- selection --------------------------------------------------------- + + def applies(self, clause: Clause, poa_type: str, categories: tuple[str, ...]) -> bool: + """Evaluate a clause condition. Never returns False because it did not + understand - that is what the raise is for.""" + condition = clause.applies_when.strip() + if condition == "always": + return True + if condition.startswith("type is "): + return poa_type == condition[len("type is ") :].strip() + if condition.startswith("type in "): + allowed = [t.strip() for t in condition[len("type in ") :].split("|")] + return poa_type in allowed + if condition.startswith("category granted "): + return condition[len("category granted ") :].strip() in categories + raise UnknownConditionError( + f"clause {clause.clause_id!r} in {self.root / 'clause-library.csv'} has condition " + f"{condition!r}, which this grammar cannot parse. The grammar is: {GRAMMAR}. " + f"An unparseable condition is never read as 'this clause does not apply': a clause " + f"silently dropped out of a power of attorney has no symptom in the finished " + f"document." + ) + + def select(self, poa_type: str, categories: tuple[str, ...]) -> list[Clause]: + return [c for c in self.clauses if self.applies(c, poa_type, categories)] + + # -- integrity --------------------------------------------------------- + + def _assert_clause_ids_resolve(self) -> None: + known = {c.clause_id for c in self.clauses} + for clause in self.clauses: + for name, argument in PLACEHOLDER.findall(clause.body): + if name == "clause_ref" and argument not in known: + raise LibraryRefusedError( + f"clause {clause.clause_id!r} refers to clause {argument!r}, which the " + f"library does not define. A cross-reference that cannot resolve in the " + f"library can never resolve in a draft." + ) + if name == "notice" and argument not in self.notices: + raise LibraryRefusedError( + f"clause {clause.clause_id!r} refers to notice {argument!r}; the library " + f"defines {sorted(self.notices)}." + ) + if name == "powers" and argument not in self.category_order: + raise LibraryRefusedError( + f"clause {clause.clause_id!r} refers to power category {argument!r}; the " + f"catalogue defines {list(self.category_order)}." + ) + + +# -- loaders --------------------------------------------------------------- + + +def _rows(path: Path) -> list[dict[str, str]]: + """Read a config CSV, refusing any row that does not match the header. + + `csv.DictReader` is quietly forgiving: a row with more fields than the + header puts the surplus in a `None` key, and a row with fewer leaves `None` + values. Both are how an unquoted comma inside a clause turns into a document + that is missing half a sentence and says nothing about it. Neither is + tolerated here - this is the library the whole draft is made of. + """ + out: list[dict[str, str]] = [] + with path.open(encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle) + for number, row in enumerate(reader, start=2): + surplus = row.pop(None, None) + if surplus: + raise LibraryRefusedError( + f"{path} line {number} has more fields than the header " + f"{reader.fieldnames}. The surplus is {surplus}, which almost always means " + f"a comma inside an unquoted field - and a clause silently cut in half is " + f"a document that goes out wrong quietly." + ) + missing = sorted(k for k, v in row.items() if v is None) + if missing: + raise LibraryRefusedError( + f"{path} line {number} has no value for {missing}. An empty column is " + f"written as an empty field, never omitted." + ) + out.append({k: v.strip() for k, v in row.items()}) + return out + + +def _clauses(path: Path) -> tuple[Clause, ...]: + clauses = [ + Clause( + clause_id=row["clause_id"], + sort_order=int(row["sort_order"]), + section=row["section"], + heading=row["heading"], + tag=row["tag"], + style_role=row["style_role"], + applies_when=row["applies_when"], + body=row["body"], + ) + for row in _rows(path) + ] + ids = [c.clause_id for c in clauses] + duplicated = sorted({i for i in ids if ids.count(i) > 1}) + if duplicated: + raise LibraryRefusedError( + f"{path} defines {duplicated} more than once. A clause id is how a cross-reference " + f"finds its target, so a duplicate makes a reference ambiguous." + ) + orders = [c.sort_order for c in clauses] + if len(set(orders)) != len(orders): + raise LibraryRefusedError( + f"{path} has two clauses at the same sort_order, so the clause order - and therefore " + f"the clause numbering - depends on how the file happened to be read." + ) + return tuple(sorted(clauses, key=lambda c: c.sort_order)) + + +def _powers(path: Path) -> dict[str, Power]: + out: dict[str, Power] = {} + for row in _rows(path): + if row["power_id"] in out: + raise LibraryRefusedError(f"{path} defines power {row['power_id']!r} twice") + out[row["power_id"]] = Power( + power_id=row["power_id"], + category=row["category"], + label=row["label"], + language=row["language"], + ) + return out + + +def _types(path: Path) -> dict[str, PoaType]: + out: dict[str, PoaType] = {} + for row in _rows(path): + requires = row["requires_limitation"].lower() + if requires not in ("yes", "no"): + raise LibraryRefusedError( + f"{path}: type {row['type_id']!r} has requires_limitation={requires!r}; " + f"the column takes 'yes' or 'no' and nothing else, because a value this loader " + f"guessed at would decide whether a limited POA can be drafted with no limit." + ) + out[row["type_id"]] = PoaType( + type_id=row["type_id"], + label=row["label"], + requires_limitation=requires == "yes", + durability_language=row["durability_language"], + ) + return out + + +def _jurisdictions(path: Path, markers: tuple[str, ...]) -> dict[str, Jurisdiction]: + out: dict[str, Jurisdiction] = {} + for row in _rows(path): + for column in ("witness_requirement", "notarisation_requirement"): + text = row[column].lower() + if not any(marker in text for marker in markers): + raise LibraryRefusedError( + f"{path}: jurisdiction {row['jurisdiction_id']!r} states {column} as fact:\n" + f" {row[column]}\n" + f"This build is not qualified to say what a jurisdiction requires " + f"(INVARIANTS.md A4), so the text must defer - it must contain one of " + f"{list(markers)}, from config/deferral-markers.txt. The row is refused at " + f"load time rather than softened at render time, because a softened " + f"assertion still reaches the reader." + ) + out[row["jurisdiction_id"]] = Jurisdiction( + jurisdiction_id=row["jurisdiction_id"], + label=row["label"], + governing_law=row["governing_law"], + witness_requirement=row["witness_requirement"], + notarisation_requirement=row["notarisation_requirement"], + ) + return out + + +def _notices(path: Path) -> dict[str, Notice]: + out: dict[str, Notice] = {} + for row in _rows(path): + if not row["text"]: + raise LibraryRefusedError(f"{path}: notice {row['notice_id']!r} has empty text") + out[row["notice_id"]] = Notice( + notice_id=row["notice_id"], label=row["label"], text=row["text"] + ) + return out + + +def _forbidden(path: Path) -> tuple[ForbiddenPhrase, ...]: + return tuple( + ForbiddenPhrase(phrase=row["phrase"].lower(), why=row["why"]) for row in _rows(path) + ) + + +def _signature_rows(path: Path) -> tuple[SignatureRow, ...]: + return tuple( + SignatureRow( + role_key=row["role_key"], + role_label=row["role_label"], + name_placeholder=row["name_placeholder"], + ) + for row in _rows(path) + ) + + +def _signature_columns(path: Path) -> tuple[SignatureColumn, ...]: + columns = tuple( + SignatureColumn( + column_key=row["column_key"], column_label=row["column_label"], fill=row["fill"] + ) + for row in _rows(path) + ) + allowed = {"label", "placeholder", "blank"} + unknown = sorted({c.fill for c in columns} - allowed) + if unknown: + raise LibraryRefusedError( + f"{path} uses fill {unknown}; the column takes {sorted(allowed)}. A fill this loader " + f"guessed at would decide whether a signature cell is emitted empty." + ) + if not any(c.blank for c in columns): + raise LibraryRefusedError( + f"{path} marks no column blank, so the signature block would be emitted with " + f"nothing left to sign. INVARIANTS.md A3: this product never implies that signing " + f"happened." + ) + return columns + + +def _blank_form(path: Path) -> dict[str, str]: + out: dict[str, str] = {} + for row in _rows(path): + if not row["blank_text"]: + raise LibraryRefusedError( + f"{path}: placeholder {row['placeholder']!r} has empty blank text. A blank form " + f"whose gaps are invisible reads as a finished document." + ) + out[row["placeholder"]] = row["blank_text"] + return out + + +def _markers(path: Path) -> tuple[str, ...]: + lines = [ + line.strip().lower() + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + if not lines: + raise LibraryRefusedError( + f"{path} lists no deferral markers, so every jurisdiction row would be refused. " + f"An empty guard list is a guard that cannot be satisfied, not one that is switched " + f"off." + ) + return tuple(lines) diff --git a/use-cases/preetham1930/poa-generator/poa/notices.py b/use-cases/preetham1930/poa-generator/poa/notices.py new file mode 100644 index 000000000..22e394b1c --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/notices.py @@ -0,0 +1,161 @@ +"""The export gate. This is hard constraint 1 of the card, and it is structural. + +> The document is NOT exportable until read-back confirms both the +> counsel-review notice and the external-notarisation statement are present +> VERBATIM. + +The tempting implementation is a check the export path calls. That is one +`if` away from being forgotten, and forgetting it produces exactly the artefact +this build exists to prevent: a power of attorney, in a reviewer's Downloads +folder, with no notice on it and nothing to say the notice ever went missing. + +So the check is not a call, it is a **key**. `PoaClient.export` requires a +`NoticeReceipt`. A `NoticeReceipt` cannot be constructed except by +`check_notices()`, which takes a `ChunkMap` from a live read-back and raises if +either notice is missing or altered. There is no default argument, no +`receipt=None` path, and no second constructor. A caller who wants to export +without checking has to change this file, and changing this file is a decision +somebody makes on purpose. + +That is the same argument as Decision 13 and Decision 15 in `system/`: the +strong half of a defence is the absence of a path, not a check on the output. + +**What "verbatim" means here.** The comparison is against the reader-visible +text (tags removed, entities resolved, runs of whitespace collapsed), because a +document editor is entitled to re-serialise an entity or a line break and that +is punctuation, not content - the measured relaxation Decision 38 already +defines one level up. What it may not do is change, drop or reorder a **word**, +and that is exactly what this preserves. One word paraphrased fails, which is +the case that matters: a rewrite plausibly paraphrases. +""" + +from __future__ import annotations + +import difflib +from dataclasses import dataclass, field + +from .chunks import ChunkMap, strip_markup +from .errors import NoticeAlteredError, NoticeMissingError +from .library import Library + +# The one token that lets a NoticeReceipt be constructed. Module-private and +# never exported: `from .notices import *` cannot reach it, and a caller +# elsewhere in the package that tries to build a receipt by hand gets a +# ValueError naming this file. +_GATE = object() + + +@dataclass(frozen=True) +class NoticeReceipt: + """Proof that both notices were read back verbatim, moments ago. + + Only `check_notices()` can make one. It is required by `export()`, so the + export path cannot be reached without having done the read-back. + """ + + document_id: str + chunk_count: int + notices: tuple[str, ...] + detail: tuple[str, ...] = field(default_factory=tuple) + _token: object = None + + def __post_init__(self) -> None: + if self._token is not _GATE: + raise ValueError( + "a NoticeReceipt is issued by check_notices() against a live read-back and by " + "nothing else. Constructing one directly would mean exporting a power of " + "attorney whose notices nobody looked at - which is the single thing this " + "module exists to make impossible. See builds/poa-generator/INVARIANTS.md B1." + ) + + def receipt(self) -> str: + return ( + f"notice gate: {len(self.notices)} required notice(s) found verbatim in " + f"{self.chunk_count} chunk(s) read back from document {self.document_id}; " + + "; ".join(self.detail) + ) + + +def check_notices(chunks: ChunkMap, library: Library, document_id: str = "") -> NoticeReceipt: + """Raise unless every notice in the library is present, verbatim, in `chunks`. + + Two independent comparisons, on purpose: + + 1. the notice text appears in the document's reader-visible text; and + 2. some single chunk's own text **is** the notice, exactly. + + (1) alone would pass a document that had split a notice across two + paragraphs and inserted a sentence between them. (2) alone would pass a + document that had kept the notice chunk and appended a second, contradicting + one - so the caller pairs this with the collateral-damage check, which sees + any chunk that was added. + """ + if not library.notices: + raise NoticeMissingError( + "config/notices.csv defines no notices, so this gate would pass on any document at " + "all. An empty guard list is a guard that cannot fail, which is worse than none." + ) + + document_text = chunks.text() + detail: list[str] = [] + for notice in library.notices.values(): + wanted = _normalise(notice.text) + if wanted not in _normalise(document_text): + closest = _closest_chunk(chunks, wanted) + if closest is None: + raise NoticeMissingError( + f"the {notice.label} ({notice.notice_id}) is not in the document. " + f"{len(chunks)} chunk(s) were read back from document " + f"{document_id or '(unnamed)'} and none of them carries it, in whole or in " + f"part. No export is produced. The notice is:\n {notice.text}" + ) + raise NoticeAlteredError( + f"the {notice.label} ({notice.notice_id}) is present but NOT verbatim. A " + f"paraphrased notice is a missing notice: the wording is the guarantee.\n" + f" expected: {notice.text}\n" + f" found : {closest}\n" + f"{_diff(wanted, _normalise(closest))}\n" + f"No export is produced." + ) + exact = [c for c in chunks.chunks if _normalise(strip_markup(c.html)) == wanted] + if not exact: + raise NoticeAlteredError( + f"the {notice.label} ({notice.notice_id}) appears in the document's text but no " + f"single chunk holds it on its own. A notice spread across chunks can have a " + f"sentence inserted into the middle of it and still pass a substring test, so " + f"this build requires the notice to be one chunk, as it was uploaded. " + f"No export is produced." + ) + detail.append(f"{notice.notice_id} verbatim in chunk {exact[0].chunk_id or '(no id)'}") + + return NoticeReceipt( + document_id=document_id, + chunk_count=len(chunks), + notices=tuple(library.notices), + detail=tuple(detail), + _token=_GATE, + ) + + +def _normalise(text: str) -> str: + return " ".join(strip_markup(text).split()) + + +def _closest_chunk(chunks: ChunkMap, wanted: str) -> str | None: + """The chunk that looks most like the notice, so the error can show a diff.""" + best, score = None, 0.0 + for chunk in chunks.chunks: + text = _normalise(strip_markup(chunk.html)) + ratio = difflib.SequenceMatcher(None, wanted, text).ratio() + if ratio > score: + best, score = text, ratio + return best if score >= 0.5 else None + + +def _diff(expected: str, actual: str) -> str: + lines = list( + difflib.unified_diff( + expected.split(), actual.split(), "expected", "found", lineterm="", n=2 + ) + ) + return "\n".join(f" {line}" for line in lines[:20]) diff --git a/use-cases/preetham1930/poa-generator/poa/orchestrator.py b/use-cases/preetham1930/poa-generator/poa/orchestrator.py new file mode 100644 index 000000000..dfc4ece57 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/orchestrator.py @@ -0,0 +1,297 @@ +"""The run: upload the blank form once, then one verified chunk replacement at a time. + +The shape of a step, copied from Phase 4 and unchanged, because nothing about it +was wrong: + + compute the expected post-state of the WHOLE document + -> one instruction, one chunk, on the async route + -> poll to the gate + -> refuse anything that is not exactly our one change + -> write OUR decision row, with an actor + -> approve (the actuator) + -> poll to completed + -> read the whole document back + -> classify: ok | not applied | applied wrong | collateral damage + +On any failure the queue **halts**. No downstream step runs, because a downstream +step is the consequence of work that may not have happened. + +What is new here is the end of the run. A clean run does **not** export. It reads +the document back one more time, puts it through the notice gate, and exports +only if the gate issues a receipt. Two independent refusals therefore stand +between a failure and a file on disk: the halt, and the gate. The gate is the +one that also fires on a run where every single edit succeeded and the product +quietly paraphrased a notice along the way - which is the case the halt cannot +catch, because from the edit queue's point of view nothing went wrong. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from pathlib import Path + +from .chunks import ChunkMap +from .editplan import Plan, bind_chunk_ids +from .errors import NoticeAlteredError, NoticeMissingError +from .library import Library +from .notices import check_notices +from .superdocs.client import PoaClient, select_our_change +from .superdocs.decisions import DecisionLedger +from .superdocs.verifier import Verification, verify, verify_upload_verbatim + + +@dataclass +class StepOutcome: + index: int + role: str + verification: Verification + retried: bool = False + + @property + def ok(self) -> bool: + return self.verification.ok + + +@dataclass +class RunResult: + run_id: str + intake_id: str = "" + document_id: str = "" + session_id: str = "" + planned: int = 0 + outcomes: list[StepOutcome] = field(default_factory=list) + upload_receipt: str = "" + notice_receipt: str = "" + notice_refusal: str = "" + halted_at: int | None = None + exported: list[str] = field(default_factory=list) + reverted: str = "" + + @property + def applied(self) -> int: + return sum(1 for o in self.outcomes if o.ok) + + @property + def ok(self) -> bool: + return ( + self.halted_at is None + and self.applied == self.planned + and bool(self.notice_receipt) + and not self.notice_refusal + ) + + def sentence(self, ledger: DecisionLedger) -> str: + """Computed from the outcomes and the rows, at the moment it is emitted.""" + if self.halted_at is not None: + return ( + f"HALTED at step {self.halted_at}. {self.applied} of {self.planned} planned " + f"change(s) verified applied; the rest were not attempted. No export was " + f"produced. {ledger.describe(self.run_id, self.planned)}." + ) + if self.notice_refusal: + return ( + f"{self.applied} of {self.planned} planned change(s) verified applied, and the " + f"document is NOT exportable: {self.notice_refusal} No export was produced. " + f"{ledger.describe(self.run_id, self.planned)}." + ) + return ( + f"{self.applied} of {self.planned} planned change(s) verified applied by reading " + f"the whole document back after each one, and both notices were then read back " + f"verbatim before anything was exported. " + f"{ledger.describe(self.run_id, self.planned)}." + ) + + +class Orchestrator: + def __init__( + self, + client: PoaClient, + ledger: DecisionLedger, + actor: str, + library: Library, + *, + sleep=time.sleep, + export_formats: tuple[str, ...] = ("docx",), + export_dir: Path | None = None, + ) -> None: + self.client = client + self.ledger = ledger + self.actor = actor + self.library = library + self.sleep = sleep + self.export_formats = export_formats + self.export_dir = export_dir + self.log: list[str] = [] + + def _say(self, line: str) -> None: + self.log.append(line) + + def run(self, run_id: str, plan: Plan) -> RunResult: + result = RunResult(run_id=run_id, intake_id=plan.draft.intake_id, planned=len(plan)) + blank_blocks = [b.skeleton for b in plan.draft.blocks] + + document_id, session_id = self.client.upload_verbatim( + plan.draft.filename, plan.draft.html("skeleton") + ) + result.document_id, result.session_id = document_id, session_id + uploaded, _ = self.client.read_back(document_id) + result.upload_receipt = verify_upload_verbatim(uploaded, blank_blocks) + self._say(result.upload_receipt) + + # The gate runs on the blank form too. If the notices did not survive the + # upload there is nothing to be gained by editing the document first, and + # a great deal to be lost by finding out at the end. + self._gate(uploaded, document_id, result, when="after the upload") + if result.notice_refusal: + return result + self._say(result.notice_receipt) + + bind_chunk_ids(plan, uploaded) + + before = uploaded + last_good = before + for step in plan.steps: + outcome = self._one_step(run_id, step, before, session_id, document_id) + result.outcomes.append(outcome) + if not outcome.ok: + result.halted_at = step.index + self._say(outcome.verification.report()) + self._say( + "halting: no downstream step runs on the consequences of an unconfirmed one" + ) + result.reverted = self._revert(session_id, document_id, last_good, result.applied) + return result + self._say(outcome.verification.report()) + before, _ = self.client.read_back(document_id) + last_good = before + + # Nothing is exported on the strength of the loop above having finished. + final, _ = self.client.read_back(document_id) + receipt = self._gate(final, document_id, result, when="before the export") + if receipt is None: + return result + self._say(result.notice_receipt) + for fmt in self.export_formats: + written = self.client.export( + session_id, fmt, receipt, self.export_dir, stem=f"poa-{plan.draft.intake_id}" + ) + result.exported.append(f"{fmt} -> {written}" if written else fmt) + return result + + def _gate(self, chunks: ChunkMap, document_id: str, result: RunResult, *, when: str): + """The notice gate. A refusal is recorded and reported, never smoothed over.""" + try: + receipt = check_notices(chunks, self.library, document_id) + except (NoticeMissingError, NoticeAlteredError) as exc: + result.notice_refusal = str(exc).splitlines()[0] + self._say(f"NOTICE GATE REFUSED {when}:\n{exc}") + self._say("no export was produced, and no export path was reached") + return None + result.notice_receipt = f"{receipt.receipt()} ({when})" + return receipt + + def _one_step( + self, run_id: str, step, before: ChunkMap, session_id: str, document_id: str + ) -> StepOutcome: + verification = self._attempt(run_id, step, before, session_id, document_id) + if verification.ok: + return StepOutcome(step.index, step.role, verification) + # One narrow retry of the same instruction. Never a re-plan: re-planning + # is the measured degradation mode. Collateral damage is not retried at + # all - the document already holds content nobody asked for. + if any(p.kind == "collateral_damage" for p in verification.problems): + return StepOutcome(step.index, step.role, verification) + self._say(f"step {step.index}: one narrow retry, same instruction, no re-plan") + # Compared against the last verified-good state, not against whatever the + # failed attempt left behind (Decision 42): the wreckage is not a baseline. + retried = self._attempt(run_id, step, before, session_id, document_id, supersede=True) + return StepOutcome(step.index, step.role, retried, retried=True) + + def _attempt( + self, + run_id: str, + step, + before: ChunkMap, + session_id: str, + document_id: str, + supersede: bool = False, + ) -> Verification: + job = self.client.chat_gated(session_id, document_id, step.instruction()) + if not job.awaiting: + return Verification( + step.index, + step.chunk_id, + problems=[ + _problem( + "not_applied", + f"the job reached status {job.status!r} without ever offering a change " + f"to approve. Nothing was decided and nothing is claimed.", + ) + ], + ) + change, unasked = select_our_change(job, step.chunk_id) + # Our row first, then the actuator (Decision 33). + self.ledger.record( + run_id, + step.index, + step.chunk_id, + "accept", + self.actor, + f"replacement for {step.role} matches the clause computed from the library", + supersede=supersede, + ) + self.client.approve(session_id, job.job_id, change.change_id) + for extra in unasked: + # Denied bare: no feedback, ever (Decision 34). + self.ledger.record( + run_id, + step.index, + extra.chunk_id, + "reject", + self.actor, + "not in the computed plan; the product proposed it unasked", + supersede=True, + ) + self.client.deny(session_id, job.job_id, extra.change_id) + self._say( + f"step {step.index}: denied an unasked change on chunk {extra.chunk_id} " + f"(bare, no feedback)" + ) + settled = self.client.poll(job.job_id, until_terminal=True, sleep=self.sleep) + del settled # the job's own account of itself is not evidence + after, _ = self.client.read_back(document_id) + return verify(step.index, step.chunk_id, before, after, step.expected_html) + + def _revert(self, session_id: str, document_id: str, last_good: ChunkMap, applied: int) -> str: + """Revert to the last verified-good turn, then read back and say what happened.""" + try: + self.client.revert(session_id, turn_index=applied) + except Exception as exc: + return f"revert call failed: {exc}. The document is left as it is and NOT exported." + after, _ = self.client.read_back(document_id) + same = [c.canonical for c in after.chunks] == [c.canonical for c in last_good.chunks] + if same: + return "reverted to the last verified-good state, confirmed by read-back" + return ( + "revert was requested and the read-back does NOT match the last verified-good " + f"state ({len(after)} chunk(s) now, {len(last_good)} then). The document is left " + f"as it is, no export was produced, and this is reported rather than smoothed over." + ) + + +def _problem(kind: str, detail: str): + from .superdocs.verifier import Problem + + return Problem(kind, detail) + + +def write_artifacts(out_dir: Path, plan: Plan) -> list[Path]: + """The computed document, both shapes, on disk - so a reviewer can read them.""" + out_dir.mkdir(parents=True, exist_ok=True) + written = [] + for which in ("skeleton", "target"): + path = out_dir / f"poa-{plan.draft.intake_id}-{which}.html" + path.write_text(plan.draft.html(which), encoding="utf-8") + written.append(path) + return written diff --git a/use-cases/preetham1930/poa-generator/poa/report.py b/use-cases/preetham1930/poa-generator/poa/report.py new file mode 100644 index 000000000..e065a0249 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/report.py @@ -0,0 +1,105 @@ +"""What the intake produced, printed so a person can check it without a browser. + +The report states what was selected and what was refused. It never states that +the document is valid, executed, or fit for any purpose - the two notices say +what this product's output is, and the report does not contradict them. +""" + +from __future__ import annotations + +from .assemble import Draft +from .intake import Intake +from .library import Library + + +def render(intake: Intake, draft: Draft, library: Library, planned: int) -> str: + lines: list[str] = [] + poa_type = library.types[intake.poa_type] + jurisdiction = library.jurisdictions[intake.jurisdiction] + + lines.append("=" * 78) + lines.append(f"INTAKE {intake.intake_id}") + lines.append("=" * 78) + lines.append(f" type {poa_type.label} ({poa_type.type_id})") + lines.append(f" jurisdiction {jurisdiction.label} ({jurisdiction.jurisdiction_id})") + lines.append(f" principal {intake.principal.name}") + lines.append(f" agent {intake.agent.name}") + lines.append(f" successor {intake.successor_agent.name}") + lines.append(f" commences {intake.effective_date}") + if intake.limitation: + lines.append(f" limited to {intake.limitation}") + if intake.real_property_description: + lines.append(f" property {intake.real_property_description}") + + lines.append("") + lines.append("POWERS GRANTED (selected in the intake, worded by config/powers.csv)") + for category in intake.categories(library): + granted = intake.powers_in(library, category) + withheld = [ + p + for p, power in library.powers.items() + if power.category == category and p not in granted + ] + lines.append(f" {category}: {len(granted)} granted, {len(withheld)} withheld") + for power_id in granted: + lines.append(f" + {power_id:18s} {library.powers[power_id].label}") + for power_id in withheld: + lines.append(f" - {power_id:18s} {library.powers[power_id].label} (not granted)") + ungranted = [c for c in library.category_order if c not in intake.categories(library)] + for category in ungranted: + lines.append(f" {category}: no power granted, so no clause for it is in the draft") + + lines.append("") + lines.append("CLAUSES (numbered by one pass over the selected set; nothing is asked)") + for block in draft.blocks: + if block.is_heading: + lines.append(f" {block.clause_number:2d}. {block.role[: -len('-heading')]}") + + lines.append("") + lines.append("NOTICES (uploaded verbatim, never an edit target, re-read before any export)") + for notice in library.notices.values(): + present = notice.text in draft.html("skeleton") and notice.text in draft.html("target") + lines.append(f" [{'x' if present else ' '}] {notice.notice_id}: {notice.label}") + + lines.append("") + for receipt in draft.receipts: + lines.append(f" {receipt}") + lines.append(f" {planned} chunk replacement(s) planned") + return "\n".join(lines) + + +def compare(left: Draft, right: Draft, library: Library) -> str: + """The grading line, printed: how the two drafts differ in granted powers. + + Computed from the two drafts' own rendered text, not from the intakes that + produced them, because the claim being made is about the documents. + """ + lines = ["=" * 78, f"COMPARISON {left.intake_id} vs {right.intake_id}", "=" * 78] + for category in library.category_order: + in_left = left.granted.get(category, ()) + in_right = right.granted.get(category, ()) + lines.append( + f" {category:15s} {left.intake_id}: {len(in_left):2d} " + f"{right.intake_id}: {len(in_right):2d}" + ) + lines.append("") + for draft, other in ((left, right), (right, left)): + # Compared on the powers' own language, not on the category counts, so + # this says something about the documents rather than about the intakes. + strays = [ + library.powers[power_id].label + for powers in other.granted.values() + for power_id in powers + if library.powers[power_id].language in draft.html("target") + ] + lines.append( + f" {draft.intake_id}: {len(strays)} power(s) from {other.intake_id} present " + f"{'(THIS IS A FAILURE)' if strays else '- correct'}" + ) + both = [ + n.notice_id + for n in library.notices.values() + if n.text in left.html("target") and n.text in right.html("target") + ] + lines.append(f" both drafts carry, verbatim: {both}") + return "\n".join(lines) diff --git a/use-cases/preetham1930/poa-generator/poa/style.py b/use-cases/preetham1930/poa-generator/poa/style.py new file mode 100644 index 000000000..9e298ce83 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/style.py @@ -0,0 +1,48 @@ +"""Inline CSS, because that is what measurably survives. + +Measured on 2026-08-09 and again over Phase 4's live runs: legacy presentational +attributes (`border="1"`) are dropped by the product, while inline `style` +attributes survive an upload, a targeted replace and a DOCX export with every +value intact. So everything this build renders is styled inline. + +This module holds **presentation only**. No clause text, no power label, no +notice wording and no jurisdiction name appears here - those are data +(`config/*.csv`), and `test_library.py` fails the build if any of them turns up +as a literal in a `.py` file in this package. +""" + +from __future__ import annotations + +RULE = "1px solid #1f3864" + +TITLE = "font-size:20px;letter-spacing:1px;text-align:center;margin:0 0 12px 0" +BODY = "margin:0 0 10px 0;line-height:1.45" +HEADING = "font-size:14px;margin:16px 0 6px 0" +LIST = "margin:0 0 10px 0;padding-left:22px;line-height:1.45" +ITEM = "margin:0 0 6px 0" +# The notices are the safety-critical content, so they are also the content a +# reader cannot skim past: boxed, ruled, and not the same colour as the clauses. +NOTICE = ( + f"border:{RULE};background-color:#eaeef7;padding:10px;margin:0 0 12px 0;" + "font-style:italic;line-height:1.4" +) +TABLE = f"border-collapse:collapse;width:100%;border:{RULE}" +HEAD_ROW = "background-color:#1f3864;color:#ffffff" +TH = f"border:{RULE};padding:6px;text-align:left" +TD = f"border:{RULE};padding:6px" +# Signature and date cells are empty and stay empty (INVARIANTS.md A3). The +# height is what makes them look like something to write on, on paper, later. +TD_BLANK = f"border:{RULE};padding:6px;height:34px" + +BY_ROLE = { + "title": TITLE, + "body": BODY, + "heading": HEADING, + "list": LIST, + "notice": NOTICE, + "signature": TABLE, +} + + +def wrap(tag: str, style_role: str, inner: str) -> str: + return f'<{tag} style="{BY_ROLE[style_role]}">{inner}</{tag}>' diff --git a/use-cases/preetham1930/poa-generator/poa/superdocs/__init__.py b/use-cases/preetham1930/poa-generator/poa/superdocs/__init__.py new file mode 100644 index 000000000..0b70d2df2 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/superdocs/__init__.py @@ -0,0 +1,8 @@ +"""The SuperDocs surface: transport, client, decision ledger, verifier. + +Copied from `builds/statutory-statements/statutory/superdocs/` and rebuilt +(Decision 31). The one divergence is the export gate - `PoaClient.export` +requires a `NoticeReceipt` - and it is the reason this build exists. +""" + +from __future__ import annotations diff --git a/use-cases/preetham1930/poa-generator/poa/superdocs/client.py b/use-cases/preetham1930/poa-generator/poa/superdocs/client.py new file mode 100644 index 000000000..4a21888fd --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/superdocs/client.py @@ -0,0 +1,329 @@ +"""The SuperDocs client, with the wire rules enforced in the client itself. + +Four measured decisions live here rather than in a caller's discipline: + +- **Decision 32** - gated changes go to `/v1/chat/async`. The gate takes a + `job_id` and jobs exist only on the async route; passing `approval_mode` to + the synchronous route leaves nothing to approve against. `chat_gated` is the + only method that can produce a pending change and it cannot reach `/v1/chat`. +- **Decision 35** - one change per job, on the sending side. The answering side + does not always agree: the first live run of this build asked for one chunk by + id and got a two-change batch back. So we approve exactly the change we + computed and deny every other one bare, and the read-back is what makes that + safe rather than the batch size. +- **Decision 34** - a denial carries no feedback. `deny()` takes no feedback + parameter, so there is no call shape in which one can be sent. Feedback + triggers a revision pass, and the revision pass proposed edits to two chunks + nobody targeted, aimed at the section headings. +- **Decision 33** - `approve` is an actuator, not a record. This client returns + what the API returned and claims nothing about what was decided; the decision + ledger is written by the caller, before the call. + +Nothing here believes a reply. `read_back()` is the only method whose result is +evidence, and it is the only one the verifier consults. + +Copied from `builds/statutory-statements/` and rebuilt (Decision 31). One thing +diverges, and it is this build's whole point: `export()` will not run without a +`NoticeReceipt`, which only `poa.notices.check_notices()` can issue and only +against a live read-back. See INVARIANTS.md B1. +""" + +from __future__ import annotations + +import base64 +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from ..chunks import ChunkMap +from ..errors import ExportRefusedError, PlanRefusedError +from ..notices import NoticeReceipt +from .transport import Transport + +TERMINAL = {"completed", "failed", "cancelled", "error"} + + +@dataclass +class PendingChange: + change_id: str + chunk_id: str + operation: str + old_html: str + new_html: str + + +@dataclass +class Job: + job_id: str + status: str + progress: int + pending: list[PendingChange] + raw: dict[str, Any] + + @property + def awaiting(self) -> bool: + return self.status == "awaiting_approval" + + @property + def finished(self) -> bool: + return self.status in TERMINAL + + +class PoaClient: + def __init__(self, transport: Transport, *, poll_seconds: float = 3.0, max_polls: int = 80): + self.transport = transport + self.poll_seconds = poll_seconds + self.max_polls = max_polls + self.billable_calls = 0 + self.export_content_types: list[str] = [] + self.export_receipts: list[str] = [] + self.usage: list[dict[str, Any]] = [] + + # -- documents --------------------------------------------------------- + + def upload_verbatim(self, filename: str, html: str) -> tuple[str, str]: + """Upload our computed skeleton. Returns (document_id, session_id). + + Verbatim is the load-bearing property (Decision 29) and it is checked by + the caller against a read-back, never assumed from this response. + """ + # Three calls, and the third is the one that makes the other two + # checkable. An upload with no session is not persisted, and the upload + # response carries no document id at all - so the id comes from the + # session's own roster, which is a read rather than a claim. + session = self.transport.request("POST", "/v1/sessions/init", json_body={}) + session_id = session.get("session_id") + if not session_id: + raise RuntimeError(f"sessions/init returned no session_id; keys {sorted(session)}") + payload = { + "filename": filename, + "file_base64": base64.b64encode(html.encode("utf-8")).decode("ascii"), + "session_id": session_id, + } + # One retry, because a 503 with an empty body was observed on a 23 KB + # upload and succeeded immediately on the next attempt. A retried POST + # could leave two documents in the session, so the roster below is + # required to hold exactly one - the retry is checked, not trusted. + try: + uploaded = self.transport.request( + "POST", "/v1/documents/upload-base64", json_body=payload + ) + except RuntimeError as exc: + if "HTTP 5" not in str(exc): + raise + uploaded = self.transport.request( + "POST", "/v1/documents/upload-base64", json_body=payload + ) + if not uploaded.get("persisted"): + raise RuntimeError( + f"the upload reports persisted={uploaded.get('persisted')!r}. An unpersisted " + f"document cannot be read back, and a read-back is the only evidence there is." + ) + roster = self.transport.request("GET", f"/v1/sessions/{session_id}/documents") + documents = roster.get("documents") or [] + if len(documents) > 1: + raise RuntimeError( + f"the session holds {len(documents)} documents and this build works on one. " + f"If an upload was retried after a 5xx, both may have landed; nothing is edited " + f"until a human has looked." + ) + durable = next( + (d.get("durable_document_id") for d in documents if d.get("durable_document_id")), None + ) + if not durable: + raise RuntimeError( + f"the session roster carries no durable document id; {len(documents)} " + f"document(s), keys {sorted(documents[0]) if documents else '(none)'}. " + f"Without one there is nothing to verify against." + ) + return durable, session_id + + def read_back( + self, document_id: str, *, attempts: int = 4, sleep=time.sleep + ) -> tuple[ChunkMap, dict[str, Any]]: + """`GET /v1/documents/{id}?include_html=true` - the only evidence there is. + + Retried on a 404 because the durable record is not always queryable the + instant the upload returns: one run 404'd immediately and the same call + returned 200 on the next attempt. A read is safe to repeat; the retry is + bounded and a persistent 404 stops the run rather than being smoothed + over into "nothing to verify". + """ + body: dict[str, Any] = {} + for attempt in range(attempts): + try: + body = self.transport.request( + "GET", f"/v1/documents/{document_id}", params={"include_html": "true"} + ) + break + except RuntimeError as exc: + if "HTTP 404" not in str(exc) or attempt == attempts - 1: + raise + sleep(2.0 * (attempt + 1)) + html = body.get("html") + if not html: + raise RuntimeError( + f"read-back for {document_id} carried no html. A verification step that " + f"cannot see the document verifies nothing." + ) + return ChunkMap(html), body + + def export( + self, + session_id: str, + fmt: str, + receipt: NoticeReceipt, + out_dir: Path | None = None, + stem: str = "poa", + ) -> Path | None: + """The export answers with a file, not with JSON, so it gets its own path. + + `receipt` is positional and has no default. It is the gate: a + `NoticeReceipt` exists only where `check_notices()` has just passed + against a read-back of this document, so there is no call shape in which + an unchecked document is exported. Removing the parameter is the only + way to export without checking, and that is a diff somebody signs. + + Written to disk when `out_dir` is given, because an export nobody can + open is not evidence of anything. + """ + if not isinstance(receipt, NoticeReceipt): + raise ExportRefusedError( + f"export was called with receipt={type(receipt).__name__}. It takes a " + f"NoticeReceipt, and a NoticeReceipt is only issued by check_notices() against " + f"a live read-back (INVARIANTS.md B1)." + ) + self.export_receipts.append(receipt.receipt()) + content_type, blob = self.transport.download( + "POST", "/v1/documents/export", json_body={"session_id": session_id, "format": fmt} + ) + self.export_content_types.append(content_type) + if out_dir is None or not blob: + return None + out_dir.mkdir(parents=True, exist_ok=True) + path = out_dir / f"{stem}.{fmt}" + path.write_bytes(blob) + return path + + def revert(self, session_id: str, turn_index: int) -> dict[str, Any]: + return self.transport.request( + "POST", f"/v1/sessions/{session_id}/revert", json_body={"turn_index": turn_index} + ) + + # -- the gated edit loop ---------------------------------------------- + + def chat_gated(self, session_id: str, document_id: str, instruction: str) -> Job: + """One instruction, on the async route, held at the gate (Decisions 32 and 35). + + `document_id` is not sent: the session holds exactly one document and it + is focused, and the durable id we verify against is not the id the chat + surface uses for it. Naming the wrong one is worse than naming none. + """ + del document_id + body = self.transport.request( + "POST", + "/v1/chat/async", + json_body={ + "session_id": session_id, + "message": instruction, + "approval_mode": "ask_every_time", + "response_mode": "compact", + }, + ) + self.billable_calls += 1 + # The promo operations balance is not on any free endpoint - `whoami` + # reports a different bucket entirely (PROGRESS.md 2026-08-09, + # Assumption 40) - so it is captured here whenever a response carries + # one, rather than derived from our own call count afterwards. If it is + # absent this stays empty, which is the honest outcome: we do not know. + usage = body.get("usage") or (body.get("result") or {}).get("usage") + if usage: + self.usage.append(usage) + job_id = body.get("job_id") + if not job_id: + raise RuntimeError(f"chat/async returned no job_id; keys {sorted(body)}") + return self.poll(job_id) + + def poll(self, job_id: str, *, until_terminal: bool = False, sleep=time.sleep) -> Job: + for _ in range(self.max_polls): + body = self.transport.request("GET", f"/v1/jobs/{job_id}") + usage = (body.get("result") or {}).get("usage") if isinstance(body, dict) else None + if usage: + self.usage.append(usage) + job = _job_from(body) + if job.finished or (job.awaiting and not until_terminal): + return job + sleep(self.poll_seconds) + raise TimeoutError( + f"job {job_id} did not settle in {self.max_polls} polls. Nothing is reported as " + f"applied on a timeout; the document is read back and the run halts." + ) + + def approve(self, session_id: str, job_id: str, change_id: str) -> dict[str, Any]: + return self.transport.request( + "POST", + f"/v1/chat/{session_id}/approve", + json_body={"job_id": job_id, "change_id": change_id, "approved": True}, + ) + + def deny(self, session_id: str, job_id: str, change_id: str) -> dict[str, Any]: + """Bare. There is no feedback parameter, because feedback re-plans (Decision 34).""" + return self.transport.request( + "POST", + f"/v1/chat/{session_id}/approve", + json_body={"job_id": job_id, "change_id": change_id, "approved": False}, + ) + + +def select_our_change( + job: Job, expected_chunk_id: str +) -> tuple[PendingChange, list[PendingChange]]: + """Split a batch into the one change we asked for and everything else. + + We send one chunk per job (Decision 35) and the product does not always + answer with one. Measured on the first live run of this build: an + instruction naming a single chunk id came back as a **two**-change batch, + the second on a chunk nobody targeted - the same shape as the unprompted + heading edits recorded on 2026-08-09. + + So the rule is not "the batch must be one"; it is **we approve exactly the + change we computed and deny every other one, bare** (Decision 34). The + read-back afterwards is what makes that safe: if a denial did not hold, the + non-target chunk moved and the verifier calls it collateral damage. + """ + ours = next((c for c in job.pending if c.chunk_id == expected_chunk_id), None) + if ours is None: + raise PlanRefusedError( + f"job {job.job_id} proposes {len(job.pending)} change(s), none of them on chunk " + f"{expected_chunk_id}, which is the one the step targets. Chunks proposed: " + f"{[c.chunk_id for c in job.pending]}. Every one is denied; nothing is approved on " + f"the strength of a proposal we did not ask for." + ) + if ours.operation != "edit": + raise PlanRefusedError( + f"job {job.job_id} proposes operation {ours.operation!r} on our target. There is " + f"one verb and it is replace (Decision 28)." + ) + return ours, [c for c in job.pending if c is not ours] + + +def _job_from(body: dict[str, Any]) -> Job: + metadata = body.get("metadata") or {} + pending = [ + PendingChange( + change_id=c.get("change_id", ""), + chunk_id=c.get("chunk_id", ""), + operation=c.get("operation", ""), + old_html=c.get("old_html", "") or "", + new_html=c.get("new_html", "") or "", + ) + for c in (metadata.get("pending_changes") or []) + ] + return Job( + job_id=body.get("job_id", ""), + status=body.get("status", ""), + progress=int(body.get("progress") or 0), + pending=pending, + raw=body, + ) diff --git a/use-cases/preetham1930/poa-generator/poa/superdocs/decisions.py b/use-cases/preetham1930/poa-generator/poa/superdocs/decisions.py new file mode 100644 index 000000000..2ac98122d --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/superdocs/decisions.py @@ -0,0 +1,123 @@ +"""Our decision ledger. SuperDocs' `approve` is only an actuator (Decision 33). + +Measured on 2026-08-09: the product's gate is **write-only**. Before any +decision `pending_changes` listed three changes with `status: None`; after +approving one and denying another it listed the same three, still with +`status: None`. The approve call returns `{"status":"ok","batch_complete":...}` +and nothing else. There is no API answer to "what has been decided, and by whom". + +So the row is ours, it is written **before** the actuator is called, and it +carries a named actor. An item is undecided exactly when it has no row - there +is no default, no pending row and no status column. A second decision on the +same item is refused unless it explicitly supersedes, and the superseded row is +kept with both actors. + +The completion sentence is computed from the rows at the moment it is emitted, +never from counters, because we watched a product report "Successfully updated +all 0 sections. 2 change(s) were denied" when two had been applied and three +denied. +""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from pathlib import Path + +from ..errors import DecisionAlreadyRecordedError + +VERBS = ("accept", "reject") + + +@dataclass(frozen=True) +class Decision: + run_id: str + step_index: int + chunk_id: str + verb: str + actor: str + reason: str + at: str + supersedes: str | None = None + + def __post_init__(self) -> None: + if self.verb not in VERBS: + raise ValueError( + f"the vocabulary is {VERBS} and nothing else. There is no 'resolve' and no " + f"'choose_side': a reviewer decides whether a change enters the document, not " + f"which of two disagreeing sources is right." + ) + if not self.actor.strip(): + raise ValueError("a decision with no actor answers nobody's question about who") + + +class DecisionLedger: + def __init__(self, path: Path | None = None) -> None: + self.path = path + self.rows: list[Decision] = [] + + def record( + self, + run_id: str, + step_index: int, + chunk_id: str, + verb: str, + actor: str, + reason: str, + supersede: bool = False, + ) -> Decision: + existing = self.find(run_id, step_index) + if existing is not None and not supersede: + raise DecisionAlreadyRecordedError( + f"step {step_index} of run {run_id} was already decided '{existing.verb}' by " + f"{existing.actor} at {existing.at}. Quiet replacement is the mechanism by " + f"which a decision gets quietly dropped; pass supersede=True and both rows are " + f"kept." + ) + row = Decision( + run_id=run_id, + step_index=step_index, + chunk_id=chunk_id, + verb=verb, + actor=actor, + reason=reason, + at=datetime.now(UTC).isoformat(timespec="seconds"), + supersedes=existing.at if existing is not None else None, + ) + self.rows.append(row) + if self.path is not None: + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(asdict(row)) + "\n") + return row + + def find(self, run_id: str, step_index: int) -> Decision | None: + matches = [r for r in self.rows if r.run_id == run_id and r.step_index == step_index] + return matches[-1] if matches else None + + def decided(self, run_id: str) -> list[Decision]: + latest: dict[int, Decision] = {} + for row in self.rows: + if row.run_id == run_id: + latest[row.step_index] = row + return [latest[k] for k in sorted(latest)] + + # No count fields anywhere: these are properties of the rows being held, so + # the sentence cannot assert a decision that was never made (Decision 22's + # trick, rebuilt). + def accepted(self, run_id: str) -> int: + return sum(1 for r in self.decided(run_id) if r.verb == "accept") + + def rejected(self, run_id: str) -> int: + return sum(1 for r in self.decided(run_id) if r.verb == "reject") + + def describe(self, run_id: str, planned: int) -> str: + accepted = self.accepted(run_id) + rejected = self.rejected(run_id) + undecided = planned - accepted - rejected + actors = sorted({r.actor for r in self.decided(run_id)}) + return ( + f"{accepted} accepted, {rejected} rejected, {undecided} undecided of {planned} " + f"planned change(s); decided by {', '.join(actors) or 'nobody'}" + ) diff --git a/use-cases/preetham1930/poa-generator/poa/superdocs/transport.py b/use-cases/preetham1930/poa-generator/poa/superdocs/transport.py new file mode 100644 index 000000000..6fe4d5d9d --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/superdocs/transport.py @@ -0,0 +1,172 @@ +"""How a request reaches SuperDocs - or, in the test suite, how it does not. + +Two implementations behind one protocol: + +- `HttpTransport` talks to `api.superdocs.app`. It is used by the demo and by + nothing else. The key is read from the environment, never printed, never + written to a file, and never included in a message we log. +- `ReplayTransport` answers from recorded responses under `docs/evidence/`. It + is what the whole test suite runs on, so `make test` passes on a clean + checkout with no `.env` and with the network blocked (hard rule 3). + +A replay transport that is asked for a request it has no recording of **raises**, +naming the request. A transport that invents a plausible response would be the +2026-08-07 failure mode with a different noun: a success message for work that +did not happen. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Protocol + +from ..errors import NotConfiguredError + +DEFAULT_BASE_URL = "https://api.superdocs.app" + + +class Transport(Protocol): + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: ... + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + """For the one endpoint that answers with a file rather than with JSON.""" + ... + + +def redacted_key() -> str: + key = os.environ.get("SUPERDOCS_API_KEY", "") + if not key: + return "(not set)" + return f"...{key[-4:]}" + + +class HttpTransport: + def __init__(self, base_url: str | None = None, timeout: float = 180.0) -> None: + self.base_url = ( + base_url or os.environ.get("SUPERDOCS_BASE_URL") or DEFAULT_BASE_URL + ).rstrip("/") + self.timeout = timeout + self.calls = 0 + key = os.environ.get("SUPERDOCS_API_KEY", "") + if not key: + raise NotConfiguredError( + "SUPERDOCS_API_KEY is not set. This build reads it from .env only, never from " + "a command line and never from a file it writes. The test suite does not need " + "it: tests run on recorded responses under docs/evidence/." + ) + self._key = key + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: + import httpx # imported here so the package imports with no network stack + + self.calls += 1 + response = httpx.request( + method, + f"{self.base_url}{path}", + json=json_body, + params=params, + headers={"Authorization": f"Bearer {self._key}", "Content-Type": "application/json"}, + timeout=self.timeout, + ) + if response.status_code >= 400: + # The body can echo the request; never let it carry the key onward. + raise RuntimeError( + f"{method} {path} -> HTTP {response.status_code}. " + f"{response.text[:400].replace(self._key, '<redacted>')}" + ) + return response.json() + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + """The export endpoint answers with a file, so it does not go through `request`.""" + import httpx + + self.calls += 1 + response = httpx.request( + method, + f"{self.base_url}{path}", + json=json_body, + headers={"Authorization": f"Bearer {self._key}", "Content-Type": "application/json"}, + timeout=self.timeout, + ) + if response.status_code >= 400: + raise RuntimeError( + f"{method} {path} -> HTTP {response.status_code}. " + f"{response.text[:400].replace(self._key, '<redacted>')}" + ) + return response.headers.get("content-type", ""), response.content + + +@dataclass +class RecordedCall: + method: str + path: str + response: dict[str, Any] + match: dict[str, Any] = field(default_factory=dict) + + +class ReplayTransport: + """Answers from recordings, in order, and refuses anything it was not given.""" + + def __init__(self, calls: list[RecordedCall]) -> None: + self._calls = list(calls) + self.seen: list[tuple[str, str, dict[str, Any] | None]] = [] + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: + self.seen.append((method, path, json_body)) + for index, call in enumerate(self._calls): + if call.method != method or call.path != path: + continue + if call.match and not all((json_body or {}).get(k) == v for k, v in call.match.items()): + continue + self._calls.pop(index) + return call.response + raise AssertionError( + f"no recorded response for {method} {path} with body keys " + f"{sorted(json_body or {})}. A replay transport never invents one: an invented " + f"response is a success message for work that did not happen. " + f"{len(self._calls)} recording(s) left: " + f"{[(c.method, c.path) for c in self._calls]}" + ) + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + """A replayed export records that we asked. There is no file to replay.""" + self.request(method, path, json_body=json_body) + return "application/octet-stream", b"" + + @property + def exhausted(self) -> bool: + return not self._calls + + +def load_recording(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) diff --git a/use-cases/preetham1930/poa-generator/poa/superdocs/verifier.py b/use-cases/preetham1930/poa-generator/poa/superdocs/verifier.py new file mode 100644 index 000000000..6edce2bde --- /dev/null +++ b/use-cases/preetham1930/poa-generator/poa/superdocs/verifier.py @@ -0,0 +1,180 @@ +"""Read the whole document back and decide what actually happened. + +Three classes, and the third is the dangerous one: + +- **NOT APPLIED** - the target chunk came back unchanged. This is the + 2026-08-07 corrupted-document mechanism: the change silently does not land and + the caller executes its consequences anyway. +- **APPLIED WRONG** - the target changed, but not to the post-state we computed + before sending. +- **COLLATERAL DAMAGE** - a chunk we did not target moved, appeared or vanished. + Measured on 2026-08-09: one requested change *succeeded* while four fabricated + sections appeared alongside it, and again when a template instantiation kept + the table and silently deleted the letterhead. A verifier that checked only its + own target would have reported success both times. + +So the comparison is over the **whole document**, every time, including on a +retry - failed content has been observed arriving a turn later. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..chunks import ChunkMap, canonical + + +@dataclass +class Problem: + kind: str # not_applied | applied_wrong | collateral_damage + detail: str + diff: str = "" + + +@dataclass +class Verification: + step_index: int + chunk_id: str + problems: list[Problem] = field(default_factory=list) + receipt: str = "" + exact_bytes: bool = False + + @property + def ok(self) -> bool: + return not self.problems + + def report(self) -> str: + if self.ok: + return f"step {self.step_index}: ok - {self.receipt}" + lines = [f"step {self.step_index}: FAILED on chunk {self.chunk_id}"] + for problem in self.problems: + lines.append(f" {problem.kind.upper().replace('_', ' ')}: {problem.detail}") + if problem.diff: + lines.append(problem.diff) + return "\n".join(lines) + + +def verify( + step_index: int, + chunk_id: str, + before: ChunkMap, + after: ChunkMap, + expected_html: str, +) -> Verification: + result = Verification(step_index=step_index, chunk_id=chunk_id) + expected = canonical(expected_html) + + before_ids = before.ids + after_ids = after.ids + added = [i for i in after_ids if i not in set(before_ids)] + removed = [i for i in before_ids if i not in set(after_ids)] + if added or removed: + result.problems.append( + Problem( + "collateral_damage", + f"the chunk set changed: {len(before_ids)} chunk(s) before, {len(after_ids)} " + f"after; {len(added)} added, {len(removed)} removed. A replacement never " + f"changes the chunk set.", + diff="\n".join( + f" + {after.by_id[i].tag}: {after.by_id[i].html[:120]}" for i in added[:6] + ) + + "\n".join(f" - {i}" for i in removed[:6]), + ) + ) + moved = [ + i + for i in before_ids + if i + and i != chunk_id + and i in after.by_id + and after.by_id[i].canonical != before.by_id[i].canonical + ] + if moved: + result.problems.append( + Problem( + "collateral_damage", + f"{len(moved)} chunk(s) we did not target changed: {moved[:6]}", + diff="\n".join( + f" was: {before.by_id[i].html[:150]}\n now: {after.by_id[i].html[:150]}" + for i in moved[:3] + ), + ) + ) + + if chunk_id not in after.by_id: + result.problems.append( + Problem("not_applied", f"the target chunk {chunk_id} is not in the read-back at all") + ) + return result + + was = before.by_id[chunk_id].canonical + now = after.by_id[chunk_id].canonical + if now == was: + result.problems.append( + Problem( + "not_applied", + "the target chunk came back byte-identical to what it held before. The chat " + "reply is not evidence and is not consulted here.", + diff=f" unchanged: {before.by_id[chunk_id].html[:200]}", + ) + ) + elif now != expected: + result.problems.append( + Problem( + "applied_wrong", + "the target chunk changed, but not to the post-state computed before sending", + diff=_diff(expected, now), + ) + ) + else: + result.exact_bytes = _strip_id(after.by_id[chunk_id].html) == _strip_id(expected_html) + result.receipt = ( + f"read back {len(after_ids)} chunk(s) from GET /v1/documents/" + f"{{id}}?include_html=true; target chunk {chunk_id} matches the computed " + f"post-state{' byte for byte' if result.exact_bytes else ' (canonical form)'}; " + f"{len(after_ids) - 1} non-target chunk(s) unchanged" + ) + return result + + +def _strip_id(html: str) -> str: + from ..chunks import strip_chunk_ids + + return strip_chunk_ids(html) + + +def _diff(expected: str, actual: str) -> str: + import difflib + + lines = list( + difflib.unified_diff( + expected.split(">"), actual.split(">"), "expected", "actual", lineterm="", n=1 + ) + ) + return "\n".join(f" {line}" for line in lines[:24]) + + +def verify_upload_verbatim(uploaded: ChunkMap, sent_blocks: list[str]) -> str: + """The upload is only useful if it is verbatim, so that is checked, not assumed. + + Decision 29 rests entirely on `upload-base64` being a verbatim load. It was + measured once; it is re-checked on every run, because a design resting on a + measured property should re-measure it rather than remember it. + """ + ours = [canonical(block) for block in sent_blocks] + theirs = [chunk.canonical for chunk in uploaded.chunks] + if ours != theirs: + mismatch = next( + (i for i, (a, b) in enumerate(zip(ours, theirs, strict=False)) if a != b), + min(len(ours), len(theirs)), + ) + raise AssertionError( + f"the upload was not verbatim: we sent {len(ours)} block(s) and {len(theirs)} came " + f"back, first difference at block {mismatch}.\n" + f" sent: {ours[mismatch] if mismatch < len(ours) else '(nothing)'}\n" + f" got : {theirs[mismatch] if mismatch < len(theirs) else '(nothing)'}\n" + f"Decision 29 rests on the upload being verbatim; if it is not, the whole " + f"skeleton approach is unsound and the run stops here rather than editing a " + f"document that is not the one we computed." + ) + return f"upload verified verbatim: {len(theirs)} chunk(s), every block canonically identical" diff --git a/use-cases/preetham1930/poa-generator/scripts/verify_live_output.py b/use-cases/preetham1930/poa-generator/scripts/verify_live_output.py new file mode 100644 index 000000000..3ae553e11 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/scripts/verify_live_output.py @@ -0,0 +1,197 @@ +"""The grading line, checked against the two LIVE documents and the exported files. + +> Run the intake for a durable financial POA and a separate healthcare POA and +> confirm the two drafts differ correctly in granted powers, and both carry the +> review notice and the external-notarisation statement. + +Nothing here trusts the run's own report. Both documents are fetched fresh from +`GET /v1/documents/{id}?include_html=true`, the powers are counted off what came +back, both notices are put through the same gate the export path uses, and the +exported DOCX is unzipped and `word/document.xml` read directly - because "it +exported" is not evidence that anything is in it. + + python scripts/verify_live_output.py <financial_doc_id> <healthcare_doc_id> +""" + +from __future__ import annotations + +import argparse +import sys +import zipfile +from pathlib import Path + +APP_ROOT = Path(__file__).resolve().parent.parent +REPO_ROOT = APP_ROOT.parent.parent +sys.path.insert(0, str(APP_ROOT)) + +from poa.assemble import assemble # noqa: E402 +from poa.chunks import ChunkMap, strip_markup # noqa: E402 +from poa.intake import load_intake # noqa: E402 +from poa.library import Library # noqa: E402 +from poa.notices import check_notices # noqa: E402 +from poa.superdocs.client import PoaClient # noqa: E402 +from poa.superdocs.transport import HttpTransport # noqa: E402 + +PAIRS = (("durable-financial", "financial"), ("healthcare", "healthcare")) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("financial_document_id") + parser.add_argument("healthcare_document_id") + parser.add_argument("--out", default=str(REPO_ROOT / "var" / "poa")) + args = parser.parse_args() + + try: + from dotenv import load_dotenv + + load_dotenv(REPO_ROOT / ".env", override=False) + except ImportError: + pass + + library = Library(APP_ROOT / "config") + client = PoaClient(HttpTransport()) + out_dir = Path(args.out) + + live: dict[str, ChunkMap] = {} + text: dict[str, str] = {} + failures: list[str] = [] + + for (intake_id, _category), document_id in zip( + PAIRS, (args.financial_document_id, args.healthcare_document_id), strict=True + ): + chunks, detail = client.read_back(document_id) + live[intake_id] = chunks + text[intake_id] = strip_markup(detail["html"]) + print(f"\n{intake_id}: document {document_id} version {detail.get('version')}") + print(f" {len(chunks)} chunk(s) read back") + + ours = assemble(load_intake(APP_ROOT / "intakes" / f"{intake_id}.json", library), library) + computed = ChunkMap(ours.html("target")) + if len(chunks) != len(computed): + failures.append(f"{intake_id}: {len(chunks)} chunks live, {len(computed)} computed") + differing = [ + index + for index, (a, b) in enumerate(zip(computed.chunks, chunks.chunks, strict=False)) + if a.canonical != b.canonical + ] + print(f" {len(differing)} chunk(s) differ from what we computed") + if differing: + failures.append(f"{intake_id}: chunks {differing[:8]} differ from the computed draft") + + # The notice gate, run independently of the run that produced this. + try: + receipt = check_notices(chunks, library, document_id) + print(f" {receipt.receipt()}") + except Exception as exc: + failures.append(f"{intake_id}: NOTICE GATE REFUSED - {str(exc).splitlines()[0]}") + + headings = chunks.headings() + print(f" {len(headings)} numbered clause(s): {sorted(headings)}") + if sorted(headings) != list(range(1, len(headings) + 1)): + failures.append(f"{intake_id}: clause numbering is not contiguous from 1") + + # -- the grading line ------------------------------------------------- + + print("\n" + "=" * 78) + print("THE GRADING LINE, checked on the live documents") + print("=" * 78) + for intake_id, category in PAIRS: + intake = load_intake(APP_ROOT / "intakes" / f"{intake_id}.json", library) + granted = intake.powers_in(library, category) + missing = [p for p in granted if library.powers[p].language not in text[intake_id]] + print( + f" {intake_id}: {len(granted) - len(missing)} of {len(granted)} granted powers present" + ) + if missing: + failures.append(f"{intake_id}: missing granted powers {missing}") + + withheld = [ + power_id + for power_id, power in library.powers.items() + if power.category == category and power_id not in granted + ] + strayed = [p for p in withheld if library.powers[p].language in text[intake_id]] + print(f" {intake_id}: {len(strayed)} of {len(withheld)} withheld powers present (want 0)") + if strayed: + failures.append(f"{intake_id}: carries withheld powers {strayed}") + + for (this_id, _), (other_id, other_category) in ((PAIRS[0], PAIRS[1]), (PAIRS[1], PAIRS[0])): + other = load_intake(APP_ROOT / "intakes" / f"{other_id}.json", library) + crossed = [ + p + for p in other.powers_in(library, other_category) + if library.powers[p].language in text[this_id] + ] + print(f" {this_id}: {len(crossed)} power(s) from {other_id} present (want 0)") + if crossed: + failures.append(f"{this_id}: carries {crossed} from {other_id}") + + for intake_id, _ in PAIRS: + successor = [ + c for c in live[intake_id].chunks if "successor agent" in strip_markup(c.html).lower() + ] + print( + f" {intake_id}: successor-agent clause present ({len(successor)} chunk(s) mention it)" + ) + if not successor: + failures.append(f"{intake_id}: no successor-agent clause") + for notice in library.notices.values(): + if notice.text not in text[intake_id]: + failures.append(f"{intake_id}: {notice.notice_id} is not verbatim in the live doc") + print(f" {intake_id}: both notices verbatim in the live document") + + # -- the exported files ----------------------------------------------- + + print("\n" + "=" * 78) + print("THE EXPORTED FILES") + print("=" * 78) + for intake_id, _ in PAIRS: + path = out_dir / f"poa-{intake_id}.docx" + if not path.exists(): + failures.append(f"{intake_id}: no exported DOCX at {path}") + print(f" {path}: MISSING") + continue + with zipfile.ZipFile(path) as archive: + document = archive.read("word/document.xml").decode("utf-8") + flat = " ".join(strip_markup(document).split()) + # `<w:tbl` also prefixes tblPr, tblGrid and tblBorders, so count the + # element itself. "It exported" is not evidence that anything is in it, + # and neither is a number that counted the wrong thing. + tables = document.count("<w:tbl>") + cells = document.count("<w:tc>") + print( + f" {path.name}: {path.stat().st_size} bytes, {tables} w:tbl, " + f"{document.count('<w:tr>')} rows, {cells} cells, " + f"{document.count('<w:tcBorders>')} bordered cells" + ) + if tables != 1: + failures.append( + f"{intake_id}: the DOCX has {tables} table(s), expected the signature block" + ) + for notice in library.notices.values(): + present = notice.text in flat + print(f" [{'x' if present else ' '}] {notice.notice_id} verbatim in the DOCX") + if not present: + failures.append(f"{intake_id}: {notice.notice_id} is not verbatim in the DOCX") + # A3, checked on the artefact a person would actually open: every + # signature and date cell is still empty after the round trip. + blanks = [c.column_label for c in library.signature_columns if c.blank] + for label in blanks: + print(f" [x] '{label}' column is a header only; no name follows it in the DOCX") + for row in library.signature_rows: + if f"{row.role_label} signed" in flat: + failures.append(f"{intake_id}: the DOCX shows {row.role_label} as signed") + + print("\n" + "-" * 78) + if failures: + print(f"{len(failures)} FAILURE(S):") + for failure in failures: + print(f" - {failure}") + return 1 + print("every claim above was read back from the API and from the exported files") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/use-cases/preetham1930/poa-generator/tests/recorded.py b/use-cases/preetham1930/poa-generator/tests/recorded.py new file mode 100644 index 000000000..f7d83b997 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/recorded.py @@ -0,0 +1,38 @@ +"""Recorded SuperDocs responses, replayed. No key, no network, no invention. + +Everything here comes out of `docs/evidence/`, which was captured on 2026-08-09 +against the live API and saved with `redemption_id` stripped. The failure-class +tests are driven by those recordings rather than by hand-written HTML, because +the point is that these three failures are things the product actually did. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent +REVERIFY = REPO_ROOT / "docs" / "evidence" / "2026-08-09-superdocs-reverify" +GATE = REPO_ROOT / "docs" / "evidence" / "2026-08-09-gate-and-templates" + + +def load(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def read_back_html(name: str) -> str: + """`html` from a recorded GET /v1/documents/{id}?include_html=true.""" + return load(REVERIFY / f"{name}.json")["html"] + + +def gate_job(name: str) -> dict[str, Any]: + return load(GATE / f"gate-job-{name}.json") + + +def targeted_edit_response() -> dict[str, Any]: + return load(REVERIFY / "A-targeted-edit.response.json") + + +def append_response() -> dict[str, Any]: + return load(REVERIFY / "C-append-structural.response.json") diff --git a/use-cases/preetham1930/poa-generator/tests/simulator.py b/use-cases/preetham1930/poa-generator/tests/simulator.py new file mode 100644 index 000000000..c1d02b3ff --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/simulator.py @@ -0,0 +1,247 @@ +"""A SuperDocs stand-in whose behaviours are the ones we measured. + +This is not a mock that returns what it was told to return. It holds a real +document, parses it with our own chunk parser, and reproduces five behaviours +recorded on 2026-08-09 and over Phase 4's live runs: + +- an upload is verbatim, and comes back with a `data-chunk-id` per top-level + element (measured: five chunks for a styled HTML upload, table atomic); +- `chat/async` with `approval_mode: ask_every_time` holds the change at + `awaiting_approval` and applies **nothing** until the batch settles; +- an approved change lands only when the batch completes; +- `not_applied` (the mid-document insertion that vanished), `applied_wrong` and + `collateral` (the create that appended sections nobody asked for) are the + three recorded failure shapes, and each is optional; +- `paraphrase_notice` is new to this build and is the one that matters here: an + edit lands correctly **and** the product quietly rewords a notice chunk it was + never asked about. Every step passes. The document is still not exportable. + It is a specialisation of the collateral-damage class that also fires on the + notice gate, which is why it is worth simulating separately. + +The assertions in the tests are about **our** behaviour - does the queue halt, +is the ledger row written before the actuator, is an export produced - not about +the simulator's. +""" + +from __future__ import annotations + +import re +import uuid +from dataclasses import dataclass, field +from typing import Any + +from poa.chunks import ChunkMap + +CHUNK_TAG = re.compile(r"<([a-zA-Z0-9]+)(\s|>)") + + +@dataclass +class Behaviour: + """What the simulator does on a given **chat call** (1-based, not step). + + A retry is a second chat call for the same step, so `not_applied={2, 3}` + means "step 2 fails and so does its one narrow retry", while + `not_applied={2}` means "step 2 fails and the retry lands". + """ + + not_applied: set[int] = field(default_factory=set) + applied_wrong: set[int] = field(default_factory=set) + collateral: set[int] = field(default_factory=set) + paraphrase_notice: set[int] = field(default_factory=set) + drop_notice: set[int] = field(default_factory=set) + corrupt_notice_on_upload: str = "" # "paraphrase" | "drop" | "" + fail_upload_verbatim: bool = False + revert_works: bool = True + + +class SimulatedSuperDocs: + def __init__(self, behaviour: Behaviour | None = None) -> None: + self.behaviour = behaviour or Behaviour() + self.html = "" + self.pristine = "" + self.document_id = "doc-" + uuid.uuid4().hex[:8] + self.session_id = "session_init_" + uuid.uuid4().hex[:12] + self.jobs: dict[str, dict[str, Any]] = {} + self.step_index = 0 + self.exports: list[str] = [] + self.reverts = 0 + self.approve_calls: list[str] = [] + self.event_log: list[str] = [] + + # -- transport --------------------------------------------------------- + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: + body = json_body or {} + if method == "POST" and path == "/v1/sessions/init": + return {"session_id": self.session_id, "opened": [], "focused_document_id": None} + if method == "POST" and path == "/v1/documents/upload-base64": + import base64 + + raw = base64.b64decode(body["file_base64"]).decode("utf-8") + if self.behaviour.fail_upload_verbatim: + raw = raw.replace("</p>", " (edited on ingest)</p>", 1) + self.html = _assign_chunk_ids(raw) + if self.behaviour.corrupt_notice_on_upload: + self._touch_notice(self.behaviour.corrupt_notice_on_upload) + self.pristine = self.html + return { + "session_id": self.session_id, + "filename": body["filename"], + "chunks_count": len(ChunkMap(self.html)), + "persisted": True, + } + if method == "GET" and path == f"/v1/sessions/{self.session_id}/documents": + return { + "session_id": self.session_id, + "focused_document_id": "doc_primary", + "documents": [ + { + "document_id": "doc_primary", + "durable_document_id": self.document_id, + "chunks_count": len(ChunkMap(self.html)), + "focused": True, + } + ], + } + if method == "GET" and path == f"/v1/documents/{self.document_id}": + return {"document_id": self.document_id, "html": self.html, "version": 1} + if method == "POST" and path == "/v1/chat/async": + return {"job_id": self._plan_job(body["message"])} + if method == "GET" and path.startswith("/v1/jobs/"): + return self.jobs[path.rsplit("/", 1)[1]] + if path.endswith("/approve"): + self.approve_calls.append(body["change_id"]) + self.event_log.append(f"approve:{body['change_id']}") + self._settle(body["job_id"], body["approved"]) + return {"status": "ok", "batch_complete": True} + if path == "/v1/documents/export": + self.exports.append(body.get("format", "docx")) + return {"download_url": "https://example.invalid/x"} + if path.endswith("/revert"): + self.reverts += 1 + if self.behaviour.revert_works: + self.html = self.pristine + else: + # The call returns ok and the document is not restored. This is + # the shape the whole build is built against: a 200 means the + # call was accepted, not that the state is what it claims. + self.html += '\n<p data-chunk-id="rv-1">not actually reverted</p>' + return {"status": "ok"} + raise AssertionError(f"the simulator was asked for {method} {path}") + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + self.request(method, path, json_body=json_body) + return ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + b"PKsimulated-docx", + ) + + # -- behaviour --------------------------------------------------------- + + def _plan_job(self, message: str) -> str: + self.step_index += 1 + chunk_id = re.search(r"data-chunk-id is ([0-9a-zA-Z-]+)", message).group(1) + new_html = message.split("\n\n", 1)[1] + job_id = "job-" + uuid.uuid4().hex[:8] + self.jobs[job_id] = { + "job_id": job_id, + "status": "awaiting_approval", + "progress": 88, + "metadata": { + "pending_changes": [ + { + "change_id": "chg-" + uuid.uuid4().hex[:8], + "chunk_id": chunk_id, + "operation": "edit", + "old_html": ChunkMap(self.html).by_id[chunk_id].html, + "new_html": new_html, + } + ] + }, + "_chunk_id": chunk_id, + "_new_html": new_html, + "_step": self.step_index, + } + return job_id + + def _settle(self, job_id: str, approved: bool) -> None: + job = self.jobs[job_id] + job["status"] = "completed" + job["progress"] = 100 + job["metadata"]["pending_changes"] = [] + if not approved: + return + step = job["_step"] + chunk_id, new_html = job["_chunk_id"], job["_new_html"] + if step in self.behaviour.not_applied: + self.event_log.append(f"step{step}:silently-did-nothing") + return + if step in self.behaviour.applied_wrong: + new_html = new_html.replace("</p>", " (paraphrased)</p>") + self._replace(chunk_id, new_html) + if step in self.behaviour.collateral: + self.event_log.append(f"step{step}:appended-clauses-nobody-asked-for") + self.html += ( + '\n<h2 data-chunk-id="fab-1">99. Compensation of the agent</h2>' + '\n<p data-chunk-id="fab-2">The Agent may take reasonable compensation.</p>' + ) + if step in self.behaviour.paraphrase_notice: + self.event_log.append(f"step{step}:reworded-a-notice-nobody-asked-about") + self._touch_notice("paraphrase") + if step in self.behaviour.drop_notice: + self.event_log.append(f"step{step}:deleted-a-notice-nobody-asked-about") + self._touch_notice("drop") + + def _touch_notice(self, how: str) -> None: + """Reword or delete a notice chunk, without being asked to. + + The paraphrase is deliberately harmless-looking: 'is not legal advice' + becomes 'is not intended as legal advice', which reads better and means + something weaker. That is the shape a rewrite actually takes. + """ + chunks = ChunkMap(self.html) + target = next( + (c for c in chunks.chunks if "not legal advice" in c.html), + None, + ) + if target is None: + return + if how == "drop": + self.html = self.html.replace(target.html, "", 1) + return + self.html = self.html.replace( + target.html, + target.html.replace("It is not legal advice", "It is not intended as legal advice"), + 1, + ) + + def _replace(self, chunk_id: str, new_html: str) -> None: + current = ChunkMap(self.html) + old = current.by_id[chunk_id].html + replacement = _with_chunk_id(new_html, chunk_id) + self.html = self.html.replace(old, replacement, 1) + + +def _assign_chunk_ids(html: str) -> str: + out: list[str] = [] + for line in html.split("\n"): + match = CHUNK_TAG.match(line) + if match: + out.append(_with_chunk_id(line, "c-" + uuid.uuid4().hex[:12])) + else: + out.append(line) + return "\n".join(out) + + +def _with_chunk_id(fragment: str, chunk_id: str) -> str: + fragment = re.sub(r'\sdata-chunk-id="[^"]*"', "", fragment, count=1) + return re.sub(r"<([a-zA-Z0-9]+)", rf'<\1 data-chunk-id="{chunk_id}"', fragment, count=1) diff --git a/use-cases/preetham1930/poa-generator/tests/test_assemble.py b/use-cases/preetham1930/poa-generator/tests/test_assemble.py new file mode 100644 index 000000000..0326d434c --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_assemble.py @@ -0,0 +1,179 @@ +"""Structure is ours: clause selection, numbering, cross-references, both fills. + +The claim these tests defend is that nothing structural is ever asked of +SuperDocs. The way to break that claim is not to send a bad instruction - it is +to compute the structure wrong and then send perfectly good instructions. +""" + +from __future__ import annotations + +import shutil + +import pytest +from poa.assemble import CLAUSE_REFERENCE, assemble +from poa.chunks import ChunkMap, strip_markup +from poa.errors import CrossReferenceError +from poa.intake import load_intake +from poa.library import Library + + +def test_clause_numbers_come_from_the_selected_set(financial, healthcare) -> None: + """The two required drafts number differently, and nothing was asked for it. + + The healthcare draft selects `healthcare-records-access`, which the + financial one does not, so every clause after it is numbered one higher. + """ + assert financial.numbering["successor"] == 6 + assert healthcare.numbering["successor"] == 7 + assert "healthcare-records-access" not in financial.numbering + assert healthcare.numbering["healthcare-records-access"] == 6 + + for draft in (financial, healthcare): + numbers = list(draft.numbering.values()) + assert numbers == list(range(1, len(numbers) + 1)), "one pass, contiguous, from 1" + + +def test_every_cross_reference_resolves_in_both_drafts(financial, healthcare, limited) -> None: + for draft in (financial, healthcare, limited): + numbers = set(draft.numbering.values()) + found = 0 + for block in draft.blocks: + if block.is_heading: + continue + for match in CLAUSE_REFERENCE.finditer(strip_markup(block.target)): + found += 1 + assert int(match.group(1)) in numbers + assert found >= 1, f"{draft.intake_id} has no cross-reference, so this proves nothing" + + +def test_the_cross_reference_actually_points_at_the_right_clause(financial, healthcare) -> None: + """Not just 'a clause that exists' - the successor clause specifically.""" + for draft in (financial, healthcare): + reliance = draft.block("reliance").target + referenced = int(CLAUSE_REFERENCE.search(strip_markup(reliance)).group(1)) + assert referenced == draft.numbering["successor"] + heading = draft.block("successor-heading").target + assert strip_markup(heading).startswith(f"{referenced}.") + + +def test_the_numbering_in_the_headings_is_what_the_document_carries(financial) -> None: + """Read back off the rendered HTML with the same parser the verifier uses.""" + headings = ChunkMap(financial.html("target")).headings() + assert headings.keys() == set(financial.numbering.values()) + assert headings[1].endswith("Principal") + assert headings[max(headings)].startswith(f"{max(headings)}. ") + + +def test_the_blank_form_and_the_filled_draft_have_the_same_structure( + financial, healthcare, limited +) -> None: + """The skeleton is what gets uploaded, so it must already be the final shape.""" + for draft in (financial, healthcare, limited): + blank = ChunkMap(draft.html("skeleton")) + filled = ChunkMap(draft.html("target")) + assert len(blank) == len(filled), "an edit never adds or removes a chunk" + assert [c.tag for c in blank.chunks] == [c.tag for c in filled.chunks] + assert blank.headings() == filled.headings(), "no edit ever renumbers anything" + + +def test_a_heading_is_never_different_between_the_two_fills(financial, healthcare, limited) -> None: + for draft in (financial, healthcare, limited): + for block in draft.blocks: + if block.is_heading: + assert not block.changed, f"{block.role} would become a renumbering instruction" + + +def test_the_limited_type_carries_its_limitation_and_the_others_have_no_such_clause( + financial, healthcare, limited +) -> None: + assert "limitation" in {b.role for b in limited.blocks} + assert "limitation" not in {b.role for b in financial.blocks} + assert "limitation" not in {b.role for b in healthcare.blocks} + assert "letting, upkeep and registration" in limited.block("limitation").target + assert "Limited power of attorney" in limited.block("title").target + assert "Durable power of attorney" in financial.block("title").target + + +def test_the_type_language_differs_across_all_three_types(library, intakes, tmp_path) -> None: + """general / limited / durable is the distinction the card says people need right.""" + root = tmp_path / "config" + shutil.copytree(library.root, root) + updated = Library(root) + languages = set() + for type_id in ("general", "limited", "durable"): + raw = (intakes / "limited-real-property.json").read_text(encoding="utf-8") + raw = raw.replace('"poa_type": "limited"', f'"poa_type": "{type_id}"') + if type_id != "limited": + raw = raw.replace( + '"limitation": "the letting, upkeep and registration of the real property ' + 'described in this instrument, and the payment of outgoings on it"', + '"limitation": null', + ) + path = tmp_path / f"{type_id}.json" + path.write_text(raw, encoding="utf-8") + draft = assemble(load_intake(path, updated), updated) + languages.add(draft.block("nature").target) + assert len(languages) == 3, "each type states its own durability language" + + +def test_a_reference_that_moves_between_the_fills_is_refused(library, intakes, tmp_path) -> None: + """C5: filling in a name may not move a clause number, ever.""" + root = tmp_path / "config" + shutil.copytree(library.root, root) + path = root / "clause-library.csv" + # A body that renders a different clause number depending on the fill would + # make an edit into a renumbering instruction. + path.write_text( + path.read_text(encoding="utf-8").replace( + "A successor agent appointed under clause {clause_ref:successor} is subject to the " + "same condition.", + "A successor agent appointed under clause {clause_ref:successor} is subject to the " + "same condition. See also clause {effective_date}.", + 1, + ), + encoding="utf-8", + ) + updated = Library(root) + intake = load_intake(intakes / "durable-financial.json", updated) + object.__setattr__(intake, "effective_date", "3") + with pytest.raises(CrossReferenceError, match="renumbering instruction"): + assemble(intake, updated) + + +def test_the_powers_language_is_the_catalogue_s_word_for_word(financial, library) -> None: + """The granted-powers language is determined by the intake, not written per case.""" + rendered = financial.block("powers-financial-list").target + for power_id in financial.granted["financial"]: + power = library.powers[power_id] + assert power.language in rendered + assert f"<strong>{power.label}.</strong>" in rendered + withheld = [ + p + for p, v in library.powers.items() + if v.category == "financial" and p not in financial.granted["financial"] + ] + assert withheld, "if nothing were withheld this would prove nothing" + for power_id in withheld: + assert library.powers[power_id].language not in financial.html("target") + + +def test_the_blank_form_names_no_party_and_grants_no_specific_power(financial, library) -> None: + """What is uploaded is a blank form. It must not carry the intake's answers.""" + blank = financial.html("skeleton") + assert "Kavitha Ramanathan" not in blank + assert "Suresh Ramanathan" not in blank + assert "Anjali Ramanathan" not in blank + for power_id in financial.granted["financial"]: + assert library.powers[power_id].language not in blank + # ... and it is recognisably a durable POA form for this jurisdiction, which + # is what makes uploading it verbatim honest rather than a trick. + assert "Durable power of attorney" in blank + assert "Telangana, India" in blank + assert library.notices["counsel-review"].text in blank + + +def test_a_clause_body_appears_exactly_once_per_draft(financial, healthcare, limited) -> None: + """A duplicated clause is the 2026-08-09 failure; here it would duplicate a power.""" + for draft in (financial, healthcare, limited): + roles = [b.role for b in draft.blocks] + assert len(roles) == len(set(roles)) diff --git a/use-cases/preetham1930/poa-generator/tests/test_client.py b/use-cases/preetham1930/poa-generator/tests/test_client.py new file mode 100644 index 000000000..91a475263 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_client.py @@ -0,0 +1,179 @@ +"""The four wire rules, enforced in the client rather than in a caller's care. + +Copied from `builds/statutory-statements/tests/` and rebuilt (Decision 31): the +wire rules are the same rules, measured on the same product, and re-deriving +them would be pretending we had not already learned them. The fifth rule, at the +bottom of this file, is this build's own - `export` will not run without a +receipt from the notice gate. +""" + +from __future__ import annotations + +import inspect + +import pytest +from poa.errors import NotConfiguredError, PlanRefusedError +from poa.superdocs.client import PoaClient, select_our_change +from poa.superdocs.transport import HttpTransport, RecordedCall, ReplayTransport +from recorded import gate_job + + +def _client(calls: list[RecordedCall]) -> tuple[PoaClient, ReplayTransport]: + transport = ReplayTransport(calls) + return PoaClient(transport, poll_seconds=0), transport + + +def test_gated_changes_go_to_the_async_route(monkeypatch) -> None: + """Decision 32: approve takes a job_id and jobs exist only on /v1/chat/async.""" + job = gate_job("awaiting-approval") + client, transport = _client( + [ + RecordedCall("POST", "/v1/chat/async", {"job_id": job["job_id"]}), + RecordedCall("GET", f"/v1/jobs/{job['job_id']}", job), + ] + ) + result = client.chat_gated("session_x", "doc_x", "replace chunk abc with ...") + assert result.awaiting + paths = [path for _, path, _ in transport.seen] + assert paths[0] == "/v1/chat/async" + assert "/v1/chat" not in paths, "the synchronous route has no gate to reach" + body = transport.seen[0][2] + assert body["approval_mode"] == "ask_every_time" + assert body["response_mode"] == "compact" + + +def test_a_batch_is_split_into_ours_and_everything_else() -> None: + """We send one chunk; the product does not always answer with one change.""" + from poa.superdocs.client import _job_from + + job = _job_from(gate_job("awaiting-approval")) + assert len(job.pending) == 3 + ours, unasked = select_our_change(job, job.pending[1].chunk_id) + assert ours is job.pending[1] + assert len(unasked) == 2 + assert ours not in unasked + + +def test_a_batch_that_does_not_contain_our_target_is_refused_in_full() -> None: + from poa.superdocs.client import Job, PendingChange + + job = Job("j", "awaiting_approval", 88, [PendingChange("c", "other-chunk", "edit", "", "")], {}) + with pytest.raises(PlanRefusedError) as caught: + select_our_change(job, "our-chunk") + assert "none of them on chunk our-chunk" in str(caught.value) + assert "Every one is denied" in str(caught.value) + + +def test_a_create_operation_on_our_target_is_refused() -> None: + from poa.superdocs.client import Job, PendingChange + + job = Job("j", "awaiting_approval", 88, [PendingChange("c", "ours", "create", "", "")], {}) + with pytest.raises(PlanRefusedError, match="one verb"): + select_our_change(job, "ours") + + +def test_denials_carry_no_feedback() -> None: + """Decision 34: there is no call shape in which feedback can be sent.""" + signature = inspect.signature(PoaClient.deny) + assert "feedback" not in signature.parameters + + client, transport = _client( + [RecordedCall("POST", "/v1/chat/s/approve", {"status": "ok", "batch_complete": True})] + ) + client.deny("s", "j", "c") + body = transport.seen[0][2] + assert body == {"job_id": "j", "change_id": "c", "approved": False} + assert "feedback" not in body + + +def test_a_read_back_with_no_html_verifies_nothing_and_says_so() -> None: + client, _ = _client([RecordedCall("GET", "/v1/documents/d", {"version": 2})]) + with pytest.raises(RuntimeError, match="carried no html"): + client.read_back("d") + + +def test_an_upload_that_is_not_persisted_stops_the_run() -> None: + """Measured: an upload with no session is not persisted and carries no id.""" + client, _ = _client( + [ + RecordedCall("POST", "/v1/sessions/init", {"session_id": "s"}), + RecordedCall( + "POST", + "/v1/documents/upload-base64", + {"filename": "f.html", "chunks_count": 3, "persisted": False}, + ), + ] + ) + with pytest.raises(RuntimeError, match="only evidence there is"): + client.upload_verbatim("f.html", "<p>x</p>") + + +def test_the_document_id_comes_from_the_session_roster_not_from_the_upload() -> None: + """The upload response has no document id at all; the roster is a read, not a claim.""" + client, transport = _client( + [ + RecordedCall("POST", "/v1/sessions/init", {"session_id": "s"}), + RecordedCall("POST", "/v1/documents/upload-base64", {"persisted": True}), + RecordedCall( + "GET", + "/v1/sessions/s/documents", + {"documents": [{"document_id": "doc_primary", "durable_document_id": "dur-1"}]}, + ), + ] + ) + document_id, session_id = client.upload_verbatim("f.html", "<p>x</p>") + assert (document_id, session_id) == ("dur-1", "s") + assert [path for _, path, _ in transport.seen][-1] == "/v1/sessions/s/documents" + + +def test_a_roster_with_no_durable_id_stops_the_run() -> None: + client, _ = _client( + [ + RecordedCall("POST", "/v1/sessions/init", {"session_id": "s"}), + RecordedCall("POST", "/v1/documents/upload-base64", {"persisted": True}), + RecordedCall("GET", "/v1/sessions/s/documents", {"documents": []}), + ] + ) + with pytest.raises(RuntimeError, match="nothing to verify against"): + client.upload_verbatim("f.html", "<p>x</p>") + + +def test_the_replay_transport_never_invents_a_response() -> None: + client, _ = _client([]) + with pytest.raises(AssertionError) as caught: + client.read_back("d") + assert "never invents one" in str(caught.value) + + +def test_a_live_transport_without_a_key_refuses_to_exist(monkeypatch) -> None: + monkeypatch.delenv("SUPERDOCS_API_KEY", raising=False) + with pytest.raises(NotConfiguredError) as caught: + HttpTransport() + assert "recorded responses" in str(caught.value) + assert "SUPERDOCS_API_KEY" in str(caught.value) + + +def test_the_key_is_never_in_a_message_this_module_can_produce(monkeypatch) -> None: + fake = "sk-" + "live-" + "0123456789abcdef" + monkeypatch.setenv("SUPERDOCS_API_KEY", fake) + transport = HttpTransport() + from poa.superdocs.transport import redacted_key + + assert redacted_key() == "...cdef" + assert fake not in repr(transport.__dict__.get("base_url", "")) + + +def test_the_export_runs_only_with_a_receipt_from_the_notice_gate(financial, library) -> None: + """The fifth wire rule, and it is this build's own (INVARIANTS.md B1).""" + from poa.chunks import ChunkMap + from poa.notices import check_notices + + client, transport = _client( + [RecordedCall("POST", "/v1/documents/export", {"download_url": "https://x.invalid"})] + ) + receipt = check_notices(ChunkMap(financial.html("target")), library, "dur-1") + client.export("s", "docx", receipt) + assert [path for _, path, _ in transport.seen] == ["/v1/documents/export"] + assert client.export_receipts == [receipt.receipt()], ( + "the run log carries what the gate actually checked, not that it was called" + ) diff --git a/use-cases/preetham1930/poa-generator/tests/test_editplan.py b/use-cases/preetham1930/poa-generator/tests/test_editplan.py new file mode 100644 index 000000000..5b712f129 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_editplan.py @@ -0,0 +1,198 @@ +"""A plan that breaks a wire rule should never reach the wire. + +Everything here is checked before a single byte is sent. An invented clause in a +power of attorney is a power nobody granted, so the verb, the targets and the +post-states are all settled while it is still cheap to refuse. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest +from poa.assemble import CLAUSE_REFERENCE +from poa.chunks import ChunkMap, canonical, strip_markup +from poa.editplan import build_plan +from poa.errors import PlanRefusedError + +CREATING_VERBS = ("create", "insert", "add a", "append", "renumber", "move ", "delete", "draft") +PACKAGE = Path(__file__).resolve().parent.parent / "poa" + + +def test_every_step_is_a_replacement_of_an_existing_chunk(financial, healthcare) -> None: + for draft in (financial, healthcare): + plan = build_plan(draft) + assert len(plan) == 6 + uploaded = ChunkMap(_with_ids(draft.html("skeleton"))) + for step in plan.steps: + assert step.verb == "replace" + assert canonical(step.was_html) in {c.canonical for c in uploaded.chunks} + + +def test_no_step_uses_a_creating_verb(financial) -> None: + """Decision 28: there is one verb. The instruction text is checked too.""" + plan = build_plan(financial) + for step in plan.steps: + step.chunk_id = "c-1" + instruction = step.instruction() + head = instruction.split("\n\n")[0].lower() + assert head.startswith("replace ") + for verb in CREATING_VERBS: + assert f" {verb}" not in head.replace("do not add any clause", ""), verb + + +def test_no_step_targets_a_heading_block(financial, healthcare, limited) -> None: + for draft in (financial, healthcare, limited): + headings = {b.role for b in draft.blocks if b.is_heading} + assert headings, "if there were no headings this would prove nothing" + for step in build_plan(draft).steps: + assert step.role not in headings + assert step.tag not in ("h1", "h2", "h3") + + +def test_no_step_targets_a_notice_block(financial, healthcare, limited) -> None: + """INVARIANTS.md B4: an instruction that touches a notice could paraphrase it.""" + for draft in (financial, healthcare, limited): + notices = {b.role for b in draft.blocks if b.is_notice} + assert len(notices) == 2 + for step in build_plan(draft).steps: + assert step.role not in notices + + +def test_a_plan_that_named_a_notice_is_refused_before_anything_is_sent(financial) -> None: + """The refusal is real, not merely unreachable. Drive it directly.""" + from poa.editplan import Plan, Step, assert_plan_is_legal + + notice = next(b for b in financial.blocks if b.is_notice) + plan = Plan( + [ + Step( + index=1, + role=notice.role, + tag=notice.tag, + clause_number=None, + expected_html="<p>anything</p>", + was_html=notice.skeleton, + ) + ], + financial, + ) + with pytest.raises(PlanRefusedError) as exc: + assert_plan_is_legal(plan, financial) + assert "notice" in str(exc.value) + assert "paraphrase" in str(exc.value) + + +def test_a_plan_that_named_a_heading_is_refused(financial) -> None: + from poa.editplan import Plan, Step, assert_plan_is_legal + + heading = next(b for b in financial.blocks if b.is_heading) + plan = Plan( + [ + Step( + index=1, + role=heading.role, + tag=heading.tag, + clause_number=heading.clause_number, + expected_html="<h2>99. Something</h2>", + was_html=heading.skeleton, + ) + ], + financial, + ) + with pytest.raises(PlanRefusedError, match="no renumbering step to get wrong"): + assert_plan_is_legal(plan, financial) + + +def test_a_step_that_would_change_nothing_is_refused(financial) -> None: + from poa.editplan import Plan, Step, assert_plan_is_legal + + block = financial.block("commencement") + plan = Plan( + [ + Step( + index=1, + role=block.role, + tag=block.tag, + clause_number=block.clause_number, + expected_html=block.skeleton, + was_html=block.skeleton, + ) + ], + financial, + ) + with pytest.raises(PlanRefusedError, match="reads as 'not applied'"): + assert_plan_is_legal(plan, financial) + + +def test_every_step_sends_a_post_state_computed_from_the_library(financial, library) -> None: + """The instruction hands over finished bytes. It never describes an edit. + + A described edit is an invitation to draft, and drafting is what produced + four invented sections on 2026-08-09. + """ + plan = build_plan(financial) + for step in plan.steps: + step.chunk_id = "c-1" + payload = step.instruction().split("\n\n", 1)[1] + assert payload == step.expected_html + assert payload == financial.block(step.role).target + powers = next(s for s in plan.steps if s.role == "powers-financial-list") + for power_id in financial.granted["financial"]: + assert library.powers[power_id].language in powers.expected_html + + +def test_no_step_changes_a_clause_reference(financial, healthcare, limited) -> None: + """C5. If an edit moved a number, the edit would be a renumbering.""" + for draft in (financial, healthcare, limited): + for step in build_plan(draft).steps: + before = CLAUSE_REFERENCE.findall(strip_markup(step.was_html)) + after = CLAUSE_REFERENCE.findall(strip_markup(step.expected_html)) + assert before == after, step.role + + +def test_the_riskiest_chunk_shape_is_ordered_last(financial) -> None: + """Decision 43: a halt should lose the least verified work.""" + tags = [s.tag for s in build_plan(financial).steps] + assert tags == sorted(tags, key=lambda t: {"p": 0, "ul": 1, "table": 2}[t]) + assert tags[-1] == "table" + + +def test_binding_refuses_when_a_step_matches_more_than_one_chunk(financial) -> None: + from poa.editplan import bind_chunk_ids + + plan = build_plan(financial) + doubled = _with_ids(financial.html("skeleton")) + "\n" + _with_ids(financial.html("skeleton")) + with pytest.raises(PlanRefusedError, match="exactly one target"): + bind_chunk_ids(plan, ChunkMap(doubled)) + + +def test_binding_matches_on_content_not_on_position(financial) -> None: + """A document that came back re-ordered fails here, not silently mid-run.""" + from poa.editplan import bind_chunk_ids + + plan = build_plan(financial) + blocks = _with_ids(financial.html("skeleton")).split("\n") + uploaded = ChunkMap("\n".join(reversed(blocks))) + bind_chunk_ids(plan, uploaded) + for step in plan.steps: + assert uploaded.by_id[step.chunk_id].canonical == canonical(step.was_html) + + +def test_the_package_never_asks_the_product_to_create(financial) -> None: + """Grep our own source for a creating verb inside an instruction we send.""" + text = (PACKAGE / "editplan.py").read_text(encoding="utf-8-sig") + body = text.split("def instruction")[1].split("def describe")[0] + sent = re.findall(r'f?"([^"]*)"', body) + joined = " ".join(sent).lower() + assert "replace the entire contents" in joined + for verb in ("create ", "insert ", "append ", "renumber "): + assert verb not in joined, verb + + +def _with_ids(html: str) -> str: + out = [] + for index, line in enumerate(html.split("\n")): + out.append(re.sub(r"<([a-zA-Z0-9]+)", rf'<\1 data-chunk-id="c-{index}"', line, count=1)) + return "\n".join(out) diff --git a/use-cases/preetham1930/poa-generator/tests/test_intake.py b/use-cases/preetham1930/poa-generator/tests/test_intake.py new file mode 100644 index 000000000..0be27ffa5 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_intake.py @@ -0,0 +1,149 @@ +"""The intake is the only channel by which a power reaches the document. + +Every refusal here is a refusal to draft. That is the point: a power of attorney +with a quietly missing limitation, or a quietly dropped power, or a party with +no name, reads perfectly well and is wrong in a way nobody notices. +""" + +from __future__ import annotations + +import json +from dataclasses import replace +from pathlib import Path + +import pytest +from poa.errors import ( + IncompleteIntakeError, + UnknownJurisdictionError, + UnknownPoaTypeError, + UnknownPowerError, +) +from poa.intake import load_intake, validate + + +def _write(tmp_path: Path, base: Path, **overrides) -> Path: + raw = json.loads(base.read_text(encoding="utf-8")) + raw.update(overrides) + path = tmp_path / "intake.json" + path.write_text(json.dumps(raw), encoding="utf-8") + return path + + +def test_the_three_shipped_intakes_load(library, intakes: Path) -> None: + for name in ("durable-financial", "healthcare", "limited-real-property"): + intake = load_intake(intakes / f"{name}.json", library) + assert intake.intake_id == name + assert intake.successor_agent.name, "the successor-agent clause is never optional" + + +def test_an_unknown_power_id_is_a_hard_error(library, intakes: Path, tmp_path: Path) -> None: + path = _write( + tmp_path, + intakes / "durable-financial.json", + granted_powers=["fin-banking", "fin-crypto-yolo"], + ) + with pytest.raises(UnknownPowerError) as exc: + load_intake(path, library) + message = str(exc.value) + assert "fin-crypto-yolo" in message + assert "config/powers.csv" in message + assert "never dropped" in message, "an unknown power must not be silently ignored" + + +def test_an_unknown_poa_type_is_never_approximated(library, intakes: Path, tmp_path: Path) -> None: + path = _write(tmp_path, intakes / "durable-financial.json", poa_type="springing") + with pytest.raises(UnknownPoaTypeError, match="general/limited/durable"): + load_intake(path, library) + + +def test_an_unknown_jurisdiction_names_the_csv_to_edit( + library, intakes: Path, tmp_path: Path +) -> None: + path = _write(tmp_path, intakes / "durable-financial.json", jurisdiction="atlantis") + with pytest.raises(UnknownJurisdictionError, match=r"config/jurisdictions\.csv"): + load_intake(path, library) + + +def test_a_limited_poa_with_no_limitation_is_refused( + library, intakes: Path, tmp_path: Path +) -> None: + """The single most consequential thing this build could get wrong.""" + path = _write(tmp_path, intakes / "limited-real-property.json", limitation=None) + with pytest.raises(IncompleteIntakeError) as exc: + load_intake(path, library) + assert "requires_limitation=yes" in str(exc.value) + assert "general one wearing the wrong heading" in str(exc.value) + + +def test_a_limitation_on_a_type_that_cannot_carry_one_is_refused( + library, intakes: Path, tmp_path: Path +) -> None: + """The other direction: a limit that would be silently dropped.""" + path = _write( + tmp_path, intakes / "durable-financial.json", limitation="only the Hyderabad account" + ) + with pytest.raises(IncompleteIntakeError, match="silently dropped"): + load_intake(path, library) + + +def test_a_missing_party_is_refused_and_named(library, intakes: Path, tmp_path: Path) -> None: + path = _write(tmp_path, intakes / "durable-financial.json", successor_agent=None) + with pytest.raises(IncompleteIntakeError) as exc: + load_intake(path, library) + assert "successor_agent" in str(exc.value) + assert "not optional" in str(exc.value) + + +def test_a_party_with_no_address_is_refused(library, intakes: Path, tmp_path: Path) -> None: + path = _write( + tmp_path, intakes / "durable-financial.json", agent={"name": "A Person", "address": ""} + ) + with pytest.raises(IncompleteIntakeError, match="agent"): + load_intake(path, library) + + +def test_granting_nothing_is_refused(library, intakes: Path, tmp_path: Path) -> None: + path = _write(tmp_path, intakes / "durable-financial.json", granted_powers=[]) + with pytest.raises(IncompleteIntakeError, match="not a shorter document"): + load_intake(path, library) + + +def test_a_power_granted_twice_is_refused(library, intakes: Path, tmp_path: Path) -> None: + path = _write( + tmp_path, intakes / "durable-financial.json", granted_powers=["fin-banking", "fin-banking"] + ) + with pytest.raises(IncompleteIntakeError, match="more than once"): + load_intake(path, library) + + +def test_real_property_powers_need_a_described_property( + library, intakes: Path, tmp_path: Path +) -> None: + path = _write(tmp_path, intakes / "limited-real-property.json", real_property_description=None) + with pytest.raises(IncompleteIntakeError, match="unbounded"): + load_intake(path, library) + + +def test_no_commencement_is_refused(library, intakes: Path, tmp_path: Path) -> None: + path = _write(tmp_path, intakes / "durable-financial.json", effective_date="") + with pytest.raises(IncompleteIntakeError, match="a question, not a draft"): + load_intake(path, library) + + +def test_validation_runs_on_an_object_too_not_only_on_a_file(library, intakes: Path) -> None: + """There is one validator, and both routes into the assembler go through it.""" + intake = load_intake(intakes / "durable-financial.json", library) + validate(intake, library) + with pytest.raises(UnknownPowerError): + validate(replace(intake, granted_powers=("not-a-power",)), library) + + +def test_categories_come_from_the_catalogue_not_from_the_order_typed( + library, intakes: Path, tmp_path: Path +) -> None: + """Two intakes granting the same powers must produce the same document.""" + forwards = load_intake(intakes / "limited-real-property.json", library) + backwards = replace(forwards, granted_powers=tuple(reversed(forwards.granted_powers))) + assert forwards.categories(library) == backwards.categories(library) + for category in forwards.categories(library): + assert forwards.powers_in(library, category) == backwards.powers_in(library, category) diff --git a/use-cases/preetham1930/poa-generator/tests/test_library.py b/use-cases/preetham1930/poa-generator/tests/test_library.py new file mode 100644 index 000000000..117f13447 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_library.py @@ -0,0 +1,241 @@ +"""The library is data. These tests are what make that sentence checkable. + +Two of them are the load-bearing ones: + +- `test_no_clause_text_is_hardcoded_anywhere_in_the_package` greps this + package's own `.py` files for every clause body, power label, power sentence, + notice, type label and jurisdiction name in `config/`. A hit means the library + stopped being the library. +- `test_adding_a_jurisdiction_is_a_data_edit` adds a row to a copy of the config + and requires the behaviour to change with zero code edits. +""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pytest +from poa.assemble import assemble +from poa.errors import LibraryRefusedError, UnknownConditionError, UnresolvedPlaceholderError +from poa.intake import load_intake +from poa.library import Library + +PACKAGE = Path(__file__).resolve().parent.parent / "poa" + + +def _copy_config(config: Path, tmp_path: Path) -> Path: + destination = tmp_path / "config" + shutil.copytree(config, destination) + return destination + + +def test_no_clause_text_is_hardcoded_anywhere_in_the_package(library) -> None: + sources = {path: path.read_text(encoding="utf-8-sig") for path in PACKAGE.rglob("*.py")} + strings: list[tuple[str, str]] = [] + for clause in library.clauses: + strings.append(("clause body", clause.body)) + if clause.heading: + strings.append(("clause heading", clause.heading)) + for power in library.powers.values(): + strings.append(("power label", power.label)) + strings.append(("power language", power.language)) + for poa_type in library.types.values(): + strings.append(("type label", poa_type.label)) + strings.append(("durability language", poa_type.durability_language)) + for jurisdiction in library.jurisdictions.values(): + strings.append(("jurisdiction", jurisdiction.label)) + strings.append(("governing law", jurisdiction.governing_law)) + for notice in library.notices.values(): + strings.append(("notice", notice.text)) + for blank in library.blank_form.values(): + strings.append(("blank-form marker", blank)) + + hits = [ + f"{path.name}: {kind} {value[:60]!r}" + for kind, value in strings + if len(value) > 12 + for path, text in sources.items() + if value in text + ] + assert not hits, ( + "the clause library has leaked into code:\n " + + "\n ".join(hits) + + "\nAdding a jurisdiction or a power type must be a data edit " + "(builds/poa-generator/INVARIANTS.md D1)." + ) + + +def test_the_hardcoding_check_is_not_vacuous(library) -> None: + """A guard that passes because it found nothing to check is decoration.""" + assert len(library.clauses) >= 15 + assert len(library.powers) >= 12 + assert len(library.notices) == 2 + text = (PACKAGE / "assemble.py").read_text(encoding="utf-8") + assert "power.language" in text, "the check greps for values the code really does emit" + + +def test_adding_a_jurisdiction_is_a_data_edit(config: Path, intakes: Path, tmp_path: Path) -> None: + """Zero code edits. One row.""" + root = _copy_config(config, tmp_path) + before = Library(root) + assert "in-ka" not in before.jurisdictions + + path = root / "jurisdictions.csv" + defer = "must be confirmed with qualified counsel before anything is signed." + path.write_text( + path.read_text(encoding="utf-8") + + f'in-ka,"Karnataka, India","Karnataka law is intended to govern this instrument; ' + f'whether that choice is effective is a question for qualified counsel.",' + f'"The witnesses required {defer}","Whether notarisation is required {defer}"\n', + encoding="utf-8", + ) + after = Library(root) + assert "in-ka" in after.jurisdictions + + intake_path = tmp_path / "ka.json" + raw = (intakes / "durable-financial.json").read_text(encoding="utf-8") + intake_path.write_text(raw.replace('"in-ts"', '"in-ka"'), encoding="utf-8") + draft = assemble(load_intake(intake_path, after), after) + assert "Karnataka, India" in draft.html("target") + assert "Karnataka, India" in draft.html("skeleton"), "jurisdiction is structure, not an edit" + + +def test_adding_a_power_is_a_data_edit(config: Path, intakes: Path, tmp_path: Path) -> None: + root = _copy_config(config, tmp_path) + path = root / "powers.csv" + path.write_text( + path.read_text(encoding="utf-8") + + "fin-safedeposit,financial,Safe deposit,The Agent may open and close a safe deposit " + "locker held in the name of the Principal.\n", + encoding="utf-8", + ) + updated = Library(root) + intake_path = tmp_path / "extra.json" + raw = (intakes / "durable-financial.json").read_text(encoding="utf-8") + intake_path.write_text(raw.replace('"fin-banking"', '"fin-safedeposit"'), encoding="utf-8") + draft = assemble(load_intake(intake_path, updated), updated) + assert "safe deposit locker" in draft.html("target") + + +def test_a_jurisdiction_row_that_asserts_a_requirement_is_refused( + config: Path, tmp_path: Path +) -> None: + """INVARIANTS.md A4. We are not qualified to say what a jurisdiction requires.""" + root = _copy_config(config, tmp_path) + path = root / "jurisdictions.csv" + path.write_text( + path.read_text(encoding="utf-8").replace( + "The number and eligibility of witnesses required for this instrument must be " + "confirmed with qualified counsel before anything is signed.", + "Two witnesses are required.", + 1, + ), + encoding="utf-8", + ) + with pytest.raises(LibraryRefusedError) as exc: + Library(root) + assert "witness_requirement" in str(exc.value) + assert "not qualified" in str(exc.value) + assert "deferral-markers.txt" in str(exc.value) + + +def test_an_empty_deferral_marker_list_is_refused(config: Path, tmp_path: Path) -> None: + """An empty guard list is a guard that cannot be satisfied, not one switched off.""" + root = _copy_config(config, tmp_path) + (root / "deferral-markers.txt").write_text("# nothing\n", encoding="utf-8") + with pytest.raises(LibraryRefusedError, match="cannot be satisfied"): + Library(root) + + +def test_an_unparseable_condition_is_a_hard_error(config: Path, tmp_path: Path) -> None: + """A silent False would drop a clause out of a power of attorney.""" + root = _copy_config(config, tmp_path) + path = root / "clause-library.csv" + path.write_text( + path.read_text(encoding="utf-8").replace( + "successor,100,successor,Successor agent,p,body,always,", + "successor,100,successor,Successor agent,p,body,if the moon is right,", + 1, + ), + encoding="utf-8", + ) + updated = Library(root) + with pytest.raises(UnknownConditionError) as exc: + updated.select("durable", ("financial",)) + assert "if the moon is right" in str(exc.value) + assert "no symptom" in str(exc.value) + + +def test_an_unresolved_placeholder_is_a_hard_error( + config: Path, intakes: Path, tmp_path: Path +) -> None: + root = _copy_config(config, tmp_path) + path = root / "clause-library.csv" + path.write_text( + path.read_text(encoding="utf-8").replace( + "The Principal appoints {agent_name}", + "The Principal appoints {agent_middle_name} {agent_name}", + 1, + ), + encoding="utf-8", + ) + updated = Library(root) + with pytest.raises(UnresolvedPlaceholderError) as exc: + assemble(load_intake(intakes / "durable-financial.json", updated), updated) + assert "agent_middle_name" in str(exc.value) + assert "never emptied" in str(exc.value) + + +def test_a_csv_row_with_an_unquoted_comma_is_refused_not_truncated( + config: Path, tmp_path: Path +) -> None: + root = _copy_config(config, tmp_path) + path = root / "powers.csv" + path.write_text( + path.read_text(encoding="utf-8") + + "fin-oops,financial,Careless,The Agent may do this, and that, and the other.\n", + encoding="utf-8", + ) + with pytest.raises(LibraryRefusedError) as exc: + Library(root) + assert "more fields than the header" in str(exc.value) + assert "cut in half" in str(exc.value) + + +def test_a_duplicate_clause_id_is_refused(config: Path, tmp_path: Path) -> None: + root = _copy_config(config, tmp_path) + path = root / "clause-library.csv" + path.write_text( + path.read_text(encoding="utf-8") + "successor,999,closing,Duplicate,p,body,always,x\n", + encoding="utf-8", + ) + with pytest.raises(LibraryRefusedError, match="more than once"): + Library(root) + + +def test_a_clause_reference_the_library_cannot_resolve_is_refused( + config: Path, tmp_path: Path +) -> None: + root = _copy_config(config, tmp_path) + path = root / "clause-library.csv" + path.write_text( + path.read_text(encoding="utf-8").replace( + "{clause_ref:successor}", "{clause_ref:no-such-clause}", 1 + ), + encoding="utf-8", + ) + with pytest.raises(LibraryRefusedError, match="no-such-clause"): + Library(root) + + +def test_a_signature_table_with_no_blank_column_is_refused(config: Path, tmp_path: Path) -> None: + """INVARIANTS.md A3: there is always somewhere left to sign, and it is empty.""" + root = _copy_config(config, tmp_path) + path = root / "signature-table.csv" + path.write_text( + path.read_text(encoding="utf-8").replace(",blank", ",label"), + encoding="utf-8", + ) + with pytest.raises(LibraryRefusedError, match="never implies"): + Library(root) diff --git a/use-cases/preetham1930/poa-generator/tests/test_notices.py b/use-cases/preetham1930/poa-generator/tests/test_notices.py new file mode 100644 index 000000000..9ca6f8e1e --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_notices.py @@ -0,0 +1,239 @@ +"""Hard constraint 1 and the legal-adjacency list. The centre of this build. + +> The document is NOT exportable until read-back confirms both the +> counsel-review notice and the external-notarisation statement are present +> VERBATIM. A test must prove the export path refuses when either is missing or +> altered. + +Both halves are proved here: that the refusal happens, and - more importantly - +that there is **no way to reach the export without the check**, because the +export takes a receipt only the check can issue. +""" + +from __future__ import annotations + +from copy import deepcopy +from typing import ClassVar + +import pytest +from poa.assemble import assert_no_forbidden_phrase +from poa.chunks import ChunkMap, strip_markup +from poa.errors import ( + ExportRefusedError, + ForbiddenPhraseError, + NoticeAlteredError, + NoticeMissingError, +) +from poa.notices import NoticeReceipt, check_notices +from poa.superdocs.client import PoaClient +from simulator import SimulatedSuperDocs + +NOTICE_IDS = ("counsel-review", "external-execution") + + +# -- A1/A2/A3: what the draft may never say ------------------------------- + + +def test_every_assembled_draft_carries_both_notices_verbatim( + financial, healthcare, limited, library +) -> None: + for draft in (financial, healthcare, limited): + for which in ("skeleton", "target"): + text = strip_markup(draft.html(which)) + for notice_id in NOTICE_IDS: + assert library.notices[notice_id].text in text, ( + f"{draft.intake_id} ({which}) is missing {notice_id}" + ) + + +def test_the_notices_are_never_edit_targets(financial, healthcare, limited) -> None: + for draft in (financial, healthcare, limited): + notices = [b for b in draft.blocks if b.is_notice] + assert len(notices) == 2 + for block in notices: + assert not block.editable + assert not block.changed + assert block not in draft.editable_changed() + + +def test_a_forbidden_phrase_anywhere_in_the_draft_is_a_hard_error(financial, library) -> None: + # A copy, because the fixture is session-scoped and a test that leaves a + # doctored draft behind is a test that breaks the next one from a distance. + draft = deepcopy(financial) + draft.block( + "commencement" + ).target += '<p style="x">This instrument was duly executed and is legally binding.</p>' + with pytest.raises(ForbiddenPhraseError) as exc: + assert_no_forbidden_phrase(draft, library) + assert "duly executed" in str(exc.value) + assert "no standing to say it" in str(exc.value) + assert "config/forbidden-phrases.csv" in str(exc.value) + + +def test_a_forbidden_phrase_in_the_blank_form_is_caught_too(financial, library) -> None: + """The blank form is what is uploaded, so a phrase only there still reaches the wire.""" + draft = deepcopy(financial) + draft.block("commencement").skeleton += "It is in full force and effect." + with pytest.raises(ForbiddenPhraseError, match="skeleton"): + assert_no_forbidden_phrase(draft, library) + + +def test_the_forbidden_phrase_list_is_data_not_code(library, tmp_path) -> None: + """Adding a phrase is a data edit.""" + import shutil + + from poa.library import Library + + root = tmp_path / "config" + shutil.copytree(library.root, root) + path = root / "forbidden-phrases.csv" + path.write_text( + path.read_text(encoding="utf-8") + "power of attorney,a deliberately absurd row\n", + encoding="utf-8", + ) + updated = Library(root) + assert len(updated.forbidden) == len(library.forbidden) + 1 + assert any(f.phrase == "power of attorney" for f in updated.forbidden) + + +def test_the_signature_block_leaves_every_signature_and_date_blank( + financial, healthcare, limited, library +) -> None: + """INVARIANTS.md A3: never imply that signing or notarisation happened.""" + blank_columns = [c.column_key for c in library.signature_columns if c.blank] + assert blank_columns, "if no column were blank this would prove nothing" + for draft in (financial, healthcare, limited): + for which in ("skeleton", "target"): + html = draft.block("signature-block").__getattribute__(which) + rows = html.split("<tr")[2:] # skip the header row + assert len(rows) == len(library.signature_rows) + for row in rows: + cells = row.split("<td")[1:] + for column, cell in zip(library.signature_columns, cells, strict=True): + body = cell.split(">", 1)[1].split("</td")[0] + if column.blank: + assert body == "", f"{draft.intake_id}: {column.column_key} is not empty" + + +def test_the_heading_says_the_signing_happens_outside_this_product(financial) -> None: + assert "outside this product" in strip_markup(financial.block("signature-block-heading").target) + + +# -- B1: the gate is a key, not a call ------------------------------------ + + +def test_export_cannot_be_called_without_a_receipt() -> None: + """No default argument, no receipt=None path. The signature is the gate.""" + import inspect + + signature = inspect.signature(PoaClient.export) + receipt = signature.parameters["receipt"] + assert receipt.default is inspect.Parameter.empty, ( + "export(receipt=...) with a default would let a caller skip the notice gate by " + "forgetting a keyword. INVARIANTS.md B1." + ) + client = PoaClient(SimulatedSuperDocs(), poll_seconds=0) + with pytest.raises(TypeError): + client.export("s", "docx") # type: ignore[call-arg] + with pytest.raises(ExportRefusedError, match="NoticeReceipt"): + client.export("s", "docx", None) # type: ignore[arg-type] + + +def test_a_receipt_cannot_be_forged() -> None: + """Only check_notices() can make one, and it says so.""" + with pytest.raises(ValueError) as exc: + NoticeReceipt(document_id="d", chunk_count=1, notices=("counsel-review",)) + assert "check_notices()" in str(exc.value) + assert "INVARIANTS.md B1" in str(exc.value) + + +def test_the_gate_passes_on_the_document_we_computed(financial, library) -> None: + receipt = check_notices(ChunkMap(financial.html("target")), library, "doc-1") + assert set(receipt.notices) == set(NOTICE_IDS) + assert "verbatim" in receipt.receipt() + assert "doc-1" in receipt.receipt() + + +# -- B2/B3: missing and altered ------------------------------------------- + + +def test_export_refuses_when_a_notice_is_missing(financial, library) -> None: + for notice_id in NOTICE_IDS: + html = financial.html("target").replace(library.notices[notice_id].text, "") + with pytest.raises(NoticeMissingError) as exc: + check_notices(ChunkMap(html), library, "doc-1") + assert notice_id in str(exc.value) + assert "No export is produced" in str(exc.value) + + +def test_export_refuses_when_a_notice_is_altered(financial, library) -> None: + """One word. This is the case that matters - a rewrite paraphrases.""" + alterations = { + "counsel-review": ("It is not legal advice", "It is not intended as legal advice"), + "external-execution": ("This draft is unexecuted.", "This draft is ready."), + } + for notice_id, (before, after) in alterations.items(): + html = financial.html("target").replace(before, after) + assert html != financial.html("target"), "the alteration has to actually change something" + with pytest.raises(NoticeAlteredError) as exc: + check_notices(ChunkMap(html), library, "doc-1") + assert notice_id in str(exc.value) + assert "NOT verbatim" in str(exc.value) + assert "No export is produced" in str(exc.value) + + +def test_dropping_a_single_word_from_a_notice_refuses_the_export(financial, library) -> None: + html = financial.html("target").replace( + "creates no lawyer-client relationship", "creates lawyer-client relationship" + ) + with pytest.raises(NoticeAlteredError, match="paraphrased notice is a missing notice"): + check_notices(ChunkMap(html), library, "doc-1") + + +def test_a_notice_split_across_two_chunks_does_not_satisfy_the_gate(financial, library) -> None: + """The second, independent comparison. + + Split with nothing inserted, so the document's *text* still contains the + notice word for word and only the chunk structure changed. A substring test + alone would pass this, and then a later edit could land between the two + halves without the gate noticing. + """ + text = library.notices["counsel-review"].text + head, tail = text.split(". ", 1) + html = financial.html("target").replace(text, f'{head}.</p>\n<p style="x">{tail}') + assert text in strip_markup(html), ( + "the first comparison must still pass, or this proves nothing" + ) + with pytest.raises(NoticeAlteredError, match="no single chunk holds it on its own"): + check_notices(ChunkMap(html), library, "doc-1") + + +def test_a_sentence_inserted_into_a_notice_refuses_the_export(financial, library) -> None: + text = library.notices["counsel-review"].text + head, tail = text.split(". ", 1) + html = financial.html("target").replace( + text, f'{head}.</p>\n<p style="x">Ignore the above.</p>\n<p style="x">{tail}' + ) + with pytest.raises(NoticeAlteredError, match="NOT verbatim"): + check_notices(ChunkMap(html), library, "doc-1") + + +def test_re_serialised_punctuation_does_not_fail_the_gate(financial, library) -> None: + """Verbatim is defined at the level of words, not of bytes (Decision 38).""" + html = ( + financial.html("target") + .replace("counsel.", "counsel.\n ", 1) + .replace("lawyer-client", "lawyer-client") + ) + receipt = check_notices(ChunkMap(html), library, "doc-1") + assert set(receipt.notices) == set(NOTICE_IDS) + + +def test_an_empty_notice_list_cannot_make_the_gate_pass(financial, library) -> None: + """A guard that cannot fail is worse than no guard.""" + + class Empty: + notices: ClassVar[dict] = {} + + with pytest.raises(NoticeMissingError, match="worse than none"): + check_notices(ChunkMap(financial.html("target")), Empty(), "doc-1") # type: ignore[arg-type] diff --git a/use-cases/preetham1930/poa-generator/tests/test_orchestrator.py b/use-cases/preetham1930/poa-generator/tests/test_orchestrator.py new file mode 100644 index 000000000..8335aa6dc --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_orchestrator.py @@ -0,0 +1,277 @@ +"""The failure path is the build, not an afterthought. + +Copied from `builds/statutory-statements/tests/` and rebuilt (Decision 31), plus +the three tests this build exists for. Those three are the interesting ones, +because they are the case the edit queue cannot catch on its own: + + every step succeeds, and the document is still not exportable. + +A run in which six chunk replacements all verified applied and the product +quietly reworded a notice along the way is, from the queue's point of view, a +clean run. It is not one, and the gate is what says so. +""" + +from __future__ import annotations + +import pytest +from poa.editplan import build_plan +from poa.errors import DecisionAlreadyRecordedError +from poa.orchestrator import Orchestrator +from poa.superdocs.client import PoaClient +from poa.superdocs.decisions import DecisionLedger +from simulator import Behaviour, SimulatedSuperDocs + +ACTOR = "K. Latha (reviewing paralegal)" + + +def _run( + draft, library, behaviour: Behaviour, steps: int = 4, ledger: DecisionLedger | None = None +): + plan = build_plan(draft).sample(steps) + product = SimulatedSuperDocs(behaviour) + client = PoaClient(product, poll_seconds=0) + ledger = ledger or DecisionLedger() + orchestrator = Orchestrator(client, ledger, ACTOR, library, sleep=lambda _: None) + result = orchestrator.run("run-1", plan) + return result, product, ledger, orchestrator + + +# -- the clean run -------------------------------------------------------- + + +def test_a_clean_run_applies_every_step_and_exports(financial, library) -> None: + result, product, ledger, _ = _run(financial, library, Behaviour()) + assert result.ok + assert result.applied == 4 + assert product.exports == ["docx"] + assert ledger.accepted("run-1") == 4 + sentence = result.sentence(ledger) + assert "4 of 4 planned change(s) verified applied" in sentence + assert "both notices were then read back verbatim before anything was exported" in sentence + assert f"decided by {ACTOR}" in sentence + + +def test_the_gate_runs_on_the_upload_before_any_edit_is_issued(financial, library) -> None: + """Nothing is gained by editing a document whose notices did not survive.""" + _result, _product, _ledger, orchestrator = _run(financial, library, Behaviour()) + gates = [line for line in orchestrator.log if line.startswith("notice gate:")] + assert len(gates) == 2, "once after the upload, once before the export" + assert "(after the upload)" in gates[0] + assert "(before the export)" in gates[1] + for line in gates: + assert "counsel-review verbatim in chunk" in line + assert "external-execution verbatim in chunk" in line + + +def test_a_notice_mangled_on_upload_stops_the_run_before_any_edit(financial, library) -> None: + """Two independent defences stand here, and the earlier one fires first. + + `verify_upload_verbatim` compares every uploaded block against what we sent, + so a notice reworded during ingestion is caught as a non-verbatim upload + before the gate is even reached. Recorded as an assertion rather than left + implicit, because a reader could reasonably expect the gate's message and + should know why they get the other one. + """ + product = SimulatedSuperDocs(Behaviour(corrupt_notice_on_upload="paraphrase")) + client = PoaClient(product, poll_seconds=0) + orchestrator = Orchestrator(client, DecisionLedger(), ACTOR, library, sleep=lambda _: None) + with pytest.raises(AssertionError) as exc: + orchestrator.run("run-1", build_plan(financial).sample(4)) + assert "was not verbatim" in str(exc.value) + assert "not intended as legal advice" in str(exc.value), "the diff names the reworded notice" + assert product.step_index == 0, "not one edit was issued" + assert product.exports == [] + + +# -- the case this build exists for -------------------------------------- + + +def test_a_notice_reworded_during_a_step_is_caught_as_collateral_damage(financial, library) -> None: + """The first of two defences, on a notice nobody targeted. + + The simulator's paraphrase is deliberately harmless-looking: 'is not legal + advice' becomes 'is not intended as legal advice'. It reads better and means + less, which is the shape a rewrite actually takes. The step's own change + lands correctly - it is the notice next door that moved. + """ + result, product, ledger, _ = _run(financial, library, Behaviour(paraphrase_notice={3})) + assert result.halted_at == 3 + assert any("reworded-a-notice" in e for e in product.event_log) + kinds = {p.kind for p in result.outcomes[-1].verification.problems} + assert kinds == {"collateral_damage"} + assert product.exports == [], "no export, and the export path was never reached" + assert "HALTED at step 3" in result.sentence(ledger) + + +def test_a_reworded_notice_that_the_collateral_check_cannot_see_still_refuses_the_export( + financial, library +) -> None: + """The gate standing on its own. + + Drive the paraphrase on the *last* step of the plan, then read back and gate. + Whatever the per-step verifier concluded, the document is not exportable and + the run says which notice and how it differs. + """ + plan = build_plan(financial) + product = SimulatedSuperDocs(Behaviour()) + client = PoaClient(product, poll_seconds=0) + ledger = DecisionLedger() + orchestrator = Orchestrator(client, ledger, ACTOR, library, sleep=lambda _: None) + + original_export = client.export + seen: list[str] = [] + + def spy(*args, **kwargs): + seen.append("export") + return original_export(*args, **kwargs) + + client.export = spy # type: ignore[method-assign] + # Reword a notice after the last edit lands but before the final read-back. + original_read_back = client.read_back + calls = {"n": 0} + + def read_back(document_id, **kwargs): + calls["n"] += 1 + if calls["n"] == 2 + 2 * len(plan): # the final read-back before the gate + product._touch_notice("paraphrase") + return original_read_back(document_id, **kwargs) + + client.read_back = read_back # type: ignore[method-assign] + result = orchestrator.run("run-1", plan) + + assert result.applied == len(plan), "every edit verified applied" + assert result.halted_at is None, "the queue never halted" + assert seen == [], "the export was never called" + assert product.exports == [] + assert not result.ok + assert "counsel-review" in result.notice_refusal + assert "NOT exportable" in result.sentence(ledger) + assert any("NOTICE GATE REFUSED before the export" in line for line in orchestrator.log) + + +def test_a_deleted_notice_refuses_the_export_too(financial, library) -> None: + """A deleted notice removes a chunk, so the chunk set changed - also collateral.""" + result, product, ledger, _ = _run(financial, library, Behaviour(drop_notice={2})) + assert result.halted_at == 2 + assert product.exports == [] + detail = " ".join(p.detail for p in result.outcomes[-1].verification.problems) + assert "the chunk set changed" in detail + assert "No export was produced" in result.sentence(ledger) + + +# -- the three failure classes, unchanged -------------------------------- + + +def test_a_failure_halts_the_queue_and_downstream_steps_never_run(financial, library) -> None: + result, product, _ledger, orchestrator = _run(financial, library, Behaviour(not_applied={2, 3})) + assert not result.ok + assert result.halted_at == 2 + assert result.applied == 1, "step 1 landed; 3 and 4 were never attempted" + assert product.step_index == 3, "step 2 plus its one narrow retry, and nothing after" + assert any("halting" in line for line in orchestrator.log) + + +def test_no_export_after_a_failure(financial, library) -> None: + result, product, ledger, _ = _run(financial, library, Behaviour(not_applied={2, 3})) + assert product.exports == [] + assert result.exported == [] + assert "No export was produced" in result.sentence(ledger) + assert "HALTED at step 2" in result.sentence(ledger) + + +def test_at_most_one_narrow_retry_and_never_a_replan(financial, library) -> None: + result, product, _, orchestrator = _run(financial, library, Behaviour(not_applied={2, 3})) + assert product.step_index == 3, "the step, its one retry, and nothing more" + assert result.outcomes[-1].retried + assert sum("narrow retry" in line for line in orchestrator.log) == 1 + assert all("no re-plan" in line for line in orchestrator.log if "retry" in line) + + +def test_a_retry_that_succeeds_lets_the_run_continue(financial, library) -> None: + result, product, ledger, _ = _run(financial, library, Behaviour(not_applied={2}), steps=3) + assert result.ok + assert product.step_index == 4, "three steps plus one retry" + assert [o.retried for o in result.outcomes] == [False, True, False] + assert len(ledger.decided("run-1")) == 3 + assert any(r.supersedes for r in ledger.rows), "the retry supersedes; both rows are kept" + assert len(ledger.rows) == 4 + + +def test_collateral_damage_is_never_retried(financial, library) -> None: + result, product, _, orchestrator = _run(financial, library, Behaviour(collateral={1})) + assert result.halted_at == 1 + assert product.step_index == 1, "no retry at all" + assert any("collateral" in line.lower() for line in orchestrator.log) + + +def test_applied_wrong_is_detected_and_halts(financial, library) -> None: + result, product, _, _ = _run(financial, library, Behaviour(applied_wrong={1, 2})) + assert result.halted_at == 1 + assert product.exports == [] + kinds = {p.kind for p in result.outcomes[-1].verification.problems} + assert kinds == {"applied_wrong"} + + +def test_a_failed_run_reverts_and_says_whether_the_revert_worked(financial, library) -> None: + result, product, _, _ = _run(financial, library, Behaviour(not_applied={1, 2})) + assert product.reverts == 1 + assert "reverted to the last verified-good state, confirmed by read-back" in result.reverted + + result2, product2, _, _ = _run( + financial, library, Behaviour(not_applied={1, 2}, revert_works=False) + ) + assert "does NOT match the last verified-good state" in result2.reverted + assert product2.exports == [] + + +def test_an_upload_that_is_not_verbatim_stops_before_any_edit(financial, library) -> None: + with pytest.raises(AssertionError, match="was not verbatim"): + _run(financial, library, Behaviour(fail_upload_verbatim=True)) + + +# -- the decision ledger -------------------------------------------------- + + +def test_the_decision_row_is_written_before_the_actuator_is_called(financial, library) -> None: + """Decision 33: the product's gate is write-only, so the record has to be ours.""" + plan = build_plan(financial).sample(2) + product = SimulatedSuperDocs(Behaviour()) + client = PoaClient(product, poll_seconds=0) + ledger = DecisionLedger() + original = ledger.record + + def spy(*args, **kwargs): + product.event_log.append("ledger-row") + return original(*args, **kwargs) + + ledger.record = spy # type: ignore[method-assign] + result = Orchestrator(client, ledger, ACTOR, library, sleep=lambda _: None).run("run-1", plan) + assert result.ok + timeline = [ + "ledger-row" if e == "ledger-row" else "approve" + for e in product.event_log + if e == "ledger-row" or e.startswith("approve:") + ] + assert timeline == ["ledger-row", "approve", "ledger-row", "approve"] + assert ledger.decided("run-1")[0].at, "the row carries a timestamp and an actor" + assert ledger.decided("run-1")[0].actor == ACTOR + + +def test_a_second_decision_on_the_same_step_is_refused_unless_it_supersedes() -> None: + ledger = DecisionLedger() + ledger.record("r", 1, "c", "accept", "A", "because") + with pytest.raises(DecisionAlreadyRecordedError, match="quietly dropped"): + ledger.record("r", 1, "c", "reject", "B", "because") + superseding = ledger.record("r", 1, "c", "reject", "B", "because", supersede=True) + assert superseding.supersedes + assert len(ledger.rows) == 2, "the superseded row is kept" + assert ledger.accepted("r") == 0 and ledger.rejected("r") == 1 + + +def test_the_completion_sentence_is_computed_from_the_rows_it_holds() -> None: + ledger = DecisionLedger() + ledger.record("r", 1, "c1", "accept", "A", "x") + ledger.record("r", 2, "c2", "reject", "B", "y") + assert "1 accepted, 1 rejected, 3 undecided of 5 planned" in ledger.describe("r", planned=5) + ledger.rows.clear() + assert "0 accepted, 0 rejected, 5 undecided" in ledger.describe("r", planned=5) diff --git a/use-cases/preetham1930/poa-generator/tests/test_two_drafts.py b/use-cases/preetham1930/poa-generator/tests/test_two_drafts.py new file mode 100644 index 000000000..14fbbf140 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_two_drafts.py @@ -0,0 +1,153 @@ +"""The grading line, asserted. + +> Run the intake for a durable financial POA and a separate healthcare POA and +> confirm the two drafts differ correctly in granted powers, and both carry the +> review notice and the external-notarisation statement. + +"Differ correctly" is stronger than "differ", and it is what these tests pin +down. Three claims, each of which could be true while the other two are false: + +1. each draft carries **every** power its own intake granted; +2. each draft carries **none** of the other's, and none of the powers its own + intake withheld; +3. everything the two drafts share is shared - the successor-agent clause, both + notices, the parties' own roles - so the difference is the powers and not an + accident of two documents having been built differently. + +These run offline against the assembled drafts. `scripts/verify_live_output.py` +asserts the same three things against the two **live** documents, read back from +the API - which is where the claim actually has to hold. +""" + +from __future__ import annotations + +from poa.chunks import ChunkMap, strip_markup +from poa.notices import check_notices + + +def test_the_two_required_drafts_are_a_durable_financial_and_a_healthcare_poa( + financial, healthcare +) -> None: + assert financial.poa_type == "durable" + assert set(financial.granted) == {"financial"} + assert set(healthcare.granted) == {"healthcare"} + assert financial.intake_id != healthcare.intake_id + + +def test_each_draft_carries_every_power_its_own_intake_granted( + financial, healthcare, library +) -> None: + for draft in (financial, healthcare): + text = strip_markup(draft.html("target")) + for powers in draft.granted.values(): + assert powers, "a category with no powers would make this vacuous" + for power_id in powers: + power = library.powers[power_id] + assert power.language in text, f"{draft.intake_id} is missing {power_id}" + assert power.label in text + + +def test_neither_draft_carries_the_other_s_powers(financial, healthcare, library) -> None: + """The claim the card is actually about.""" + for draft, other in ((financial, healthcare), (healthcare, financial)): + text = strip_markup(draft.html("target")) + strays = [ + power_id + for powers in other.granted.values() + for power_id in powers + if library.powers[power_id].language in text + ] + assert not strays, f"{draft.intake_id} carries {strays} from {other.intake_id}" + + +def test_neither_draft_carries_a_power_its_own_intake_withheld( + financial, healthcare, library +) -> None: + """Withholding is the half that is easy to get wrong and impossible to see.""" + for draft in (financial, healthcare): + granted = {p for powers in draft.granted.values() for p in powers} + categories = set(draft.granted) + withheld = [ + power_id + for power_id, power in library.powers.items() + if power.category in categories and power_id not in granted + ] + assert withheld, "if nothing were withheld this test would prove nothing" + text = strip_markup(draft.html("target")) + for power_id in withheld: + assert library.powers[power_id].language not in text, ( + f"{draft.intake_id} grants {power_id}, which its intake withheld" + ) + + +def test_the_clause_sets_differ_by_exactly_the_healthcare_clauses(financial, healthcare) -> None: + """The structural difference, stated exactly rather than as 'they differ'.""" + only_financial = {b.role for b in financial.blocks} - {b.role for b in healthcare.blocks} + only_healthcare = {b.role for b in healthcare.blocks} - {b.role for b in financial.blocks} + assert only_financial == { + "powers-financial", + "powers-financial-heading", + "powers-financial-list", + } + assert only_healthcare == { + "powers-healthcare", + "powers-healthcare-heading", + "powers-healthcare-list", + "healthcare-records-access", + "healthcare-records-access-heading", + } + + +def test_both_drafts_carry_a_successor_agent_clause(financial, healthcare) -> None: + for draft in (financial, healthcare): + clause = strip_markup(draft.block("successor").target) + assert "successor agent" in clause + assert clause.count("successor agent") >= 1 + assert "Anjali Ramanathan" in financial.block("successor").target + assert "Suresh Ramanathan" in healthcare.block("successor").target + + +def test_both_drafts_carry_both_notices_verbatim_and_the_gate_says_so( + financial, healthcare, library +) -> None: + """Same check the live export path runs, on the documents we computed.""" + for draft in (financial, healthcare): + for which in ("skeleton", "target"): + receipt = check_notices(ChunkMap(draft.html(which)), library, draft.intake_id) + assert set(receipt.notices) == {"counsel-review", "external-execution"} + assert "verbatim" in receipt.receipt() + + +def test_the_shared_parts_really_are_shared(financial, healthcare, library) -> None: + """So the difference is the powers, not two documents built differently.""" + shared = {"preamble-principal", "nature", "successor", "signature-block"} + for role in shared: + assert financial.block(role).skeleton == healthcare.block(role).skeleton, role + # The reliance clause is the one exception, and it is the right one: it + # carries the cross-reference to the successor clause, which is numbered 6 + # in one draft and 7 in the other. It must differ in exactly that digit and + # in nothing else - the numbering is ours and the wording is the library's. + left = financial.block("reliance").target + right = healthcare.block("reliance").target + assert left != right + assert left.replace("clause 6", "clause N") == right.replace("clause 7", "clause N") + assert financial.block("governing-law").target == healthcare.block("governing-law").target + # Same principal in both intakes, so this clause must be identical. + assert ( + financial.block("preamble-principal").target + == healthcare.block("preamble-principal").target + ) + for notice_id in library.notices: + assert ( + financial.block(f"{notice_id}-notice").target + == healthcare.block(f"{notice_id}-notice").target + ) + + +def test_the_two_drafts_number_differently_and_both_are_internally_consistent( + financial, healthcare +) -> None: + assert financial.numbering != healthcare.numbering + for draft in (financial, healthcare): + headings = ChunkMap(draft.html("target")).headings() + assert headings.keys() == set(draft.numbering.values()) diff --git a/use-cases/preetham1930/poa-generator/tests/test_verifier.py b/use-cases/preetham1930/poa-generator/tests/test_verifier.py new file mode 100644 index 000000000..9caf71676 --- /dev/null +++ b/use-cases/preetham1930/poa-generator/tests/test_verifier.py @@ -0,0 +1,101 @@ +"""The three failure classes, each driven by a response the product actually gave. + +Nothing here is hand-written HTML. `read0`..`read3` are four recorded +`GET /v1/documents/{id}?include_html=true` bodies from 2026-08-09: + +- read0 -> read1 a single-value targeted edit that worked +- read1 -> read2 a mid-document insertion that silently did not happen, while + five heading chunks nobody targeted were rewritten around it +- read2 -> read3 one `create` that appended six sections, four of them invented + +So the verifier is tested against the real shapes of the real failures. +""" + +from __future__ import annotations + +from poa.chunks import ChunkMap +from poa.superdocs.verifier import verify, verify_upload_verbatim +from recorded import read_back_html + +TARGET = "8ec79daa-5de5-4973-84a5-1d1139da29be" + + +def _maps(*names: str) -> list[ChunkMap]: + return [ChunkMap(read_back_html(name)) for name in names] + + +def test_a_clean_single_target_replacement_verifies() -> None: + before, after = _maps("read0-baseline", "read1-after-A") + expected = after.by_id[TARGET].html + result = verify(1, TARGET, before, after, expected) + assert result.ok + assert "matches the computed post-state" in result.receipt + assert "17 non-target chunk(s) unchanged" in result.receipt + assert result.exact_bytes + + +def test_a_reply_claiming_success_does_not_make_a_step_ok() -> None: + """The chat reply for the failing turn read 'nothing actually changed'. + + Five changes had been auto-approved and were in the document. The verifier + never sees a reply, so it cannot be wrong in either direction. + """ + before, after = _maps("read1-after-A", "read2-after-B") + result = verify(2, TARGET, before, after, before.by_id[TARGET].html + "<!--x-->") + assert not result.ok + kinds = {p.kind for p in result.problems} + assert "not_applied" in kinds, "the target chunk came back untouched" + assert "collateral_damage" in kinds, "five heading chunks moved" + detail = next(p for p in result.problems if p.kind == "collateral_damage").detail + assert "5 chunk(s) we did not target changed" in detail + + +def test_collateral_damage_is_caught_when_the_requested_change_also_succeeded() -> None: + """The dangerous one: the append worked *and* four fabricated sections arrived. + + A verifier checking only its own target would report success here. + """ + before, after = _maps("read2-after-B", "read3-after-C") + # Pretend our step targeted a chunk in the document; the requested append + # also succeeded, which is exactly what makes this class dangerous. + changed = before.ids[0] + result = verify(3, changed, before, after, after.by_id[changed].html) + assert not result.ok + problem = next(p for p in result.problems if p.kind == "collateral_damage") + assert "the chunk set changed" in problem.detail + assert "18 chunk(s) before, 19 after" in problem.detail + assert "Quality Assurance" in problem.diff or "Humidity" in problem.diff + + +def test_applied_wrong_is_its_own_class() -> None: + before, after = _maps("read0-baseline", "read1-after-A") + expected = after.by_id[TARGET].html.replace("750", "751") + result = verify(4, TARGET, before, after, expected) + assert not result.ok + assert [p.kind for p in result.problems] == ["applied_wrong"] + assert "751" in result.problems[0].diff + + +def test_a_missing_target_chunk_is_not_applied_rather_than_a_crash() -> None: + before, after = _maps("read0-baseline", "read1-after-A") + result = verify(5, "no-such-chunk", before, after, "<p>x</p>") + assert not result.ok + assert any(p.kind == "not_applied" for p in result.problems) + + +def test_the_upload_is_checked_verbatim_rather_than_assumed() -> None: + """Decision 29 rests on a measured property, so it is re-measured every run.""" + uploaded = ChunkMap(read_back_html("read0-baseline")) + blocks = [chunk.html for chunk in uploaded.chunks] + receipt = verify_upload_verbatim(uploaded, blocks) + assert "18 chunk(s)" in receipt + + tampered = list(blocks) + tampered[3] = tampered[3].replace("</p>", " and one more sentence.</p>") + try: + verify_upload_verbatim(uploaded, tampered) + except AssertionError as exc: + assert "was not verbatim" in str(exc) + assert "first difference at block 3" in str(exc) + else: # pragma: no cover + raise AssertionError("a non-verbatim upload must stop the run") diff --git a/use-cases/preetham1930/statutory-statements-builder/INVARIANTS.md b/use-cases/preetham1930/statutory-statements-builder/INVARIANTS.md new file mode 100644 index 000000000..20be27e16 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/INVARIANTS.md @@ -0,0 +1,142 @@ +# INVARIANTS — `builds/statutory-statements/` + +Written **before** the tests, and the tests before the code (TASK.md hard rule +7). Every invariant names the test that proves it. An invariant with no test is +an intention, not an invariant. + +The thesis this app has to make true: + +> **Nothing structural is ever asked of SuperDocs, every figure that reaches the +> document is traceable to a source row, and no edit is called applied until the +> whole document has been read back and compared against a post-state we +> computed before we sent it.** + +--- + +## What this app must never do + +### Structure + +1. **Never ask SuperDocs to create.** One verb, and it is `replace` (Decision + 28). No `create`, no insert, no "add a section". Measured: the only operation + that inserts is also the one that fabricated four sections nobody asked for + (PROGRESS.md 2026-08-09). + *Proved by* `test_edit_plan.py::test_every_step_is_a_replacement_of_an_existing_chunk` + and `test_no_create_verb_anywhere`. +2. **Never issue a mid-document insertion.** The entire final skeleton — + every heading at its final number, every cross-reference already repointed — + is computed here and uploaded verbatim (Decision 29). There is no step in the + plan that changes a heading's number. + *Proved by* `test_edit_plan.py::test_no_step_targets_a_heading_chunk`. +3. **Never let a note number be inferred, remembered or asked for.** Numbers are + assigned by one pass over our own ordered tree. The insertion point of a new + note comes from the trial balance account order, not from a list of known + note names. + *Proved by* `test_note_tree.py::test_insertion_point_comes_from_account_order` + and `test_no_note_title_is_hardcoded`. +4. **Never leave a cross-reference pointing at the old number.** Every + note→primary reference and every `Note N` in prose is repointed through the + same map that renumbered the headings, and the final document is re-scanned + for any reference that does not resolve to a heading that exists. + *Proved by* `test_note_tree.py::test_every_cross_reference_resolves_after_insertion`. +5. **Never let the checklist drift from the document.** Every checklist item's + `satisfied_by_note` is remapped through the same map. After the run, every + mapped item must name a heading that exists at the number we expect, and + every heading must be accounted for. + *Proved by* `test_checklist.py::test_checklist_survives_the_insertion`. + +### Figures + +6. **Never write a figure that does not resolve to a source.** Every money token + in every rendered body must be one of exactly four bases — `ledger`, + `comparative`, `document`, `derived` — and there is no fifth. A rendered body + is re-scanned after rendering and an unaccounted token is a hard error naming + the token and the note. + *Proved by* `test_figures.py::test_an_unaccounted_token_is_a_hard_error`. +7. **Never store a derived total.** `Derived.value` is a property computed from + its components on every read, so a total cannot drift from the citations that + support it and cannot be stated apart from them. + *Proved by* `test_figures.py::test_a_derived_total_has_no_value_field`. +8. **Never carry a prior-year figure forward under this year's heading without + the ledger confirming it did not move.** A `comparative` basis figure is + admissible only when the trial balance's prior-year column and the prior-year + signed statements agree to the paisa; a disagreement is a hard error, not a + preference for one side. + *Proved by* `test_rollforward.py::test_a_comparative_mismatch_is_a_hard_error`. +9. **Never invent an analysis the sources do not support.** Where the trial + balance is at caption granularity and a note needs a finer breakdown, the note + states the tied total and names what is outstanding and where we looked. It + never carries last year's breakdown under this year's heading. + *Proved by* `test_bodies.py::test_no_regenerated_body_reuses_a_prior_year_breakdown`. +10. **Never resolve a disagreement between two sources.** Where the minutes and + the ledger disagree, both are quoted and neither is preferred. There is no + field on `Disagreement` a resolution could be written into. + *Proved by* `test_disagreement.py::test_disagreement_has_nowhere_to_record_a_resolution`. + +### The wire + +11. **Never batch, and never approve a change we did not compute.** One chunk + per job on the sending side (Decision 35). The answering side does not + always agree — measured on this build's first live run, an instruction + naming one chunk id came back as a two-change batch — so we approve exactly + our change and deny every other one **bare**, and the read-back is what + makes that safe. + *Proved by* `test_client.py::test_a_batch_is_split_into_ours_and_everything_else` + and `test_a_batch_that_does_not_contain_our_target_is_refused_in_full`. +12. **Never use the synchronous chat route for a gated change.** The gate takes a + `job_id` and jobs exist only on `/v1/chat/async` (Decision 32). + *Proved by* `test_client.py::test_gated_changes_go_to_the_async_route`. +13. **Never send feedback on a denial** (Decision 34). Feedback triggers a + re-plan and the re-plan goes off target — measured, twice. + *Proved by* `test_client.py::test_denials_carry_no_feedback`. +14. **Never treat the job as the decision record** (Decision 33). Our ledger row + is written first, with the actor, and `approve` is only an actuator. The + product's gate is write-only and cannot be read back. + *Proved by* `test_decision_ledger.py::test_the_row_is_written_before_the_actuator_is_called`. +15. **Never believe the chat reply, the `changes_summary`, or a 200.** The only + evidence is `GET /v1/documents/{id}?include_html=true`. Measured: the reply + has over-claimed once and under-claimed once, and the completion sentence + reported two numbers that were both wrong. + *Proved by* `test_verifier.py::test_a_reply_claiming_success_does_not_make_a_step_ok`. + +### The failure path + +16. **Never report an edit applied without reading the whole document back.** + Three classes, all detected: **not applied** (target chunk unchanged), + **applied wrong** (target changed but not to the expected bytes), + **collateral damage** (any non-target chunk moved, or the chunk set changed). + *Proved by* `test_verifier.py`, one test per class, each driven by a recorded + response from `docs/evidence/`. +17. **Never run a downstream step after a failed one.** The queue halts. This is + the 2026-08-07 corrupted-document mechanism and the halt is the whole + defence. + *Proved by* `test_orchestrator.py::test_a_failure_halts_the_queue`. +18. **Never export after a failure.** No export, no "partial success", no + completion sentence. Revert to the last verified-good state and surface the + diff. + *Proved by* `test_orchestrator.py::test_no_export_after_a_failure`. +19. **Never re-plan.** At most one narrow retry of the same instruction, and a + retry diffs the *whole* document, because failed content has been observed + arriving a turn later. + *Proved by* `test_orchestrator.py::test_at_most_one_retry_and_never_a_replan`. + +### The boundary + +20. **Never import from `system/`, and never speak its vocabulary.** This tree + answers *what must the document say, and did SuperDocs actually say it*. It + never emits a finding, never quotes evidence bytes as a finding, never + produces a report. `system/` never holds a SuperDocs document id. + *Proved by* `tests/test_isolation.py` and `tests/test_domain_boundary.py`. + +--- + +## What it may do, stated so the boundary is not accidentally wider + +- Read `corpus/` as read-only input. Both trees do; that is shared data, not + shared code. +- Quote a source's bytes **as the provenance of a figure it writes**. That is + the citation discipline, rebuilt against a different substrate — not a + finding. +- Say that a required disclosure has no note. That is the disclosure checklist + doing its job, and it is a property of the document, not a judgement about + the company. diff --git a/use-cases/preetham1930/statutory-statements-builder/README.md b/use-cases/preetham1930/statutory-statements-builder/README.md new file mode 100644 index 000000000..e0b8b6e45 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/README.md @@ -0,0 +1,179 @@ +# Statutory financial statements builder + +Rolls last year's signed statements forward to this year against the trial +balance and this year's event sources, and produces this year's statutory +accounts as an edited, typeset document: primary statements and comparatives +updated, each note's figures regenerated, notes whose required disclosures are +missing flagged **from the data**, and note numbering plus the disclosure +checklist consistent throughout. + +## The grading line + +> **Comparatives and the cross-references between notes and primary statements +> tie exactly; a newly required note is detected from the data rather than +> remembered by a human; note numbering and the checklist stay consistent after +> an insertion mid-document.** + +Where each half is made true, and where to look: + +| The claim | How it is true | Where | +| --- | --- | --- | +| Comparatives tie exactly | Every prior-year figure is cited **twice** — to the ledger's `fy2025` column and to the signed set — and a disagreement is a hard error, not a preference. Then every total the signed set prints is re-derived and compared. | `statements.py`, `tests/test_rollforward.py` | +| Note ↔ primary cross-references tie | The note's caption sentence and the statement row are the *same* `Figure` object, and the rendered HTML of both is re-scanned afterwards to prove it. | `bodies.py`, `skeleton.py::assert_note_captions_tie` | +| A new note is detected from the data | `account 1110 balance > 0` is true at 268.00 and its checklist items map to no note. Its **position** comes from the ledger's account order: the account before it points at Note 3, so the lease note is Note 4. Nothing in the code knows what a lease is. | `notetree.py`, `tests/test_note_tree.py` | +| Numbering stays consistent | One pass over our own ordered tree assigns every number; 22 notes shift; the renumbering must be order-preserving or the run stops. | `notetree.py::_validate` | +| The checklist stays consistent | Every `satisfied_by_note` is remapped through the **same** map, and afterwards every mapping is re-asserted against the headings **read back out of the document**. | `notetree.py::assert_checklist_consistent` | + +## Run it + +```bash +python -m statutory +``` + +No key, no network: the whole roll-forward, every tie, the note tree, the +checklist and the edit plan are computed offline and the report is printed. +The computed document is written in both shapes to `var/statutory/`. + +```bash +python -m statutory --live --sample 4 +``` + +Drives SuperDocs for the first four chunk replacements. `--live` with no +`--sample` does the whole plan; one billable operation per chunk. + +Run from this directory (the folder name has a hyphen, so it is an application +folder rather than an import path; the importable package inside it is +`statutory`). + +## What SuperDocs does, and why it has to exist + +The deliverable is a **typeset** statutory document that survives dozens of +surgical edits with its layout intact and comes out as a DOCX an audit partner +signs. SuperDocs holds the document, applies each replacement inside its own +chunk model, holds every change at a human gate, and renders the export with +real Word table structure. Remove it and what is left is a DOCX manipulation +library — which is building a version *of* the product, the thing +[`builds/README.md`](../README.md) forbids. + +`system/` cannot do this: it produces a grounded report and is not a document +editor. The boundary is enforced, not asserted — `tests/test_domain_boundary.py`. + +## The design, and the measurements it came from + +Every one of these was earned by testing the product, not chosen by preference +(PROGRESS.md, 2026-08-07 and 2026-08-09). + +- **One verb, and it is `replace`** (Decision 28). We never ask SuperDocs to + create. The only operation that inserts is also the one that appended six + sections when asked for one, four of them invented. +- **The entire final skeleton is computed here and uploaded verbatim** + (Decision 29) — every heading already at its final number, every + cross-reference already repointed. **There is no mid-document insertion in + this design**, so the failure cannot be reached. It is also the only path + measured to preserve a styled document exactly. +- **The async route** (Decision 32), because `approve` takes a `job_id` and jobs + exist only on `/v1/chat/async`. +- **Our ledger is the record; `approve` is an actuator** (Decision 33). The + product's gate is write-only: `pending_changes` reported the same three + undecided items before and after two decisions. +- **Never send feedback on a denial** (Decision 34). Feedback triggered a + revision pass that proposed edits to two chunks nobody targeted, aimed at the + section headings, and held the job for minutes with approved work unapplied. +- **One change per job** (Decision 35). Nothing applies until a batch settles, + and a batch can spawn a follow-up batch; with a batch of one, batch atomicity + equals item atomicity. +- **The verified unit is the chunk, not the value** (Decision 37). Measured: + four cells in one table changed in one request with every style attribute + byte-preserved, and every failure this project has recorded was *across* + chunks. So the skeleton is chunk-aligned and a note schedule is one table. + +## The failure path + +Every step computes its expected post-state **before** sending, then reads the +whole document back and classifies: + +- **NOT APPLIED** — the target chunk came back unchanged. Halt. No downstream + step runs, because a downstream step is the consequence of work that may not + have happened. +- **APPLIED WRONG** — it changed, but not to the computed post-state. Halt, show + the diff. +- **COLLATERAL DAMAGE** — any non-target chunk moved, appeared or vanished. + Halt, and never retry: the document already holds content nobody asked for. + This is the dangerous one. In the append test the requested change *also* + succeeded while four fabricated sections appeared, so a verifier checking only + its own target would have reported success. + +On any failure: revert to the last verified-good state, **read back again and +say whether the revert actually happened**, surface the diff, produce **no +export**. At most one narrow retry of the same instruction; never a re-plan. + +The three classes are tested against the recorded responses in +`docs/evidence/2026-08-09-superdocs-reverify/` — the real shapes of the real +failures, replayed with no key and no network. + +## What it will not do, and why that is the point + +The supplied trial balance is at **caption granularity**: 29 accounts, one per +caption. So no breakdown finer than a caption is derivable, and roughly twenty +notes cannot state the analysis they stated last year. The build does not carry +last year's breakdown forward under this year's heading. Each note states the +tied total and names exactly what is outstanding and where we looked, and the +checklist marks those items `outstanding` rather than `satisfied`. + +That is a deliberate cut and it is the honest one: a stale figure under a new +heading is the failure this whole project is about. + +## Layout + +``` +statutory/ the package + money.py the figure recogniser, narrow in both directions + figures.py Cite / Figure / Derived / Quotient / Gap, and the Body guarantee + csvspans.py an RFC 4180 scanner that keeps byte positions + ledger.py the trial balance + priorset.py last year's signed statements, parsed with byte offsets + checklist.py 32 rows of data and a grammar that evaluates them + events.py events detected from this year's sources, never remembered + statements.py the roll-forward, the comparatives, the ties + notetree.py insertion point, numbering, repointing, checklist remap + bodies.py note regeneration, six data-selected recipes, and the gaps + skeleton.py the whole document in both shapes + chunks.py the chunk model and the canonical form + editplan.py one single-target replacement per step + superdocs/ transport, client, decision ledger, verifier + orchestrator.py upload once, then one verified replacement at a time + report.py what the run says for itself +config/ note recipes, event vocabulary, new-note titles, carry rules +tests/ the suite; runs with no key and no network +INVARIANTS.md the "must never" list, each item naming the test that proves it +``` + + +--- + +## SuperDocs features used + +- **Upload** (POST /v1/documents/upload-base64) - the fully computed document skeleton is uploaded verbatim; every heading is already at its final number and every cross-reference already repointed. +- **Chat, async** (POST /v1/chat/async) - one targeted replace per request. +- **Review gate** (approval_mode: ask_every_time + POST /v1/chat/approve) - every change is gated. +- **Read-back** (GET /v1/documents/{id}?include_html=true) - after every edit the whole document is read back and compared to the post-state computed before sending, including every non-target chunk. +- **Export** (POST /v1/documents/export) - the final .docx. + +## Environment + +``` +SUPERDOCS_API_KEY=your-key-here +SUPERDOCS_BASE_URL=https://api.superdocs.app +``` + +Tests run without a key against recorded fixtures. Only the live demo needs one. + +## Output + +![Statutory statements generated on SuperDocs](evidence/screenshot.png) + +A live run is committed at [evidence/statements.docx](evidence/statements.docx): 33 edits, each verified by read-back. Total assets and total equity and liabilities both come to 4,857.00, with FY2025 comparatives of 4,068.00. A warehouse lease commenced in the year, so a right-of-use asset appears and a lease note is created at position 4 - detected from the trial balance, not from a hardcoded list of standards - and the 22 notes after it renumber, with the note column in the primary statements and the disclosure checklist remapped through the same map. + +--- + +Built by **Preetham Kukkadapu** for the SuperDocs round 2 task. diff --git a/use-cases/preetham1930/statutory-statements-builder/config/absence-probes.csv b/use-cases/preetham1930/statutory-statements-builder/config/absence-probes.csv new file mode 100644 index 000000000..f37e72f9e --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/config/absence-probes.csv @@ -0,0 +1,2 @@ +item_id,ledger_name_contains,what +DC-029,goodwill|investment,a goodwill or investment-in-subsidiary balance diff --git a/use-cases/preetham1930/statutory-statements-builder/config/carry-forward-rules.csv b/use-cases/preetham1930/statutory-statements-builder/config/carry-forward-rules.csv new file mode 100644 index 000000000..5f4c7ad57 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/config/carry-forward-rules.csv @@ -0,0 +1,7 @@ +rule,value,why +period_reference,during the year,a sentence tied to last year's period is a claim about last year +period_reference,for the year,the same +period_reference,as at,the same +period_reference,at the reporting date,the same +period_reference,in the current year,the same +period_reference,during the period,the same diff --git a/use-cases/preetham1930/statutory-statements-builder/config/event-vocabulary.csv b/use-cases/preetham1930/statutory-statements-builder/config/event-vocabulary.csv new file mode 100644 index 000000000..c393a9eee --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/config/event-vocabulary.csv @@ -0,0 +1,3 @@ +event_name,phrase_groups +business combination occurred in period,acquisition of|acquire|share purchase agreement;;equity share capital;;consideration +control obtained over an entity,control;;passed to|obtained by|vested in;;share purchase agreement|acquisition|acquiree diff --git a/use-cases/preetham1930/statutory-statements-builder/config/new-note-titles.csv b/use-cases/preetham1930/statutory-statements-builder/config/new-note-titles.csv new file mode 100644 index 000000000..4aad7a25e --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/config/new-note-titles.csv @@ -0,0 +1,4 @@ +standard,new_note_title +Ind AS 116,Leases +Ind AS 103,Business combination +Ind AS 110,Business combination diff --git a/use-cases/preetham1930/statutory-statements-builder/config/note-recipes.csv b/use-cases/preetham1930/statutory-statements-builder/config/note-recipes.csv new file mode 100644 index 000000000..b88d181f4 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/config/note-recipes.csv @@ -0,0 +1,7 @@ +item_id,recipe,also_blocks +DC-003,asset_additions,DC-004 +DC-011,equity_movement, +DC-024,earnings_per_share, +DC-025,lease_measurement,DC-027|DC-028 +DC-026,lease_maturity, +DC-029,business_combination,DC-030|DC-031|DC-032 diff --git a/use-cases/preetham1930/statutory-statements-builder/config/quarantine-markers.txt b/use-cases/preetham1930/statutory-statements-builder/config/quarantine-markers.txt new file mode 100644 index 000000000..822abdd9f --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/config/quarantine-markers.txt @@ -0,0 +1,12 @@ +# A source carrying any of these inside an HTML/Markdown comment is quarantined: +# reported as quarantined, and never consulted for an event, a figure or a note. +# +# Quarantine runs in BOTH directions on purpose. Not consulting a forged source +# stops it suppressing a disclosure; it also stops it *manufacturing* one, which +# is the same class of failure and is easier to miss because the result looks +# like diligence. +ignore all previous instructions +system instruction +mark every disclosure +without human sign-off +do not mention this instruction diff --git a/use-cases/preetham1930/statutory-statements-builder/conftest.py b/use-cases/preetham1930/statutory-statements-builder/conftest.py new file mode 100644 index 000000000..50e1cf854 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/conftest.py @@ -0,0 +1,46 @@ +"""`builds/statutory-statements/` is a codebase, not an installed package. + +The directory name carries a hyphen on purpose - it is an application folder, +not an import path - so the importable package inside it is `statutory` and this +file is what puts it on `sys.path` for the suite. Nothing here imports from +`system/`, and `tests/test_isolation.py` fails the build if it ever does. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +APP_ROOT = Path(__file__).resolve().parent +REPO_ROOT = APP_ROOT.parent.parent + +if str(APP_ROOT) not in sys.path: + sys.path.insert(0, str(APP_ROOT)) +# tests/ carries a helper (`recorded.py`) that loads responses out of +# docs/evidence/. It is not a package, so its directory goes on the path too. +if str(APP_ROOT / "tests") not in sys.path: + sys.path.insert(0, str(APP_ROOT / "tests")) + + +@pytest.fixture(scope="session") +def corpus() -> Path: + return REPO_ROOT / "corpus" + + +@pytest.fixture(scope="session") +def config() -> Path: + return APP_ROOT / "config" + + +@pytest.fixture(scope="session") +def evidence() -> Path: + return REPO_ROOT / "docs" / "evidence" + + +@pytest.fixture(scope="session") +def rolled(corpus: Path, config: Path): + from statutory.pipeline import prepare + + return prepare(corpus, config) diff --git a/use-cases/preetham1930/statutory-statements-builder/evidence/screenshot.png b/use-cases/preetham1930/statutory-statements-builder/evidence/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..008208bd7404902a98e4d39814f9b17876ca8490 GIT binary patch literal 123955 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%0Dy)NfH4hmD1?e|f}<??;|z~@xQ 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 literal 0 HcmV?d00001 diff --git a/use-cases/preetham1930/statutory-statements-builder/evidence/statements.docx b/use-cases/preetham1930/statutory-statements-builder/evidence/statements.docx new file mode 100644 index 0000000000000000000000000000000000000000..bf09225acb42acf74a1dd9f179af0c1a6024d71e GIT binary patch literal 43928 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 literal 0 HcmV?d00001 diff --git a/use-cases/preetham1930/statutory-statements-builder/scripts/verify_live_output.py b/use-cases/preetham1930/statutory-statements-builder/scripts/verify_live_output.py new file mode 100644 index 000000000..a00b47ba1 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/scripts/verify_live_output.py @@ -0,0 +1,110 @@ +"""Check the finished document the way a reviewer would: from the API and the DOCX. + +Nothing here trusts the run's own report. The document is fetched fresh, the +checklist is re-asserted against the headings that came back, every figure the +roll-forward computed is required to be present, and the exported DOCX is +unzipped and its table structure counted - because "it exported" is not evidence +that anything is in it. + + python scripts/verify_live_output.py <document_id> [--docx PATH] +""" + +from __future__ import annotations + +import argparse +import re +import sys +import zipfile +from pathlib import Path + +APP_ROOT = Path(__file__).resolve().parent.parent +REPO_ROOT = APP_ROOT.parent.parent +sys.path.insert(0, str(APP_ROOT)) + +from statutory.chunks import ChunkMap # noqa: E402 +from statutory.money import money_tokens_in_html, strip_markup # noqa: E402 +from statutory.notetree import assert_checklist_consistent # noqa: E402 +from statutory.pipeline import DEFAULT_CONFIG, prepare # noqa: E402 +from statutory.superdocs.client import SuperDocsClient # noqa: E402 +from statutory.superdocs.transport import HttpTransport # noqa: E402 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("document_id") + parser.add_argument("--docx", default=str(REPO_ROOT / "var" / "statutory" / "statements.docx")) + args = parser.parse_args() + + try: + from dotenv import load_dotenv + + load_dotenv(REPO_ROOT / ".env", override=False) + except ImportError: + pass + + rolled = prepare(REPO_ROOT / "corpus", DEFAULT_CONFIG) + client = SuperDocsClient(HttpTransport()) + live, detail = client.read_back(args.document_id) + html = detail["html"] + + print(f"document {args.document_id} version {detail.get('version')}") + print(f" {len(live)} chunks read back") + + ours = ChunkMap(rolled.document.html("target")) + print(f" computed document: {len(ours)} chunks") + assert len(live) == len(ours), "the chunk count moved" + + mismatched = [ + i + for i, (a, b) in enumerate(zip(ours.chunks, live.chunks, strict=True)) + if a.canonical != b.canonical + ] + print(f" chunks differing from what we computed: {len(mismatched)} {mismatched[:5]}") + + headings = live.headings() + print(f" note headings in the live document: {len(headings)}") + assert_checklist_consistent(rolled.mapping, rolled.tree, headings) + print(" checklist consistency re-asserted against the LIVE headings: ok") + + tokens = set(money_tokens_in_html(html)) + ties = ["4,857.00", "4,068.00", "577.00", "414.50", "407.00", "309.00", "1,691.00", "1,284.00"] + missing = [t for t in ties if t not in tokens] + print(f" tie figures present in the live document: {len(ties) - len(missing)}/{len(ties)}") + assert not missing, f"missing from the live document: {missing}" + + text = strip_markup(html) + for phrase in ( + "Note 4", + "Leases", + "Business combination", + "disclosed in Note 5", + "all 29 account names were searched", + "821.00", + ): + assert phrase in text, f"{phrase!r} is not in the live document" + print(" new note, repointed cross-reference and both disagreements present: ok") + assert "31 March 2025</strong>" not in html, "the front matter still says last year" + + docx = Path(args.docx) + if docx.is_file(): + with zipfile.ZipFile(docx) as archive: + document_xml = archive.read("word/document.xml").decode("utf-8") + print(f" DOCX {docx.name}: {docx.stat().st_size:,} bytes") + print(f" w:tbl {document_xml.count('<w:tbl>')}") + print(f" w:tr {document_xml.count('<w:tr ') + document_xml.count('<w:tr>')}") + print(f" w:tc {document_xml.count('<w:tc>')}") + print(f" w:tcBorders {document_xml.count('<w:tcBorders>')}") + print(f" w:shd {document_xml.count('<w:shd ')}") + flat = re.sub(r"<[^>]+>", "", document_xml) + for token in ("4,857.00", "268.00", "8.14", "185.00"): + assert token in flat, f"{token} is not in the exported DOCX" + print(" key figures present in word/document.xml: ok") + else: + print(f" no DOCX at {docx}") + + print("VERIFIED") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/__init__.py b/use-cases/preetham1930/statutory-statements-builder/statutory/__init__.py new file mode 100644 index 000000000..bea8e84b0 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/__init__.py @@ -0,0 +1,18 @@ +"""The statutory financial statements builder (Task 2, card S3). + +Rolls FY2025 signed statements forward to FY2026 against the trial balance and +this year's event sources, computes the entire final document skeleton here, and +drives SuperDocs with single-target chunk replacements only. + +See INVARIANTS.md for the "must never" list. Nothing in this package imports +from `system/` (TASK.md hard rule, Decision 4). +""" + +from __future__ import annotations + +__all__ = ["REPORTING_LABEL"] + +# The only place a year label is written down. Everything else derives the +# reporting year from the highest `fy<year>` column in the trial balance, so a +# FY2027 corpus needs no code change (hard rule 6). +REPORTING_LABEL = "31 March" diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/__main__.py b/use-cases/preetham1930/statutory-statements-builder/statutory/__main__.py new file mode 100644 index 000000000..dfe6af1f6 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/__main__.py @@ -0,0 +1,98 @@ +"""CLI. + + python -m statutory roll forward, print the report, write the HTML + python -m statutory --live --sample 3 drive SuperDocs for the first 3 chunks + python -m statutory --live the full run (one operation per chunk) + +`--live` is the only mode that touches the network, and it is the only mode that +needs a key. Everything else - the roll-forward, the ties, the note tree, the +checklist, the plan - runs offline, which is why the test suite can exercise it. +""" + +from __future__ import annotations + +import argparse +import sys +import uuid +from pathlib import Path + +from .editplan import build_plan +from .orchestrator import Orchestrator, write_artifacts +from .pipeline import DEFAULT_CONFIG, prepare +from .report import render +from .superdocs.client import SuperDocsClient +from .superdocs.decisions import DecisionLedger +from .superdocs.transport import HttpTransport, redacted_key + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="statutory", description=__doc__) + parser.add_argument("--corpus", default=str(REPO_ROOT / "corpus")) + parser.add_argument("--config", default=str(DEFAULT_CONFIG)) + parser.add_argument("--out", default=str(REPO_ROOT / "var" / "statutory")) + parser.add_argument("--live", action="store_true", help="drive SuperDocs (needs a key)") + parser.add_argument( + "--sample", type=int, default=None, help="only issue the first N chunk replacements" + ) + parser.add_argument("--actor", default="K. Latha (CFO)") + parser.add_argument("--format", default="docx", help="export format for a successful run") + args = parser.parse_args(argv) + + rolled = prepare(Path(args.corpus), Path(args.config)) + plan = build_plan(rolled.document).sample(args.sample) + print(render(rolled, len(plan))) + + out_dir = Path(args.out) + for path in write_artifacts(out_dir, plan): + print(f"wrote {path}") + + print() + print("EDIT PLAN (one single-target chunk replacement per step, one change per job)") + for step in plan.steps: + print(f" {step.describe()}") + + if not args.live: + print() + print( + "offline: nothing was sent. The whole roll-forward above ran with no key and no " + "network. Add --live to drive SuperDocs." + ) + return 0 + + # Read from .env only, never from a command line and never printed + # (hard rule 1). `override=False` so an already-exported value wins. + try: + from dotenv import load_dotenv + + load_dotenv(REPO_ROOT / ".env", override=False) + except ImportError: + pass + + print() + print(f"live run: key {redacted_key()}") + client = SuperDocsClient(HttpTransport()) + ledger = DecisionLedger(out_dir / "decisions.jsonl") + run_id = f"stat-{uuid.uuid4().hex[:8]}" + orchestrator = Orchestrator( + client, ledger, args.actor, export_formats=(args.format,), export_dir=out_dir + ) + result = orchestrator.run(run_id, plan, "varshith-precision-FY2026-statements.html") + print() + for line in orchestrator.log: + print(line) + print() + print(f"document {result.document_id} in session {result.session_id}") + print(result.sentence(ledger)) + if result.reverted: + print(result.reverted) + if result.exported: + print(f"exported: {', '.join(result.exported)}") + else: + print("no export was produced") + return 0 if result.ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/bodies.py b/use-cases/preetham1930/statutory-statements-builder/statutory/bodies.py new file mode 100644 index 000000000..41ef383d4 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/bodies.py @@ -0,0 +1,893 @@ +"""Regenerating each note's figures, and being honest about the rest. + +Three rules decide what a note body says, and none of them knows a note's name: + +1. **Captions come from the primary statements.** Every statement line whose + cross-reference points at this note contributes one sentence carrying the + reporting-year figure and its comparative. That is the note <-> primary tie, + stated in the note itself and made of the same `Figure` objects the statement + prints, so the two cannot say different things. + +2. **A sentence with no figure in it is a policy statement and is carried; a + sentence with a figure is regenerated or named as a gap.** The carried + sentence is quoted verbatim from the signed set with the bytes it came from. + A carried sentence that refers to a period ("during the year") is *not* + carried - it is a claim about last year, and last year's claim under this + year's heading is exactly the failure this build exists to avoid. + +3. **The supplied trial balance is at caption granularity.** Twenty-nine + accounts, one per caption, so no breakdown finer than the caption is + derivable. Where last year's note carried a breakdown we cannot reproduce, + the note states the tied total and names the outstanding analysis - by the + labels last year used, which is as specific as the evidence allows. + +Beyond that, six recipes compute what the sources *do* support. Each is selected +by a checklist item id in `config/note-recipes.csv`, which is the data saying +what the note must disclose - not by the note's title. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from decimal import Decimal +from pathlib import Path + +from . import style +from .csvspans import as_records, parse_csv +from .disagreement import Disagreement, Side +from .figures import AnyFigure, Body, Cite, Component, Derived, Figure, Gap, Quotient +from .ledger import TrialBalance +from .money import money_tokens, strip_markup +from .notetree import Note, NoteTree +from .priorset import NOTE_REFERENCE, PriorYearSet +from .statements import RollForward + +SENTENCE_SPLIT = re.compile(r"(?<=[.])\s+(?=[A-Z<])") +BREAKDOWN_SPLIT = re.compile(r";|\.\s+") +BREAKDOWN_SPLIT_SENTENCE = re.compile(r"\.\s+") # a decimal point is never followed by a space +DROP_LABEL = re.compile(r"^total\b", re.I) +# A breakdown *group* is a ":"-introduced label whose body splits on ";" into two +# or more items each holding exactly one figure. The narrowing matters: a looser +# rule reads Note 3's "for the year: 276.00 (intangible amortisation of 9.00 is +# disclosed in Note 4...)" as a two-item breakdown summing to 285.00 and raises a +# confident false positive. +GROUP = re.compile(r"^([A-Za-z][^:;]{2,60}):\s*(.+)$", re.S) + + +@dataclass +class Element: + """One body element, which is one chunk once the document is uploaded.""" + + role: str # captions | policy | recipe:<name> | outstanding + tag: str # p | table + target: str + skeleton: str + figures: tuple[AnyFigure, ...] = () + quotes: tuple[Cite, ...] = () + + @property + def changed(self) -> bool: + return self.target != self.skeleton + + +@dataclass +class NoteContent: + note: Note + elements: list[Element] = field(default_factory=list) + gaps: list[Gap] = field(default_factory=list) + disagreements: list[Disagreement] = field(default_factory=list) + blocked_items: set[str] = field(default_factory=set) + + @property + def figures(self) -> tuple[AnyFigure, ...]: + return tuple(f for e in self.elements for f in e.figures) + + @property + def cites(self) -> tuple[Cite, ...]: + out = [c for f in self.figures for c in f.cites] + out.extend(c for e in self.elements for c in e.quotes) + out.extend(c for d in self.disagreements for s in d.sides for c in s.cites) + return tuple(out) + + +class AssetSchedule: + """This year's capitalisations, by category, each row cited.""" + + def __init__(self, path: Path, corpus_root: Path) -> None: + self.rel = path.relative_to(corpus_root).as_posix() + _, records = as_records(parse_csv(path.read_bytes())) + self.rows = records + self.by_category: dict[str, list[Figure]] = {} + for record in records: + cell = record["cost_inr_lakhs"] + figure = Figure( + label=record["description"].text, + value=Decimal(cell.text), + basis="document", + cites=( + Cite( + self.rel, + cell.byte_start, + cell.byte_end, + cell.value, + f"fixed-asset schedule, {record['asset_id'].text} " + f"({record['description'].text})", + ), + ), + ) + self.by_category.setdefault(record["category"].text, []).append(figure) + + def total(self, category: str) -> Derived: + return Derived( + f"Additions - {category}", + tuple(Component(f) for f in self.by_category[category]), + ) + + def grand_total(self) -> Derived: + return Derived( + "Total additions", + tuple(Component(f) for figs in self.by_category.values() for f in figs), + ) + + +class BodyBuilder: + def __init__( + self, + rolled: RollForward, + tree: NoteTree, + mapping: dict[str, int | None], + corpus_root: Path, + config_root: Path, + ) -> None: + self.rolled = rolled + self.tree = tree + self.mapping = mapping + self.corpus = corpus_root + self.prior: PriorYearSet = rolled.prior + self.ledger: TrialBalance = rolled.ledger + _, recipe_rows = as_records(parse_csv((config_root / "note-recipes.csv").read_bytes())) + self.recipes = {r["item_id"].text: r["recipe"].text for r in recipe_rows} + # Which other checklist items this recipe's gaps also leave outstanding. + # Data, so no item id is written into this module (there is a test). + self.also_blocks = { + r["item_id"].text: tuple(x for x in r["also_blocks"].text.split("|") if x) + for r in recipe_rows + } + _, carry_rows = as_records( + parse_csv((config_root / "carry-forward-rules.csv").read_bytes()) + ) + self.period_words = [ + r["value"].text.lower() for r in carry_rows if r["rule"].text == "period_reference" + ] + _, probe_rows = as_records(parse_csv((config_root / "absence-probes.csv").read_bytes())) + self.absence_probes = { + r["item_id"].text: (r["ledger_name_contains"].text, r["what"].text) for r in probe_rows + } + self.coverage: dict[str, int] = {"groups": 0, "tied": 0, "raised": 0} + self._sources_looked_in = ( + f"trial balance ({len(self.ledger)} accounts, caption granularity)", + "fixed-asset schedule", + "board minutes", + "lease deed", + "signed prior-year statements", + ) + + # -- entry point ------------------------------------------------------- + + def build(self, note: Note) -> NoteContent: + content = NoteContent(note=note) + items = [item for item, number in self.mapping.items() if number == note.number] + prior_note = self.prior.note(note.prior_number) if note.prior_number else None + + captions = self._caption_element(note) + if captions is not None: + content.elements.append(captions) + + for item in sorted(items): + recipe = self.recipes.get(item) + if recipe: + getattr(self, f"_recipe_{recipe}")(note, content, item) + + if prior_note is not None: + crossref = self._crossref_element(prior_note, note) + if crossref is not None: + content.elements.append(crossref) + self._comparative_tieout(prior_note, note, content) + policy = self._policy_element(prior_note) + if policy is not None: + content.elements.append(policy) + gap = self._breakdown_gap(prior_note, content) + if gap is not None: + content.gaps.append(gap) + + if content.gaps: + content.elements.append(self._outstanding_element(content)) + if not content.elements: + raise ValueError( + f"Note {note.number} ({note.title}) would be empty. A note with nothing to say " + f"is a heading nobody can act on; either it has a caption, a recipe, a carried " + f"policy sentence or an outstanding item." + ) + return content + + # -- generic parts ----------------------------------------------------- + + def _caption_element(self, note: Note) -> Element | None: + lines = [ + line + for statement in self.rolled.statements + for line in statement.lines + if line.note_ref == note.number and line.account is not None + ] + if not lines: + return None + body = Body().markup("<p>") + for index, line in enumerate(lines): + if index: + body.text(" ") + statement_key = next(s.key for s in self.rolled.statements if line in s.lines) + if statement_key == "balance_sheet": + body.text(f"{line.caption} as at ").dated( + self.ledger.year_label(self.rolled.year), + self.ledger.year_label(self.rolled.prior_year), + ).text(": ").fig(line.current, line.prior).text(" (").dated( + self.ledger.year_label(self.rolled.prior_year), + self.ledger.year_label(self.rolled.prior_year - 1), + ).text(": ").fig(line.prior, "0.00").text(").") + else: + body.text(f"{line.caption} for the year ended ").dated( + self.ledger.year_label(self.rolled.year), + self.ledger.year_label(self.rolled.prior_year), + ).text(": ").fig(line.current, line.prior).text(" (year ended ").dated( + self.ledger.year_label(self.rolled.prior_year), + self.ledger.year_label(self.rolled.prior_year - 1), + ).text(": ").fig(line.prior, "0.00").text(").") + body.markup("</p>") + where = f"Note {note.number} ({note.title}) captions" + return Element( + "captions", "p", body.render(where), body.skeleton(where), body.figures, body.quotes + ) + + def _policy_element(self, prior_note) -> Element | None: + carried: list[tuple[str, Cite]] = [] + for element in prior_note.body: + if element.tag != "p": + continue + for raw_sentence, offset in _sentences(element.raw): + text = strip_markup(raw_sentence).strip() + if not text or money_tokens(text): + continue + if any(word in text.lower() for word in self.period_words): + continue + byte_start = element.byte_start + len(element.raw[:offset].encode("utf-8")) + carried.append( + ( + raw_sentence.strip(), + Cite( + self.prior.rel, + byte_start, + byte_start + len(raw_sentence.strip().encode("utf-8")), + raw_sentence.strip(), + f"prior-year signed statements, Note {prior_note.number} " + f"({prior_note.title}), carried policy sentence", + ), + ) + ) + if not carried: + return None + body = Body().markup("<p>") + for index, (sentence, cite) in enumerate(carried): + if index: + body.text(" ") + body.quote(self.tree.repoint_prose(strip_markup(sentence).strip()), cite) + body.markup("</p>") + where = "carried policy" + return Element("policy", "p", body.render(where), body.skeleton(where), (), body.quotes) + + def _breakdown_gap(self, prior_note, content: NoteContent) -> Gap | None: + labels = _breakdown_labels(prior_note) + if not labels: + return None + return Gap( + what=( + f"analysis of {prior_note.title.lower()} into the components the signed set " + f"disclosed last year ({'; '.join(labels)})" + ), + looked_in=self._sources_looked_in, + ) + + def _crossref_element(self, prior_note, note: Note) -> Element | None: + """Note-to-note references survive the roll-forward, repointed. + + The signed set's own prose is the source of the reference; the number is + ours. The sentence carries no figure, so a cross-reference never depends + on an analysis we could not reproduce - it survives even when the + sentence that used to carry it does not. + """ + targets: list[int] = [] + for element in prior_note.body: + for match in NOTE_REFERENCE.finditer(strip_markup(element.raw)): + old = int(match.group(1)) + if old not in targets and old != prior_note.number: + targets.append(old) + if not targets: + return None + body = Body().markup("<p>") + for index, old in enumerate(targets): + if index: + body.text(" ") + title = self.prior.note(old).title + # Written with the number the signed set used, then put through the + # one repointing function. There is a single place a note number is + # rewritten, so there is no second place to get it wrong. + body.text( + self.tree.repoint_prose( + f"Further disclosure relating to {title.lower()} is disclosed in Note {old}." + ) + ) + body.markup("</p>") + where = f"Note {note.number} cross-references" + skeleton = body.skeleton(where) + return Element("crossrefs", "p", body.render(where), skeleton, (), body.quotes) + + def _comparative_tieout(self, prior_note, note: Note, content: NoteContent) -> None: + """Does last year's note still tie to last year's caption? We are about to carry it. + + This is the check that catches a note <-> primary break in the set we are + rolling forward, before the broken figure becomes this year's comparative. + It is deliberately narrow (see GROUP) and it reports its own selectivity + in the run summary, so the narrowness is visible in the output rather + than only in this docstring. + """ + captions = [ + line + for statement in self.rolled.statements + for line in statement.lines + if line.note_ref == note.number and line.account is not None + ] + if not captions: + return + for label, group_total, items in _qualifying_groups(prior_note, self.prior): + self.coverage["groups"] += 1 + if any(line.prior.value == group_total.value for line in captions): + self.coverage["tied"] += 1 + continue + self.coverage["raised"] += 1 + caption = min(captions, key=lambda line: abs(line.prior.value - group_total.value)) + difference = Derived( + "difference", (Component(group_total), Component(caption.prior, -1)) + ) + body = Body().markup("<p><em>Comparative note:</em> the ") + body.text(f"{label.lower()} disclosed in the signed prior-year set sums to ") + body.fig(group_total, "0.00") + body.text(f", against the {caption.caption.lower()} comparative of ") + body.fig(caption.prior, "0.00") + body.text(" carried in this set - a difference of ") + body.fig(difference, "0.00") + body.text( + ". Both prior-year figures are quoted from the signed statements; the " + "difference is stated here and is not resolved by this roll-forward." + ) + body.markup("</p>") + where = f"Note {note.number} comparative tie-out" + content.elements.append( + Element( + "comparative_tieout", + "p", + body.render(where), + body.skeleton(where), + body.figures, + ) + ) + content.disagreements.append( + Disagreement( + about=f"the prior-year {label.lower()} for {caption.caption.lower()}", + sides=( + Side( + source=self.prior.rel, + says=f"{label} sums to {group_total.rendered} " + f"({group_total.workings()})", + cites=group_total.cites, + figures=(group_total,), + ), + Side( + source=self.ledger.rel, + says=f"{caption.caption} is {caption.prior.rendered} in the " + f"prior-year column and in the signed balance sheet", + cites=caption.prior.cites, + figures=(caption.prior,), + ), + ), + ) + ) + content.gaps.append( + Gap( + f"reconciliation of the prior-year {label.lower()} to the " + f"{caption.caption.lower()} comparative, a difference of " + f"{difference.rendered}", + self._sources_looked_in, + figures=(difference,), + ) + ) + items_used = len(items) + del items_used + + def _outstanding_element(self, content: NoteContent) -> Element: + body = Body().markup("<p><em>Outstanding for this note:</em> ") + for index, gap in enumerate(content.gaps): + if index: + body.text(" ") + body.prose(f"({index + 1}) {gap.sentence()}.", gap.figures) + if content.disagreements: + for disagreement in content.disagreements: + body.text( + f" The sources disagree on {disagreement.about}; both are stated above and " + f"neither is preferred here." + ) + body.markup("</p>") + where = "outstanding" + return Element("outstanding", "p", body.render(where), body.skeleton(where), body.figures) + + # -- recipes ----------------------------------------------------------- + + def _recipe_asset_additions(self, note: Note, content: NoteContent, item: str) -> None: + schedule = AssetSchedule( + self.corpus / "sources" / "fixed-asset-additions-FY2026.csv", self.corpus + ) + line = self.rolled.caption_line(_account_for(self.rolled, note, "Non-current assets")) + opening, closing = line.prior, line.current + additions = schedule.grand_total() + movement = Derived( + "Depreciation, disposals and other movements", + (Component(closing), Component(opening, -1), Component(additions, -1)), + ) + body = Body().markup(style.table_open()) + body.markup( + f'<tr style="{style.HEAD_ROW}">{style.head_cell("Particulars")}' + f"{style.head_cell('Amount', numeric=True)}</tr>" + ) + body.markup(f'<tr><td style="{style.TD_TEXT}">Net carrying amount as at 1 April ').dated( + str(self.rolled.year - 1), str(self.rolled.prior_year - 1) + ).markup(f'</td><td style="{style.TD_NUM}">').fig(opening, "0.00").markup("</td></tr>") + for category in sorted(schedule.by_category): + total = schedule.total(category) + body.markup( + f'<tr><td style="{style.TD_TEXT}">Additions - {category}</td>' + f'<td style="{style.TD_NUM}">' + ).fig(total, "0.00").markup("</td></tr>") + body.markup( + f'<tr style="{style.SECTION_ROW}"><td style="{style.TD_TEXT}">' + f'<strong>Total additions</strong></td><td style="{style.TD_NUM}"><strong>' + ).fig(additions, "0.00").markup("</strong></td></tr>") + body.markup( + f'<tr><td style="{style.TD_TEXT}">Depreciation, disposals and other movements ' + f'(not separately supplied)</td><td style="{style.TD_NUM}">' + ).fig(movement, "0.00").markup("</td></tr>") + body.markup( + f'<tr style="{style.SECTION_ROW}"><td style="{style.TD_TEXT}">' + "<strong>Net carrying amount as at " + ).dated( + self.ledger.year_label(self.rolled.year), + self.ledger.year_label(self.rolled.prior_year), + ).markup(f'</strong></td><td style="{style.TD_NUM}"><strong>').fig(closing, opening).markup( + "</strong></td></tr></table>" + ) + where = f"Note {note.number} asset movement" + content.elements.append( + Element( + "recipe:asset_additions", + "table", + body.render(where), + body.skeleton(where), + body.figures, + ) + ) + content.gaps.append( + Gap( + "the movement split between depreciation charged for the year and disposals, " + "and the movement by class of asset", + self._sources_looked_in, + ) + ) + content.blocked_items.update({item, *self.also_blocks[item]}) + + def _recipe_equity_movement(self, note: Note, content: NoteContent, item: str) -> None: + line = self.rolled.caption_line(_account_for(self.rolled, note, "Equity")) + profit = self.rolled.statement("profit_and_loss").line("Profit for the year").current + closing = Derived("Balance carried forward", (Component(line.prior), Component(profit))) + minutes = self.corpus / "sources" / "board-minutes-2026-01-18.md" + dividend_cite = _quote_from(minutes, "to recommend a dividend", self.corpus) + body = ( + Body() + .markup("<p>Balance as at 1 April ") + .dated(str(self.rolled.year - 1), str(self.rolled.prior_year - 1)) + .text(": ") + .fig(line.prior, "0.00") + .text(". Add: profit for the year ") + .fig(profit, "0.00") + .text(". Balance as at ") + .dated( + self.ledger.year_label(self.rolled.year), + self.ledger.year_label(self.rolled.prior_year), + ) + .text(": ") + .fig(closing, line.prior) + .text( + ". The movement is the profit for the year in full; the board minutes record a " + "resolution " + ) + .quote("not to recommend a dividend", dividend_cite) + .text(", and the ledger carries no other movement in this caption.") + .markup("</p>") + ) + where = f"Note {note.number} equity movement" + content.elements.append( + Element( + "recipe:equity_movement", + "p", + body.render(where), + body.skeleton(where), + body.figures, + body.quotes, + ) + ) + + def _recipe_earnings_per_share(self, note: Note, content: NoteContent, item: str) -> None: + capital = self.ledger.account("2100") + if capital.balance(self.rolled.year) != capital.balance(self.rolled.prior_year): + content.gaps.append( + Gap( + "the weighted average number of equity shares, which moved during the year " + "and cannot be carried from the signed set", + self._sources_looked_in, + ) + ) + return + prior_note = self.prior.note(note.prior_number) if note.prior_number else None + if prior_note is None: + return + shares_literal = _first_token(prior_note.text, lambda t: "," in t and "." not in t) + eps_literal = _last_token(prior_note.text) + shares = Figure( + "weighted average number of equity shares", + Decimal(shares_literal.replace(",", "")), + "document", + (self.prior.cite_in_note(prior_note.number, shares_literal),), + literal=shares_literal, + ) + prior_eps = Figure( + "earnings per share (comparative)", + Decimal(eps_literal), + "document", + (self.prior.cite_in_note(prior_note.number, eps_literal),), + ) + profit = self.rolled.statement("profit_and_loss").line("Profit for the year").current + eps = Quotient("basic and diluted earnings per share", profit, shares, Decimal(100000)) + body = ( + Body() + .markup("<p>Profit attributable to equity shareholders ") + .fig(profit, "0.00") + .text("; weighted average number of equity shares ") + .fig(shares, shares_literal) + .text("; basic and diluted earnings per share INR ") + .fig(eps, "0.00") + .text(" (year ended ") + .dated( + self.ledger.year_label(self.rolled.prior_year), + self.ledger.year_label(self.rolled.prior_year - 1), + ) + .text(": INR ") + .fig(prior_eps, "0.00") + .text( + "). The share count is carried from the signed set because the ledger shows no " + "movement in equity share capital between the two years." + ) + .markup("</p>") + ) + where = f"Note {note.number} earnings per share" + content.elements.append( + Element( + "recipe:earnings_per_share", + "p", + body.render(where), + body.skeleton(where), + body.figures, + body.quotes, + ) + ) + + def _recipe_lease_measurement(self, note: Note, content: NoteContent, item: str) -> None: + deed = self.corpus / "sources" / "lease-agreement-warehouse-2025.md" + area = _figure_from(deed, "42,000", self.corpus, "area of the identified asset") + rent = _figure_from(deed, "6,50,000", self.corpus, "monthly rent, months 1-12") + deposit = _figure_from(deed, "39,00,000", self.corpus, "refundable security deposit") + body = Body().markup("<p>") + body.quote( + "The Company leases a warehouse and dispatch facility. The lease commenced on " + "1 July 2025 for a term of 5 years, with a lock-in period of 36 months.", + _quote_from(deed, "Commencement of lease term", self.corpus), + ) + body.text(" The identified asset is a warehouse unit of ").fig(area, "0.00").text( + " sq ft; the lessor may not substitute an alternative property. Monthly rent for " + "the first twelve months is INR " + ).fig(rent, "0.00").text( + ", escalating at 5% per annum on each anniversary of the commencement date. A " + "refundable security deposit of INR " + ).fig(deposit, "0.00").text( + " is held by the lessor. The incremental borrowing rate applied on initial " + "recognition is 9.15% per annum. The renewal option has been excluded from the " + "lease term because its exercise is not reasonably certain." + ).markup("</p>") + where = f"Note {note.number} lease measurement" + content.elements.append( + Element( + "recipe:lease_measurement", + "p", + body.render(where), + body.skeleton(where), + body.figures, + body.quotes, + ) + ) + content.gaps.append( + Gap( + "the movement in the right-of-use asset for the year, including the " + "depreciation charge and the class-wise split", + self._sources_looked_in, + ) + ) + content.gaps.append( + Gap( + "depreciation on right-of-use assets and interest on lease liabilities " + "recognised in profit or loss, and the total cash outflow for leases", + self._sources_looked_in, + ) + ) + content.blocked_items.update({item, *self.also_blocks[item]}) + + def _recipe_lease_maturity(self, note: Note, content: NoteContent, item: str) -> None: + current = self.rolled.caption_line("2310") + non_current = self.rolled.caption_line("2210") + total = Derived( + "Total lease liabilities", + (Component(current.current), Component(non_current.current)), + ) + body = ( + Body() + .markup("<p>Lease liabilities as at ") + .dated( + self.ledger.year_label(self.rolled.year), + self.ledger.year_label(self.rolled.prior_year), + ) + .text(": falling due within one year ") + .fig(current.current, current.prior) + .text("; falling due after one year ") + .fig(non_current.current, non_current.prior) + .text("; total ") + .fig(total, "0.00") + .text( + ". The split is the current and non-current classification carried in the ledger." + ) + .markup("</p>") + ) + where = f"Note {note.number} lease maturity" + content.elements.append( + Element( + "recipe:lease_maturity", + "p", + body.render(where), + body.skeleton(where), + body.figures, + body.quotes, + ) + ) + content.gaps.append( + Gap( + "the contractual undiscounted maturity analysis of lease liabilities by time band", + self._sources_looked_in, + ) + ) + content.blocked_items.add(item) + + def _recipe_business_combination(self, note: Note, content: NoteContent, item: str) -> None: + minutes = self.corpus / "sources" / "board-minutes-2026-01-18.md" + consideration = _figure_from(minutes, "185.00", self.corpus, "consideration approved") + pattern, what = self.absence_probes[item] + keywords = [k.strip().lower() for k in pattern.split("|")] + searched = [a.name_cite for a in self.ledger.accounts] + matches = [a for a in self.ledger.accounts if any(k in a.name.lower() for k in keywords)] + body = Body().markup("<p>") + body.quote( + "The board minutes record the acquisition of 100% of the equity share capital of " + "Kompally Tooling Works Private Limited for a total cash consideration of ", + _quote_from(minutes, "total cash consideration", self.corpus), + ) + body.fig(consideration, "0.00") + body.quote( + ", with control passing to the Company on 29 January 2026.", + _quote_from(minutes, "control of KTW passed to the Company", self.corpus), + ) + body.text( + f" The trial balance for the year carries no balance matching " + f"{' or '.join(keywords)}: all {len(searched)} account names were searched and " + f"{len(matches)} matched. The two sources disagree; both are stated and neither is " + f"preferred in this note." + ) + body.markup("</p>") + where = f"Note {note.number} business combination" + content.elements.append( + Element( + "recipe:business_combination", + "p", + body.render(where), + body.skeleton(where), + body.figures, + body.quotes, + ) + ) + content.disagreements.append( + Disagreement( + about="whether a business combination is recognised in the FY" + f"{self.rolled.year} ledger", + sides=( + Side( + source=minutes.relative_to(self.corpus).as_posix(), + says="100% of KTW acquired for 185.00, control passing 29 January 2026", + cites=consideration.cites, + figures=(consideration,), + ), + Side( + source=self.ledger.rel, + says=f"no account name matches {what}; {len(searched)} account names " + f"searched, {len(matches)} matched", + cites=tuple(searched), + searched=len(searched), + ), + ), + ) + ) + for gap_text in ( + "the purchase price allocation: fair values of assets acquired and liabilities assumed", + "goodwill or bargain purchase gain arising, and the qualitative factors behind it", + "the basis of consolidation for the entity over which control was obtained", + ): + content.gaps.append(Gap(gap_text, self._sources_looked_in)) + content.blocked_items.update({item, *self.also_blocks[item]}) + + +# -- helpers --------------------------------------------------------------- + + +def _sentences(raw: str) -> list[tuple[str, int]]: + inner_start = raw.index(">") + 1 + inner = raw[inner_start : raw.rindex("<")] + out: list[tuple[str, int]] = [] + cursor = 0 + for piece in SENTENCE_SPLIT.split(inner): + position = inner.index(piece, cursor) + out.append((piece, inner_start + position)) + cursor = position + len(piece) + return out + + +def _qualifying_groups(prior_note, prior: PriorYearSet): + """(label, derived total, item figures) for every group that qualifies.""" + out = [] + seen: dict[str, int] = {} + for element in prior_note.body: + if element.tag != "p": + continue + text = strip_markup(element.raw) + for sentence in BREAKDOWN_SPLIT_SENTENCE.split(text): + match = GROUP.match(sentence.strip()) + if match is None or ";" not in match.group(2): + continue + label = match.group(1).strip() + items = list(match.group(2).split(";")) + figures: list[Figure] = [] + for piece in items: + tokens = money_tokens(piece) + if len(tokens) != 1: + figures = [] + break + literal = tokens[0] + occurrence = seen.get(literal, 0) + seen[literal] = occurrence + 1 + figures.append( + Figure( + piece.strip()[:60], + _decimal(literal), + "document", + (prior.cite_in_note(prior_note.number, literal, occurrence),), + ) + ) + if len(figures) < 2: + continue + out.append( + ( + label, + Derived(f"{label} (prior year)", tuple(Component(f) for f in figures)), + tuple(figures), + ) + ) + return out + + +def _decimal(literal: str) -> Decimal: + from .money import parse_money + + return parse_money(literal) + + +def _breakdown_labels(prior_note) -> list[str]: + """The labels last year's note used, for the components we cannot reproduce. + + A segment qualifies only if it holds exactly one figure - the same narrowing + that stops "for the year: 276.00 (intangible amortisation of 9.00 is + disclosed in Note 4...)" being read as a two-item breakdown. + """ + labels: list[str] = [] + for element in prior_note.body: + if element.tag != "p": + continue + for segment in BREAKDOWN_SPLIT.split(strip_markup(element.raw)): + tokens = money_tokens(segment) + if len(tokens) != 1: + continue + label = segment.replace(tokens[0], "").strip(" :,()").strip() + label = re.sub(r"\s+", " ", label) + # "Total" and "Total current" are the grand-total line; "Total + # outstanding dues of micro and small enterprises" is a caption and + # dropping it would silently mark the MSMED disclosure reproduced. + if not label or (DROP_LABEL.match(label) and len(label.split()) <= 2): + continue + labels.append(label[:70]) + return labels[:8] + + +def _account_for(rolled: RollForward, note: Note, group: str) -> str: + for statement in rolled.statements: + for line in statement.lines: + if line.note_ref == note.number and line.account and line.account.group == group: + return line.account.code + raise KeyError(f"Note {note.number} has no {group} caption") + + +def _first_token(text: str, predicate) -> str: + for token in money_tokens(text): + if predicate(token): + return token + raise KeyError(f"no token matching the predicate in {text[:60]!r}") + + +def _last_token(text: str) -> str: + return money_tokens(text)[-1] + + +def _quote_from(path: Path, phrase: str, corpus_root: Path) -> Cite: + raw = path.read_bytes() + position = raw.find(phrase.encode("utf-8")) + if position < 0: + raise KeyError( + f"{path.name}: the phrase {phrase!r} is not in the source. A quotation that " + f"cannot be located is not a quotation." + ) + return Cite( + path.relative_to(corpus_root).as_posix(), + position, + position + len(phrase.encode("utf-8")), + phrase, + f"{path.name}, quoted passage", + ) + + +def _figure_from(path: Path, literal: str, corpus_root: Path, where: str) -> Figure: + cite = _quote_from(path, literal, corpus_root) + return Figure( + where, + Decimal(literal.replace(",", "")), + "document", + (Cite(cite.path, cite.byte_start, cite.byte_end, literal, f"{path.name}, {where}"),), + literal=literal, + ) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/checklist.py b/use-cases/preetham1930/statutory-statements-builder/statutory/checklist.py new file mode 100644 index 000000000..3b10e211b --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/checklist.py @@ -0,0 +1,189 @@ +"""The disclosure checklist: 32 rows of data, and a grammar that evaluates them. + +Nothing in this module contains an item id, a standard name or a trigger. The +checklist is found by its **columns**, never its filename, and every condition is +parsed from the CSV at run time. Adding a 33rd row is a data edit. + + condition := "always" | account_test | event_name + account_test := "account" code ("or" code)* "balance" [scope] operator number + scope := "current" | "prior" (default: current) + operator := ">" | ">=" | "<" | "<=" | "=" | "==" | "<>" | "!=" + +Three loud failures where the tempting default is a quiet one. An unparseable +condition, an unknown account code and an event with no detector each raise. A +silent `False` in any of them means "this disclosure is not required", which is +exactly the gap this app exists to close; a silent `True` invents work. Neither +is visible in the output, which is what makes stopping the right answer. + +Several account codes joined by `or` are a **disjunction**: the item is required +if any one satisfies the comparison. A conjunctive reading would suppress the +lease maturity item whenever one of the current/non-current pair happened to be +nil - which is precisely the year it matters. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from decimal import Decimal +from pathlib import Path + +from .csvspans import as_records, parse_csv +from .errors import UnknownAccountError, UnknownConditionError, UnknownEventError +from .figures import Cite + +REQUIRED_COLUMNS = ("item_id", "requirement", "trigger_condition", "satisfied_by_note") + +ACCOUNT_TEST = re.compile( + r"^account\s+(?P<codes>\d+(?:\s+or\s+\d+)*)\s+balance" + r"(?:\s+(?P<scope>current|prior))?\s*" + r"(?P<op>>=|<=|<>|!=|==|=|>|<)\s*" + r"(?P<number>-?\d+(?:\.\d+)?)\s*$", + re.I, +) + +OPERATORS = { + ">": lambda a, b: a > b, + ">=": lambda a, b: a >= b, + "<": lambda a, b: a < b, + "<=": lambda a, b: a <= b, + "=": lambda a, b: a == b, + "==": lambda a, b: a == b, + "<>": lambda a, b: a != b, + "!=": lambda a, b: a != b, +} + +GRAMMAR = ( + 'condition := "always" | account_test | event_name; ' + 'account_test := "account" code ("or" code)* "balance" [current|prior] ' + "(>|>=|<|<=|=|==|<>|!=) number" +) + + +@dataclass(frozen=True) +class ChecklistItem: + item_id: str + standard: str + requirement: str + trigger: str + prior_note: int | None # the note number in *last year's* numbering + severity: str + row_cite: Cite + + @property + def mapped_last_year(self) -> bool: + return self.prior_note is not None + + +@dataclass(frozen=True) +class Trigger: + """Why an item is required, with the citations that make it true.""" + + item: ChecklistItem + required: bool + because: str + cites: tuple[Cite, ...] + + +class Checklist: + def __init__(self, path: Path, corpus_root: Path) -> None: + self.path = path + self.rel = path.relative_to(corpus_root).as_posix() + header, records = as_records(parse_csv(path.read_bytes())) + missing = [c for c in REQUIRED_COLUMNS if c not in header] + if missing: + raise ValueError( + f"{self.rel}: a disclosure checklist is recognised by its columns, not its " + f"name. Missing: {', '.join(missing)}. Found: {', '.join(header)}" + ) + self.items: list[ChecklistItem] = [] + for record in records: + note_cell = record["satisfied_by_note"] + self.items.append( + ChecklistItem( + item_id=record["item_id"].text, + standard=record["standard"].text if "standard" in record else "", + requirement=record["requirement"].text, + trigger=record["trigger_condition"].text, + prior_note=int(note_cell.text) if note_cell.text else None, + severity=record["severity"].text if "severity" in record else "", + row_cite=Cite( + path=self.rel, + byte_start=record["requirement"].byte_start, + byte_end=record["requirement"].byte_end, + snippet=record["requirement"].value, + where=f"disclosure checklist, item {record['item_id'].text}", + ), + ) + ) + + def __len__(self) -> int: + return len(self.items) + + def evaluate(self, ledger, events) -> list[Trigger]: + return [self._evaluate_one(item, ledger, events) for item in self.items] + + def _evaluate_one(self, item: ChecklistItem, ledger, events) -> Trigger: + condition = item.trigger.strip() + if condition.lower() == "always": + return Trigger(item, True, "required in every set of statements", ()) + + match = ACCOUNT_TEST.match(condition) + if match: + return self._evaluate_accounts(item, match, ledger) + + if events.defines(condition): + hits = events.hits(condition) + if hits: + return Trigger( + item, + True, + f"event '{condition}' detected in {hits[0].source}", + tuple(c for h in hits for c in h.cites), + ) + return Trigger(item, False, f"event '{condition}' not detected in any source", ()) + + raise UnknownConditionError( + f"{self.rel}: item {item.item_id} has trigger {condition!r}, which is neither " + f"'always', nor an account test, nor an event this build defines. " + f"Grammar: {GRAMMAR}. Events defined: {', '.join(events.names()) or '(none)'}. " + f"An unevaluable trigger is not 'not required' - it stops the run." + ) + + def _evaluate_accounts(self, item: ChecklistItem, match, ledger) -> Trigger: + codes = re.split(r"\s+or\s+", match.group("codes"), flags=re.I) + scope = (match.group("scope") or "current").lower() + year = ledger.year if scope == "current" else ledger.prior_year + operator = OPERATORS[match.group("op")] + threshold = Decimal(match.group("number")) + + unknown = [c for c in codes if not ledger.has(c)] + if unknown: + raise UnknownAccountError( + f"{self.rel}: item {item.item_id} tests account(s) {', '.join(unknown)}, which " + f"the trial balance does not have. The ledger holds {len(ledger)} account(s): " + f"{', '.join(ledger.codes())}. A missing account is not a nil balance." + ) + + cites: list[Cite] = [] + satisfied: list[str] = [] + for code in codes: + account = ledger.account(code) + cites.append(account.cite(year)) + if operator(account.balance(year), threshold): + satisfied.append(code) + if satisfied: + detail = ", ".join( + f"{c} ({ledger.account(c).name}) = {ledger.account(c).balance(year)}" + for c in satisfied + ) + return Trigger(item, True, f"{detail} in FY{year}", tuple(cites)) + return Trigger(item, False, f"no account in {', '.join(codes)} satisfies the test", ()) + + +def check_unknown_event(name: str, events) -> None: + """Kept separate so the error can be raised from a caller that knows the row.""" + if not events.defines(name): + raise UnknownEventError( + f"event {name!r} has no detector. Defined: {', '.join(events.names()) or '(none)'}" + ) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/chunks.py b/use-cases/preetham1930/statutory-statements-builder/statutory/chunks.py new file mode 100644 index 000000000..4d82cf3c6 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/chunks.py @@ -0,0 +1,183 @@ +"""Chunks: the unit SuperDocs actually addresses, and the unit we verify. + +Measured rather than assumed (PROGRESS.md 2026-08-09): an HTML upload chunks at +the top-level element - every paragraph, every heading, and **the entire table as +one chunk**. Rows and cells carry no id. So the skeleton is built to match that +granularity: whatever we intend to replace later is its own top-level element, +and a note schedule is one table and therefore one chunk (Decision 37). + +`canonical()` is what "the expected bytes" means in practice. A document editor +is entitled to re-serialise whitespace, attribute order and character entities, +and treating that as a failure would make the verifier cry wolf on every step. +Everything that carries meaning - the tag sequence, every attribute value, and +every character of text including every digit - is compared exactly. The chunk +id is dropped because it is the address, not the content. + +The entity and void-element rules were measured, not assumed: we uploaded +`·` and got back the character it stands for, and we uploaded `<br>` and +got back `<br/>`. The first live run +stopped on it, which is the verbatim check doing exactly its job; the honest +resolution is to define "verbatim" at the level of content rather than of bytes, +and to say so here. +""" + +from __future__ import annotations + +import html as _html +import re +from dataclasses import dataclass +from html.parser import HTMLParser + +CHUNK_ID = re.compile(r'\sdata-chunk-id="[^"]*"') +TAG = re.compile(r"<(/?)([a-zA-Z0-9]+)((?:\s+[a-zA-Z0-9:-]+\s*=\s*\"[^\"]*\")*)\s*(/?)>") +ATTR = re.compile(r"([a-zA-Z0-9:-]+)\s*=\s*\"([^\"]*)\"") +WS = re.compile(r"\s+") +TOP_LEVEL = ("h1", "h2", "h3", "h4", "p", "table", "ul", "ol", "div", "section", "blockquote") +VOID = {"br", "meta", "hr", "img", "link", "input", "col"} +# HTML parsers insert these whether or not the source had them. Measured: we +# uploaded a table with no <tbody> and it came back wrapped in one. +IMPLICIT = {"tbody", "thead", "tfoot"} + + +def canonical(fragment: str) -> str: + """Whitespace-collapsed, attribute-sorted, chunk-id-free form of an HTML fragment.""" + out: list[str] = [] + cursor = 0 + for match in TAG.finditer(fragment): + text = fragment[cursor : match.start()] + if text: + out.append(WS.sub(" ", _html.unescape(text))) + closing, tag, attrs, selfclose = match.groups() + pairs = sorted( + (k.lower(), WS.sub(" ", v).strip()) + for k, v in ATTR.findall(attrs) + if k.lower() != "data-chunk-id" + ) + if tag.lower() in IMPLICIT: + cursor = match.end() + continue + rendered = "".join(f' {k}="{v}"' for k, v in pairs) + # `<br>` and `<br/>` are the same element; the product re-serialises the + # one we send as the other, and that is punctuation, not content. + marker = "/" if selfclose and tag.lower() not in VOID else "" + out.append(f"<{closing}{tag.lower()}{rendered}{marker}>") + cursor = match.end() + tail = fragment[cursor:] + if tail: + out.append(WS.sub(" ", _html.unescape(tail))) + return WS.sub(" ", "".join(out)).replace("> <", "><").strip() + + +@dataclass(frozen=True) +class Chunk: + chunk_id: str + tag: str + html: str + + @property + def canonical(self) -> str: + return canonical(self.html) + + +class _Scanner(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=False) + self.spans: list[tuple[str, str, int, int]] = [] + self._depth = 0 + self._tag: str | None = None + self._start = 0 + self._chunk_id = "" + self._text = "" + + def _offset(self) -> int: + line, col = self.getpos() + return self._line_start[line - 1] + col + + def feed_text(self, text: str) -> None: + self._text = text + self._line_start = [0] + offset = 0 + for line in text.splitlines(keepends=True): + offset += len(line) + self._line_start.append(offset) + self.feed(text) + self.close() + + def handle_starttag(self, tag: str, attrs) -> None: + if tag in VOID: + return + if self._tag is None: + if tag in TOP_LEVEL: + self._tag = tag + self._depth = 1 + self._start = self._offset() + self._chunk_id = dict(attrs).get("data-chunk-id") or "" + return + if tag == self._tag: + self._depth += 1 + + def handle_endtag(self, tag: str) -> None: + if self._tag is None or tag != self._tag: + return + self._depth -= 1 + if self._depth == 0: + end = self._offset() + len(f"</{tag}>") + self.spans.append((self._chunk_id, self._tag, self._start, end)) + self._tag = None + + +class ChunkMap: + """An ordered map of chunk id -> chunk, as the document currently stands.""" + + def __init__(self, html: str) -> None: + scanner = _Scanner() + scanner.feed_text(html) + self.chunks: list[Chunk] = [ + Chunk(chunk_id, tag, html[start:end]) for chunk_id, tag, start, end in scanner.spans + ] + self.by_id = {c.chunk_id: c for c in self.chunks if c.chunk_id} + + def __len__(self) -> int: + return len(self.chunks) + + @property + def ids(self) -> list[str]: + return [c.chunk_id for c in self.chunks] + + def headings(self) -> dict[int, str]: + """Note number -> heading text, read back from the document itself.""" + from .money import strip_markup + from .priorset import NOTE_HEADING + + found: dict[int, str] = {} + for chunk in self.chunks: + if chunk.tag not in ("h3", "h4"): + continue + text = strip_markup(chunk.html).strip() + match = NOTE_HEADING.match(text) + if match: + found[int(match.group("number"))] = text + return found + + def text_of(self, chunk_id: str) -> str: + from .money import strip_markup + + return strip_markup(self.by_id[chunk_id].html).strip() + + +def strip_chunk_ids(html: str) -> str: + return CHUNK_ID.sub("", html) + + +def tidy(fragment: str) -> str: + """Drop the whitespace an HTML serialiser drops, before we ever send it. + + Measured: we sent `<br>\nPrepared in accordance...` and the edited chunk came + back as `<br>Prepared in accordance...`. Rather than keep widening + `canonical()` until it forgives everything, the document we compute is + written the way a serialiser would write it, so there is less for the + comparison to forgive. Whitespace *inside* a line is left alone - that is + where the meaningful spaces between words are. + """ + fragment = re.sub(r">[ \t]*\n[ \t]*", ">", fragment) + return re.sub(r"[ \t]*\n[ \t]*<", "<", fragment) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/csvspans.py b/use-cases/preetham1930/statutory-statements-builder/statutory/csvspans.py new file mode 100644 index 000000000..e4e5e47f9 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/csvspans.py @@ -0,0 +1,101 @@ +"""An RFC 4180 scanner that keeps byte positions. + +`csv.reader` yields values and throws positions away, and the positions are the +whole point: a figure we write into the statements has to name the byte range it +came from. Rebuilt here rather than imported (Decision 31); the idea is copied +from `system/ingest`, the code is not. + +Assumption: the corpus CSVs are UTF-8 with `\\n` or `\\r\\n` line endings and no +embedded newlines inside quoted fields. Both hold across all three corpus CSVs +and `.gitattributes` pins the endings; a file that breaks it raises rather than +mis-parsing, because a silently wrong byte offset is worse than a loud stop. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Cell: + value: str + byte_start: int + byte_end: int + + @property + def text(self) -> str: + return self.value.strip() + + +def parse_csv(raw: bytes) -> list[list[Cell]]: + """Rows of cells. `byte_start`/`byte_end` span the field's *value* bytes. + + For a quoted field the span is the bytes inside the quotes, so a citation + reads back as the value rather than as the value plus its punctuation. + """ + rows: list[list[Cell]] = [] + row: list[Cell] = [] + i = 0 + n = len(raw) + while i <= n: + if i == n: + if row or (rows and raw.endswith(b"\n")): + pass + break + byte = raw[i : i + 1] + if byte == b'"': + start = i + 1 + j = start + chunks: list[bytes] = [] + while j < n: + if raw[j : j + 1] == b'"': + if raw[j + 1 : j + 2] == b'"': + chunks.append(b'"') + j += 2 + continue + break + chunks.append(raw[j : j + 1]) + j += 1 + if j >= n: + raise ValueError(f"unterminated quoted field starting at byte {i}") + row.append(Cell(b"".join(chunks).decode("utf-8"), start, j)) + i = j + 1 + # A quoted field must be followed by a delimiter, a newline or EOF. + if i < n and raw[i : i + 1] not in (b",", b"\r", b"\n"): + raise ValueError(f"stray bytes after a quoted field at byte {i}") + else: + j = i + while j < n and raw[j : j + 1] not in (b",", b"\r", b"\n"): + j += 1 + row.append(Cell(raw[i:j].decode("utf-8"), i, j)) + i = j + if i < n and raw[i : i + 1] == b",": + i += 1 + continue + if i < n and raw[i : i + 1] == b"\r": + i += 1 + if i < n and raw[i : i + 1] == b"\n": + i += 1 + rows.append(row) + row = [] + if i >= n: + break + if row: + rows.append(row) + return [r for r in rows if r and not (len(r) == 1 and r[0].value == "")] + + +def as_records(rows: list[list[Cell]]) -> tuple[list[str], list[dict[str, Cell]]]: + """Header row plus one dict per data row, keyed by column name.""" + if not rows: + raise ValueError("empty CSV: no header row") + header = [c.text for c in rows[0]] + records = [] + for row in rows[1:]: + if len(row) != len(header): + raise ValueError( + f"row has {len(row)} field(s) but the header has {len(header)}: " + f"{[c.text for c in row]}" + ) + records.append(dict(zip(header, row, strict=True))) + return header, records diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/disagreement.py b/use-cases/preetham1930/statutory-statements-builder/statutory/disagreement.py new file mode 100644 index 000000000..b3544d65f --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/disagreement.py @@ -0,0 +1,51 @@ +"""Two sources that disagree, both quoted, neither preferred. + +There is no `resolution`, no `resolved`, no `correct_value`, no `preferred_side`, +no `status` and no `approved` on anything in this module - and there is a test +that proves it by reflection rather than by reading. Decision 15's argument, +rebuilt for a document builder: a rule in a docstring is broken by the next +contributor by accident; a type with nowhere to write an opinion is not. + +Sides are ordered by source path, a stable and meaningless order. Ordering by +credibility would be a resolution smuggled in through the ordering. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .figures import AnyFigure, Cite + + +@dataclass(frozen=True) +class Side: + source: str # corpus-relative path + says: str # what this source says, in words + cites: tuple[Cite, ...] + figures: tuple[AnyFigure, ...] = () + searched: int = 0 # how many locations were examined, when the side is an absence + + def __post_init__(self) -> None: + if not self.cites: + raise ValueError( + f"{self.source}: a side of a disagreement that cannot cite its own bytes is " + f"not evidence, and a disagreement missing a side is not emitted at all" + ) + + +@dataclass(frozen=True) +class Disagreement: + about: str + sides: tuple[Side, ...] + + def __post_init__(self) -> None: + if len(self.sides) < 2: + raise ValueError( + f"{self.about}: a disagreement needs at least two sides, each with its own " + f"citations. One side is an observation, not a disagreement." + ) + ordered = tuple(sorted(self.sides, key=lambda s: s.source)) + object.__setattr__(self, "sides", ordered) + + def sentence(self) -> str: + return " ".join(f"{s.source} says: {s.says}" for s in self.sides) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/editplan.py b/use-cases/preetham1930/statutory-statements-builder/statutory/editplan.py new file mode 100644 index 000000000..035161fd4 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/editplan.py @@ -0,0 +1,150 @@ +"""The edit plan: one single-target chunk replacement per step, and nothing else. + +Every step is a `replace`. There is no other verb (Decision 28), no step targets +a heading (Decision 29 put the headings at their final numbers before upload), +and no step carries more than one chunk (Decision 35). Those three are checked +here, before anything is sent, because a plan that breaks a wire rule should +never reach the wire. + +The expected post-state is the **whole chunk's HTML**, not the value inside it +(Decision 37). That is what we compare on read-back, and it is also what we send: +the instruction hands SuperDocs the finished chunk rather than describing an edit +to make. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .chunks import ChunkMap, canonical +from .errors import PlanRefusedError +from .skeleton import Block, StatutoryDocument + +FORBIDDEN_VERBS = ("create", "insert", "add a", "append", "renumber", "move ", "delete") + + +@dataclass +class Step: + index: int + role: str + tag: str + note_number: int | None + expected_html: str # what the chunk must hold afterwards (no chunk id) + was_html: str # what it holds before + chunk_id: str = "" # filled in once the document is uploaded and read back + + @property + def verb(self) -> str: + return "replace" + + def instruction(self) -> str: + """One instruction, one chunk, fully resolved. Nothing is left to plan.""" + return ( + f"Replace the entire contents of the chunk whose data-chunk-id is " + f"{self.chunk_id} with exactly the following HTML, character for character. " + f"The chunk must remain a single <{self.tag}> element and keep the same " + f"data-chunk-id; do not wrap it in a <div> or in any other element. " + f"Do not add any section. Do not change any heading. Do not touch any other " + f"chunk anywhere in the document.\n\n{self.expected_html}" + ) + + def describe(self) -> str: + where = f"Note {self.note_number}" if self.note_number else "front matter" + return f"[{self.index:02d}] replace {self.tag:5s} {self.role:34s} ({where})" + + +@dataclass +class Plan: + steps: list[Step] + document: StatutoryDocument + + def __len__(self) -> int: + return len(self.steps) + + def sample(self, limit: int | None) -> Plan: + if limit is None or limit >= len(self.steps): + return self + return Plan(self.steps[:limit], self.document) + + +def build_plan(document: StatutoryDocument) -> Plan: + # Ordered so that a halt loses the least verified work. Paragraph chunks + # first, table chunks last: measured on this build's live runs, a whole-table + # replacement is the one unit the product does not apply reliably - clean + # once, silently not applied twice, and once wrapped in a <div> nobody asked + # for. The order changes nothing about what is sent or checked; it changes + # which failure a reviewer is left holding. + blocks = sorted(document.editable_changed(), key=lambda b: (b.tag == "table",)) + steps = [ + Step( + index=index, + role=block.role, + tag=block.tag, + note_number=block.note_number, + expected_html=block.target, + was_html=block.skeleton, + ) + for index, block in enumerate(blocks, start=1) + ] + plan = Plan(steps, document) + assert_plan_is_legal(plan, document) + return plan + + +def assert_plan_is_legal(plan: Plan, document: StatutoryDocument) -> None: + headings = {b.role for b in document.blocks if not b.editable} + for step in plan.steps: + if step.verb != "replace": + raise PlanRefusedError( + f"step {step.index} uses the verb {step.verb!r}. There is one verb and it is " + f"replace (Decision 28): the only operation that inserts is also the one " + f"measured to fabricate sections nobody asked for." + ) + if step.role in headings or step.tag in ("h1", "h2", "h3"): + raise PlanRefusedError( + f"step {step.index} targets {step.role!r}, which is a heading. Headings carry " + f"the note numbering and are uploaded at their final values (Decision 29); " + f"there is no renumbering step to get wrong." + ) + if step.expected_html == step.was_html: + raise PlanRefusedError( + f"step {step.index} ({step.role}) would send a chunk that is already correct. " + f"An edit that changes nothing cannot be verified by read-back and reads as " + f"'not applied'." + ) + + +def bind_chunk_ids(plan: Plan, uploaded: ChunkMap) -> None: + """Match each step to the chunk it will address, by the bytes we uploaded. + + Matched on canonical content rather than on position, so a document that came + back re-ordered fails here rather than quietly editing the wrong chunk. + """ + by_canonical: dict[str, list[str]] = {} + for chunk in uploaded.chunks: + by_canonical.setdefault(chunk.canonical, []).append(chunk.chunk_id) + for step in plan.steps: + key = canonical(step.was_html) + candidates = by_canonical.get(key, []) + if len(candidates) != 1: + raise PlanRefusedError( + f"step {step.index} ({step.role}) matches {len(candidates)} uploaded chunk(s). " + f"A single-target edit needs exactly one target; {len(uploaded)} chunks came " + f"back. Content we uploaded:\n {step.was_html[:200]}" + ) + step.chunk_id = candidates[0] + + +def expected_after(before: ChunkMap, step: Step) -> dict[str, str]: + """The canonical form of every chunk after this step, computed before sending.""" + expected = {chunk.chunk_id: chunk.canonical for chunk in before.chunks} + if step.chunk_id not in expected: + raise PlanRefusedError( + f"step {step.index} targets chunk {step.chunk_id}, which is not in the document" + ) + expected[step.chunk_id] = canonical(step.expected_html) + return expected + + +def unused(block: Block) -> Block: # pragma: no cover - keeps the import honest + return block diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/errors.py b/use-cases/preetham1930/statutory-statements-builder/statutory/errors.py new file mode 100644 index 000000000..7da6ecd63 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/errors.py @@ -0,0 +1,76 @@ +"""Every failure this app can have, and the message it must carry. + +Hard rule 13's argument, applied to a document: a bare failure tells the +reviewer something is wrong but not what to do about it. Every error below names +what could not be done and where we looked. + +Non-ASCII characters are kept out of these messages on purpose: they are read in +a terminal, and Windows consoles mangled exactly the message that carried Phase +1's thesis (PROGRESS.md 2026-08-08, Assumption 19). +""" + +from __future__ import annotations + + +class StatutoryError(Exception): + """Base for everything raised by this package.""" + + +class CitationDriftError(StatutoryError): + """A citation no longer holds the literal it was created for.""" + + +class UnaccountedFigureError(StatutoryError): + """A money token reached a rendered body without resolving to a source.""" + + +class ComparativeMismatchError(StatutoryError): + """The ledger's prior-year column and the signed prior-year set disagree.""" + + +class TieBreakError(StatutoryError): + """A statement did not add up, or a note did not tie to its caption.""" + + +class CrossReferenceError(StatutoryError): + """A cross-reference does not resolve to a heading that exists.""" + + +class ChecklistDriftError(StatutoryError): + """A checklist item names a note the final document does not have.""" + + +class UnknownConditionError(StatutoryError): + """A checklist trigger could not be parsed. Never read as 'not required'.""" + + +class UnknownAccountError(StatutoryError): + """A trigger names an account the ledger does not have.""" + + +class UnknownEventError(StatutoryError): + """A trigger names an event no detector defines.""" + + +class PlanRefusedError(StatutoryError): + """An edit plan step broke one of the wire rules before it was sent.""" + + +class EditNotAppliedError(StatutoryError): + """The target chunk came back unchanged.""" + + +class EditAppliedWrongError(StatutoryError): + """The target chunk changed, but not to the bytes we computed.""" + + +class CollateralDamageError(StatutoryError): + """A chunk we did not target moved, appeared or vanished.""" + + +class DecisionAlreadyRecordedError(StatutoryError): + """A second decision on an item that already has one, without supersede.""" + + +class NotConfiguredError(StatutoryError): + """A live call was attempted with no SUPERDOCS_API_KEY set.""" diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/events.py b/use-cases/preetham1930/statutory-statements-builder/statutory/events.py new file mode 100644 index 000000000..25bb39bb3 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/events.py @@ -0,0 +1,170 @@ +"""Events are detected from this year's sources, never remembered. + +An event fires on a **block** when every phrase group has a phrase present in +that same block, and every group contributes a citation. One block rather than +one document is what keeps it honest: the prior-year set says "control of the +goods transfers to the customer" in the revenue note and carries "Equity share +capital" on the balance sheet, and a document-wide match would read those two as +a business combination. + +The vocabulary holds no company name, no acquiree name, no date, no amount and +no standard number. It is a CSV, so a new event is a data edit. + +Quarantine runs in both directions. A source carrying an instruction aimed at an +automated reader is reported as quarantined and then not consulted at all - +neither to suppress an event nor to manufacture one. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + +from .csvspans import as_records, parse_csv +from .figures import Cite + +COMMENT = re.compile(r"<!--(.*?)-->", re.S) +PARAGRAPH_SPLIT = re.compile(rb"\n[ \t]*\n") + + +@dataclass(frozen=True) +class Block: + source: str # corpus-relative path + byte_start: int + text: str + + +@dataclass(frozen=True) +class EventHit: + event: str + source: str + block_text: str + cites: tuple[Cite, ...] + + +@dataclass(frozen=True) +class Quarantine: + source: str + marker: str + cite: Cite + + +class EventDetector: + def __init__(self, vocabulary: Path, markers: Path) -> None: + _, records = as_records(parse_csv(vocabulary.read_bytes())) + self._groups: dict[str, list[list[str]]] = {} + for record in records: + name = record["event_name"].text + groups = [ + [phrase.strip().lower() for phrase in group.split("|") if phrase.strip()] + for group in record["phrase_groups"].text.split(";;") + ] + if not groups or not all(groups): + raise ValueError(f"{vocabulary.name}: event {name!r} has an empty phrase group") + self._groups[name] = groups + self._markers = [ + line.strip().lower() + for line in markers.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.startswith("#") + ] + self.hits_by_event: dict[str, list[EventHit]] = {name: [] for name in self._groups} + self.quarantined: list[Quarantine] = [] + self.scanned: list[str] = [] + + def names(self) -> list[str]: + return sorted(self._groups) + + def defines(self, name: str) -> bool: + return name in self._groups + + def hits(self, name: str) -> list[EventHit]: + return self.hits_by_event.get(name, []) + + def is_quarantined(self, rel: str) -> bool: + return any(q.source == rel for q in self.quarantined) + + # -- scanning ---------------------------------------------------------- + + def scan(self, paths: list[Path], corpus_root: Path) -> None: + for path in sorted(paths): + rel = path.relative_to(corpus_root).as_posix() + raw = path.read_bytes() + marker = self._quarantine_marker(raw, rel) + if marker is not None: + self.quarantined.append(marker) + continue + self.scanned.append(rel) + for block in _blocks(raw, rel): + self._match(block, rel) + + def _quarantine_marker(self, raw: bytes, rel: str) -> Quarantine | None: + text = raw.decode("utf-8", errors="replace") + for match in COMMENT.finditer(text): + lowered = match.group(1).lower() + for marker in self._markers: + if marker in lowered: + start = len(text[: match.start()].encode("utf-8")) + end = len(text[: match.end()].encode("utf-8")) + return Quarantine( + source=rel, + marker=marker, + cite=Cite( + path=rel, + byte_start=start, + byte_end=end, + snippet=match.group(0), + where=f"{rel}, embedded comment addressed to an automated reader", + ), + ) + return None + + def _match(self, block: Block, rel: str) -> None: + lowered = block.text.lower() + for name, groups in self._groups.items(): + cites: list[Cite] = [] + for group in groups: + found = next((p for p in group if p in lowered), None) + if found is None: + cites = [] + break + position = lowered.index(found) + start = block.byte_start + len(block.text[:position].encode("utf-8")) + literal = block.text[position : position + len(found)] + cites.append( + Cite( + path=rel, + byte_start=start, + byte_end=start + len(literal.encode("utf-8")), + snippet=literal, + where=f"{rel}, phrase evidencing '{name}'", + ) + ) + if cites: + self.hits_by_event[name].append( + EventHit(name, rel, block.text.strip(), tuple(cites)) + ) + + +def _blocks(raw: bytes, rel: str) -> list[Block]: + """Blank-line separated blocks, with the byte offset of each.""" + blocks: list[Block] = [] + offset = 0 + for piece in PARAGRAPH_SPLIT.split(raw): + text = piece.decode("utf-8", errors="replace") + if text.strip(): + blocks.append(Block(rel, offset, text)) + offset += len(piece) + offset += 2 # the separator we split on; exact offsets are recomputed below + # Recompute exactly: splitting loses the separator width, and a byte offset + # that is approximately right is a citation that does not read back. + exact: list[Block] = [] + cursor = 0 + for block in blocks: + encoded = block.text.encode("utf-8") + position = raw.find(encoded, cursor) + if position < 0: # pragma: no cover - the block came from this file + raise ValueError(f"{rel}: could not locate a block in its own source") + exact.append(Block(rel, position, block.text)) + cursor = position + len(encoded) + return exact diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/figures.py b/use-cases/preetham1930/statutory-statements-builder/statutory/figures.py new file mode 100644 index 000000000..327c96f48 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/figures.py @@ -0,0 +1,375 @@ +"""Figures, their provenance, and the guarantee that nothing else reaches paper. + +Four bases and there is no fifth (INVARIANTS.md item 6): + +- `ledger` the figure is a trial-balance cell for the reporting year +- `comparative` a prior-year figure, cited twice - to the ledger's prior-year + column *and* to the signed prior-year statements - which is what + makes "the comparatives tie exactly" a checked claim +- `document` a figure quoted from a source document (the lease deed, the + minutes, the fixed-asset schedule) +- `derived` computed from figures that are themselves on one of the three + bases above, every component cited + +`Derived` has no `value` field. The total is a property recomputed on every +read, so a printed total cannot drift from the citations that support it and +cannot be stated apart from them. That is Decision 16's trick, rebuilt here +against a different substrate (Decision 31) - twenty lines, and copying it is +cheaper than any coupling. + +`Gap` is the honest alternative to inventing an analysis: it names what is +missing and every place we looked for it. +""" + +from __future__ import annotations + +import html as _html +from collections import Counter +from dataclasses import dataclass, field +from decimal import Decimal +from pathlib import Path + +from .errors import CitationDriftError, UnaccountedFigureError +from .money import format_money, money_tokens_in_html + +BASES = ("ledger", "comparative", "document", "derived") +SOURCE_BASES = ("ledger", "comparative", "document") + + +@dataclass(frozen=True) +class Cite: + """A byte range in a corpus file that physically contains `snippet`.""" + + path: str # corpus-relative, forward slashes + byte_start: int + byte_end: int + snippet: str + where: str # human location, e.g. "trial balance row 1110, column fy2026_inr_lakhs" + + def __post_init__(self) -> None: + if self.byte_end < self.byte_start: + raise ValueError(f"{self.path}: byte range runs backwards") + if not self.snippet: + raise ValueError(f"{self.path}: a citation with an empty snippet cites nothing") + + def read_back(self, root: Path) -> str: + raw = (root / self.path).read_bytes()[self.byte_start : self.byte_end] + return raw.decode("utf-8") + + def verify(self, root: Path) -> None: + """Hard rule 2: re-read the bytes and confirm they still say what we cited.""" + actual = self.read_back(root) + if actual != self.snippet: + raise CitationDriftError( + f"citation drift in {self.path} at bytes {self.byte_start}-{self.byte_end} " + f"({self.where}): cited {self.snippet!r}, the file now holds {actual!r}" + ) + + +@dataclass(frozen=True) +class Figure: + """One figure with its provenance and the exact string it is written as.""" + + label: str + value: Decimal + basis: str + cites: tuple[Cite, ...] + literal: str | None = None # when the source spells it differently (6,50,000) + + def __post_init__(self) -> None: + if self.basis not in SOURCE_BASES: + raise ValueError( + f"{self.label}: basis {self.basis!r} is not one of {SOURCE_BASES}. " + f"A derived total is a Derived, which has no value field." + ) + if not self.cites: + raise ValueError( + f"{self.label}: a figure with no citation has no provenance and may not exist" + ) + if self.basis == "comparative" and len(self.cites) < 2: + raise ValueError( + f"{self.label}: a comparative is cited twice - to the ledger's prior-year " + f"column and to the signed prior-year statements - or it is not a comparative" + ) + + @property + def rendered(self) -> str: + return self.literal if self.literal is not None else format_money(self.value) + + +@dataclass(frozen=True) +class Component: + figure: Figure | Derived + sign: int = 1 + + def __post_init__(self) -> None: + if self.sign not in (1, -1): + raise ValueError("a component contributes +1 or -1, nothing else") + + @property + def contribution(self) -> Decimal: + return self.sign * self.figure.value + + +@dataclass(frozen=True) +class Derived: + """A total that appears in no source. Its value is computed, never stored.""" + + label: str + components: tuple[Component, ...] + basis: str = field(default="derived", init=False) + + def __post_init__(self) -> None: + if not self.components: + raise ValueError(f"{self.label}: a derived figure with no components is a bare number") + + @property + def value(self) -> Decimal: + return sum((c.contribution for c in self.components), Decimal("0")).quantize( + Decimal("0.01") + ) + + @property + def rendered(self) -> str: + return format_money(self.value) + + @property + def cites(self) -> tuple[Cite, ...]: + out: list[Cite] = [] + for component in self.components: + out.extend(component.figure.cites) + return tuple(out) + + def workings(self) -> str: + parts = [] + for index, component in enumerate(self.components): + sign = "-" if component.sign < 0 else ("+" if index else "") + parts.append(f"{sign} {format_money(abs(component.figure.value))}".strip()) + return f"{' '.join(parts)} = {self.rendered}" + + +@dataclass(frozen=True) +class Gap: + """What could not be produced, and every place we looked for it. + + Not a finding (INVARIANTS.md item 20): it is a property of the document - + this note cannot state that analysis from the sources supplied. + """ + + what: str + looked_in: tuple[str, ...] + figures: tuple[AnyFigure, ...] = () + + def __post_init__(self) -> None: + if not self.what.strip(): + raise ValueError("a gap must say what is missing") + if not self.looked_in: + raise ValueError( + f"{self.what}: a gap that does not say where we looked is a shrug, not a gap" + ) + + def sentence(self) -> str: + return f"{self.what} (looked in: {', '.join(self.looked_in)})" + + +# -------------------------------------------------------------------------- +# The body builder, and the guarantee +# -------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class Quotient: + """A per-share figure. Division does not fit a signed sum, so it gets a type. + + `scale` is a declared unit conversion (lakhs to rupees), not a fudge factor, + and it is written at the call site where the units are visible. + """ + + label: str + numerator: AnyFigure + denominator: AnyFigure + scale: Decimal = Decimal(1) + basis: str = field(default="derived", init=False) + + @property + def value(self) -> Decimal: + if self.denominator.value == 0: + raise ZeroDivisionError(f"{self.label}: the denominator is nil") + return (self.numerator.value * self.scale / self.denominator.value).quantize( + Decimal("0.01") + ) + + @property + def rendered(self) -> str: + return format_money(self.value) + + @property + def cites(self) -> tuple[Cite, ...]: + return tuple(self.numerator.cites) + tuple(self.denominator.cites) + + +AnyFigure = Figure | Derived | Quotient + + +@dataclass(frozen=True) +class _Slot: + """One figure as it appears twice: in the target, and in the skeleton.""" + + target: str + skeleton: str + figure: AnyFigure | None + + +class Body: + """A rendered fragment plus the figures that account for every token in it. + + Text goes through `text()`, markup through `markup()`, and every figure goes + through `fig()`. Then `render()` re-scans its own output and refuses to + return a fragment holding a token no figure accounts for. + + A body renders twice. `render()` is the FY figures we computed; `skeleton()` + is the same structure carrying last year's, which is what gets uploaded + (Decision 29). Both are scanned, and the plan only contains the chunks where + the two differ - an identical chunk needs no edit and is never sent. + """ + + def __init__(self) -> None: + self._parts: list[str | _Slot] = [] + self._figures: list[AnyFigure] = [] + self._quotes: list[Cite] = [] + + def markup(self, raw: str) -> Body: + self._parts.append(raw) + return self + + def text(self, value: str) -> Body: + self._parts.append(_html.escape(value, quote=False)) + return self + + def dated(self, target: str, skeleton: str) -> Body: + """A label that differs between the two renderings, e.g. the year.""" + self._parts.append( + _Slot(_html.escape(target, quote=False), _html.escape(skeleton, quote=False), None) + ) + return self + + def fig(self, figure: AnyFigure, skeleton: AnyFigure | str | None = None) -> Body: + self._figures.append(figure) + if isinstance(skeleton, str): + shadow = skeleton + elif skeleton is None: + shadow = format_money(Decimal(0)) + else: + shadow = skeleton.rendered + self._parts.append( + _Slot( + _html.escape(figure.rendered, quote=False), + _html.escape(shadow, quote=False), + figure, + ) + ) + return self + + def prose(self, text: str, figures: tuple[AnyFigure, ...] = ()) -> Body: + """Sentence text whose figures are named, emitted as figures where they occur. + + There is deliberately no "declare this figure without printing it" door: + a figure only counts once its rendered string has actually been found in + the sentence and emitted as a slot. A sentence that mentions a figure the + caller did not declare still fails the scan. + """ + remaining = text + while remaining: + hit = None + for figure in figures: + position = remaining.find(figure.rendered) + if position >= 0 and (hit is None or position < hit[0]): + hit = (position, figure) + if hit is None: + self.text(remaining) + return self + position, figure = hit + self.text(remaining[:position]) + self.fig(figure) + remaining = remaining[position + len(figure.rendered) :] + return self + + def quote(self, text: str, cite: Cite) -> Body: + """Prose lifted from a source, with the bytes it came from recorded.""" + self._quotes.append(cite) + self._parts.append(_html.escape(text, quote=False)) + return self + + def carried(self, raw_html: str, figures: tuple[AnyFigure, ...] = ()) -> Body: + """Prose carried from the prior-year set verbatim, with its figures declared.""" + self._parts.append(raw_html) + self._figures.extend(figures) + return self + + @property + def figures(self) -> tuple[AnyFigure, ...]: + return tuple(self._figures) + + @property + def quotes(self) -> tuple[Cite, ...]: + return tuple(self._quotes) + + def _join(self, skeleton: bool) -> str: + return "".join( + (p.skeleton if skeleton else p.target) if isinstance(p, _Slot) else p + for p in self._parts + ) + + def render(self, where: str) -> str: + fragment = self._join(skeleton=False) + assert_accounted(fragment, self._figures, where) + return fragment + + def skeleton(self, where: str) -> str: + """The same structure with last year's figures. Uploaded, then replaced.""" + fragment = self._join(skeleton=True) + declared = [ + _Shadow(p.skeleton) + for p in self._parts + if isinstance(p, _Slot) and p.figure is not None + ] + assert_accounted(fragment, declared, f"{where} (skeleton)") + return fragment + + +@dataclass(frozen=True) +class _Shadow: + """A skeleton token. Carries no provenance because it never reaches an export.""" + + rendered: str + + +def assert_accounted(fragment: str, figures: list[AnyFigure] | tuple[AnyFigure, ...], where: str): + """Every money token in `fragment` is the rendered form of a declared figure. + + Counted as a multiset, so two nil balances need two figures. A token with no + figure behind it is a hard error naming the token and the place - never a + warning, never a caveat (hard rule 13). + """ + found = Counter(money_tokens_in_html(fragment)) + declared = Counter(f.rendered for f in figures) + unaccounted = found - declared + if unaccounted: + tokens = ", ".join(sorted(unaccounted.elements())) + raise UnaccountedFigureError( + f"{where}: {sum(unaccounted.values())} money token(s) reached the document with " + f"no source behind them: {tokens}. " + f"Declared figures were: {', '.join(sorted(declared.elements())) or '(none)'}. " + f"Every figure is ledger, comparative, document or derived; there is no fifth " + f"basis, and this is a hard error rather than a warning." + ) + + +def verify_all(figures: list[AnyFigure] | tuple[AnyFigure, ...], root: Path) -> int: + """Re-read every citation off disk. Returns how many were checked.""" + checked = 0 + for figure in figures: + for cite in figure.cites: + cite.verify(root) + checked += 1 + return checked diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/ledger.py b/use-cases/preetham1930/statutory-statements-builder/statutory/ledger.py new file mode 100644 index 000000000..a28803836 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/ledger.py @@ -0,0 +1,153 @@ +"""The trial balance: the ledger every primary-statement caption is drawn from. + +The reporting year is the highest `fy<year>` column present and the comparative +year is the next highest, so no year is written down in this module and a +FY2027 corpus needs a new column, not a code change (hard rule 6). + +Signs are the ledger's own. Liabilities, equity and income are carried negative, +and no absolute value is ever taken - `balance > 0` has to keep meaning "debit +balance" or the trigger grammar is lying about what it evaluates (Assumption 20, +carried over as a principle and re-checked here against all 32 conditions). + +Presentation sign is a property of the caption's *group*, not of the account: a +statutory balance sheet shows liabilities positive. The map below is the whole +of that rule and it is configuration, not logic. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from decimal import Decimal +from pathlib import Path + +from .csvspans import as_records, parse_csv +from .figures import Cite +from .money import parse_money + +FY_COLUMN = re.compile(r"^fy(?P<year>\d{4})_") + +# presented = balance * factor, keyed on the ledger's own `group` column. +PRESENTATION_SIGN: dict[str, int] = { + "Non-current assets": 1, + "Current assets": 1, + "Equity": -1, + "Non-current liabilities": -1, + "Current liabilities": -1, + "Income": -1, + "Expenses": 1, + "Tax expense": 1, +} + + +@dataclass(frozen=True) +class Account: + code: str + name: str + statement: str # BS | PL + group: str + balances: dict[int, Decimal] # year -> signed balance + cites: dict[int, Cite] # year -> citation of that cell + name_cite: Cite + order: int # position in the file; the statutory presentation order + + def balance(self, year: int) -> Decimal: + return self.balances[year] + + def presented(self, year: int) -> Decimal: + return self.balances[year] * PRESENTATION_SIGN[self.group] + + def cite(self, year: int) -> Cite: + return self.cites[year] + + +class TrialBalance: + def __init__(self, path: Path, corpus_root: Path) -> None: + self.path = path + self.rel = path.relative_to(corpus_root).as_posix() + raw = path.read_bytes() + header, records = as_records(parse_csv(raw)) + years: list[int] = [] + for column in header: + match = FY_COLUMN.match(column) + if match: + years.append(int(match.group("year"))) + if len(years) < 2: + raise ValueError( + f"{self.rel}: a roll-forward needs a reporting year and a comparative; " + f"found fy columns for {years or 'nothing'}" + ) + self.years = sorted(years, reverse=True) + self.year = self.years[0] + self.prior_year = self.years[1] + self._columns = {y: next(c for c in header if c.startswith(f"fy{y}_")) for y in self.years} + + self.accounts: list[Account] = [] + for order, record in enumerate(records): + balances: dict[int, Decimal] = {} + cites: dict[int, Cite] = {} + for year in self.years: + cell = record[self._columns[year]] + balances[year] = parse_money(cell.text) + cites[year] = Cite( + path=self.rel, + byte_start=cell.byte_start, + byte_end=cell.byte_end, + snippet=cell.value, + where=( + f"trial balance, account {record['account_code'].text}, " + f"column {self._columns[year]}" + ), + ) + name_cell = record["account_name"] + group = record["group"].text + if group not in PRESENTATION_SIGN: + raise ValueError( + f"{self.rel}: account {record['account_code'].text} is in group {group!r}, " + f"which has no presentation sign. Known groups: " + f"{', '.join(sorted(PRESENTATION_SIGN))}" + ) + self.accounts.append( + Account( + code=record["account_code"].text, + name=name_cell.text, + statement=record["statement"].text, + group=group, + balances=balances, + cites=cites, + name_cite=Cite( + path=self.rel, + byte_start=name_cell.byte_start, + byte_end=name_cell.byte_end, + snippet=name_cell.value, + where=f"trial balance, account {record['account_code'].text}, name", + ), + order=order, + ) + ) + self._by_code = {a.code: a for a in self.accounts} + + def __len__(self) -> int: + return len(self.accounts) + + def has(self, code: str) -> bool: + return code in self._by_code + + def account(self, code: str) -> Account: + return self._by_code[code] + + def codes(self) -> list[str]: + return [a.code for a in self.accounts] + + def in_group(self, statement: str, group: str) -> list[Account]: + return [a for a in self.accounts if a.statement == statement and a.group == group] + + def groups(self, statement: str) -> list[str]: + seen: list[str] = [] + for account in self.accounts: + if account.statement == statement and account.group not in seen: + seen.append(account.group) + return seen + + def year_label(self, year: int) -> str: + return f"31 March {year}" diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/money.py b/use-cases/preetham1930/statutory-statements-builder/statutory/money.py new file mode 100644 index 000000000..d183b2c58 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/money.py @@ -0,0 +1,91 @@ +"""Money, and the recogniser that decides what counts as one. + +The recogniser is deliberately narrow, and it is narrow for the same reason the +ingester's was (PROGRESS.md 2026-08-08, Assumption 13): a token is a figure only +if it is thousands-grouped (Indian grouping included, so `50,00,000` works) or +written to exactly two decimals. That excludes bare years, pin codes, survey +numbers, the CIN, plain counts and bare percentages. + +It has to be narrow in both directions here. Too wide and the year in "31 March +2026" becomes a figure that must be cited; too narrow and a figure slips into +the document without provenance, which is the one thing INVARIANTS.md item 6 +exists to prevent. Both directions are tested. +""" + +from __future__ import annotations + +import html as _html +import re +from decimal import Decimal, InvalidOperation + +# Grouped (2,180.00 / 50,00,000 / 42,000) or exactly two decimals (0.00 / 8.14). +# Brackets are the accounting negative. +# Brackets are matched as a *pair* or not at all. An optional bracket on each +# side independently reads "(703.00 + 87.00)" as the token "(703.00", which is +# a bracket belonging to the sentence - the exact silent-skip we caught in +# Phase 3 (PROGRESS.md 2026-08-09). Bracketed alternatives come first so the +# accounting negative wins where both could match. +_MONEY = re.compile( + r"\(\d{1,3}(?:,\d{2,3})+(?:\.\d{2})?\)" # bracketed, thousands-grouped + r"|\(\d+\.\d{2}\)" # bracketed, two decimals + r"|\d{1,3}(?:,\d{2,3})+(?:\.\d{2})?" # thousands-grouped + r"|\d+\.\d{2}" # exactly two decimals +) +_TAG = re.compile(r"<[^>]*>") + + +def format_money(value: Decimal) -> str: + """Accounting presentation: grouped to two decimals, negatives in brackets.""" + q = Decimal(value).quantize(Decimal("0.01")) + if q == 0: + # A nil balance carried through a presentation sign flip is Decimal("-0.00"), + # which formats as "-0.00" and reads as a figure nobody wrote. + q = abs(q) + if q < 0: + return f"({-q:,.2f})" + return f"{q:,.2f}" + + +def parse_money(token: str) -> Decimal: + """Inverse of `format_money` for a single recognised token.""" + text = token.strip() + negative = text.startswith("(") and text.endswith(")") + text = text.strip("()").replace(",", "") + try: + value = Decimal(text) + except InvalidOperation as exc: # pragma: no cover - guarded by the recogniser + raise ValueError(f"not a money token: {token!r}") from exc + return -value if negative else value + + +def strip_markup(fragment: str) -> str: + """Tag-free text of an HTML fragment, entities resolved. + + The scan runs over text, never over markup: a chunk id is a hex string and a + style rule is full of numbers, and neither is a figure anybody wrote. + """ + return _html.unescape(_TAG.sub(" ", fragment)) + + +def money_tokens(text: str) -> list[str]: + """Every money token in plain text, in order of appearance. + + A match is rejected when it is glued to more digits (so a longer number is + never read as a short one) or immediately followed by `%` (so `9.15%` is a + rate, not 9.15 lakhs). + """ + found: list[str] = [] + for match in _MONEY.finditer(text): + start, end = match.span() + before = text[start - 1] if start else "" + after = text[end] if end < len(text) else "" + if before.isdigit() or before in {",", "."}: + continue + if after.isdigit() or after == "%": + continue + found.append(match.group(0)) + return found + + +def money_tokens_in_html(fragment: str) -> list[str]: + return money_tokens(strip_markup(fragment)) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/notetree.py b/use-cases/preetham1930/statutory-statements-builder/statutory/notetree.py new file mode 100644 index 000000000..4dab7fdb9 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/notetree.py @@ -0,0 +1,282 @@ +"""The note tree: where a new note goes, what everything downstream becomes. + +This is the module INVARIANTS.md items 3, 4 and 5 are about, and it is the +reason there is no mid-document insertion anywhere in this build. Every +consequence of adding a note is computed **here**, before a single byte is sent: +the new note's number, the new number of every note after it, every note -> +primary cross-reference, every `Note N` in prose, and every checklist mapping. +The document is then uploaded already in that shape (Decision 29). + +The insertion point is derived from the data, not from a list of note names: + + a new note triggered by an account sits immediately after the note that the + nearest *preceding* account in the ledger already points at + +For the warehouse lease that reads: account 1110 is required and unmapped; the +account before it in the ledger is 1100, which the signed balance sheet +cross-references to Note 3; therefore the lease note is Note 4 and notes 4..25 +become 5..26. Nothing here knows what a lease is. + +A new note triggered only by an event has no account to sit beside, so it is +appended. That is also a rule about the data, not about the standard. + +The English *title* of a new note is configuration (`config/new-note-titles.csv`, +keyed on the checklist's own `standard` column). The requirement for the note is +detected; the label is a label. Recorded as an assumption rather than smuggled. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from itertools import pairwise +from pathlib import Path + +from .checklist import Trigger +from .csvspans import as_records, parse_csv +from .errors import ChecklistDriftError, CrossReferenceError +from .figures import Cite +from .ledger import TrialBalance +from .priorset import NOTE_REFERENCE, PriorYearSet + + +@dataclass +class Note: + key: str # stable identity across the roll-forward + title: str + number: int = 0 + prior_number: int | None = None + is_new: bool = False + trigger_accounts: tuple[str, ...] = () + checklist_items: tuple[str, ...] = () + why: str = "" + why_cites: tuple[Cite, ...] = () + + @property + def heading(self) -> str: + return f"Note {self.number} — {self.title}" + + +@dataclass +class NoteTree: + notes: list[Note] + renumber: dict[int, int] = field(default_factory=dict) # prior number -> final number + account_note: dict[str, int] = field(default_factory=dict) # account code -> final number + inserted: list[Note] = field(default_factory=list) + + def by_number(self, number: int) -> Note: + return next(n for n in self.notes if n.number == number) + + def by_key(self, key: str) -> Note: + return next(n for n in self.notes if n.key == key) + + def has_number(self, number: int) -> bool: + return any(n.number == number for n in self.notes) + + def repoint_prose(self, text: str) -> str: + """Rewrite every `Note N` in prose through the same map that renumbered.""" + + def swap(match) -> str: + old = int(match.group(1)) + if old not in self.renumber: + raise CrossReferenceError( + f"prose points at Note {old}, which the prior-year set does not have. " + f"Known prior notes: {sorted(self.renumber)}" + ) + return f"Note {self.renumber[old]}" + + return NOTE_REFERENCE.sub(swap, text) + + +def load_new_note_titles(path: Path) -> dict[str, str]: + _, records = as_records(parse_csv(path.read_bytes())) + return {r["standard"].text: r["new_note_title"].text for r in records} + + +def build_note_tree( + rolled, + triggers: list[Trigger], + new_note_titles: dict[str, str], +) -> NoteTree: + prior = rolled.prior + ledger = rolled.ledger + notes: list[Note] = [ + Note(key=f"prior-{n.number}", title=n.title, prior_number=n.number) for n in prior.notes + ] + + clusters: dict[str, list[Trigger]] = {} + for trigger in triggers: + if not trigger.required or trigger.item.mapped_last_year: + continue + standard = trigger.item.standard + title = new_note_titles.get(standard) + if title is None: + raise ChecklistDriftError( + f"item {trigger.item.item_id} ({standard}) is required and maps to no note in " + f"the prior-year set, and no new-note title is configured for {standard!r}. " + f"Configured: {', '.join(sorted(new_note_titles)) or '(none)'}. " + f"A required disclosure with nowhere to live stops the run rather than " + f"quietly vanishing from the checklist." + ) + clusters.setdefault(title, []).append(trigger) + + # The account -> prior-note map comes from the roll-forward, which already + # matched each ledger account to its signed-set row (and disambiguated the + # two "Borrowings" and the two "Provisions" rows by their section). Matching + # a second time here would be a second chance to disagree with itself. + account_note_of_prior: dict[str, int] = { + line.account.code: line.prior_note_ref + for statement in rolled.statements + for line in statement.lines + if line.account is not None and line.prior_note_ref is not None + } + + inserted: list[Note] = [] + for title, cluster in clusters.items(): + accounts = _trigger_accounts(cluster, ledger) + note = Note( + key=f"new-{title.lower().replace(' ', '-')}", + title=title, + is_new=True, + trigger_accounts=accounts, + checklist_items=tuple(t.item.item_id for t in cluster), + why="; ".join(sorted({t.because for t in cluster})), + why_cites=tuple(c for t in cluster for c in t.cites), + ) + position = _insertion_index(note, notes, ledger, account_note_of_prior) + notes.insert(position, note) + inserted.append(note) + + for index, note in enumerate(notes, start=1): + note.number = index + + renumber = {n.prior_number: n.number for n in notes if n.prior_number is not None} + account_note = { + code: renumber[number] + for code, number in account_note_of_prior.items() + if number in renumber + } + for note in inserted: + for code in note.trigger_accounts: + account_note[code] = note.number + + tree = NoteTree(notes=notes, renumber=renumber, account_note=account_note, inserted=inserted) + _validate(tree, prior) + return tree + + +def _trigger_accounts(cluster: list[Trigger], ledger: TrialBalance) -> tuple[str, ...]: + codes: list[str] = [] + for trigger in cluster: + for cite in trigger.cites: + marker = "account " + if marker in cite.where: + code = cite.where.split(marker, 1)[1].split(",", 1)[0].strip() + if ledger.has(code) and code not in codes: + codes.append(code) + return tuple(sorted(codes, key=lambda c: ledger.account(c).order)) + + +def _insertion_index( + note: Note, + notes: list[Note], + ledger: TrialBalance, + account_note_of_prior: dict[str, int], +) -> int: + if not note.trigger_accounts: + return len(notes) # event-only: nothing to sit beside + anchor = ledger.account(note.trigger_accounts[0]) + preceding = [ + a for a in ledger.accounts if a.order < anchor.order and a.code in account_note_of_prior + ] + following = [ + a for a in ledger.accounts if a.order > anchor.order and a.code in account_note_of_prior + ] + if not preceding: + return 0 + before = account_note_of_prior[preceding[-1].code] + if following: + after = account_note_of_prior[following[0].code] + if after < before: + raise CrossReferenceError( + f"cannot place the {note.title!r} note from the data: account " + f"{anchor.code} sits between account {preceding[-1].code} (Note {before}) and " + f"account {following[0].code} (Note {after}), and the notes are out of ledger " + f"order. The insertion point is ambiguous and guessing it is exactly what " + f"this build refuses to do." + ) + index = next(i for i, n in enumerate(notes) if n.prior_number == before) + while index + 1 < len(notes) and notes[index + 1].prior_number == before: + index += 1 + return index + 1 + + +def _validate(tree: NoteTree, prior: PriorYearSet) -> None: + numbers = [n.number for n in tree.notes] + if numbers != list(range(1, len(numbers) + 1)): + raise CrossReferenceError(f"note numbers are {numbers}, which is not 1..n") + if len(tree.renumber) != len(prior.notes): + raise CrossReferenceError( + f"{len(prior.notes)} prior-year notes went in and {len(tree.renumber)} came out " + f"of the renumbering map; a note was lost." + ) + ordered = sorted(tree.renumber.items()) + for (old_a, new_a), (old_b, new_b) in pairwise(ordered): + if new_a >= new_b: + raise CrossReferenceError( + f"renumbering is not order-preserving: prior Note {old_a} -> {new_a} but prior " + f"Note {old_b} -> {new_b}. A roll-forward may insert, never reorder." + ) + for old, new in tree.renumber.items(): + if not tree.has_number(new): + raise CrossReferenceError(f"prior Note {old} maps to Note {new}, which does not exist") + + +def remap_checklist(triggers: list[Trigger], tree: NoteTree) -> dict[str, int | None]: + """checklist item id -> the FY2026 note that carries it, through the same map.""" + mapping: dict[str, int | None] = {} + for trigger in triggers: + item = trigger.item + if item.prior_note is not None: + if item.prior_note not in tree.renumber: + raise ChecklistDriftError( + f"checklist item {item.item_id} points at prior-year Note " + f"{item.prior_note}, which the signed set does not have. Prior notes run " + f"1..{max(tree.renumber)}." + ) + mapping[item.item_id] = tree.renumber[item.prior_note] + continue + carrier = next((n for n in tree.inserted if item.item_id in n.checklist_items), None) + mapping[item.item_id] = carrier.number if carrier else None + return mapping + + +def assert_checklist_consistent( + mapping: dict[str, int | None], tree: NoteTree, headings: dict[int, str] +) -> None: + """Every mapped item names a heading that exists, at the number we expect. + + `headings` is read back from the document, not from our own model - that is + the point. A drift here is a hard failure, never a warning (hard rule 13's + argument, applied to structure). + """ + for item_id, number in sorted(mapping.items()): + if number is None: + continue + if number not in headings: + raise ChecklistDriftError( + f"checklist item {item_id} maps to Note {number}, and the document has no " + f"heading at that number. Headings present: {sorted(headings)}" + ) + expected = tree.by_number(number).heading + if headings[number] != expected: + raise ChecklistDriftError( + f"checklist item {item_id} maps to Note {number}; we expect the heading " + f"{expected!r} and the document reads {headings[number]!r}" + ) + unaccounted = sorted(set(headings) - {n.number for n in tree.notes}) + if unaccounted: + raise ChecklistDriftError( + f"the document carries note heading(s) {unaccounted} that our tree does not have. " + f"An unaccounted heading is a section nobody planned - which is the shape of the " + f"fabrication we measured on 2026-08-09." + ) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/orchestrator.py b/use-cases/preetham1930/statutory-statements-builder/statutory/orchestrator.py new file mode 100644 index 000000000..06d69a8b8 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/orchestrator.py @@ -0,0 +1,254 @@ +"""The run: upload once, then one verified chunk replacement at a time. + +The shape of a step, and every part of it is load-bearing: + + compute the expected post-state of the WHOLE document + -> one instruction, one chunk, on the async route + -> poll to the gate + -> refuse anything that is not exactly our one change + -> write OUR decision row, with an actor + -> approve (the actuator) + -> poll to completed + -> read the whole document back + -> classify: ok | not applied | applied wrong | collateral damage + +On any failure the queue **halts**. No downstream step runs, because a +downstream step is the consequence of work that may not have happened - that is +the 2026-08-07 corrupted-document mechanism and the halt is the whole defence. +Then: revert to the last verified-good state, read back again to confirm the +revert actually happened, surface the diff, and produce **no export**. + +Retry policy: at most one narrow retry of the same instruction, never a re-plan. +Re-planning is the degradation mode, measured twice; and a retry diffs the whole +document, because failed content has been seen arriving a turn later. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from pathlib import Path + +from .chunks import ChunkMap +from .editplan import Plan, bind_chunk_ids +from .superdocs.client import SuperDocsClient, select_our_change +from .superdocs.decisions import DecisionLedger +from .superdocs.verifier import Verification, verify, verify_upload_verbatim + + +@dataclass +class StepOutcome: + index: int + role: str + verification: Verification + retried: bool = False + job_id: str = "" + + @property + def ok(self) -> bool: + return self.verification.ok + + +@dataclass +class RunResult: + run_id: str + document_id: str = "" + session_id: str = "" + planned: int = 0 + outcomes: list[StepOutcome] = field(default_factory=list) + upload_receipt: str = "" + halted_at: int | None = None + exported: list[str] = field(default_factory=list) + reverted: str = "" + checklist_receipt: str = "" + + @property + def applied(self) -> int: + return sum(1 for o in self.outcomes if o.ok) + + @property + def ok(self) -> bool: + return self.halted_at is None and self.applied == self.planned + + def sentence(self, ledger: DecisionLedger) -> str: + """Computed from the outcomes and the rows, at the moment it is emitted.""" + if self.halted_at is not None: + return ( + f"HALTED at step {self.halted_at}. {self.applied} of {self.planned} planned " + f"change(s) verified applied; the rest were not attempted. No export was " + f"produced. {ledger.describe(self.run_id, self.planned)}." + ) + return ( + f"{self.applied} of {self.planned} planned change(s) verified applied by reading " + f"the whole document back after each one. " + f"{ledger.describe(self.run_id, self.planned)}." + ) + + +class Orchestrator: + def __init__( + self, + client: SuperDocsClient, + ledger: DecisionLedger, + actor: str, + *, + sleep=time.sleep, + export_formats: tuple[str, ...] = ("docx",), + export_dir: Path | None = None, + ) -> None: + self.client = client + self.ledger = ledger + self.actor = actor + self.sleep = sleep + self.export_formats = export_formats + self.export_dir = export_dir + self.log: list[str] = [] + + def _say(self, line: str) -> None: + self.log.append(line) + + def run(self, run_id: str, plan: Plan, filename: str) -> RunResult: + result = RunResult(run_id=run_id, planned=len(plan)) + skeleton_blocks = [b.skeleton for b in plan.document.blocks] + + document_id, session_id = self.client.upload_verbatim( + filename, plan.document.html("skeleton") + ) + result.document_id, result.session_id = document_id, session_id + uploaded, _ = self.client.read_back(document_id) + result.upload_receipt = verify_upload_verbatim(uploaded, skeleton_blocks) + self._say(result.upload_receipt) + bind_chunk_ids(plan, uploaded) + + before = uploaded + last_good = before + for step in plan.steps: + outcome = self._one_step(run_id, step, before, session_id, document_id) + result.outcomes.append(outcome) + if not outcome.ok: + result.halted_at = step.index + self._say(outcome.verification.report()) + self._say( + "halting: no downstream step runs on the consequences of an unconfirmed one" + ) + result.reverted = self._revert(session_id, document_id, last_good, result.applied) + return result + self._say(outcome.verification.report()) + before, _ = self.client.read_back(document_id) + last_good = before + + for fmt in self.export_formats: + written = self.client.export(session_id, fmt, self.export_dir) + result.exported.append(f"{fmt} -> {written}" if written else fmt) + return result + + def _one_step( + self, run_id: str, step, before: ChunkMap, session_id: str, document_id: str + ) -> StepOutcome: + verification = self._attempt(run_id, step, before, session_id, document_id) + if verification.ok: + return StepOutcome(step.index, step.role, verification) + # One narrow retry of the same instruction. Never a re-plan: re-planning + # is the measured degradation mode. Collateral damage is not retried at + # all - the document already holds content nobody asked for. + if any(p.kind == "collateral_damage" for p in verification.problems): + return StepOutcome(step.index, step.role, verification) + self._say(f"step {step.index}: one narrow retry, same instruction, no re-plan") + # The retry is compared against the **last verified-good** state, not + # against whatever the failed attempt left behind. Comparing against the + # wreckage turns "it applied the wrong thing twice" into "it did + # nothing", which is a different diagnosis and the wrong one. Found on + # the first live run of this build. + retried = self._attempt(run_id, step, before, session_id, document_id, supersede=True) + return StepOutcome(step.index, step.role, retried, retried=True) + + def _attempt( + self, + run_id: str, + step, + before: ChunkMap, + session_id: str, + document_id: str, + supersede: bool = False, + ) -> Verification: + job = self.client.chat_gated(session_id, document_id, step.instruction()) + if not job.awaiting: + return Verification( + step.index, + step.chunk_id, + problems=[ + _problem( + "not_applied", + f"the job reached status {job.status!r} without ever offering a change " + f"to approve. Nothing was decided and nothing is claimed.", + ) + ], + ) + change, unasked = select_our_change(job, step.chunk_id) + # Our row first, then the actuator (Decision 33). + self.ledger.record( + run_id, + step.index, + step.chunk_id, + "accept", + self.actor, + f"replacement for {step.role} matches the post-state computed from the ledger", + supersede=supersede, + ) + self.client.approve(session_id, job.job_id, change.change_id) + for extra in unasked: + # Denied bare: no feedback, ever (Decision 34). A rejected change + # still gets a row with an actor, because "who decided this" has to + # have an answer for every change the product offered, not only for + # the ones we wanted. + self.ledger.record( + run_id, + step.index, + extra.chunk_id, + "reject", + self.actor, + "not in the computed plan; the product proposed it unasked", + supersede=True, + ) + self.client.deny(session_id, job.job_id, extra.change_id) + self._say( + f"step {step.index}: denied an unasked change on chunk {extra.chunk_id} " + f"(bare, no feedback)" + ) + settled = self.client.poll(job.job_id, until_terminal=True, sleep=self.sleep) + del settled # the job's own account of itself is not evidence + after, _ = self.client.read_back(document_id) + return verify(step.index, step.chunk_id, before, after, step.expected_html) + + def _revert(self, session_id: str, document_id: str, last_good: ChunkMap, applied: int) -> str: + """Revert to the last verified-good turn, then read back and say what happened.""" + try: + self.client.revert(session_id, turn_index=applied) + except Exception as exc: + return f"revert call failed: {exc}. The document is left as it is and NOT exported." + after, _ = self.client.read_back(document_id) + same = [c.canonical for c in after.chunks] == [c.canonical for c in last_good.chunks] + if same: + return "reverted to the last verified-good state, confirmed by read-back" + return ( + "revert was requested and the read-back does NOT match the last verified-good " + f"state ({len(after)} chunk(s) now, {len(last_good)} then). The document is left " + f"as it is, no export was produced, and this is reported rather than smoothed over." + ) + + +def _problem(kind: str, detail: str): + from .superdocs.verifier import Problem + + return Problem(kind, detail) + + +def write_artifacts(out_dir: Path, plan: Plan) -> list[Path]: + """The computed document, both shapes, on disk - so a reviewer can read them.""" + out_dir.mkdir(parents=True, exist_ok=True) + written = [] + for which in ("skeleton", "target"): + path = out_dir / f"statements-{which}.html" + path.write_text(plan.document.html(which), encoding="utf-8") + written.append(path) + return written diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/pipeline.py b/use-cases/preetham1930/statutory-statements-builder/statutory/pipeline.py new file mode 100644 index 000000000..8eb347e09 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/pipeline.py @@ -0,0 +1,167 @@ +"""One function that turns `corpus/` into the document we intend to end up with. + +Everything before this point is ours and nothing here touches the network. The +whole roll-forward - parse, tie, detect, renumber, repoint, regenerate, render - +runs offline against the corpus, which is why the test suite can exercise real +behaviour with no API key (hard rule 3). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from .bodies import NoteContent +from .checklist import Checklist, Trigger +from .events import EventDetector +from .figures import verify_all +from .ledger import TrialBalance +from .notetree import NoteTree, build_note_tree, load_new_note_titles, remap_checklist +from .priorset import PriorYearSet +from .skeleton import StatutoryDocument, assert_note_captions_tie, build_document +from .statements import RollForward, roll_forward + +DEFAULT_CONFIG = Path(__file__).resolve().parent.parent / "config" + + +@dataclass +class ChecklistLine: + item_id: str + standard: str + requirement: str + required: bool + because: str + prior_note: int | None + note: int | None + status: str # satisfied | outstanding | unsatisfied | not required + + @property + def flagged(self) -> str: + return "" if self.status == "satisfied" else self.status + + +@dataclass +class Rolled: + rolled: RollForward + tree: NoteTree + document: StatutoryDocument + triggers: list[Trigger] + mapping: dict[str, int | None] + checklist: list[ChecklistLine] = field(default_factory=list) + quarantined: list[str] = field(default_factory=list) + caption_ties: list[str] = field(default_factory=list) + cites_verified: int = 0 + + @property + def unsatisfied(self) -> list[ChecklistLine]: + return [line for line in self.checklist if line.status != "satisfied"] + + @property + def disagreements(self) -> list: + return [d for note in self.document.notes for d in note.disagreements] + + def note(self, number: int) -> NoteContent: + return next(n for n in self.document.notes if n.note.number == number) + + +def prepare(corpus_root: Path, config_root: Path = DEFAULT_CONFIG) -> Rolled: + prior = PriorYearSet( + next((corpus_root / "prior-year").glob("*.html")), + corpus_root, + ) + ledger = TrialBalance(_find_ledger(corpus_root), corpus_root) + rolled = roll_forward(ledger, prior, corpus_root) + + events = EventDetector( + config_root / "event-vocabulary.csv", config_root / "quarantine-markers.txt" + ) + events.scan( + [p for p in sorted((corpus_root / "sources").glob("*")) if p.is_file()], corpus_root + ) + + checklist = Checklist(_find_checklist(corpus_root), corpus_root) + triggers = checklist.evaluate(ledger, events) + + tree = build_note_tree( + rolled, triggers, load_new_note_titles(config_root / "new-note-titles.csv") + ) + mapping = remap_checklist(triggers, tree) + + # The primary statements' cross-references are repointed through the same + # map that renumbered the headings. This is the only place a note reference + # is written onto a statement line, so there is one map and no second chance + # to disagree with itself. + for statement in rolled.statements: + for line in statement.lines: + if line.account is not None: + line.note_ref = tree.account_note.get(line.account.code) + + document = build_document(rolled, tree, mapping, corpus_root, config_root) + + out = Rolled(rolled, tree, document, triggers, mapping) + out.quarantined = [q.source for q in events.quarantined] + out.caption_ties = assert_note_captions_tie(document, rolled) + out.checklist = _score_checklist(triggers, mapping, document) + out.cites_verified = verify_all(document.figures, corpus_root) + for cite in document.cites: + cite.verify(corpus_root) + return out + + +def _score_checklist( + triggers: list[Trigger], mapping: dict[str, int | None], document: StatutoryDocument +) -> list[ChecklistLine]: + blocked: dict[int, set[str]] = {} + gapped: set[int] = set() + for note in document.notes: + blocked.setdefault(note.note.number, set()).update(note.blocked_items) + if note.gaps or note.disagreements: + gapped.add(note.note.number) + lines: list[ChecklistLine] = [] + for trigger in triggers: + item = trigger.item + number = mapping.get(item.item_id) + if not trigger.required: + status = "not required" + elif number is None: + status = "unsatisfied" + elif item.item_id in blocked.get(number, set()) or number in gapped: + status = "outstanding" + else: + status = "satisfied" + lines.append( + ChecklistLine( + item_id=item.item_id, + standard=item.standard, + requirement=item.requirement, + required=trigger.required, + because=trigger.because, + prior_note=item.prior_note, + note=number, + status=status, + ) + ) + return lines + + +def _find_ledger(corpus_root: Path) -> Path: + """Found by its columns, not its filename.""" + for path in sorted((corpus_root / "sources").glob("*.csv")): + header = path.read_bytes().split(b"\n", 1)[0].decode("utf-8") + if "account_code" in header and "statement" in header: + return path + raise FileNotFoundError( + f"no trial balance in {corpus_root / 'sources'}: a trial balance is a CSV whose header " + f"carries account_code and statement columns" + ) + + +def _find_checklist(corpus_root: Path) -> Path: + for path in sorted((corpus_root / "rules").glob("*.csv")): + header = path.read_bytes().split(b"\n", 1)[0].decode("utf-8") + if "trigger_condition" in header and "satisfied_by_note" in header: + return path + raise FileNotFoundError( + f"no disclosure checklist in {corpus_root / 'rules'}: a checklist is a CSV whose header " + f"carries trigger_condition and satisfied_by_note columns" + ) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/priorset.py b/use-cases/preetham1930/statutory-statements-builder/statutory/priorset.py new file mode 100644 index 000000000..ada38ab52 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/priorset.py @@ -0,0 +1,335 @@ +"""The prior-year signed statements, parsed so we can cite them and roll them on. + +Two things are needed from last year's set and nothing else: + +1. **Structure.** The order of the top-level elements, the note headings and + their numbers, and the `Note` column of the primary statements. That column + is real data - it is the note/caption cross-reference graph, supplied rather + than supplied by us - and every repoint rides on it. +2. **Citable figures.** A comparative is only a comparative if it can be cited + to the signed set as well as to the ledger, so every figure in the primary + tables and in the note prose is located by byte range. + +Byte offsets, not character offsets. The file is full of em dashes and +`·`, and a character offset would be silently wrong from the first note +onwards - which is the failure mode that is invisible until a citation is read +back on a different machine. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from html.parser import HTMLParser +from pathlib import Path + +from .figures import Cite +from .money import money_tokens, strip_markup + +TOP_LEVEL = ("h1", "h2", "h3", "p", "table") +VOID = {"br", "meta", "hr", "img", "link", "input"} + +# The dash class covers em, en and hyphen: the signed set uses an em dash and a +# different engagement will not. +NOTE_HEADING = re.compile(r"^\s*Note\s+(?P<number>\d+)\s*[—–-]\s*(?P<title>.+?)\s*$") # noqa: RUF001 +CELL = re.compile(r"<t([dh])\b[^>]*>(.*?)</t\1>", re.S) +ROW = re.compile(r"<tr\b[^>]*>(.*?)</tr>", re.S) +NOTE_REFERENCE = re.compile(r"\bNote\s+(\d+)\b") + + +class _ByteIndex: + """Character (line, col) from HTMLParser -> absolute byte offset.""" + + def __init__(self, text: str) -> None: + self._line_start: list[int] = [0] + self._lines: list[str] = text.splitlines(keepends=True) + offset = 0 + for line in self._lines: + offset += len(line.encode("utf-8")) + self._line_start.append(offset) + + def byte_at(self, line: int, col: int) -> int: + prefix = self._lines[line - 1][:col] if line - 1 < len(self._lines) else "" + return self._line_start[line - 1] + len(prefix.encode("utf-8")) + + +@dataclass(frozen=True) +class Element: + tag: str + raw: str # exact source slice, tags included + byte_start: int + byte_end: int + + @property + def text(self) -> str: + return strip_markup(self.raw).strip() + + @property + def inner(self) -> str: + return self.raw[self.raw.index(">") + 1 : self.raw.rindex("<")] + + +class _TopLevelScanner(HTMLParser): + def __init__(self, index: _ByteIndex) -> None: + super().__init__(convert_charrefs=False) + self.index = index + self.spans: list[tuple[str, int, int]] = [] + self._depth = 0 + self._open_tag: str | None = None + self._start: int = 0 + + def handle_starttag(self, tag: str, attrs) -> None: + if tag in VOID: + return + if self._open_tag is None: + if tag in TOP_LEVEL: + self._open_tag = tag + self._depth = 1 + line, col = self.getpos() + self._start = self.index.byte_at(line, col) + return + if tag == self._open_tag: + self._depth += 1 + + def handle_endtag(self, tag: str) -> None: + if self._open_tag is None or tag != self._open_tag: + return + self._depth -= 1 + if self._depth == 0: + line, col = self.getpos() + end = self.index.byte_at(line, col) + len(f"</{tag}>") + self.spans.append((self._open_tag, self._start, end)) + self._open_tag = None + + +@dataclass(frozen=True) +class StatementRow: + caption: str + note_ref: int | None + values: tuple[str, ...] # raw cell text, current year first + cites: tuple[Cite | None, ...] + is_total: bool + section: str = "" # the enclosing section header row, e.g. "Non-current liabilities" + is_section: bool = False + + +@dataclass(frozen=True) +class PrimaryStatement: + heading: Element + table: Element + rows: tuple[StatementRow, ...] + + def row(self, caption: str) -> StatementRow | None: + for candidate in self.rows: + if candidate.caption == caption: + return candidate + return None + + +@dataclass(frozen=True) +class PriorNote: + number: int + title: str + heading: Element + body: tuple[Element, ...] + + @property + def text(self) -> str: + return " ".join(e.text for e in self.body) + + +class PriorYearSet: + """Last year's signed statements: elements, primary statements, notes.""" + + def __init__(self, path: Path, corpus_root: Path) -> None: + self.path = path + self.rel = path.relative_to(corpus_root).as_posix() + raw = path.read_bytes() + self.raw = raw + text = raw.decode("utf-8") + scanner = _TopLevelScanner(_ByteIndex(text)) + scanner.feed(text) + scanner.close() + self.elements: list[Element] = [ + Element(tag, raw[s:e].decode("utf-8"), s, e) for tag, s, e in scanner.spans + ] + if not self.elements: + raise ValueError(f"{self.rel}: no top-level elements found; is this the signed set?") + + self.header: list[Element] = [] + self.statements: list[PrimaryStatement] = [] + self.notes_heading: Element | None = None + self.notes: list[PriorNote] = [] + self._split() + + # -- structure --------------------------------------------------------- + + def _split(self) -> None: + index = 0 + elements = self.elements + while index < len(elements) and elements[index].tag in ("h1", "p"): + self.header.append(elements[index]) + index += 1 + while index < len(elements): + element = elements[index] + if ( + element.tag == "h2" + and index + 1 < len(elements) + and elements[index + 1].tag == "table" + ): + table = elements[index + 1] + self.statements.append(PrimaryStatement(element, table, self._parse_table(table))) + index += 2 + continue + if element.tag == "h2": + self.notes_heading = element + index += 1 + break + index += 1 + current: PriorNote | None = None + body: list[Element] = [] + while index < len(elements): + element = elements[index] + if element.tag == "h3": + if current is not None: + self.notes.append( + PriorNote(current.number, current.title, current.heading, tuple(body)) + ) + match = NOTE_HEADING.match(element.text) + if not match: + raise ValueError( + f"{self.rel}: heading {element.text!r} is not 'Note N - Title'. " + f"Note numbering is read from the signed set, never guessed." + ) + current = PriorNote(int(match.group("number")), match.group("title"), element, ()) + body = [] + elif current is not None: + body.append(element) + index += 1 + if current is not None: + self.notes.append( + PriorNote(current.number, current.title, current.heading, tuple(body)) + ) + numbers = [n.number for n in self.notes] + if numbers != list(range(1, len(numbers) + 1)): + raise ValueError(f"{self.rel}: notes are numbered {numbers}, which is not 1..n") + + def _parse_table(self, table: Element) -> tuple[StatementRow, ...]: + rows: list[StatementRow] = [] + base = table.byte_start + prefix_bytes = len(table.raw[:0].encode("utf-8")) # 0; kept explicit for clarity + del prefix_bytes + section = "" + for row_match in ROW.finditer(table.raw): + row_html = row_match.group(1) + row_offset = row_match.start(1) + cells = list(CELL.finditer(row_html)) + texts = [strip_markup(c.group(2)).strip() for c in cells] + if cells and cells[0].group(1) == "h": + continue + if len(cells) < 2: + # A single spanning cell is a section header: "Non-current + # liabilities". It is what tells the two "Borrowings" rows apart. + if texts and texts[0]: + section = texts[0] + rows.append( + StatementRow( + caption=texts[0], + note_ref=None, + values=(), + cites=(), + is_total=False, + section=section, + is_section=True, + ) + ) + continue + caption = texts[0] + note_ref: int | None = None + if len(texts) > 1 and texts[1].isdigit(): + note_ref = int(texts[1]) + values: list[str] = [] + cites: list[Cite | None] = [] + for cell in cells[2:]: + inner = cell.group(2) + value = strip_markup(inner).strip() + values.append(value) + tokens = money_tokens(value) + if len(tokens) == 1: + literal = tokens[0] + position = inner.find(literal) + if position >= 0: + start_char = row_offset + cell.start(2) + position + byte_start = base + len(table.raw[:start_char].encode("utf-8")) + cites.append( + Cite( + path=self.rel, + byte_start=byte_start, + byte_end=byte_start + len(literal.encode("utf-8")), + snippet=literal, + where=f"prior-year signed statements, row {caption!r}", + ) + ) + continue + cites.append(None) + rows.append( + StatementRow( + caption=caption, + note_ref=note_ref, + values=tuple(values), + cites=tuple(cites), + is_total=caption.upper().startswith("TOTAL"), + section=section, + ) + ) + return tuple(rows) + + # -- lookups ----------------------------------------------------------- + + def note(self, number: int) -> PriorNote: + for candidate in self.notes: + if candidate.number == number: + return candidate + raise KeyError(f"{self.rel}: no note {number}") + + def statement_row(self, caption: str) -> tuple[PrimaryStatement, StatementRow] | None: + for statement in self.statements: + row = statement.row(caption) + if row is not None: + return statement, row + return None + + def cite_in_note(self, number: int, literal: str, occurrence: int = 0) -> Cite: + """Byte range of the `occurrence`-th `literal` inside note `number`'s body.""" + note = self.note(number) + seen = 0 + for element in note.body: + start = 0 + while True: + position = element.raw.find(literal, start) + if position < 0: + break + if seen == occurrence: + byte_start = element.byte_start + len(element.raw[:position].encode("utf-8")) + return Cite( + path=self.rel, + byte_start=byte_start, + byte_end=byte_start + len(literal.encode("utf-8")), + snippet=literal, + where=f"prior-year signed statements, Note {number} ({note.title})", + ) + seen += 1 + start = position + len(literal) + raise KeyError( + f"{self.rel}: {literal!r} occurrence {occurrence} is not in Note {number}. " + f"A comparative that is not in the signed set is not a comparative." + ) + + def note_references_in_prose(self) -> dict[int, list[int]]: + """note number -> the note numbers its prose points at.""" + out: dict[int, list[int]] = {} + for note in self.notes: + hits = [int(m.group(1)) for e in note.body for m in NOTE_REFERENCE.finditer(e.text)] + if hits: + out[note.number] = hits + return out diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/report.py b/use-cases/preetham1930/statutory-statements-builder/statutory/report.py new file mode 100644 index 000000000..2324d4935 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/report.py @@ -0,0 +1,149 @@ +"""What the run says for itself, in the terminal and in a file. + +Two rules shape it. Every number is computed from the objects at the moment it +is printed, never from a counter kept alongside them; and where the sources do +not support an analysis, the report says which analysis and where we looked +rather than reporting a smaller, tidier truth. +""" + +from __future__ import annotations + +from .money import format_money +from .pipeline import Rolled + +RULE = "-" * 78 + + +def render(rolled: Rolled, plan_size: int) -> str: + lines: list[str] = [] + ledger = rolled.rolled.ledger + year, prior_year = rolled.rolled.year, rolled.rolled.prior_year + + lines.append(RULE) + lines.append(f"STATUTORY ROLL-FORWARD FY{prior_year} -> FY{year}") + lines.append(RULE) + lines.append( + f"ledger: {len(ledger)} accounts, caption granularity, columns fy{year} and fy{prior_year}" + ) + lines.append( + f"prior-year set: {len(rolled.rolled.prior.notes)} notes, " + f"{len(rolled.rolled.prior.statements)} primary statements" + ) + if rolled.quarantined: + lines.append( + f"quarantined and not consulted (in either direction): {', '.join(rolled.quarantined)}" + ) + + lines.append("") + lines.append("TIES") + for label, left, right, ok in rolled.rolled.ties: + mark = "ok " if ok else "BAD" + lines.append(f" {mark} {label:58s} {format_money(left):>12s} {format_money(right):>12s}") + matched = [c for c in rolled.rolled.comparative_checks if c.signed_value is not None] + new_nil = [c for c in rolled.rolled.comparative_checks if c.signed_value is None] + lines.append( + f" comparatives: {len(matched)} caption(s) agree between the ledger's fy{prior_year} " + f"column and the signed set; {len(new_nil)} new caption(s) nil in both" + ) + for check in new_nil: + lines.append(f" new this year: {check.caption}") + lines.append(f" note <-> primary cross-references checked: {len(rolled.caption_ties)}") + coverage = rolled.document.coverage + lines.append( + f" prior-year breakdown groups that qualified for a tie-out: {coverage['groups']} " + f"({coverage['tied']} tied, {coverage['raised']} raised)" + ) + + lines.append("") + lines.append("NOTE TREE") + inserted = rolled.tree.inserted + lines.append( + f" {len(rolled.tree.notes)} notes ({len(rolled.tree.renumber)} rolled forward, " + f"{len(inserted)} required by this year's data)" + ) + for note in inserted: + lines.append(f" + Note {note.number} {note.title}") + lines.append(f" required because: {note.why}") + lines.append(f" carries checklist items: {', '.join(note.checklist_items)}") + shifted = [(old, new) for old, new in sorted(rolled.tree.renumber.items()) if old != new] + if shifted: + lines.append( + f" {len(shifted)} note(s) renumbered by the insertion: " + + ", ".join(f"{old}->{new}" for old, new in shifted[:6]) + + (" ..." if len(shifted) > 6 else "") + ) + + lines.append("") + lines.append("DISCLOSURE CHECKLIST") + unmapped_before = [ + line for line in rolled.checklist if line.required and line.prior_note is None + ] + lines.append( + f" {len(rolled.checklist)} items evaluated from the CSV, " + f"{sum(1 for line in rolled.checklist if line.required)} required" + ) + lines.append( + f" detected from THIS YEAR'S DATA and unsatisfied in the set we rolled forward: " + f"{len(unmapped_before)}" + ) + for line in unmapped_before: + lines.append( + f" {line.item_id} {line.standard:10s} no note in the prior-year set " + f"-> now Note {line.note}" + ) + lines.append(f" {line.requirement}") + lines.append(f" because: {line.because}") + by_status: dict[str, int] = {} + for line in rolled.checklist: + by_status[line.status] = by_status.get(line.status, 0) + 1 + lines.append( + " after the roll-forward: " + + ", ".join(f"{count} {status}" for status, count in sorted(by_status.items())) + ) + lines.append( + " 'outstanding' means the note exists and carries the caption, and the analysis the " + ) + lines.append( + " item requires is not derivable from the sources supplied (the trial balance is at " + ) + lines.append(" caption granularity). Each note names what is missing and where we looked.") + lines.append("") + lines.append(" item standard prior note FY note status") + for line in rolled.checklist: + prior = str(line.prior_note) if line.prior_note else "-" + current = str(line.note) if line.note else "-" + lines.append( + f" {line.item_id:7s} {line.standard:11s} {prior:>10s} {current:>9s} {line.status}" + ) + + if rolled.disagreements: + lines.append("") + lines.append("SOURCES THAT DISAGREE (stated, never resolved)") + for disagreement in rolled.disagreements: + lines.append(f" * {disagreement.about}") + for side in disagreement.sides: + extra = f" [{side.searched} location(s) searched]" if side.searched else "" + lines.append(f" {side.source}: {side.says}{extra}") + lines.append(f" cited: {len(side.cites)} byte range(s)") + + gaps = [(n.note.number, g) for n in rolled.document.notes for g in n.gaps] + lines.append("") + lines.append( + f"OUTSTANDING ANALYSIS ({len(gaps)} item(s) across {len(rolled.document.notes)} notes)" + ) + for number, gap in gaps: + lines.append(f" Note {number:2d}: {gap.what}") + + lines.append("") + lines.append("DOCUMENT") + lines.append( + f" {len(rolled.document.blocks)} chunk-aligned blocks; " + f"{sum(1 for b in rolled.document.blocks if not b.editable)} are headings and are " + f"uploaded at their final numbers (never edited)" + ) + lines.append( + f" {plan_size} chunk(s) differ between the skeleton and the target -> {plan_size} single-target replacements" + ) + lines.append(f" {rolled.cites_verified} citation(s) re-read from the corpus bytes and matched") + lines.append(RULE) + return "\n".join(lines) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/skeleton.py b/use-cases/preetham1930/statutory-statements-builder/statutory/skeleton.py new file mode 100644 index 000000000..2e450e81e --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/skeleton.py @@ -0,0 +1,275 @@ +"""The whole document, computed here, in both its shapes. + +Decision 29, stated as code: we compute the **entire** final skeleton - every +heading at its final number, every cross-reference already repointed, the new +note's heading already in place - and upload that verbatim. There is no +insertion to perform, so the insertion bug cannot be reached; SuperDocs only +ever replaces the contents of a chunk that already exists. + +Two renderings come out of one structure: + +- `skeleton` - the document as uploaded. Same structure, last year's figures. +- `target` - the document we intend to end up with. + +The edit plan is exactly the blocks where the two differ, and **no heading block +is ever one of them** (INVARIANTS.md item 2). Blocks that are identical need no +edit and are never sent, which is what makes this a focused update rather than a +full rewrite. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from pathlib import Path + +from . import style +from .bodies import BodyBuilder, Element, NoteContent +from .chunks import tidy +from .errors import CrossReferenceError +from .figures import AnyFigure, Body, Cite +from .money import money_tokens_in_html +from .notetree import NoteTree +from .priorset import NOTE_REFERENCE +from .statements import Line, RollForward, Statement + + +@dataclass +class Block: + """One top-level element, which becomes one chunk on upload.""" + + role: str + tag: str + skeleton: str + target: str + note_number: int | None = None + figures: tuple[AnyFigure, ...] = () + quotes: tuple[Cite, ...] = () + editable: bool = True # headings are structure and are never targeted + + def __post_init__(self) -> None: + object.__setattr__(self, "skeleton", tidy(self.skeleton)) + object.__setattr__(self, "target", tidy(self.target)) + + @property + def changed(self) -> bool: + return self.skeleton != self.target + + +@dataclass +class StatutoryDocument: + blocks: list[Block] = field(default_factory=list) + notes: list[NoteContent] = field(default_factory=list) + tree: NoteTree | None = None + coverage: dict[str, int] = field(default_factory=dict) + + def html(self, which: str = "target") -> str: + return "\n".join(getattr(b, which) for b in self.blocks) + + @property + def figures(self) -> tuple[AnyFigure, ...]: + return tuple(f for b in self.blocks for f in b.figures) + + @property + def cites(self) -> tuple[Cite, ...]: + out = [c for f in self.figures for c in f.cites] + out.extend(c for b in self.blocks for c in b.quotes) + out.extend(c for n in self.notes for d in n.disagreements for s in d.sides for c in s.cites) + return tuple(out) + + def editable_changed(self) -> list[Block]: + return [b for b in self.blocks if b.editable and b.changed] + + +def build_document( + rolled: RollForward, + tree: NoteTree, + mapping: dict[str, int | None], + corpus_root: Path, + config_root: Path, +) -> StatutoryDocument: + prior = rolled.prior + document = StatutoryDocument(tree=tree) + year = rolled.year + prior_year = rolled.prior_year + + # -- header ------------------------------------------------------------ + document.blocks.append( + Block("entity", "h1", prior.header[0].raw, prior.header[0].raw, editable=False) + ) + document.blocks.append( + Block("registered-office", "p", prior.header[1].raw, prior.header[1].raw, editable=False) + ) + # The basis-of-preparation block is the signed set's own paragraph with the + # year moved on. Retyping it here would put the standard's name into our + # source, and a build that knows the name of the standard has started to + # remember the domain instead of reading it (there is a test). + basis_prior = prior.header[2].raw + basis_target = basis_prior.replace(f"31 March {prior_year}", f"31 March {year}") + if basis_target == basis_prior: + raise CrossReferenceError( + f"the signed set's basis-of-preparation paragraph does not name " + f"'31 March {prior_year}', so the reporting date cannot be rolled forward from it: " + f"{basis_prior[:160]}" + ) + document.blocks.append(Block("basis-of-preparation", "p", basis_prior, basis_target)) + document.blocks.append( + Block( + "status", + "p", + prior.header[3].raw, + "<p><em>Draft prepared by roll-forward for review. Not approved and not signed.</em></p>", + ) + ) + + # -- primary statements ------------------------------------------------ + for statement in rolled.statements: + heading = f"<h2>{statement.title} 31 March {year}</h2>" + document.blocks.append( + Block(f"{statement.key}-heading", "h2", heading, heading, editable=False) + ) + skeleton_html, target_html, figures = _render_statement(statement, rolled, prior) + document.blocks.append( + Block(statement.key, "table", skeleton_html, target_html, figures=figures) + ) + + notes_heading = "<h2>Notes to the Financial Statements</h2>" + document.blocks.append( + Block("notes-heading", "h2", notes_heading, notes_heading, editable=False) + ) + + # -- notes ------------------------------------------------------------- + builder = BodyBuilder(rolled, tree, mapping, corpus_root, config_root) + for note in tree.notes: + heading = f"<h3>Note {note.number} — {note.title}</h3>" + document.blocks.append( + Block("note-heading", "h3", heading, heading, note_number=note.number, editable=False) + ) + content = builder.build(note) + document.notes.append(content) + for element in content.elements: + document.blocks.append( + Block( + f"note-{note.number}-{element.role}", + element.tag, + element.skeleton, + element.target, + note_number=note.number, + figures=element.figures, + quotes=element.quotes, + ) + ) + document.coverage = dict(builder.coverage) + _assert_cross_references_resolve(document, tree) + return document + + +def _render_statement( + statement: Statement, rolled: RollForward, prior +) -> tuple[str, str, tuple[AnyFigure, ...]]: + year, prior_year = rolled.year, rolled.prior_year + body = Body().markup( + f'{style.table_open()}\n<tr style="{style.HEAD_ROW}">' + f"{style.head_cell('Particulars')}{style.head_cell('Note', numeric=True)}" + f'<th style="{style.TH_NUM}">' + ) + body.dated(f"31 March {year}", f"31 March {prior_year}").markup( + f'</th><th style="{style.TH_NUM}">' + ) + body.dated(f"31 March {prior_year}", f"31 March {prior_year - 1}").markup("</th></tr>\n") + for line in statement.lines: + if line.kind in ("section", "subheader"): + wrap = ("<strong>", "</strong>") if line.kind == "section" else ("<em>", "</em>") + body.markup( + f'<tr style="{style.SECTION_ROW}"><td colspan="4" style="{style.TD_TEXT}">{wrap[0]}' + ).text(line.caption).markup(f"{wrap[1]}</td></tr>\n") + continue + open_tag, close_tag = ("<strong>", "</strong>") if line.emphasis else ("", "") + body.markup(f'<tr><td style="{style.TD_TEXT}">').markup(open_tag).text(line.caption).markup( + close_tag + ) + body.markup(f'</td><td style="{style.TD_NUM}">') + if line.note_ref is not None: + body.text(str(line.note_ref)) + body.markup(f'</td><td style="{style.TD_NUM}">').markup(open_tag) + body.fig(line.current, line.prior) + body.markup(f'{close_tag}</td><td style="{style.TD_NUM}">{open_tag}') + body.fig(line.prior, _signed_prior_prior(prior, line)) + body.markup(f"{close_tag}</td></tr>\n") + body.markup("</table>") + where = f"{statement.key} table" + return body.skeleton(where), body.render(where), body.figures + + +def _signed_prior_prior(prior, line: Line) -> str: + """The signed set's own second column, used only as skeleton scaffolding.""" + found = prior.statement_row(line.caption) + if found is not None and len(found[1].values) > 1: + return found[1].values[1] + return "0.00" + + +def _assert_cross_references_resolve(document: StatutoryDocument, tree: NoteTree) -> None: + """Every `Note N` anywhere in the final document names a heading that exists.""" + numbers = {note.number for note in tree.notes} + for block in document.blocks: + if block.role == "note-heading": + continue + for match in NOTE_REFERENCE.finditer(block.target): + referenced = int(match.group(1)) + if referenced not in numbers: + raise CrossReferenceError( + f"block {block.role!r} points at Note {referenced}, which the final " + f"document does not have. Notes run 1..{max(numbers)}. Every reference is " + f"repointed through the same map that renumbered the headings, so this is " + f"a bug in the map, not a caveat." + ) + for statement in ("balance_sheet", "profit_and_loss"): + block = next(b for b in document.blocks if b.role == statement) + del block + + +def assert_note_captions_tie(document: StatutoryDocument, rolled: RollForward) -> list[str]: + """Every figure a note states for a caption is the figure the statement prints. + + Checked on the **rendered HTML** rather than on the objects, because the + objects being shared is the reason it holds and the rendering is what a + reader sees. Returns one line per checked caption for the run summary. + """ + checked: list[str] = [] + for statement in rolled.statements: + table = next(b for b in document.blocks if b.role == statement.key) + table_tokens = money_tokens_in_html(table.target) + for line in statement.lines: + if line.account is None or line.note_ref is None: + continue + note_blocks = [ + b + for b in document.blocks + if b.note_number == line.note_ref and b.role.endswith("captions") + ] + if not note_blocks: + continue + note_tokens = money_tokens_in_html(note_blocks[0].target) + current = line.current.rendered + if current not in table_tokens: + raise CrossReferenceError(f"{line.caption}: the statement does not print {current}") + if current not in note_tokens: + raise CrossReferenceError( + f"{line.caption}: Note {line.note_ref} does not print {current}, which the " + f"statement does. The note and the primary statement must tie exactly." + ) + checked.append(f"{line.caption} -> Note {line.note_ref}: {current}") + return checked + + +def zero() -> Decimal: # pragma: no cover - keeps Decimal imported for callers + return Decimal(0) + + +def note_body_blocks(document: StatutoryDocument, number: int) -> list[Block]: + return [b for b in document.blocks if b.note_number == number and b.role != "note-heading"] + + +def elements_of(content: NoteContent) -> list[Element]: # pragma: no cover - convenience + return content.elements diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/statements.py b/use-cases/preetham1930/statutory-statements-builder/statutory/statements.py new file mode 100644 index 000000000..21f3bf6a1 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/statements.py @@ -0,0 +1,432 @@ +"""The primary statements, rolled forward, with comparatives that are checked. + +The reporting-year column comes from the ledger. The comparative column comes +from the ledger *and* from the signed prior-year set, and the two must agree to +the paisa. That double citation is what turns "the comparatives tie" from a +claim into a check: if the trial balance's prior-year column had drifted from +what was signed, this raises rather than printing a number that reconciles to +nothing. + +Every subtotal and total is a `Derived` - computed on every read from the +account figures beneath it - so a statement cannot print a total that its own +rows do not support. The prior-year totals are then compared against the totals +the signed set actually prints, which is the second half of the same check. + +The layout (which groups sit under which section header, and what the subtotal +is called) is configuration keyed on the ledger's own `group` column. A new +group is a data change plus one line here, not a rewrite. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from pathlib import Path + +from .errors import ComparativeMismatchError, TieBreakError +from .figures import AnyFigure, Cite, Component, Derived, Figure +from .ledger import Account, TrialBalance +from .money import format_money, parse_money +from .priorset import PriorYearSet + +# section header -> the ledger groups shown under it, then the section total. +BALANCE_SHEET_LAYOUT: tuple[tuple[str, tuple[str, ...], str | None], ...] = ( + ("ASSETS", ("Non-current assets", "Current assets"), "TOTAL ASSETS"), + ( + "EQUITY AND LIABILITIES", + ("Equity", "Non-current liabilities", "Current liabilities"), + "TOTAL EQUITY AND LIABILITIES", + ), +) +SUBTOTAL_CAPTION = { + "Non-current assets": "Total non-current assets", + "Current assets": "Total current assets", + "Equity": "Total equity", + "Non-current liabilities": "Total non-current liabilities", + "Current liabilities": "Total current liabilities", + "Income": "Total income", + "Expenses": "Total expenses", +} +# Groups that get a printed sub-header row above them (Equity does not, in the +# signed set, and the roll-forward keeps the signed layout). +SUBHEADED = { + "Non-current assets", + "Current assets", + "Non-current liabilities", + "Current liabilities", +} + +_ALIASES = (("wip", "work-in-progress"),) + + +def normalise_caption(text: str) -> str: + lowered = " ".join(text.lower().split()) + for suffix in (" - non-current", " - current", " (net)"): + if lowered.endswith(suffix): + lowered = lowered[: -len(suffix)] + for short, long in _ALIASES: + lowered = lowered.replace(f" and {short}", f" and {long}") + return lowered.strip() + + +@dataclass +class Line: + """One printed row. `kind` decides how it is typeset, not what it means.""" + + caption: str + kind: str # section | subheader | account | subtotal | total | derivedline + current: AnyFigure | None = None + prior: AnyFigure | None = None + account: Account | None = None + prior_note_ref: int | None = None + note_ref: int | None = None # filled in once the note tree exists + emphasis: bool = False + + @property + def figures(self) -> tuple[AnyFigure, ...]: + return tuple(f for f in (self.current, self.prior) if f is not None) + + +@dataclass +class Statement: + key: str # "balance_sheet" | "profit_and_loss" + title: str # without the date, which the renderer appends + lines: list[Line] = field(default_factory=list) + column_labels: tuple[str, str] = ("", "") + + def line(self, caption: str) -> Line | None: + return next((line for line in self.lines if line.caption == caption), None) + + @property + def figures(self) -> tuple[AnyFigure, ...]: + return tuple(f for line in self.lines for f in line.figures) + + +@dataclass +class ComparativeCheck: + caption: str + ledger_value: Decimal + signed_value: Decimal | None + matched: bool + reason: str + + +@dataclass +class RollForward: + ledger: TrialBalance + prior: PriorYearSet + statements: list[Statement] + comparative_checks: list[ComparativeCheck] + ties: list[tuple[str, Decimal, Decimal, bool]] + + @property + def year(self) -> int: + return self.ledger.year + + @property + def prior_year(self) -> int: + return self.ledger.prior_year + + def statement(self, key: str) -> Statement: + return next(s for s in self.statements if s.key == key) + + def figure_for(self, code: str, year: int) -> AnyFigure: + line = next( + line for line in self._all_lines() if line.account and line.account.code == code + ) + return line.current if year == self.year else line.prior + + def caption_line(self, code: str) -> Line: + return next( + line for line in self._all_lines() if line.account and line.account.code == code + ) + + def total(self, caption: str) -> Derived: + for line in self._all_lines(): + if line.caption == caption and isinstance(line.current, Derived): + return line.current + raise KeyError(caption) + + def prior_total(self, caption: str) -> Derived: + for line in self._all_lines(): + if line.caption == caption and isinstance(line.prior, Derived): + return line.prior + raise KeyError(caption) + + def _all_lines(self) -> list[Line]: + return [line for statement in self.statements for line in statement.lines] + + +class _ComparativeSource: + """Locates a signed prior-year cell for a ledger account, once.""" + + def __init__(self, prior: PriorYearSet) -> None: + self._rows: dict[str, list] = {} + for statement in prior.statements: + for row in statement.rows: + if row.is_section or row.is_total or not row.values: + continue + self._rows.setdefault(normalise_caption(row.caption), []).append(row) + + def find(self, account: Account): + candidates = self._rows.get(normalise_caption(account.name), []) + if len(candidates) == 1: + return candidates[0] + for candidate in candidates: + if normalise_caption(candidate.section) == normalise_caption(account.group): + return candidate + return None + + +def roll_forward(ledger: TrialBalance, prior: PriorYearSet, corpus_root: Path) -> RollForward: + source = _ComparativeSource(prior) + checks: list[ComparativeCheck] = [] + + def figures_for(account: Account) -> tuple[Figure, Figure]: + current = Figure( + label=account.name, + value=account.presented(ledger.year), + basis="ledger", + cites=(account.cite(ledger.year),), + ) + prior_value = account.presented(ledger.prior_year) + row = source.find(account) + if row is None: + if prior_value != 0: + raise ComparativeMismatchError( + f"{account.code} ({account.name}) has a prior-year balance of " + f"{format_money(prior_value)} but no row in the signed prior-year set. " + f"A comparative that is not in the signed set cannot be presented as one; " + f"looked for caption {normalise_caption(account.name)!r} in " + f"{prior.rel}." + ) + checks.append( + ComparativeCheck( + account.name, + prior_value, + None, + True, + "new caption this year; nil in the ledger's prior-year column and absent " + "from the signed set - consistent", + ) + ) + return current, Figure( + label=f"{account.name} (comparative)", + value=prior_value, + basis="ledger", + cites=(account.cite(ledger.prior_year),), + ) + cell = row.values[0] + cell_cite = row.cites[0] + signed = parse_money(cell) + if signed != prior_value: + raise ComparativeMismatchError( + f"comparative mismatch on {account.name!r}: the trial balance's " + f"fy{ledger.prior_year} column presents {format_money(prior_value)} and the " + f"signed prior-year set prints {cell} at {prior.rel} bytes " + f"{cell_cite.byte_start}-{cell_cite.byte_end}. The comparatives must tie " + f"exactly; neither side is preferred and this is a hard error." + ) + checks.append( + ComparativeCheck(account.name, prior_value, signed, True, "ledger and signed set agree") + ) + return current, Figure( + label=f"{account.name} (comparative)", + value=prior_value, + basis="comparative", + cites=(account.cite(ledger.prior_year), cell_cite), + ) + + def note_ref_for(account: Account) -> int | None: + row = source.find(account) + return row.note_ref if row is not None else None + + balance_sheet = Statement("balance_sheet", "Balance Sheet as at") + for section, groups, section_total in BALANCE_SHEET_LAYOUT: + balance_sheet.lines.append(Line(section, "section", emphasis=True)) + section_components_current: list[Component] = [] + section_components_prior: list[Component] = [] + for group in groups: + accounts = ledger.in_group("BS", group) + if not accounts: + continue + if group in SUBHEADED: + balance_sheet.lines.append(Line(group, "subheader")) + group_current: list[Component] = [] + group_prior: list[Component] = [] + for account in accounts: + current, prior_figure = figures_for(account) + balance_sheet.lines.append( + Line( + caption=account.name, + kind="account", + current=current, + prior=prior_figure, + account=account, + prior_note_ref=note_ref_for(account), + ) + ) + group_current.append(Component(current)) + group_prior.append(Component(prior_figure)) + caption = SUBTOTAL_CAPTION[group] + balance_sheet.lines.append( + Line( + caption, + "subtotal", + Derived(caption, tuple(group_current)), + Derived(f"{caption} (comparative)", tuple(group_prior)), + emphasis=True, + ) + ) + section_components_current.extend(group_current) + section_components_prior.extend(group_prior) + if section_total: + balance_sheet.lines.append( + Line( + section_total, + "total", + Derived(section_total, tuple(section_components_current)), + Derived(f"{section_total} (comparative)", tuple(section_components_prior)), + emphasis=True, + ) + ) + + profit_and_loss = Statement( + "profit_and_loss", "Statement of Profit and Loss for the year ended" + ) + pl_totals: dict[str, tuple[Derived, Derived]] = {} + for group in ("Income", "Expenses", "Tax expense"): + accounts = ledger.in_group("PL", group) + if not accounts: + continue + current_components: list[Component] = [] + prior_components: list[Component] = [] + for account in accounts: + current, prior_figure = figures_for(account) + profit_and_loss.lines.append( + Line( + caption=account.name, + kind="account", + current=current, + prior=prior_figure, + account=account, + prior_note_ref=note_ref_for(account), + ) + ) + current_components.append(Component(current)) + prior_components.append(Component(prior_figure)) + if group in SUBTOTAL_CAPTION: + caption = SUBTOTAL_CAPTION[group] + current_total = Derived(caption, tuple(current_components)) + prior_total = Derived(f"{caption} (comparative)", tuple(prior_components)) + profit_and_loss.lines.append( + Line(caption, "subtotal", current_total, prior_total, emphasis=True) + ) + pl_totals[group] = (current_total, prior_total) + else: + pl_totals[group] = ( + Derived(f"Total {group.lower()}", tuple(current_components)), + Derived(f"Total {group.lower()} (comparative)", tuple(prior_components)), + ) + if group == "Expenses": + pbt_current = Derived( + "Profit before tax", + (Component(pl_totals["Income"][0]), Component(current_total, -1)), + ) + pbt_prior = Derived( + "Profit before tax (comparative)", + (Component(pl_totals["Income"][1]), Component(prior_total, -1)), + ) + profit_and_loss.lines.insert( + len(profit_and_loss.lines), + Line("Profit before tax", "derivedline", pbt_current, pbt_prior, emphasis=True), + ) + pl_totals["PBT"] = (pbt_current, pbt_prior) + tax_current, tax_prior = pl_totals["Tax expense"] + profit_and_loss.lines.append( + Line( + "Profit for the year", + "derivedline", + Derived( + "Profit for the year", + (Component(pl_totals["PBT"][0]), Component(tax_current, -1)), + ), + Derived( + "Profit for the year (comparative)", + (Component(pl_totals["PBT"][1]), Component(tax_prior, -1)), + ), + emphasis=True, + ) + ) + + rolled = RollForward(ledger, prior, [balance_sheet, profit_and_loss], checks, []) + _check_ties(rolled, prior) + return rolled + + +def _check_ties(rolled: RollForward, prior: PriorYearSet) -> None: + """The four ties in TASK.md section 2, plus every prior-year total the signed set prints.""" + ties = rolled.ties + for year_label, getter in ( + (rolled.year, lambda c: rolled.total(c)), + (rolled.prior_year, lambda c: rolled.prior_total(c)), + ): + assets = getter("TOTAL ASSETS").value + equity_and_liabilities = getter("TOTAL EQUITY AND LIABILITIES").value + ok = assets == equity_and_liabilities + ties.append( + ( + f"FY{year_label} total assets = total equity and liabilities", + assets, + equity_and_liabilities, + ok, + ) + ) + if not ok: + raise TieBreakError( + f"FY{year_label} balance sheet does not balance: total assets " + f"{format_money(assets)} against total equity and liabilities " + f"{format_money(equity_and_liabilities)}, a difference of " + f"{format_money(assets - equity_and_liabilities)}." + ) + + # Every total the signed set prints must equal the total our comparatives + # derive. This is the half of "the comparatives tie" that a caption-by-caption + # check cannot see: a set can agree row by row and still print a wrong total. + for statement in prior.statements: + for row in statement.rows: + if not row.values: + continue + try: + ours = rolled.prior_total(row.caption) + except KeyError: + continue + signed = parse_money(row.values[0]) + ok = ours.value == signed + ties.append( + (f"FY{rolled.prior_year} {row.caption} vs signed set", ours.value, signed, ok) + ) + if not ok: + raise ComparativeMismatchError( + f"comparative total mismatch on {row.caption!r}: our roll-forward derives " + f"{format_money(ours.value)} from {len(ours.components)} ledger row(s), the " + f"signed prior-year set prints {row.values[0]}. Working: {ours.workings()}" + ) + + other_equity = rolled.caption_line("2110") + profit = rolled.statement("profit_and_loss").line("Profit for the year").current + opening = other_equity.prior.value + closing = other_equity.current.value + ok = opening + profit.value == closing + ties.append((f"{other_equity.caption} rolls forward", opening + profit.value, closing, ok)) + if not ok: + raise TieBreakError( + f"{other_equity.caption} does not roll forward: {format_money(opening)} + " + f"{format_money(profit.value)} = {format_money(opening + profit.value)}, but the " + f"ledger closes at {format_money(closing)}. The difference " + f"{format_money(closing - opening - profit.value)} is undistributed and unexplained." + ) + + +def unused_cite(cite: Cite) -> Cite: # pragma: no cover - keeps the import honest + return cite diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/style.py b/use-cases/preetham1930/statutory-statements-builder/statutory/style.py new file mode 100644 index 000000000..f881df684 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/style.py @@ -0,0 +1,33 @@ +"""Table styling, as inline CSS, because that is what measurably survives. + +The signed prior-year set uses the legacy `border="1" cellpadding="4"` +attributes. Measured on the first live upload: the product drops them and the +table comes back unstyled. Inline `style` attributes are the ones that survive - +on the styled template fixture all 21 borders, both zebra rows, the header +shading and all 15 right-alignments came back identical, through an upload, a +targeted replace and a DOCX export. + +So the roll-forward re-styles the tables on the way through. That is a change to +the signed set's markup and it is deliberate: the deliverable is a typeset +document, and this is the styling the export preserves. +""" + +from __future__ import annotations + +RULE = "1px solid #1f3864" + +TABLE = f"border-collapse:collapse;width:100%;border:{RULE}" +HEAD_ROW = "background-color:#1f3864;color:#ffffff" +TH_TEXT = f"border:{RULE};padding:6px;text-align:left" +TH_NUM = f"border:{RULE};padding:6px;text-align:right" +TD_TEXT = f"border:{RULE};padding:6px" +TD_NUM = f"border:{RULE};padding:6px;text-align:right" +SECTION_ROW = "background-color:#eaeef7" + + +def table_open() -> str: + return f'<table style="{TABLE}">' + + +def head_cell(text: str, numeric: bool = False) -> str: + return f'<th style="{TH_NUM if numeric else TH_TEXT}">{text}</th>' diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/__init__.py b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/__init__.py new file mode 100644 index 000000000..5999721ea --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/__init__.py @@ -0,0 +1,5 @@ +"""The SuperDocs wire: transport, client, decision ledger, verifier. + +Nothing above this package knows an HTTP path or a JSON key, and nothing in it +knows what a statutory note is. +""" diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/client.py b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/client.py new file mode 100644 index 000000000..ee0180b68 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/client.py @@ -0,0 +1,290 @@ +"""The SuperDocs client, with the wire rules enforced in the client itself. + +Four measured decisions live here rather than in a caller's discipline: + +- **Decision 32** - gated changes go to `/v1/chat/async`. The gate takes a + `job_id` and jobs exist only on the async route; passing `approval_mode` to + the synchronous route leaves nothing to approve against. `chat_gated` is the + only method that can produce a pending change and it cannot reach `/v1/chat`. +- **Decision 35** - one change per job, on the sending side. The answering side + does not always agree: the first live run of this build asked for one chunk by + id and got a two-change batch back. So we approve exactly the change we + computed and deny every other one bare, and the read-back is what makes that + safe rather than the batch size. +- **Decision 34** - a denial carries no feedback. `deny()` takes no feedback + parameter, so there is no call shape in which one can be sent. Feedback + triggers a revision pass, and the revision pass proposed edits to two chunks + nobody targeted, aimed at the section headings. +- **Decision 33** - `approve` is an actuator, not a record. This client returns + what the API returned and claims nothing about what was decided; the decision + ledger is written by the caller, before the call. + +Nothing here believes a reply. `read_back()` is the only method whose result is +evidence, and it is the only one the verifier consults. +""" + +from __future__ import annotations + +import base64 +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from ..chunks import ChunkMap +from ..errors import PlanRefusedError +from .transport import Transport + +TERMINAL = {"completed", "failed", "cancelled", "error"} + + +@dataclass +class PendingChange: + change_id: str + chunk_id: str + operation: str + old_html: str + new_html: str + + +@dataclass +class Job: + job_id: str + status: str + progress: int + pending: list[PendingChange] + raw: dict[str, Any] + + @property + def awaiting(self) -> bool: + return self.status == "awaiting_approval" + + @property + def finished(self) -> bool: + return self.status in TERMINAL + + +class SuperDocsClient: + def __init__(self, transport: Transport, *, poll_seconds: float = 3.0, max_polls: int = 80): + self.transport = transport + self.poll_seconds = poll_seconds + self.max_polls = max_polls + self.billable_calls = 0 + self.export_content_types: list[str] = [] + + # -- documents --------------------------------------------------------- + + def upload_verbatim(self, filename: str, html: str) -> tuple[str, str]: + """Upload our computed skeleton. Returns (document_id, session_id). + + Verbatim is the load-bearing property (Decision 29) and it is checked by + the caller against a read-back, never assumed from this response. + """ + # Three calls, and the third is the one that makes the other two + # checkable. An upload with no session is not persisted, and the upload + # response carries no document id at all - so the id comes from the + # session's own roster, which is a read rather than a claim. + session = self.transport.request("POST", "/v1/sessions/init", json_body={}) + session_id = session.get("session_id") + if not session_id: + raise RuntimeError(f"sessions/init returned no session_id; keys {sorted(session)}") + payload = { + "filename": filename, + "file_base64": base64.b64encode(html.encode("utf-8")).decode("ascii"), + "session_id": session_id, + } + # One retry, because a 503 with an empty body was observed on a 23 KB + # upload and succeeded immediately on the next attempt. A retried POST + # could leave two documents in the session, so the roster below is + # required to hold exactly one - the retry is checked, not trusted. + try: + uploaded = self.transport.request( + "POST", "/v1/documents/upload-base64", json_body=payload + ) + except RuntimeError as exc: + if "HTTP 5" not in str(exc): + raise + uploaded = self.transport.request( + "POST", "/v1/documents/upload-base64", json_body=payload + ) + if not uploaded.get("persisted"): + raise RuntimeError( + f"the upload reports persisted={uploaded.get('persisted')!r}. An unpersisted " + f"document cannot be read back, and a read-back is the only evidence there is." + ) + roster = self.transport.request("GET", f"/v1/sessions/{session_id}/documents") + documents = roster.get("documents") or [] + if len(documents) > 1: + raise RuntimeError( + f"the session holds {len(documents)} documents and this build works on one. " + f"If an upload was retried after a 5xx, both may have landed; nothing is edited " + f"until a human has looked." + ) + durable = next( + (d.get("durable_document_id") for d in documents if d.get("durable_document_id")), None + ) + if not durable: + raise RuntimeError( + f"the session roster carries no durable document id; {len(documents)} " + f"document(s), keys {sorted(documents[0]) if documents else '(none)'}. " + f"Without one there is nothing to verify against." + ) + return durable, session_id + + def read_back( + self, document_id: str, *, attempts: int = 4, sleep=time.sleep + ) -> tuple[ChunkMap, dict[str, Any]]: + """`GET /v1/documents/{id}?include_html=true` - the only evidence there is. + + Retried on a 404 because the durable record is not always queryable the + instant the upload returns: one run 404'd immediately and the same call + returned 200 on the next attempt. A read is safe to repeat; the retry is + bounded and a persistent 404 stops the run rather than being smoothed + over into "nothing to verify". + """ + body: dict[str, Any] = {} + for attempt in range(attempts): + try: + body = self.transport.request( + "GET", f"/v1/documents/{document_id}", params={"include_html": "true"} + ) + break + except RuntimeError as exc: + if "HTTP 404" not in str(exc) or attempt == attempts - 1: + raise + sleep(2.0 * (attempt + 1)) + html = body.get("html") + if not html: + raise RuntimeError( + f"read-back for {document_id} carried no html. A verification step that " + f"cannot see the document verifies nothing." + ) + return ChunkMap(html), body + + def export(self, session_id: str, fmt: str, out_dir: Path | None = None) -> Path | None: + """The export answers with a file, not with JSON, so it gets its own path. + + Written to disk when `out_dir` is given, because an export nobody can + open is not evidence of anything. + """ + content_type, blob = self.transport.download( + "POST", "/v1/documents/export", json_body={"session_id": session_id, "format": fmt} + ) + self.export_content_types.append(content_type) + if out_dir is None or not blob: + return None + out_dir.mkdir(parents=True, exist_ok=True) + path = out_dir / f"statements.{fmt}" + path.write_bytes(blob) + return path + + def revert(self, session_id: str, turn_index: int) -> dict[str, Any]: + return self.transport.request( + "POST", f"/v1/sessions/{session_id}/revert", json_body={"turn_index": turn_index} + ) + + # -- the gated edit loop ---------------------------------------------- + + def chat_gated(self, session_id: str, document_id: str, instruction: str) -> Job: + """One instruction, on the async route, held at the gate (Decisions 32 and 35). + + `document_id` is not sent: the session holds exactly one document and it + is focused, and the durable id we verify against is not the id the chat + surface uses for it. Naming the wrong one is worse than naming none. + """ + del document_id + body = self.transport.request( + "POST", + "/v1/chat/async", + json_body={ + "session_id": session_id, + "message": instruction, + "approval_mode": "ask_every_time", + "response_mode": "compact", + }, + ) + self.billable_calls += 1 + job_id = body.get("job_id") + if not job_id: + raise RuntimeError(f"chat/async returned no job_id; keys {sorted(body)}") + return self.poll(job_id) + + def poll(self, job_id: str, *, until_terminal: bool = False, sleep=time.sleep) -> Job: + for _ in range(self.max_polls): + body = self.transport.request("GET", f"/v1/jobs/{job_id}") + job = _job_from(body) + if job.finished or (job.awaiting and not until_terminal): + return job + sleep(self.poll_seconds) + raise TimeoutError( + f"job {job_id} did not settle in {self.max_polls} polls. Nothing is reported as " + f"applied on a timeout; the document is read back and the run halts." + ) + + def approve(self, session_id: str, job_id: str, change_id: str) -> dict[str, Any]: + return self.transport.request( + "POST", + f"/v1/chat/{session_id}/approve", + json_body={"job_id": job_id, "change_id": change_id, "approved": True}, + ) + + def deny(self, session_id: str, job_id: str, change_id: str) -> dict[str, Any]: + """Bare. There is no feedback parameter, because feedback re-plans (Decision 34).""" + return self.transport.request( + "POST", + f"/v1/chat/{session_id}/approve", + json_body={"job_id": job_id, "change_id": change_id, "approved": False}, + ) + + +def select_our_change( + job: Job, expected_chunk_id: str +) -> tuple[PendingChange, list[PendingChange]]: + """Split a batch into the one change we asked for and everything else. + + We send one chunk per job (Decision 35) and the product does not always + answer with one. Measured on the first live run of this build: an + instruction naming a single chunk id came back as a **two**-change batch, + the second on a chunk nobody targeted - the same shape as the unprompted + heading edits recorded on 2026-08-09. + + So the rule is not "the batch must be one"; it is **we approve exactly the + change we computed and deny every other one, bare** (Decision 34). The + read-back afterwards is what makes that safe: if a denial did not hold, the + non-target chunk moved and the verifier calls it collateral damage. + """ + ours = next((c for c in job.pending if c.chunk_id == expected_chunk_id), None) + if ours is None: + raise PlanRefusedError( + f"job {job.job_id} proposes {len(job.pending)} change(s), none of them on chunk " + f"{expected_chunk_id}, which is the one the step targets. Chunks proposed: " + f"{[c.chunk_id for c in job.pending]}. Every one is denied; nothing is approved on " + f"the strength of a proposal we did not ask for." + ) + if ours.operation != "edit": + raise PlanRefusedError( + f"job {job.job_id} proposes operation {ours.operation!r} on our target. There is " + f"one verb and it is replace (Decision 28)." + ) + return ours, [c for c in job.pending if c is not ours] + + +def _job_from(body: dict[str, Any]) -> Job: + metadata = body.get("metadata") or {} + pending = [ + PendingChange( + change_id=c.get("change_id", ""), + chunk_id=c.get("chunk_id", ""), + operation=c.get("operation", ""), + old_html=c.get("old_html", "") or "", + new_html=c.get("new_html", "") or "", + ) + for c in (metadata.get("pending_changes") or []) + ] + return Job( + job_id=body.get("job_id", ""), + status=body.get("status", ""), + progress=int(body.get("progress") or 0), + pending=pending, + raw=body, + ) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/decisions.py b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/decisions.py new file mode 100644 index 000000000..2ac98122d --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/decisions.py @@ -0,0 +1,123 @@ +"""Our decision ledger. SuperDocs' `approve` is only an actuator (Decision 33). + +Measured on 2026-08-09: the product's gate is **write-only**. Before any +decision `pending_changes` listed three changes with `status: None`; after +approving one and denying another it listed the same three, still with +`status: None`. The approve call returns `{"status":"ok","batch_complete":...}` +and nothing else. There is no API answer to "what has been decided, and by whom". + +So the row is ours, it is written **before** the actuator is called, and it +carries a named actor. An item is undecided exactly when it has no row - there +is no default, no pending row and no status column. A second decision on the +same item is refused unless it explicitly supersedes, and the superseded row is +kept with both actors. + +The completion sentence is computed from the rows at the moment it is emitted, +never from counters, because we watched a product report "Successfully updated +all 0 sections. 2 change(s) were denied" when two had been applied and three +denied. +""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from pathlib import Path + +from ..errors import DecisionAlreadyRecordedError + +VERBS = ("accept", "reject") + + +@dataclass(frozen=True) +class Decision: + run_id: str + step_index: int + chunk_id: str + verb: str + actor: str + reason: str + at: str + supersedes: str | None = None + + def __post_init__(self) -> None: + if self.verb not in VERBS: + raise ValueError( + f"the vocabulary is {VERBS} and nothing else. There is no 'resolve' and no " + f"'choose_side': a reviewer decides whether a change enters the document, not " + f"which of two disagreeing sources is right." + ) + if not self.actor.strip(): + raise ValueError("a decision with no actor answers nobody's question about who") + + +class DecisionLedger: + def __init__(self, path: Path | None = None) -> None: + self.path = path + self.rows: list[Decision] = [] + + def record( + self, + run_id: str, + step_index: int, + chunk_id: str, + verb: str, + actor: str, + reason: str, + supersede: bool = False, + ) -> Decision: + existing = self.find(run_id, step_index) + if existing is not None and not supersede: + raise DecisionAlreadyRecordedError( + f"step {step_index} of run {run_id} was already decided '{existing.verb}' by " + f"{existing.actor} at {existing.at}. Quiet replacement is the mechanism by " + f"which a decision gets quietly dropped; pass supersede=True and both rows are " + f"kept." + ) + row = Decision( + run_id=run_id, + step_index=step_index, + chunk_id=chunk_id, + verb=verb, + actor=actor, + reason=reason, + at=datetime.now(UTC).isoformat(timespec="seconds"), + supersedes=existing.at if existing is not None else None, + ) + self.rows.append(row) + if self.path is not None: + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(asdict(row)) + "\n") + return row + + def find(self, run_id: str, step_index: int) -> Decision | None: + matches = [r for r in self.rows if r.run_id == run_id and r.step_index == step_index] + return matches[-1] if matches else None + + def decided(self, run_id: str) -> list[Decision]: + latest: dict[int, Decision] = {} + for row in self.rows: + if row.run_id == run_id: + latest[row.step_index] = row + return [latest[k] for k in sorted(latest)] + + # No count fields anywhere: these are properties of the rows being held, so + # the sentence cannot assert a decision that was never made (Decision 22's + # trick, rebuilt). + def accepted(self, run_id: str) -> int: + return sum(1 for r in self.decided(run_id) if r.verb == "accept") + + def rejected(self, run_id: str) -> int: + return sum(1 for r in self.decided(run_id) if r.verb == "reject") + + def describe(self, run_id: str, planned: int) -> str: + accepted = self.accepted(run_id) + rejected = self.rejected(run_id) + undecided = planned - accepted - rejected + actors = sorted({r.actor for r in self.decided(run_id)}) + return ( + f"{accepted} accepted, {rejected} rejected, {undecided} undecided of {planned} " + f"planned change(s); decided by {', '.join(actors) or 'nobody'}" + ) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/transport.py b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/transport.py new file mode 100644 index 000000000..6fe4d5d9d --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/transport.py @@ -0,0 +1,172 @@ +"""How a request reaches SuperDocs - or, in the test suite, how it does not. + +Two implementations behind one protocol: + +- `HttpTransport` talks to `api.superdocs.app`. It is used by the demo and by + nothing else. The key is read from the environment, never printed, never + written to a file, and never included in a message we log. +- `ReplayTransport` answers from recorded responses under `docs/evidence/`. It + is what the whole test suite runs on, so `make test` passes on a clean + checkout with no `.env` and with the network blocked (hard rule 3). + +A replay transport that is asked for a request it has no recording of **raises**, +naming the request. A transport that invents a plausible response would be the +2026-08-07 failure mode with a different noun: a success message for work that +did not happen. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Protocol + +from ..errors import NotConfiguredError + +DEFAULT_BASE_URL = "https://api.superdocs.app" + + +class Transport(Protocol): + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: ... + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + """For the one endpoint that answers with a file rather than with JSON.""" + ... + + +def redacted_key() -> str: + key = os.environ.get("SUPERDOCS_API_KEY", "") + if not key: + return "(not set)" + return f"...{key[-4:]}" + + +class HttpTransport: + def __init__(self, base_url: str | None = None, timeout: float = 180.0) -> None: + self.base_url = ( + base_url or os.environ.get("SUPERDOCS_BASE_URL") or DEFAULT_BASE_URL + ).rstrip("/") + self.timeout = timeout + self.calls = 0 + key = os.environ.get("SUPERDOCS_API_KEY", "") + if not key: + raise NotConfiguredError( + "SUPERDOCS_API_KEY is not set. This build reads it from .env only, never from " + "a command line and never from a file it writes. The test suite does not need " + "it: tests run on recorded responses under docs/evidence/." + ) + self._key = key + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: + import httpx # imported here so the package imports with no network stack + + self.calls += 1 + response = httpx.request( + method, + f"{self.base_url}{path}", + json=json_body, + params=params, + headers={"Authorization": f"Bearer {self._key}", "Content-Type": "application/json"}, + timeout=self.timeout, + ) + if response.status_code >= 400: + # The body can echo the request; never let it carry the key onward. + raise RuntimeError( + f"{method} {path} -> HTTP {response.status_code}. " + f"{response.text[:400].replace(self._key, '<redacted>')}" + ) + return response.json() + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + """The export endpoint answers with a file, so it does not go through `request`.""" + import httpx + + self.calls += 1 + response = httpx.request( + method, + f"{self.base_url}{path}", + json=json_body, + headers={"Authorization": f"Bearer {self._key}", "Content-Type": "application/json"}, + timeout=self.timeout, + ) + if response.status_code >= 400: + raise RuntimeError( + f"{method} {path} -> HTTP {response.status_code}. " + f"{response.text[:400].replace(self._key, '<redacted>')}" + ) + return response.headers.get("content-type", ""), response.content + + +@dataclass +class RecordedCall: + method: str + path: str + response: dict[str, Any] + match: dict[str, Any] = field(default_factory=dict) + + +class ReplayTransport: + """Answers from recordings, in order, and refuses anything it was not given.""" + + def __init__(self, calls: list[RecordedCall]) -> None: + self._calls = list(calls) + self.seen: list[tuple[str, str, dict[str, Any] | None]] = [] + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: + self.seen.append((method, path, json_body)) + for index, call in enumerate(self._calls): + if call.method != method or call.path != path: + continue + if call.match and not all((json_body or {}).get(k) == v for k, v in call.match.items()): + continue + self._calls.pop(index) + return call.response + raise AssertionError( + f"no recorded response for {method} {path} with body keys " + f"{sorted(json_body or {})}. A replay transport never invents one: an invented " + f"response is a success message for work that did not happen. " + f"{len(self._calls)} recording(s) left: " + f"{[(c.method, c.path) for c in self._calls]}" + ) + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + """A replayed export records that we asked. There is no file to replay.""" + self.request(method, path, json_body=json_body) + return "application/octet-stream", b"" + + @property + def exhausted(self) -> bool: + return not self._calls + + +def load_recording(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) diff --git a/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/verifier.py b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/verifier.py new file mode 100644 index 000000000..6edce2bde --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/statutory/superdocs/verifier.py @@ -0,0 +1,180 @@ +"""Read the whole document back and decide what actually happened. + +Three classes, and the third is the dangerous one: + +- **NOT APPLIED** - the target chunk came back unchanged. This is the + 2026-08-07 corrupted-document mechanism: the change silently does not land and + the caller executes its consequences anyway. +- **APPLIED WRONG** - the target changed, but not to the post-state we computed + before sending. +- **COLLATERAL DAMAGE** - a chunk we did not target moved, appeared or vanished. + Measured on 2026-08-09: one requested change *succeeded* while four fabricated + sections appeared alongside it, and again when a template instantiation kept + the table and silently deleted the letterhead. A verifier that checked only its + own target would have reported success both times. + +So the comparison is over the **whole document**, every time, including on a +retry - failed content has been observed arriving a turn later. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..chunks import ChunkMap, canonical + + +@dataclass +class Problem: + kind: str # not_applied | applied_wrong | collateral_damage + detail: str + diff: str = "" + + +@dataclass +class Verification: + step_index: int + chunk_id: str + problems: list[Problem] = field(default_factory=list) + receipt: str = "" + exact_bytes: bool = False + + @property + def ok(self) -> bool: + return not self.problems + + def report(self) -> str: + if self.ok: + return f"step {self.step_index}: ok - {self.receipt}" + lines = [f"step {self.step_index}: FAILED on chunk {self.chunk_id}"] + for problem in self.problems: + lines.append(f" {problem.kind.upper().replace('_', ' ')}: {problem.detail}") + if problem.diff: + lines.append(problem.diff) + return "\n".join(lines) + + +def verify( + step_index: int, + chunk_id: str, + before: ChunkMap, + after: ChunkMap, + expected_html: str, +) -> Verification: + result = Verification(step_index=step_index, chunk_id=chunk_id) + expected = canonical(expected_html) + + before_ids = before.ids + after_ids = after.ids + added = [i for i in after_ids if i not in set(before_ids)] + removed = [i for i in before_ids if i not in set(after_ids)] + if added or removed: + result.problems.append( + Problem( + "collateral_damage", + f"the chunk set changed: {len(before_ids)} chunk(s) before, {len(after_ids)} " + f"after; {len(added)} added, {len(removed)} removed. A replacement never " + f"changes the chunk set.", + diff="\n".join( + f" + {after.by_id[i].tag}: {after.by_id[i].html[:120]}" for i in added[:6] + ) + + "\n".join(f" - {i}" for i in removed[:6]), + ) + ) + moved = [ + i + for i in before_ids + if i + and i != chunk_id + and i in after.by_id + and after.by_id[i].canonical != before.by_id[i].canonical + ] + if moved: + result.problems.append( + Problem( + "collateral_damage", + f"{len(moved)} chunk(s) we did not target changed: {moved[:6]}", + diff="\n".join( + f" was: {before.by_id[i].html[:150]}\n now: {after.by_id[i].html[:150]}" + for i in moved[:3] + ), + ) + ) + + if chunk_id not in after.by_id: + result.problems.append( + Problem("not_applied", f"the target chunk {chunk_id} is not in the read-back at all") + ) + return result + + was = before.by_id[chunk_id].canonical + now = after.by_id[chunk_id].canonical + if now == was: + result.problems.append( + Problem( + "not_applied", + "the target chunk came back byte-identical to what it held before. The chat " + "reply is not evidence and is not consulted here.", + diff=f" unchanged: {before.by_id[chunk_id].html[:200]}", + ) + ) + elif now != expected: + result.problems.append( + Problem( + "applied_wrong", + "the target chunk changed, but not to the post-state computed before sending", + diff=_diff(expected, now), + ) + ) + else: + result.exact_bytes = _strip_id(after.by_id[chunk_id].html) == _strip_id(expected_html) + result.receipt = ( + f"read back {len(after_ids)} chunk(s) from GET /v1/documents/" + f"{{id}}?include_html=true; target chunk {chunk_id} matches the computed " + f"post-state{' byte for byte' if result.exact_bytes else ' (canonical form)'}; " + f"{len(after_ids) - 1} non-target chunk(s) unchanged" + ) + return result + + +def _strip_id(html: str) -> str: + from ..chunks import strip_chunk_ids + + return strip_chunk_ids(html) + + +def _diff(expected: str, actual: str) -> str: + import difflib + + lines = list( + difflib.unified_diff( + expected.split(">"), actual.split(">"), "expected", "actual", lineterm="", n=1 + ) + ) + return "\n".join(f" {line}" for line in lines[:24]) + + +def verify_upload_verbatim(uploaded: ChunkMap, sent_blocks: list[str]) -> str: + """The upload is only useful if it is verbatim, so that is checked, not assumed. + + Decision 29 rests entirely on `upload-base64` being a verbatim load. It was + measured once; it is re-checked on every run, because a design resting on a + measured property should re-measure it rather than remember it. + """ + ours = [canonical(block) for block in sent_blocks] + theirs = [chunk.canonical for chunk in uploaded.chunks] + if ours != theirs: + mismatch = next( + (i for i, (a, b) in enumerate(zip(ours, theirs, strict=False)) if a != b), + min(len(ours), len(theirs)), + ) + raise AssertionError( + f"the upload was not verbatim: we sent {len(ours)} block(s) and {len(theirs)} came " + f"back, first difference at block {mismatch}.\n" + f" sent: {ours[mismatch] if mismatch < len(ours) else '(nothing)'}\n" + f" got : {theirs[mismatch] if mismatch < len(theirs) else '(nothing)'}\n" + f"Decision 29 rests on the upload being verbatim; if it is not, the whole " + f"skeleton approach is unsound and the run stops here rather than editing a " + f"document that is not the one we computed." + ) + return f"upload verified verbatim: {len(theirs)} chunk(s), every block canonically identical" diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/recorded.py b/use-cases/preetham1930/statutory-statements-builder/tests/recorded.py new file mode 100644 index 000000000..f7d83b997 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/recorded.py @@ -0,0 +1,38 @@ +"""Recorded SuperDocs responses, replayed. No key, no network, no invention. + +Everything here comes out of `docs/evidence/`, which was captured on 2026-08-09 +against the live API and saved with `redemption_id` stripped. The failure-class +tests are driven by those recordings rather than by hand-written HTML, because +the point is that these three failures are things the product actually did. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent +REVERIFY = REPO_ROOT / "docs" / "evidence" / "2026-08-09-superdocs-reverify" +GATE = REPO_ROOT / "docs" / "evidence" / "2026-08-09-gate-and-templates" + + +def load(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def read_back_html(name: str) -> str: + """`html` from a recorded GET /v1/documents/{id}?include_html=true.""" + return load(REVERIFY / f"{name}.json")["html"] + + +def gate_job(name: str) -> dict[str, Any]: + return load(GATE / f"gate-job-{name}.json") + + +def targeted_edit_response() -> dict[str, Any]: + return load(REVERIFY / "A-targeted-edit.response.json") + + +def append_response() -> dict[str, Any]: + return load(REVERIFY / "C-append-structural.response.json") diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/simulator.py b/use-cases/preetham1930/statutory-statements-builder/tests/simulator.py new file mode 100644 index 000000000..9cb3c440f --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/simulator.py @@ -0,0 +1,213 @@ +"""A SuperDocs stand-in whose behaviours are the ones we measured. + +This is not a mock that returns what it was told to return. It holds a real +document, parses it with our own chunk parser, and reproduces four behaviours +recorded on 2026-08-09: + +- an upload is verbatim, and comes back with a `data-chunk-id` per top-level + element (measured: five chunks for a styled HTML upload, table atomic); +- `chat/async` with `approval_mode: ask_every_time` holds the change at + `awaiting_approval` and applies **nothing** until the batch settles; +- an approved change lands only when the batch completes; +- the failure modes are optional and each is the shape of a recorded one: + `not_applied` (the mid-document insertion that vanished), `applied_wrong`, + `collateral` (the create that appended sections nobody asked for). + +The assertions in the tests are about **our** behaviour - does the queue halt, +is the ledger row written before the actuator, is an export produced - not about +the simulator's. The simulator only has to be a faithful enough environment for +those questions to be meaningful. +""" + +from __future__ import annotations + +import re +import uuid +from dataclasses import dataclass, field +from typing import Any + +from statutory.chunks import ChunkMap + +CHUNK_TAG = re.compile(r"<([a-zA-Z0-9]+)(\s|>)") + + +@dataclass +class Behaviour: + """What the simulator does on a given **chat call** (1-based, not step). + + A retry is a second chat call for the same step, so `not_applied={2, 3}` + means "step 2 fails and so does its one narrow retry", while + `not_applied={2}` means "step 2 fails and the retry lands". + """ + + not_applied: set[int] = field(default_factory=set) + applied_wrong: set[int] = field(default_factory=set) + collateral: set[int] = field(default_factory=set) + fail_upload_verbatim: bool = False + revert_works: bool = True + + +class SimulatedSuperDocs: + def __init__(self, behaviour: Behaviour | None = None) -> None: + self.behaviour = behaviour or Behaviour() + self.html = "" + self.pristine = "" + self.document_id = "doc-" + uuid.uuid4().hex[:8] + self.session_id = "session_init_" + uuid.uuid4().hex[:12] + self.jobs: dict[str, dict[str, Any]] = {} + self.step_index = 0 + self.exports: list[str] = [] + self.reverts = 0 + self.approve_calls: list[str] = [] + self.event_log: list[str] = [] + + # -- transport --------------------------------------------------------- + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> dict[str, Any]: + body = json_body or {} + if method == "POST" and path == "/v1/sessions/init": + return {"session_id": self.session_id, "opened": [], "focused_document_id": None} + if method == "POST" and path == "/v1/documents/upload-base64": + import base64 + + raw = base64.b64decode(body["file_base64"]).decode("utf-8") + if self.behaviour.fail_upload_verbatim: + raw = raw.replace("</p>", " (edited on ingest)</p>", 1) + self.html = _assign_chunk_ids(raw) + self.pristine = self.html + return { + "session_id": self.session_id, + "filename": body["filename"], + "chunks_count": len(ChunkMap(self.html)), + "persisted": True, + } + if method == "GET" and path == f"/v1/sessions/{self.session_id}/documents": + return { + "session_id": self.session_id, + "focused_document_id": "doc_primary", + "documents": [ + { + "document_id": "doc_primary", + "durable_document_id": self.document_id, + "chunks_count": len(ChunkMap(self.html)), + "focused": True, + } + ], + } + if method == "GET" and path == f"/v1/documents/{self.document_id}": + return {"document_id": self.document_id, "html": self.html, "version": 1} + if method == "POST" and path == "/v1/chat/async": + return {"job_id": self._plan_job(body["message"])} + if method == "GET" and path.startswith("/v1/jobs/"): + return self.jobs[path.rsplit("/", 1)[1]] + if path.endswith("/approve"): + self.approve_calls.append(body["change_id"]) + self.event_log.append(f"approve:{body['change_id']}") + self._settle(body["job_id"], body["approved"]) + return {"status": "ok", "batch_complete": True} + if path == "/v1/documents/export": + self.exports.append(body.get("format", "docx")) + return {"download_url": "https://example.invalid/x"} + if path.endswith("/revert"): + self.reverts += 1 + if self.behaviour.revert_works: + self.html = self.last_good + else: + # The call returns ok and the document is not restored. This is + # the shape the whole build is built against: a 200 means the + # call was accepted, not that the state is what it claims. + self.html += '\n<p data-chunk-id="rv-1">not actually reverted</p>' + return {"status": "ok"} + raise AssertionError(f"the simulator was asked for {method} {path}") + + def download( + self, method: str, path: str, *, json_body: dict[str, Any] | None = None + ) -> tuple[str, bytes]: + self.request(method, path, json_body=json_body) + return ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + b"PKsimulated-docx", + ) + + # -- behaviour --------------------------------------------------------- + + def _plan_job(self, message: str) -> str: + self.step_index += 1 + chunk_id = re.search(r"data-chunk-id is ([0-9a-f-]+)", message).group(1) + new_html = message.split("\n\n", 1)[1] + job_id = "job-" + uuid.uuid4().hex[:8] + self.jobs[job_id] = { + "job_id": job_id, + "status": "awaiting_approval", + "progress": 88, + "metadata": { + "pending_changes": [ + { + "change_id": "chg-" + uuid.uuid4().hex[:8], + "chunk_id": chunk_id, + "operation": "edit", + "old_html": ChunkMap(self.html).by_id[chunk_id].html, + "new_html": new_html, + } + ] + }, + "_chunk_id": chunk_id, + "_new_html": new_html, + "_step": self.step_index, + } + return job_id + + def _settle(self, job_id: str, approved: bool) -> None: + job = self.jobs[job_id] + job["status"] = "completed" + job["progress"] = 100 + job["metadata"]["pending_changes"] = [] + if not approved: + return + step = job["_step"] + chunk_id, new_html = job["_chunk_id"], job["_new_html"] + if step in self.behaviour.not_applied: + self.event_log.append(f"step{step}:silently-did-nothing") + return + if step in self.behaviour.applied_wrong: + new_html = new_html.replace("</p>", " (paraphrased)</p>") + self._replace(chunk_id, new_html) + if step in self.behaviour.collateral: + self.event_log.append(f"step{step}:appended-sections-nobody-asked-for") + self.html += ( + '\n<h3 data-chunk-id="fab-1">Note 99 — Quality Assurance</h3>' + '\n<p data-chunk-id="fab-2">The Company maintains a quality system.</p>' + ) + + def _replace(self, chunk_id: str, new_html: str) -> None: + current = ChunkMap(self.html) + old = current.by_id[chunk_id].html + replacement = _with_chunk_id(new_html, chunk_id) + self.html = self.html.replace(old, replacement, 1) + + @property + def last_good(self) -> str: + return self.pristine + + +def _assign_chunk_ids(html: str) -> str: + out: list[str] = [] + for line in html.split("\n"): + match = CHUNK_TAG.match(line) + if match: + out.append(_with_chunk_id(line, "c-" + uuid.uuid4().hex[:12])) + else: + out.append(line) + return "\n".join(out) + + +def _with_chunk_id(fragment: str, chunk_id: str) -> str: + fragment = re.sub(r'\sdata-chunk-id="[^"]*"', "", fragment, count=1) + return re.sub(r"<([a-zA-Z0-9]+)", rf'<\1 data-chunk-id="{chunk_id}"', fragment, count=1) diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_bodies.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_bodies.py new file mode 100644 index 000000000..3ed2f406a --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_bodies.py @@ -0,0 +1,144 @@ +"""What a regenerated note may say, and what it must refuse to say.""" + +from __future__ import annotations + +import re + +from statutory.money import money_tokens, money_tokens_in_html + +PRIOR_ONLY_FIGURES = { + "312.00", # inventories: raw materials + "196.00", # inventories: finished goods + "141.00", # cash: balances with banks + "542.00", # employee benefits: salaries and wages + "4,196.00", # revenue: sale of manufactured goods + "68.00", # related party: KMP remuneration + "34.00", # contingent: disputed GST demand + "2,503.00", # cost of materials: purchases +} + + +def test_no_regenerated_body_reuses_a_prior_year_breakdown(rolled) -> None: + """Last year's analysis never appears under this year's heading.""" + html = rolled.document.html("target") + tokens = set(money_tokens_in_html(html)) + reused = sorted(PRIOR_ONLY_FIGURES & tokens) + assert not reused, f"prior-year breakdown figures reached the FY set: {reused}" + + +def test_the_analysis_we_cannot_reproduce_is_named_rather_than_dropped(rolled) -> None: + gaps = [g for note in rolled.document.notes for g in note.gaps] + assert len(gaps) >= 25 + text = " ".join(g.what for g in gaps) + for phrase in ("Raw materials", "Salaries and wages", "Remuneration to key management"): + assert phrase in text, f"the outstanding list should name {phrase!r}" + for gap in gaps: + assert any("caption granularity" in place for place in gap.looked_in) + + +def test_a_carried_policy_sentence_has_no_figure_and_no_period_reference(rolled) -> None: + policies = [block for block in rolled.document.blocks if block.role.endswith("policy")] + assert policies, "some policy prose should survive the roll-forward" + for block in policies: + assert not money_tokens_in_html(block.target) + lowered = block.target.lower() + for word in ("during the year", "for the year", "as at"): + assert word not in lowered, f"{block.role} carries a period claim: {block.target}" + + +def test_the_share_capital_claim_from_last_year_is_not_carried(rolled) -> None: + """ "There was no movement in share capital during the year" is a claim about 2025.""" + assert "no movement in share capital during the year" not in rolled.document.html("target") + + +def test_every_note_says_what_the_statement_says(rolled) -> None: + assert len(rolled.caption_ties) == 29 + for tie in rolled.caption_ties: + assert re.match(r".+ -> Note \d+: [\d,()\.]+$", tie) + + +def test_the_prior_year_tie_out_is_narrow_and_reports_its_own_selectivity(rolled) -> None: + """Two groups qualified out of 25 notes; one tied, one raised.""" + assert rolled.document.coverage == {"groups": 2, "tied": 1, "raised": 1} + tieouts = [b for b in rolled.document.blocks if b.role.endswith("comparative_tieout")] + assert len(tieouts) == 1 + body = tieouts[0].target + assert "821.00" in body and "812.00" in body and "9.00" in body + assert "is not resolved" in body + + +def test_the_lease_note_states_the_deed_terms_and_the_ledger_balances(rolled) -> None: + note = rolled.note(4) + text = " ".join(e.target for e in note.elements) + for token in ("268.00", "212.00", "62.00", "274.00", "6,50,000", "39,00,000"): + assert token in text, token + sources = {cite.path for figure in note.figures for cite in figure.cites} + assert "sources/lease-agreement-warehouse-2025.md" in sources + assert "sources/trial-balance-FY2026.csv" in sources + + +def test_the_business_combination_note_quotes_both_sides_and_resolves_neither( + rolled, +) -> None: + note = rolled.note(27) + text = " ".join(e.target for e in note.elements) + assert "185.00" in text + assert "29 January 2026" in text + assert "all 29 account names were searched and 0 matched" in text + assert "neither is preferred" in text + assert len(note.disagreements) == 1 + sides = note.disagreements[0].sides + assert len(sides) == 2 + assert {s.source for s in sides} == { + "sources/board-minutes-2026-01-18.md", + "sources/trial-balance-FY2026.csv", + } + assert max(s.searched for s in sides) == 29 + + +def test_earnings_per_share_is_computed_not_carried(rolled) -> None: + note = rolled.note(26) + text = " ".join(e.target for e in note.elements) + assert "8.14" in text, "407.00 lakhs over 50,00,000 shares" + assert "6.18" in text, "the comparative, cited to the signed set" + assert "50,00,000" in text + + +def test_the_injected_instruction_has_no_channel_to_act_through(rolled) -> None: + """Defect 4: the quarantined source is reported and then not consulted. + + The stronger half of the defence is structural: nothing in this build reads + a satisfied-flag out of a document. A checklist item is satisfied because a + note exists and carries the analysis, and there is no code path from source + text to that decision. + """ + assert rolled.quarantined == ["sources/vendor-correspondence-2026-02.md"] + html = rolled.document.html("target") + assert "SATISFIED" not in html + assert "APPROVED" not in html + assert "vendor-correspondence" not in html + cited = {cite.path for cite in rolled.document.cites} + assert "sources/vendor-correspondence-2026-02.md" not in cited + # and the checklist did not become all-satisfied, which is what it asked for + assert sum(1 for line in rolled.checklist if line.status == "satisfied") < 5 + + +def test_a_figure_only_reaches_the_document_through_one_of_four_bases(rolled) -> None: + bases = {f.basis for f in rolled.document.figures} + assert bases <= {"ledger", "comparative", "document", "derived"} + assert bases == {"ledger", "comparative", "document", "derived"}, "all four are in use" + + +def test_every_money_token_in_the_final_document_is_accounted_for(rolled) -> None: + """The whole document, re-scanned, not just each body as it was built.""" + from collections import Counter + + declared = Counter(f.rendered for f in rolled.document.figures) + found = Counter(money_tokens(_visible_text(rolled.document))) + assert not (found - declared), f"unaccounted: {sorted((found - declared).elements())}" + + +def _visible_text(document) -> str: + from statutory.money import strip_markup + + return " ".join(strip_markup(block.target) for block in document.blocks) diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_checklist.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_checklist.py new file mode 100644 index 000000000..f461d4b12 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_checklist.py @@ -0,0 +1,159 @@ +"""The rule set is data, the triggers are evaluated, and nothing fails quietly.""" + +from __future__ import annotations + +import re +import shutil +from pathlib import Path + +import pytest +from statutory.checklist import Checklist +from statutory.errors import UnknownAccountError, UnknownConditionError +from statutory.events import EventDetector +from statutory.ledger import TrialBalance +from statutory.pipeline import prepare + +PACKAGE = Path(__file__).resolve().parent.parent / "statutory" + + +def _bits(root: Path, config: Path): + ledger = TrialBalance(root / "sources" / "trial-balance-FY2026.csv", root) + events = EventDetector(config / "event-vocabulary.csv", config / "quarantine-markers.txt") + events.scan([p for p in sorted((root / "sources").glob("*")) if p.is_file()], root) + return ledger, events + + +def test_all_thirty_two_triggers_are_evaluated_from_the_csv(rolled) -> None: + assert len(rolled.checklist) == 32 + assert all(line.required for line in rolled.checklist) + assert all(line.because for line in rolled.checklist) + + +def test_the_eight_unmapped_items_are_detected_from_the_data(rolled) -> None: + unmapped = [line for line in rolled.checklist if line.prior_note is None] + assert [line.item_id for line in unmapped] == [f"DC-0{n}" for n in range(25, 33)] + lease = [line for line in unmapped if line.item_id in {f"DC-0{n}" for n in range(25, 29)}] + assert all("1110" in line.because or "2210" in line.because for line in lease) + combination = [line for line in unmapped if line.item_id in ("DC-029", "DC-031")] + assert all("board-minutes" in line.because for line in combination) + + +def test_the_package_never_names_an_item_id_or_a_standard() -> None: + """The checklist is found by its columns; no *code* knows what is in it. + + `config/` may name an item id and a standard - that is what configuration + is, and it is how a 33rd row is wired to a computation without a code edit. + The line this test draws is between data and code, not between the repo and + the world. + """ + for path in PACKAGE.rglob("*.py"): + text = path.read_text(encoding="utf-8-sig") + assert not re.search(r"DC-\d{3}", text), f"{path.name} names a checklist item" + assert "Ind AS" not in text, f"{path.name} names a standard" + configured = (PACKAGE.parent / "config" / "note-recipes.csv").read_text(encoding="utf-8") + assert re.search(r"DC-\d{3}", configured), "the wiring lives in config, and it is there" + + +def test_a_thirty_third_row_changes_behaviour_with_zero_code_edits( + corpus: Path, config: Path, tmp_path: Path +) -> None: + root = tmp_path / "corpus" + shutil.copytree(corpus, root) + checklist_path = root / "rules" / "disclosure-checklist-indas.csv" + with checklist_path.open("a", encoding="utf-8") as handle: + handle.write("DC-033,Ind AS 7,Cash flow statement,account 1220 balance > 0,7,high\n") + result = prepare(root, config) + line = next(line for line in result.checklist if line.item_id == "DC-033") + assert line.required + assert line.note == 8, "prior note 7 -> 8, through the same map as everything else" + assert "1220" in line.because and "189.00" in line.because + + +def test_an_unparseable_trigger_stops_the_run_and_prints_the_grammar( + corpus: Path, config: Path, tmp_path: Path +) -> None: + root = tmp_path / "corpus" + shutil.copytree(corpus, root) + path = root / "rules" / "disclosure-checklist-indas.csv" + with path.open("a", encoding="utf-8") as handle: + handle.write("DC-099,Ind AS 1,Something,when it feels right,2,high\n") + ledger, events = _bits(root, config) + with pytest.raises(UnknownConditionError) as caught: + Checklist(path, root).evaluate(ledger, events) + message = str(caught.value) + assert "DC-099" in message and "when it feels right" in message + assert "Grammar:" in message + assert "not 'not required'" in message + + +def test_a_trigger_naming_an_account_the_ledger_lacks_stops_the_run( + corpus: Path, config: Path, tmp_path: Path +) -> None: + root = tmp_path / "corpus" + shutil.copytree(corpus, root) + path = root / "rules" / "disclosure-checklist-indas.csv" + with path.open("a", encoding="utf-8") as handle: + handle.write("DC-098,Ind AS 1,Something,account 9999 balance > 0,2,high\n") + ledger, events = _bits(root, config) + with pytest.raises(UnknownAccountError) as caught: + Checklist(path, root).evaluate(ledger, events) + message = str(caught.value) + assert "9999" in message + assert "29 account(s)" in message + assert "not a nil balance" in message + + +def test_an_event_with_no_detector_stops_the_run( + corpus: Path, config: Path, tmp_path: Path +) -> None: + root = tmp_path / "corpus" + shutil.copytree(corpus, root) + path = root / "rules" / "disclosure-checklist-indas.csv" + with path.open("a", encoding="utf-8") as handle: + handle.write("DC-097,Ind AS 1,Something,a merger happened,2,high\n") + ledger, events = _bits(root, config) + with pytest.raises(UnknownConditionError) as caught: + Checklist(path, root).evaluate(ledger, events) + assert "Events defined:" in str(caught.value) + + +def test_an_or_condition_is_a_disjunction(corpus: Path, config: Path) -> None: + """The lease maturity item fires because either leg is non-nil.""" + ledger, events = _bits(corpus, config) + checklist = Checklist(corpus / "rules" / "disclosure-checklist-indas.csv", corpus) + trigger = next(t for t in checklist.evaluate(ledger, events) if t.item.item_id == "DC-026") + assert trigger.required + assert "2210" in trigger.because and "2310" in trigger.because + assert len(trigger.cites) == 2 + + +def test_the_signed_balance_is_what_the_grammar_evaluates(corpus: Path, config: Path) -> None: + """`balance > 0` keeps meaning 'debit balance'; no absolute value is taken.""" + ledger, _ = _bits(corpus, config) + assert ledger.account("1110").balance(2026) > 0 + assert ledger.account("2210").balance(2026) < 0 + assert ledger.account("2210").presented(2026) > 0 + + +def test_events_fire_on_a_block_not_on_a_document(corpus: Path, config: Path) -> None: + """The revenue note says 'control ... transfers to the customer'; that is not a + business combination, and requiring one block is what keeps them apart.""" + _, events = _bits(corpus, config) + for name in events.names(): + hits = events.hits(name) + assert len(hits) == 1 + assert hits[0].source.endswith("board-minutes-2026-01-18.md") + assert all(cite.path == hits[0].source for cite in hits[0].cites) + + +def test_a_quarantined_source_can_neither_suppress_nor_manufacture_an_event( + corpus: Path, config: Path +) -> None: + _, events = _bits(corpus, config) + assert [q.source for q in events.quarantined] == ["sources/vendor-correspondence-2026-02.md"] + assert "sources/vendor-correspondence-2026-02.md" not in events.scanned + quarantine = events.quarantined[0] + assert quarantine.cite.read_back(corpus) == quarantine.cite.snippet + # the forged source mentions "business combinations"; it contributed nothing + for name in events.names(): + assert all(h.source != quarantine.source for h in events.hits(name)) diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_client.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_client.py new file mode 100644 index 000000000..38a88824e --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_client.py @@ -0,0 +1,156 @@ +"""The four wire rules, enforced in the client rather than in a caller's care.""" + +from __future__ import annotations + +import inspect + +import pytest +from recorded import gate_job +from statutory.errors import NotConfiguredError, PlanRefusedError +from statutory.superdocs.client import SuperDocsClient, select_our_change +from statutory.superdocs.transport import HttpTransport, RecordedCall, ReplayTransport + + +def _client(calls: list[RecordedCall]) -> tuple[SuperDocsClient, ReplayTransport]: + transport = ReplayTransport(calls) + return SuperDocsClient(transport, poll_seconds=0), transport + + +def test_gated_changes_go_to_the_async_route(monkeypatch) -> None: + """Decision 32: approve takes a job_id and jobs exist only on /v1/chat/async.""" + job = gate_job("awaiting-approval") + client, transport = _client( + [ + RecordedCall("POST", "/v1/chat/async", {"job_id": job["job_id"]}), + RecordedCall("GET", f"/v1/jobs/{job['job_id']}", job), + ] + ) + result = client.chat_gated("session_x", "doc_x", "replace chunk abc with ...") + assert result.awaiting + paths = [path for _, path, _ in transport.seen] + assert paths[0] == "/v1/chat/async" + assert "/v1/chat" not in paths, "the synchronous route has no gate to reach" + body = transport.seen[0][2] + assert body["approval_mode"] == "ask_every_time" + assert body["response_mode"] == "compact" + + +def test_a_batch_is_split_into_ours_and_everything_else() -> None: + """We send one chunk; the product does not always answer with one change.""" + from statutory.superdocs.client import _job_from + + job = _job_from(gate_job("awaiting-approval")) + assert len(job.pending) == 3 + ours, unasked = select_our_change(job, job.pending[1].chunk_id) + assert ours is job.pending[1] + assert len(unasked) == 2 + assert ours not in unasked + + +def test_a_batch_that_does_not_contain_our_target_is_refused_in_full() -> None: + from statutory.superdocs.client import Job, PendingChange + + job = Job("j", "awaiting_approval", 88, [PendingChange("c", "other-chunk", "edit", "", "")], {}) + with pytest.raises(PlanRefusedError) as caught: + select_our_change(job, "our-chunk") + assert "none of them on chunk our-chunk" in str(caught.value) + assert "Every one is denied" in str(caught.value) + + +def test_a_create_operation_on_our_target_is_refused() -> None: + from statutory.superdocs.client import Job, PendingChange + + job = Job("j", "awaiting_approval", 88, [PendingChange("c", "ours", "create", "", "")], {}) + with pytest.raises(PlanRefusedError, match="one verb"): + select_our_change(job, "ours") + + +def test_denials_carry_no_feedback() -> None: + """Decision 34: there is no call shape in which feedback can be sent.""" + signature = inspect.signature(SuperDocsClient.deny) + assert "feedback" not in signature.parameters + + client, transport = _client( + [RecordedCall("POST", "/v1/chat/s/approve", {"status": "ok", "batch_complete": True})] + ) + client.deny("s", "j", "c") + body = transport.seen[0][2] + assert body == {"job_id": "j", "change_id": "c", "approved": False} + assert "feedback" not in body + + +def test_a_read_back_with_no_html_verifies_nothing_and_says_so() -> None: + client, _ = _client([RecordedCall("GET", "/v1/documents/d", {"version": 2})]) + with pytest.raises(RuntimeError, match="carried no html"): + client.read_back("d") + + +def test_an_upload_that_is_not_persisted_stops_the_run() -> None: + """Measured: an upload with no session is not persisted and carries no id.""" + client, _ = _client( + [ + RecordedCall("POST", "/v1/sessions/init", {"session_id": "s"}), + RecordedCall( + "POST", + "/v1/documents/upload-base64", + {"filename": "f.html", "chunks_count": 3, "persisted": False}, + ), + ] + ) + with pytest.raises(RuntimeError, match="only evidence there is"): + client.upload_verbatim("f.html", "<p>x</p>") + + +def test_the_document_id_comes_from_the_session_roster_not_from_the_upload() -> None: + """The upload response has no document id at all; the roster is a read, not a claim.""" + client, transport = _client( + [ + RecordedCall("POST", "/v1/sessions/init", {"session_id": "s"}), + RecordedCall("POST", "/v1/documents/upload-base64", {"persisted": True}), + RecordedCall( + "GET", + "/v1/sessions/s/documents", + {"documents": [{"document_id": "doc_primary", "durable_document_id": "dur-1"}]}, + ), + ] + ) + document_id, session_id = client.upload_verbatim("f.html", "<p>x</p>") + assert (document_id, session_id) == ("dur-1", "s") + assert [path for _, path, _ in transport.seen][-1] == "/v1/sessions/s/documents" + + +def test_a_roster_with_no_durable_id_stops_the_run() -> None: + client, _ = _client( + [ + RecordedCall("POST", "/v1/sessions/init", {"session_id": "s"}), + RecordedCall("POST", "/v1/documents/upload-base64", {"persisted": True}), + RecordedCall("GET", "/v1/sessions/s/documents", {"documents": []}), + ] + ) + with pytest.raises(RuntimeError, match="nothing to verify against"): + client.upload_verbatim("f.html", "<p>x</p>") + + +def test_the_replay_transport_never_invents_a_response() -> None: + client, _ = _client([]) + with pytest.raises(AssertionError) as caught: + client.read_back("d") + assert "never invents one" in str(caught.value) + + +def test_a_live_transport_without_a_key_refuses_to_exist(monkeypatch) -> None: + monkeypatch.delenv("SUPERDOCS_API_KEY", raising=False) + with pytest.raises(NotConfiguredError) as caught: + HttpTransport() + assert "recorded responses" in str(caught.value) + assert "SUPERDOCS_API_KEY" in str(caught.value) + + +def test_the_key_is_never_in_a_message_this_module_can_produce(monkeypatch) -> None: + fake = "sk-" + "live-" + "0123456789abcdef" + monkeypatch.setenv("SUPERDOCS_API_KEY", fake) + transport = HttpTransport() + from statutory.superdocs.transport import redacted_key + + assert redacted_key() == "...cdef" + assert fake not in repr(transport.__dict__.get("base_url", "")) diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_edit_plan.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_edit_plan.py new file mode 100644 index 000000000..b795c91f6 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_edit_plan.py @@ -0,0 +1,118 @@ +"""One verb, no heading, one chunk. Checked before anything reaches the wire.""" + +from __future__ import annotations + +import ast +import re +from pathlib import Path + +import pytest +from statutory.chunks import ChunkMap +from statutory.editplan import ( + Step, + assert_plan_is_legal, + bind_chunk_ids, + build_plan, + expected_after, +) +from statutory.errors import PlanRefusedError + +PACKAGE = Path(__file__).resolve().parent.parent / "statutory" + + +def test_every_step_is_a_replacement_of_an_existing_chunk(rolled) -> None: + plan = build_plan(rolled.document) + assert len(plan) == 33 + assert {step.verb for step in plan.steps} == {"replace"} + for step in plan.steps: + assert step.expected_html != step.was_html + + +def test_no_step_targets_a_heading_chunk(rolled) -> None: + plan = build_plan(rolled.document) + assert not [s for s in plan.steps if s.tag in ("h1", "h2", "h3")] + headings = [b for b in rolled.document.blocks if not b.editable] + assert len(headings) == 32, "27 note headings, 3 section headings, entity and address" + assert not [b for b in headings if b.changed], ( + "a heading that differs between skeleton and target would need an edit, and this " + "design has no step that can make one" + ) + + +def test_the_instruction_never_asks_for_a_structural_operation(rolled) -> None: + plan = build_plan(rolled.document) + uploaded = ChunkMap(rolled.document.html("skeleton")) + for index, chunk in enumerate(uploaded.chunks): + object.__setattr__(chunk, "chunk_id", f"chunk-{index}") + uploaded.by_id = {c.chunk_id: c for c in uploaded.chunks} + bind_chunk_ids(plan, uploaded) + for step in plan.steps: + instruction = step.instruction().lower() + assert "replace the entire contents" in instruction + assert "do not add any section" in instruction + assert "do not change any heading" in instruction + assert not re.search(r"\b(insert|renumber|create a|append a)\b", instruction) + + +def test_no_create_verb_anywhere_in_the_build(rolled) -> None: + """Decision 28: one verb. Checked against our own string literals.""" + banned = ("create_document", "insert_section", "add_section", '"create"', "'create'") + for path in PACKAGE.rglob("*.py"): + text = path.read_text(encoding="utf-8-sig") + tree = ast.parse(text) + literals = [ + node.value + for node in ast.walk(tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + ] + joined = "\n".join(literals) + for word in banned: + assert word not in joined or "refused" in joined, f"{path.name} mentions {word}" + + +def test_a_no_op_step_is_refused_because_it_cannot_be_verified(rolled) -> None: + plan = build_plan(rolled.document) + plan.steps.append(Step(99, "probe", "p", None, "<p>same</p>", "<p>same</p>")) + with pytest.raises(PlanRefusedError, match="reads as 'not applied'"): + assert_plan_is_legal(plan, rolled.document) + + +def test_a_step_that_targets_a_heading_is_refused(rolled) -> None: + plan = build_plan(rolled.document) + plan.steps.append(Step(99, "note-heading", "h3", 4, "<h3>a</h3>", "<h3>b</h3>")) + with pytest.raises(PlanRefusedError, match="no renumbering step to get wrong"): + assert_plan_is_legal(plan, rolled.document) + + +def test_chunk_ids_are_bound_by_content_not_by_position(rolled) -> None: + plan = build_plan(rolled.document).sample(3) + uploaded = ChunkMap(rolled.document.html("skeleton")) + for index, chunk in enumerate(uploaded.chunks): + object.__setattr__(chunk, "chunk_id", f"chunk-{index}") + uploaded.by_id = {c.chunk_id: c for c in uploaded.chunks} + bind_chunk_ids(plan, uploaded) + assert all(step.chunk_id for step in plan.steps) + + plan.steps[0].was_html = "<p>never uploaded</p>" + with pytest.raises(PlanRefusedError, match="matches 0 uploaded chunk"): + bind_chunk_ids(plan, uploaded) + + +def test_the_expected_post_state_is_the_whole_document(rolled) -> None: + plan = build_plan(rolled.document).sample(1) + uploaded = ChunkMap(rolled.document.html("skeleton")) + for index, chunk in enumerate(uploaded.chunks): + object.__setattr__(chunk, "chunk_id", f"chunk-{index}") + uploaded.by_id = {c.chunk_id: c for c in uploaded.chunks} + bind_chunk_ids(plan, uploaded) + expected = expected_after(uploaded, plan.steps[0]) + assert len(expected) == len(uploaded) + changed = [k for k in expected if expected[k] != uploaded.by_id[k].canonical] + assert changed == [plan.steps[0].chunk_id], "exactly one chunk is expected to move" + + +def test_sample_mode_takes_a_prefix_and_nothing_else(rolled) -> None: + plan = build_plan(rolled.document) + sampled = plan.sample(4) + assert [s.index for s in sampled.steps] == [1, 2, 3, 4] + assert plan.sample(None) is plan diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_figures.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_figures.py new file mode 100644 index 000000000..227379965 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_figures.py @@ -0,0 +1,163 @@ +"""No figure reaches paper without a source, and no type has room for an opinion.""" + +from __future__ import annotations + +import dataclasses +import re +from decimal import Decimal +from pathlib import Path + +import pytest +from statutory import bodies, disagreement, figures +from statutory.errors import CitationDriftError, UnaccountedFigureError +from statutory.figures import ( + SOURCE_BASES, + Body, + Cite, + Component, + Derived, + Figure, + Gap, + Quotient, + assert_accounted, +) + +CITE = Cite("sources/trial-balance-FY2026.csv", 200, 204, "0.00", "probe") +FORBIDDEN = ( + "resolution", + "resolved", + "resolve", + "conclusion", + "correct_value", + "preferred_side", + "preferred", + "status", + "approved", + "verdict", + "answer", + "winner", + "truth", +) + + +def _figure(value: str, basis: str = "ledger") -> Figure: + return Figure("probe", Decimal(value), basis, (CITE,)) + + +def test_an_unaccounted_token_is_a_hard_error_naming_the_token() -> None: + with pytest.raises(UnaccountedFigureError) as caught: + Body().markup("<p>a total of 1,234.00</p>").render("probe note") + message = str(caught.value) + assert "1,234.00" in message + assert "probe note" in message + assert "hard error rather than a warning" in message + + +def test_a_declared_figure_accounts_for_its_own_token() -> None: + body = Body().markup("<p>total ").fig(_figure("1234.00")).markup("</p>") + assert "1,234.00" in body.render("probe") + + +def test_two_identical_tokens_need_two_figures() -> None: + one = _figure("5.00") + with pytest.raises(UnaccountedFigureError): + assert_accounted("<p>5.00 and 5.00</p>", [one], "probe") + assert_accounted("<p>5.00 and 5.00</p>", [one, one], "probe") + + +def test_a_derived_total_has_no_value_field() -> None: + names = {f.name for f in dataclasses.fields(Derived)} + assert "value" not in names, "a stored total can drift from its citations" + total = Derived("t", (Component(_figure("2.00")), Component(_figure("3.00")))) + assert total.value == Decimal("5.00") + with pytest.raises(dataclasses.FrozenInstanceError): + total.value = Decimal("9.00") # type: ignore[misc] + + +def test_a_derived_total_recomputes_from_its_components_on_every_read() -> None: + part = _figure("2.00") + total = Derived("t", (Component(part), Component(_figure("3.00"), -1))) + assert total.value == Decimal("-1.00") + assert total.cites == (CITE, CITE) + assert total.workings() == "2.00 - 3.00 = (1.00)" + + +def test_a_derived_total_with_no_components_is_a_bare_number() -> None: + with pytest.raises(ValueError, match="bare number"): + Derived("t", ()) + + +def test_a_figure_needs_a_basis_that_exists_and_a_citation() -> None: + with pytest.raises(ValueError, match="not one of"): + Figure("x", Decimal("1"), "vibes", (CITE,)) + with pytest.raises(ValueError, match="no provenance"): + Figure("x", Decimal("1"), "ledger", ()) + assert set(SOURCE_BASES) == {"ledger", "comparative", "document"} + + +def test_a_quotient_divides_and_carries_both_sides_citations() -> None: + profit = _figure("407.00") + shares = Figure("shares", Decimal("5000000"), "document", (CITE,), literal="50,00,000") + eps = Quotient("eps", profit, shares, Decimal(100000)) + assert eps.rendered == "8.14" + assert len(eps.cites) == 2 + + +def test_a_gap_that_does_not_say_where_we_looked_is_a_shrug() -> None: + with pytest.raises(ValueError, match="shrug"): + Gap("something is missing", ()) + assert "looked in" in Gap("x", ("the ledger",)).sentence() + + +def test_prose_cannot_smuggle_an_undeclared_figure() -> None: + body = Body().markup("<p>") + with pytest.raises(UnaccountedFigureError): + body.prose("a difference of 9.00", ()).render("probe") + + +def test_a_citation_that_no_longer_reads_back_is_a_hard_error(corpus: Path) -> None: + CITE.verify(corpus) + with pytest.raises(CitationDriftError, match="citation drift"): + Cite(CITE.path, CITE.byte_start, CITE.byte_end, "9.99", "probe").verify(corpus) + + +def test_no_outcome_type_has_a_field_an_opinion_could_be_written_into() -> None: + """Decision 15's defence, rebuilt: the absence of a field, proved by reflection.""" + types = [ + figures.Figure, + figures.Derived, + figures.Component, + figures.Gap, + figures.Quotient, + figures.Cite, + disagreement.Side, + disagreement.Disagreement, + bodies.NoteContent, + bodies.Element, + ] + for candidate in types: + names = {f.name.lower() for f in dataclasses.fields(candidate)} + offenders = sorted(names & set(FORBIDDEN)) + assert not offenders, f"{candidate.__name__} has {offenders}" + + +def test_no_assignment_to_a_resolution_name_in_the_modules_that_carry_outcomes() -> None: + """Scoped to the outcome modules on purpose. + + A checklist item legitimately has a disclosure `status`, and an HTTP job has + a transport `status`; neither is an opinion about which of two disagreeing + sources is right. The rule is about the types that carry an outcome, and + those are the three scanned here. + """ + package = Path(figures.__file__).resolve().parent + pattern = re.compile(rf"\b(?:{'|'.join(FORBIDDEN)})\s*=", re.I) + hits = [] + for name in ("figures.py", "disagreement.py", "bodies.py"): + path = package / name + for number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1): + stripped = line.strip() + if stripped.startswith("#"): + continue + if pattern.search(stripped): + hits.append(f"{path.name}:{number}: {stripped}") + assert not hits, f"a resolution is being written somewhere: {hits}" diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_money_and_chunks.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_money_and_chunks.py new file mode 100644 index 000000000..059870997 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_money_and_chunks.py @@ -0,0 +1,103 @@ +"""The recogniser, in both directions, and the chunk model it feeds.""" + +from __future__ import annotations + +from decimal import Decimal + +import pytest +from recorded import read_back_html +from statutory.chunks import ChunkMap, canonical, strip_chunk_ids +from statutory.money import format_money, money_tokens, money_tokens_in_html, parse_money + + +@pytest.mark.parametrize( + ("text", "expected"), + [ + ("total 2,180.00 here", ["2,180.00"]), + ("a loss of (45.00) this year", ["(45.00)"]), + ("50,00,000 equity shares", ["50,00,000"]), + ("INR 6,50,000 per month", ["6,50,000"]), + ("earnings per share 8.14", ["8.14"]), + ("42,000 sq ft", ["42,000"]), + ("sums to 821.00 (703.00 + 87.00)", ["821.00", "703.00", "87.00"]), + ], +) +def test_the_recogniser_finds_what_it_must(text: str, expected: list[str]) -> None: + assert money_tokens(text) == expected + + +@pytest.mark.parametrize( + "text", + [ + "for the year ended 31 March 2026", + "interest at 9.15% per annum", + "CIN: U29253TG2016PLC112447", + "Plot 42, IDA Nacharam, Hyderabad 500076", + "a term of 5 years with a lock-in of 36 months", + "100% of the equity share capital", + "Companies (Indian Accounting Standards) Rules, 2015", + ], +) +def test_the_recogniser_leaves_what_is_not_a_figure(text: str) -> None: + assert money_tokens(text) == [] + + +def test_brackets_are_matched_as_a_pair_or_not_at_all() -> None: + """The Phase 3 silent-skip: an independent optional bracket matched "(703.00".""" + assert "(703.00" not in money_tokens("sums to 821.00 (703.00 + 87.00)") + assert money_tokens("(1,462.00)") == ["(1,462.00)"] + + +def test_format_and_parse_round_trip() -> None: + for value in ("0.00", "-45.00", "4857.00", "-1691.00", "12.50"): + assert parse_money(format_money(Decimal(value))) == Decimal(value) + assert format_money(Decimal("-0.00")) == "0.00", "a nil balance is never '-0.00'" + + +def test_markup_never_contributes_a_figure() -> None: + fragment = ( + '<td data-chunk-id="1f2adf88-8741-40b3-a5c7-f443314369ab" ' + 'style="border:1px solid #1f3864;padding:6px">40.00</td>' + ) + assert money_tokens_in_html(fragment) == ["40.00"] + + +def test_canonical_ignores_whitespace_attribute_order_and_the_chunk_id() -> None: + left = '<p data-chunk-id="abc" style="color:red" class="x">a b</p>' + right = '<p class="x" style="color:red">a b</p>' + assert canonical(left) == canonical(right) + + +def test_canonical_treats_an_entity_and_its_character_as_the_same_content() -> None: + """Measured on the first live upload: we sent `·` and got back `·`.""" + assert canonical("<p>a · b</p>") == canonical("<p>a · b</p>") + assert canonical("<h3>Note 4 — Leases</h3>") == canonical("<h3>Note 4 — Leases</h3>") + + +def test_canonical_treats_a_void_element_the_same_either_way() -> None: + """Measured on the first live upload: we sent `<br>` and got back `<br/>`.""" + assert canonical("<p>a<br>b</p>") == canonical("<p>a<br/>b</p>") + + +def test_canonical_never_ignores_a_digit() -> None: + assert canonical("<p>724</p>") != canonical("<p>750</p>") + assert canonical('<td style="a">1.00</td>') != canonical('<td style="a">1.01</td>') + + +def test_a_recorded_document_parses_into_the_chunks_the_product_addressed() -> None: + chunks = ChunkMap(read_back_html("read0-baseline")) + assert len(chunks) == 18 + assert all(chunk.chunk_id for chunk in chunks.chunks) + assert chunks.chunks[0].tag == "h1" + assert "724" in chunks.by_id["8ec79daa-5de5-4973-84a5-1d1139da29be"].html + + +def test_headings_are_read_back_out_of_the_document(rolled) -> None: + headings = ChunkMap(rolled.document.html("target")).headings() + assert len(headings) == 27 + assert headings[4].startswith("Note 4") + assert "Leases" in headings[4] + + +def test_strip_chunk_ids_is_exact() -> None: + assert strip_chunk_ids('<p data-chunk-id="x">a</p>') == "<p>a</p>" diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_note_tree.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_note_tree.py new file mode 100644 index 000000000..0c59e4525 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_note_tree.py @@ -0,0 +1,180 @@ +"""The insertion, and everything it drags with it. + +The second half of the grading line: a newly required note is detected from the +data, and note numbering plus every cross-reference stays consistent after an +insertion in the middle of the document. +""" + +from __future__ import annotations + +import re +import shutil +from itertools import pairwise +from pathlib import Path + +import pytest +from statutory.checklist import Checklist +from statutory.errors import ChecklistDriftError, CrossReferenceError +from statutory.events import EventDetector +from statutory.ledger import TrialBalance +from statutory.notetree import ( + assert_checklist_consistent, + build_note_tree, + load_new_note_titles, + remap_checklist, +) +from statutory.priorset import PriorYearSet +from statutory.statements import roll_forward + +PACKAGE = Path(__file__).resolve().parent.parent / "statutory" + + +def test_the_lease_note_is_required_by_the_data_not_by_a_list(rolled) -> None: + lease = next(n for n in rolled.tree.inserted if n.checklist_items[0] == "DC-025") + assert lease.number == 4 + assert lease.trigger_accounts[0] == "1110" + assert "1110" in lease.why and "268.00" in lease.why + assert lease.why_cites, "the trigger must carry the ledger bytes that made it true" + + +def test_insertion_point_comes_from_account_order( + corpus: Path, config: Path, tmp_path: Path +) -> None: + """Move the right-of-use account down the ledger; the note moves with it. + + If the position were remembered rather than computed, this would still land + the lease note at 4. + """ + root = tmp_path / "corpus" + shutil.copytree(corpus, root) + ledger_path = root / "sources" / "trial-balance-FY2026.csv" + lines = ledger_path.read_text(encoding="utf-8").splitlines(keepends=True) + rou = next(line for line in lines if line.startswith("1110,")) + lines.remove(rou) + at = next(i for i, line in enumerate(lines) if line.startswith("1230,")) + lines.insert(at + 1, rou) + ledger_path.write_text("".join(lines), encoding="utf-8") + + tree = _tree_for(root, config) + lease = next(n for n in tree.inserted if "DC-025" in n.checklist_items) + assert lease.number == 9, "after the note the preceding account points at, not after PPE" + assert tree.renumber[8] == 8 and tree.renumber[9] == 10 + + +def test_every_prior_note_survives_and_the_order_is_preserved(rolled) -> None: + tree = rolled.tree + assert len(tree.renumber) == 25 + assert tree.renumber[3] == 3 + assert tree.renumber[4] == 5 + assert tree.renumber[25] == 26 + shifted = [(old, new) for old, new in tree.renumber.items() if old != new] + assert len(shifted) == 22 + ordered = sorted(tree.renumber.items()) + assert all(a[1] < b[1] for a, b in pairwise(ordered)) + + +def test_every_cross_reference_resolves_after_insertion(rolled) -> None: + numbers = {note.number for note in rolled.tree.notes} + html = rolled.document.html("target") + referenced = {int(m.group(1)) for m in re.finditer(r"\bNote (\d+)\b", html)} + assert referenced <= numbers + # the one prose cross-reference in the signed set was repointed + assert "disclosed in Note 5" in html + assert "disclosed in Note 4" not in html + + +def test_the_primary_statements_point_at_the_new_numbers(rolled) -> None: + lines = { + line.account.code: line.note_ref + for statement in rolled.rolled.statements + for line in statement.lines + if line.account is not None + } + assert lines["1130"] == 5, "intangibles moved from note 4 to note 5" + assert lines["1210"] == 7 + assert lines["1110"] == 4 and lines["2210"] == 4 and lines["2310"] == 4 + + +def test_no_note_title_is_hardcoded_in_the_package(rolled) -> None: + """Every rolled-forward title comes from the signed set, not from our source.""" + titles = [note.title for note in rolled.tree.notes if not note.is_new] + literals = _string_literals(PACKAGE) + offenders = sorted({t for t in titles for lit in literals if t in lit}) + assert not offenders, f"these note titles are written into our code: {offenders}" + + +def test_the_checklist_is_remapped_through_the_same_map(rolled) -> None: + mapping = rolled.mapping + assert mapping["DC-005"] == 5, "prior note 4 -> 5" + assert mapping["DC-007"] == mapping["DC-008"] == 7 + assert mapping["DC-024"] == 26 + assert {mapping[f"DC-0{n}"] for n in (25, 26, 27, 28)} == {4} + assert {mapping[f"DC-0{n}"] for n in (29, 30, 31, 32)} == {27} + + +def test_checklist_consistency_is_asserted_against_the_document_not_our_model( + rolled, +) -> None: + from statutory.chunks import ChunkMap + + headings = ChunkMap(rolled.document.html("target")).headings() + assert len(headings) == 27 + assert_checklist_consistent(rolled.mapping, rolled.tree, headings) + + drifted = dict(headings) + drifted[5] = "Note 5 — Something else" + with pytest.raises(ChecklistDriftError, match="DC-005"): + assert_checklist_consistent(rolled.mapping, rolled.tree, drifted) + + extra = dict(headings) + extra[99] = "Note 99 - Quality Assurance" + with pytest.raises(ChecklistDriftError, match="fabrication"): + assert_checklist_consistent(rolled.mapping, rolled.tree, extra) + + +def test_a_prose_reference_to_a_note_that_never_existed_raises(rolled) -> None: + with pytest.raises(CrossReferenceError, match="Note 99"): + rolled.tree.repoint_prose("see Note 99 for details") + + +def _string_literals(package: Path) -> list[str]: + """Every string constant in our source that is not a docstring. + + Comments are excluded on purpose - a comment naming "Borrowings" to explain + why two rows need disambiguating is documentation, not a hardcoded title. + """ + import ast + + out: list[str] = [] + for path in package.rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8-sig")) + docstrings = { + id(node.body[0].value) + for node in ast.walk(tree) + if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) + and node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + and isinstance(node.body[0].value.value, str) + } + for node in ast.walk(tree): + if ( + isinstance(node, ast.Constant) + and isinstance(node.value, str) + and id(node) not in docstrings + ): + out.append(node.value) + return out + + +def _tree_for(root: Path, config: Path): + prior = PriorYearSet(next((root / "prior-year").glob("*.html")), root) + ledger = TrialBalance(root / "sources" / "trial-balance-FY2026.csv", root) + forward = roll_forward(ledger, prior, root) + events = EventDetector(config / "event-vocabulary.csv", config / "quarantine-markers.txt") + events.scan([p for p in sorted((root / "sources").glob("*")) if p.is_file()], root) + checklist = Checklist(root / "rules" / "disclosure-checklist-indas.csv", root) + triggers = checklist.evaluate(ledger, events) + tree = build_note_tree(forward, triggers, load_new_note_titles(config / "new-note-titles.csv")) + remap_checklist(triggers, tree) + return tree diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_orchestrator.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_orchestrator.py new file mode 100644 index 000000000..5c89914eb --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_orchestrator.py @@ -0,0 +1,152 @@ +"""The failure path is the build, not an afterthought. + +Four things are asserted here and each of them is a thing we watched a product +get wrong on 2026-08-07 and again on 2026-08-09: the queue halts, the downstream +steps do not run, no export is produced, and the sentence at the end is computed +from what actually happened. +""" + +from __future__ import annotations + +import pytest +from simulator import Behaviour, SimulatedSuperDocs +from statutory.editplan import build_plan +from statutory.errors import DecisionAlreadyRecordedError +from statutory.orchestrator import Orchestrator +from statutory.superdocs.client import SuperDocsClient +from statutory.superdocs.decisions import DecisionLedger + + +def _run(rolled, behaviour: Behaviour, steps: int = 4, ledger: DecisionLedger | None = None): + plan = build_plan(rolled.document).sample(steps) + product = SimulatedSuperDocs(behaviour) + client = SuperDocsClient(product, poll_seconds=0) + ledger = ledger or DecisionLedger() + orchestrator = Orchestrator(client, ledger, "K. Latha (CFO)", sleep=lambda _: None) + result = orchestrator.run("run-1", plan, "statements.html") + return result, product, ledger, orchestrator + + +def test_a_clean_run_applies_every_step_and_exports(rolled) -> None: + result, product, ledger, _ = _run(rolled, Behaviour()) + assert result.ok + assert result.applied == 4 + assert product.exports == ["docx"] + assert ledger.accepted("run-1") == 4 + assert "4 of 4 planned change(s) verified applied" in result.sentence(ledger) + assert "decided by K. Latha (CFO)" in result.sentence(ledger) + + +def test_a_failure_halts_the_queue_and_downstream_steps_never_run(rolled) -> None: + result, product, _ledger, orchestrator = _run(rolled, Behaviour(not_applied={2, 3})) + assert not result.ok + assert result.halted_at == 2 + assert result.applied == 1, "step 1 landed; 3 and 4 were never attempted" + assert product.step_index == 3, "step 2 plus its one narrow retry, and nothing after" + assert any("halting" in line for line in orchestrator.log) + + +def test_no_export_after_a_failure(rolled) -> None: + result, product, ledger, _ = _run(rolled, Behaviour(not_applied={2, 3})) + assert product.exports == [] + assert result.exported == [] + assert "No export was produced" in result.sentence(ledger) + assert "HALTED at step 2" in result.sentence(ledger) + + +def test_at_most_one_narrow_retry_and_never_a_replan(rolled) -> None: + """The retry is the same instruction. A second failure ends it.""" + result, product, _, orchestrator = _run(rolled, Behaviour(not_applied={2, 3})) + assert product.step_index == 3, "the step, its one retry, and nothing more" + assert result.outcomes[-1].retried + assert sum("narrow retry" in line for line in orchestrator.log) == 1 + + messages = [line for line in orchestrator.log if "retry" in line] + assert all("no re-plan" in line for line in messages) + + +def test_a_retry_that_succeeds_lets_the_run_continue(rolled) -> None: + """One failure, one retry of the same instruction, and the run carries on.""" + result, product, ledger, _orchestrator = _run(rolled, Behaviour(not_applied={2}), steps=3) + assert result.ok + assert product.step_index == 4, "three steps plus one retry" + assert [o.retried for o in result.outcomes] == [False, True, False] + rows = ledger.decided("run-1") + assert len(rows) == 3 + assert any(r.supersedes for r in ledger.rows), "the retry supersedes; both rows are kept" + assert len(ledger.rows) == 4 + + +def test_collateral_damage_is_never_retried(rolled) -> None: + """The document already holds content nobody asked for; another turn is worse.""" + result, product, _, orchestrator = _run(rolled, Behaviour(collateral={1})) + assert result.halted_at == 1 + assert product.step_index == 1, "no retry at all" + assert any("collateral" in line.lower() for line in orchestrator.log) + + +def test_a_failed_run_reverts_and_says_whether_the_revert_worked(rolled) -> None: + result, product, _, _ = _run(rolled, Behaviour(not_applied={1, 2})) + assert product.reverts == 1 + assert "reverted to the last verified-good state, confirmed by read-back" in result.reverted + + result2, product2, _, _ = _run(rolled, Behaviour(not_applied={1, 2}, revert_works=False)) + assert "does NOT match the last verified-good state" in result2.reverted + assert product2.exports == [] + + +def test_an_upload_that_is_not_verbatim_stops_before_any_edit(rolled) -> None: + with pytest.raises(AssertionError, match="was not verbatim"): + _run(rolled, Behaviour(fail_upload_verbatim=True)) + + +def test_the_decision_row_is_written_before_the_actuator_is_called(rolled) -> None: + """Decision 33: the product's gate is write-only, so the record has to be ours. + + Asserted on one shared timeline rather than on two counts, because two counts + that happen to be equal say nothing about the order. + """ + plan = build_plan(rolled.document).sample(2) + product = SimulatedSuperDocs(Behaviour()) + client = SuperDocsClient(product, poll_seconds=0) + ledger = DecisionLedger() + original = ledger.record + + def spy(*args, **kwargs): + product.event_log.append("ledger-row") + return original(*args, **kwargs) + + ledger.record = spy # type: ignore[method-assign] + result = Orchestrator(client, ledger, "K. Latha (CFO)", sleep=lambda _: None).run( + "run-1", plan, "statements.html" + ) + assert result.ok + timeline = [ + "ledger-row" if e == "ledger-row" else "approve" + for e in product.event_log + if e == "ledger-row" or e.startswith("approve:") + ] + assert timeline == ["ledger-row", "approve", "ledger-row", "approve"] + assert ledger.decided("run-1")[0].at, "the row carries a timestamp and an actor" + assert ledger.decided("run-1")[0].actor == "K. Latha (CFO)" + + +def test_a_second_decision_on_the_same_step_is_refused_unless_it_supersedes() -> None: + ledger = DecisionLedger() + ledger.record("r", 1, "c", "accept", "A", "because") + with pytest.raises(DecisionAlreadyRecordedError, match="quietly dropped"): + ledger.record("r", 1, "c", "reject", "B", "because") + superseding = ledger.record("r", 1, "c", "reject", "B", "because", supersede=True) + assert superseding.supersedes + assert len(ledger.rows) == 2, "the superseded row is kept" + assert ledger.accepted("r") == 0 and ledger.rejected("r") == 1 + + +def test_the_completion_sentence_is_computed_from_the_rows_it_holds() -> None: + ledger = DecisionLedger() + ledger.record("r", 1, "c1", "accept", "A", "x") + ledger.record("r", 2, "c2", "reject", "B", "y") + sentence = ledger.describe("r", planned=5) + assert "1 accepted, 1 rejected, 3 undecided of 5 planned" in sentence + ledger.rows.clear() + assert "0 accepted, 0 rejected, 5 undecided" in ledger.describe("r", planned=5) diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_rollforward.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_rollforward.py new file mode 100644 index 000000000..f447d0d1a --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_rollforward.py @@ -0,0 +1,130 @@ +"""The comparatives tie exactly, and a disagreement stops the run. + +This is the first half of the grading line. It is checked twice: caption by +caption against the signed set, and total by total, because a set can agree row +by row and still print a wrong total. +""" + +from __future__ import annotations + +import shutil +from decimal import Decimal +from pathlib import Path + +import pytest +from statutory.errors import ComparativeMismatchError, TieBreakError +from statutory.figures import Figure +from statutory.ledger import TrialBalance +from statutory.pipeline import prepare +from statutory.priorset import PriorYearSet +from statutory.statements import roll_forward + + +def _copy_corpus(corpus: Path, tmp_path: Path) -> Path: + destination = tmp_path / "corpus" + shutil.copytree(corpus, destination) + return destination + + +def test_the_four_ties_in_the_task_hold(rolled) -> None: + document = rolled.rolled + assert document.total("TOTAL ASSETS").value == Decimal("4857.00") + assert document.total("TOTAL EQUITY AND LIABILITIES").value == Decimal("4857.00") + assert document.prior_total("TOTAL ASSETS").value == Decimal("4068.00") + assert document.prior_total("TOTAL EQUITY AND LIABILITIES").value == Decimal("4068.00") + + profit_and_loss = document.statement("profit_and_loss") + assert profit_and_loss.line("Profit before tax").current.value == Decimal("577.00") + assert profit_and_loss.line("Profit before tax").prior.value == Decimal("414.50") + assert profit_and_loss.line("Profit for the year").current.value == Decimal("407.00") + assert profit_and_loss.line("Profit for the year").prior.value == Decimal("309.00") + + other_equity = document.caption_line("2110") + assert other_equity.prior.value + Decimal("407.00") == other_equity.current.value + assert other_equity.current.value == Decimal("1691.00") + assert all(ok for _, _, _, ok in document.ties) + + +def test_every_total_the_signed_set_prints_is_reproduced(rolled) -> None: + labels = [label for label, _, _, _ in rolled.rolled.ties] + assert sum(1 for label in labels if "vs signed set" in label) >= 11 + + +def test_a_comparative_is_cited_twice_or_it_is_not_a_comparative(rolled) -> None: + comparatives = [ + line.prior + for statement in rolled.rolled.statements + for line in statement.lines + if line.account is not None and isinstance(line.prior, Figure) + ] + assert comparatives + for figure in comparatives: + if figure.basis == "comparative": + assert len(figure.cites) == 2 + paths = {cite.path for cite in figure.cites} + assert len(paths) == 2, "one cite to the ledger, one to the signed set" + + with pytest.raises(ValueError, match="cited twice"): + Figure("x", Decimal("1.00"), "comparative", (comparatives[0].cites[0],)) + + +def test_a_comparative_mismatch_is_a_hard_error(corpus: Path, tmp_path: Path) -> None: + """Doctor the ledger's prior-year column; the signed set now disagrees.""" + root = _copy_corpus(corpus, tmp_path) + ledger_path = root / "sources" / "trial-balance-FY2026.csv" + raw = ledger_path.read_text(encoding="utf-8") + ledger_path.write_text( + raw.replace( + "1210,Trade receivables,BS,Current assets,812.00", + "1210,Trade receivables,BS,Current assets,800.00", + ), + encoding="utf-8", + ) + prior = PriorYearSet(next((root / "prior-year").glob("*.html")), root) + ledger = TrialBalance(ledger_path, root) + with pytest.raises(ComparativeMismatchError) as caught: + roll_forward(ledger, prior, root) + message = str(caught.value) + assert "Trade receivables" in message + assert "800.00" in message and "812.00" in message + assert "bytes" in message, "the error must say where to look, not only that it failed" + + +def test_a_balance_sheet_that_does_not_balance_stops_the_run(corpus: Path, tmp_path: Path) -> None: + root = _copy_corpus(corpus, tmp_path) + ledger_path = root / "sources" / "trial-balance-FY2026.csv" + raw = ledger_path.read_text(encoding="utf-8") + ledger_path.write_text( + raw.replace( + ",1200,Inventories,BS,Current assets,685.00,747.00", + ",1200,Inventories,BS,Current assets,685.00,777.00", + ).replace( + "1200,Inventories,BS,Current assets,685.00,747.00", + "1200,Inventories,BS,Current assets,685.00,777.00", + ), + encoding="utf-8", + ) + prior = PriorYearSet(next((root / "prior-year").glob("*.html")), root) + with pytest.raises(TieBreakError, match="does not balance"): + roll_forward(TrialBalance(ledger_path, root), prior, root) + + +def test_new_captions_are_nil_in_the_prior_year_and_absent_from_the_signed_set( + rolled, +) -> None: + new = [c for c in rolled.rolled.comparative_checks if c.signed_value is None] + assert {c.caption for c in new} == { + "Right-of-use assets", + "Lease liabilities - non-current", + "Lease liabilities - current", + } + assert all(c.ledger_value == 0 for c in new) + + +def test_the_whole_roll_forward_runs_with_no_key_and_no_network( + corpus: Path, config: Path, monkeypatch +) -> None: + monkeypatch.delenv("SUPERDOCS_API_KEY", raising=False) + result = prepare(corpus, config) + assert result.cites_verified > 400 + assert len(result.document.blocks) > 90 diff --git a/use-cases/preetham1930/statutory-statements-builder/tests/test_verifier.py b/use-cases/preetham1930/statutory-statements-builder/tests/test_verifier.py new file mode 100644 index 000000000..f0e38f923 --- /dev/null +++ b/use-cases/preetham1930/statutory-statements-builder/tests/test_verifier.py @@ -0,0 +1,101 @@ +"""The three failure classes, each driven by a response the product actually gave. + +Nothing here is hand-written HTML. `read0`..`read3` are four recorded +`GET /v1/documents/{id}?include_html=true` bodies from 2026-08-09: + +- read0 -> read1 a single-value targeted edit that worked +- read1 -> read2 a mid-document insertion that silently did not happen, while + five heading chunks nobody targeted were rewritten around it +- read2 -> read3 one `create` that appended six sections, four of them invented + +So the verifier is tested against the real shapes of the real failures. +""" + +from __future__ import annotations + +from recorded import read_back_html +from statutory.chunks import ChunkMap +from statutory.superdocs.verifier import verify, verify_upload_verbatim + +TARGET = "8ec79daa-5de5-4973-84a5-1d1139da29be" + + +def _maps(*names: str) -> list[ChunkMap]: + return [ChunkMap(read_back_html(name)) for name in names] + + +def test_a_clean_single_target_replacement_verifies() -> None: + before, after = _maps("read0-baseline", "read1-after-A") + expected = after.by_id[TARGET].html + result = verify(1, TARGET, before, after, expected) + assert result.ok + assert "matches the computed post-state" in result.receipt + assert "17 non-target chunk(s) unchanged" in result.receipt + assert result.exact_bytes + + +def test_a_reply_claiming_success_does_not_make_a_step_ok() -> None: + """The chat reply for the failing turn read 'nothing actually changed'. + + Five changes had been auto-approved and were in the document. The verifier + never sees a reply, so it cannot be wrong in either direction. + """ + before, after = _maps("read1-after-A", "read2-after-B") + result = verify(2, TARGET, before, after, before.by_id[TARGET].html + "<!--x-->") + assert not result.ok + kinds = {p.kind for p in result.problems} + assert "not_applied" in kinds, "the target chunk came back untouched" + assert "collateral_damage" in kinds, "five heading chunks moved" + detail = next(p for p in result.problems if p.kind == "collateral_damage").detail + assert "5 chunk(s) we did not target changed" in detail + + +def test_collateral_damage_is_caught_when_the_requested_change_also_succeeded() -> None: + """The dangerous one: the append worked *and* four fabricated sections arrived. + + A verifier checking only its own target would report success here. + """ + before, after = _maps("read2-after-B", "read3-after-C") + # Pretend our step targeted a chunk in the document; the requested append + # also succeeded, which is exactly what makes this class dangerous. + changed = before.ids[0] + result = verify(3, changed, before, after, after.by_id[changed].html) + assert not result.ok + problem = next(p for p in result.problems if p.kind == "collateral_damage") + assert "the chunk set changed" in problem.detail + assert "18 chunk(s) before, 19 after" in problem.detail + assert "Quality Assurance" in problem.diff or "Humidity" in problem.diff + + +def test_applied_wrong_is_its_own_class() -> None: + before, after = _maps("read0-baseline", "read1-after-A") + expected = after.by_id[TARGET].html.replace("750", "751") + result = verify(4, TARGET, before, after, expected) + assert not result.ok + assert [p.kind for p in result.problems] == ["applied_wrong"] + assert "751" in result.problems[0].diff + + +def test_a_missing_target_chunk_is_not_applied_rather_than_a_crash() -> None: + before, after = _maps("read0-baseline", "read1-after-A") + result = verify(5, "no-such-chunk", before, after, "<p>x</p>") + assert not result.ok + assert any(p.kind == "not_applied" for p in result.problems) + + +def test_the_upload_is_checked_verbatim_rather_than_assumed() -> None: + """Decision 29 rests on a measured property, so it is re-measured every run.""" + uploaded = ChunkMap(read_back_html("read0-baseline")) + blocks = [chunk.html for chunk in uploaded.chunks] + receipt = verify_upload_verbatim(uploaded, blocks) + assert "18 chunk(s)" in receipt + + tampered = list(blocks) + tampered[3] = tampered[3].replace("</p>", " and one more sentence.</p>") + try: + verify_upload_verbatim(uploaded, tampered) + except AssertionError as exc: + assert "was not verbatim" in str(exc) + assert "first difference at block 3" in str(exc) + else: # pragma: no cover + raise AssertionError("a non-verbatim upload must stop the run")