From 023cdd73ec7d98f940ee1e82ffcb6ece8de78426 Mon Sep 17 00:00:00 2001 From: Shashank Shekhar Singh Date: Wed, 5 Aug 2026 05:18:40 +0530 Subject: [PATCH] =?UTF-8?q?plan=20plans,=20go=20goes=20=E2=80=94=20and=20t?= =?UTF-8?q?he=20first=20hour=20finally=20has=20a=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One day's worth of making the product mean what it says: - The live view is a designed SVG dashboard (no CDN, packaged frontend): per-node status/tokens/cost, goal in the header, violet proposed nodes, approval banner, replay scrubber; goal and approve-command ride the trace. - plan/go split: `plan` proposes + admits + saves plan.json and STOPS; `go` executes the newest saved plan (re-admitted on the way in); `plan --go` is the one-shot. Registry chain: flag/config > ./registry.py > built-ins, with `--default` forcing the built-ins. - No silent scripts in real commands: a model is required; `--scripted` is the explicit, labeled rehearsal. - Local-model planning made reliable: -block-safe JSON extraction, slim proposal schema for Ollama's grammar decoder, retry notes carrying the offending reply + a diamond worked example (so 8B models fan out independent work instead of chaining it), --model-arg/--workspace/ --max-planning-failures, and a registry-keyed generated-policy cache (a stale policy could silently unlock the mutating kind). - Onboarding: bare `grapharc` orients, `help` works, `start` explains every term in plain words, `init` scaffolds a registry whose first free run demonstrates refuse-then-admit and whose gather does real work. - Traces default under .grapharc/runs; serve writes a discovery marker; plan/go always print a probed `watch :` URL. Public site under docs/site with a Pages workflow; README repositioned around the admission gate. Co-Authored-By: Claude Fable 5 --- .github/workflows/pages.yml | 34 + CHANGELOG.md | 6 + README.md | 41 +- docs/cookbook/05-governance.md | 54 ++ docs/cookbook/06-serving-and-ops.md | 22 +- docs/cookbook/07-slack.md | 17 +- docs/site/README.md | 20 + .../assets/brand/grapharc-favicon-180.png | Bin 0 -> 12043 bytes .../site/assets/brand/grapharc-favicon-32.png | Bin 0 -> 1675 bytes .../assets/brand/grapharc-favicon-512.png | Bin 0 -> 42880 bytes docs/site/assets/brand/grapharc-favicon.svg | 25 + .../assets/brand/grapharc-logo-ondark.svg | 38 ++ docs/site/assets/media/grapharc-decompose.mp4 | Bin 0 -> 353611 bytes docs/site/assets/site.css | 415 ++++++++++++ docs/site/assets/site.js | 43 ++ docs/site/assets/tokens.css | 58 ++ docs/site/index.html | 257 ++++++++ grapharc/cli/config.py | 1 + grapharc/cli/generate.py | 36 +- grapharc/cli/init_cmd.py | 421 +++++++++++++ grapharc/cli/main.py | 254 +++++++- grapharc/cli/plan.py | 590 +++++++++++++++++- grapharc/cli/serve.py | 41 +- grapharc/cli/start.py | 187 ++++++ grapharc/gateway/ollama.py | 10 +- grapharc/observe/__init__.py | 4 + grapharc/observe/cost.py | 11 +- grapharc/observe/layout.py | 264 ++++++++ grapharc/observe/metrics.py | 34 +- grapharc/observe/status.py | 85 +++ grapharc/observe/viewmodel.py | 436 +++++++++++++ grapharc/planner/__init__.py | 4 + grapharc/planner/loop.py | 65 +- grapharc/planner/proposal.py | 161 ++++- grapharc/runtime/parsing.py | 107 +++- grapharc/server/live.py | 293 +++------ grapharc/server/static/index.html | 22 + grapharc/server/static/signin.html | 28 + grapharc/server/static/view.css | 349 +++++++++++ grapharc/server/static/view.html | 48 ++ grapharc/server/static/view.js | 483 ++++++++++++++ grapharc/slack/bot.py | 26 +- grapharc/slack/command.py | 2 +- grapharc/slack/live.py | 69 +- grapharc/stdlib.py | 91 ++- tests/test_approval.py | 14 +- tests/test_cli.py | 579 +++++++++++++++-- tests/test_cli_style.py | 20 +- tests/test_config.py | 13 +- tests/test_generate.py | 80 ++- tests/test_graph_layout.py | 129 ++++ tests/test_graph_viewmodel.py | 184 ++++++ tests/test_node_status.py | 104 +++ tests/test_packaging.py | 5 + tests/test_parsing.py | 85 +++ tests/test_plan_docs.py | 2 + tests/test_planner_loop.py | 1 + tests/test_readme.py | 25 +- tests/test_server_live.py | 136 ++++ tests/test_slack_live.py | 26 +- tests/test_slim_proposal.py | 216 +++++++ tests/test_stdlib.py | 31 + 62 files changed, 6331 insertions(+), 471 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 docs/site/README.md create mode 100644 docs/site/assets/brand/grapharc-favicon-180.png create mode 100644 docs/site/assets/brand/grapharc-favicon-32.png create mode 100644 docs/site/assets/brand/grapharc-favicon-512.png create mode 100644 docs/site/assets/brand/grapharc-favicon.svg create mode 100644 docs/site/assets/brand/grapharc-logo-ondark.svg create mode 100644 docs/site/assets/media/grapharc-decompose.mp4 create mode 100644 docs/site/assets/site.css create mode 100644 docs/site/assets/site.js create mode 100644 docs/site/assets/tokens.css create mode 100644 docs/site/index.html create mode 100644 grapharc/cli/init_cmd.py create mode 100644 grapharc/cli/start.py create mode 100644 grapharc/observe/layout.py create mode 100644 grapharc/observe/status.py create mode 100644 grapharc/observe/viewmodel.py create mode 100644 grapharc/server/static/index.html create mode 100644 grapharc/server/static/signin.html create mode 100644 grapharc/server/static/view.css create mode 100644 grapharc/server/static/view.html create mode 100644 grapharc/server/static/view.js create mode 100644 tests/test_graph_layout.py create mode 100644 tests/test_graph_viewmodel.py create mode 100644 tests/test_node_status.py create mode 100644 tests/test_slim_proposal.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..39266ed --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,34 @@ +# Publishes docs/site/ — and nothing else — to GitHub Pages. +# One-time repo setting: Settings → Pages → Source: "GitHub Actions". + +name: pages + +on: + push: + branches: [main] + paths: ["docs/site/**", ".github/workflows/pages.yml"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/site + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9b874..0edad79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ Entries are newest-last within a release, matching the order they were written. - the `/live` **token was accepted in the query string on every route**, and a URL is the one place a secret cannot be taken back from: the uvicorn request line, the nginx access log, browser history, and the referrer of anything the page opens. The index made it worse by writing the token into every link it rendered, so clicking a trace filed the secret in history a second time. It is refused off `/live/api/stream` now — that route keeps it because a browser `EventSource` cannot set a header and has no other way in — with a 401 whose reason says *where* to put the token rather than that it is wrong. A browser gets a sign-in page instead of a bare 401 and trades the token for a cookie: a SHA-256 digest of it rather than the token itself, `HttpOnly`, `SameSite=Strict`, scoped to `/live`, and always ASCII, so a non-ASCII secret survives the latin-1 header encoding that a `Bearer` header cannot. Links carry no token at all. The residual exposure — the SSE request line — is now named in the cookbook next to `--live-token`, with what to scrub. Every confinement the reader already enforced is untouched: `../`, `%2e%2e%2f`, absolute paths, NUL bytes and symlinked traces are the same 404s, and a hostile token is still a 401 rather than a crash. (#41) - the live page was **blind for the whole planning phase**, which is where a governed run spends its budget and does its refusing. `plan`, `admission` and `round` events were on disk — 2,081 tokens spent before any node ran, in the report — and the page rendered none of them, because it keys the graph off the `topology` event that only lands once a round is admitted and materialised. A run refused on every round produces no topology at all, so the most governance-relevant run there is showed nothing from start to "finished". The snapshot now carries a `planning` block folded from those same events (no new trace events): per round, the proposal size, the admission status, the checks that failed and the rejection codes, the planner tokens, and whether it executed; plus the loop's stop reason and detail when it stopped without a graph. The page renders it as a panel, and a round that has begun and not closed reads as *active* rather than idle — a planner mid-inference writes nothing for a minute at a time, which is exactly the "is it thinking or is it wedged?" the report describes. A run that never planned has no `planning` field and renders exactly as before. (#47) - a finished trace **rendered as a done deal**: instantly all-green, with the amber `running` styling unreachable for every run that is already over — and for any live run whose nodes finish between two SSE polls. `?replay=1` on the stream walks the recorded events in timestamp order and emits the snapshots the run would have sent, so a node is amber for its recorded window and green after; `&speed=N` divides the wall clock and the whole replay is capped at 40 seconds, so a 40-minute incident trace is watchable. Frames are rebuilt by the same snapshot code a live stream uses, pointed at a prefix of the file, and depend on no clock: a trace replayed twice renders identically. Without the parameter nothing changed. (#48) +- a qwen3-class model's **`` block could beat its own answer**: JSON extraction ranked object spans longest-first, so a longer draft inside the reasoning block outranked the real reply outside it, and a fenced draft inside the block won outright (only the first fence was ever tried). The visible text — reasoning tags stripped — is scanned first now, the original text is a fallback tier (a reply that is *entirely* think-block still parses, and a `` inside a JSON string is data, because a reply that already parses whole is never rewritten), every fence is tried in order, and a trailing comma is repaired only on candidates that already failed to parse — a trailing comma is never valid JSON, so no valid document can be rewritten. +- the planner pushed **`Subgraph`'s own JSON schema at local grammar-constrained decoders** — recursive (`ProposedNode.subgraph → Subgraph`), every field required under strict mode including the `proposal_id`/`origin` it discards on arrival, ~3.5 KB of embedded docstrings — and small models reliably choked on it. Backends now declare `reliable_structured_output`; Ollama says no and gets the text path: a three-key slim shape (`nodes`, `edges` — pair, object and from/to forms all accepted — `rationale`) with a worked example in the prompt, re-validated through the real constructors so admission judges exactly what it always judged. A parse failure's retry note now shows the model a truncated snippet of its own reply plus the example, instead of a bare error string; `--max-planning-failures` makes the allowance operator-settable. +- the generated-policy cache **was not keyed by registry**, so a `.grapharc/generated-policy.toml` written for the incident demo (`deny *->deploy`) silently governed a later stdlib run — overriding stdlib's own `deny *->apply_change` and making the mutating kind reachable with no operator decision anywhere. Generated policies are keyed by registry target now (`generated-policy..toml`); a legacy un-keyed file is never honoured implicitly when the run can say which registry it is — the run falls through to generation or the registry default and *says so* — and the file survives untouched for an explicit `--policy`. +- **the CLI never joined the runs it starts to the live view that draws them.** `plan`'s default trace went to a tempdir no server serves, and no command printed a URL. Defaults compose now: traces land under `.grapharc/runs//`, `serve --live-root` writes a discovery marker (`.grapharc/live-server.json` — URL, root, pid, never the token; removed on clean shutdown), and `plan`/`go` end with a `watch :` line — the exact page URL when a marker names a server that answers one loopback connect, the command that would start one otherwise. The goal now rides the loop's topology and approval events (operator-supplied text, deliberately shown — the second state field after `termination_reason`), so the page can say what a graph is *for*; a parked run shows its proposed nodes in violet with a copy-ready `grapharc approve ` banner. +- **`plan` planned nothing and executed everything** — the name lied. The verbs are split now: `grapharc plan` proposes, the gate admits, and the run STOPS with the admitted plan saved to `plan.json` next to its trace (exit 0, `stopped: planned`); `grapharc go` executes the newest saved plan (`go ` for a specific one), replaying the stored proposal through the full governed loop so admission judges it again on the way in — a hand-edited plan.json is a new proposal, not a pre-approved one; `plan --go` (and `go "a goal"`) does both in one run. Looking at a plan and then typing `go` *is* the approval; `--approve` remains for parking one-shot runs mid-flight. Registry resolution is now one visible chain shared by both commands: flag/config first, else a `registry.py` in the directory (yours wins), else the built-in general-purpose kinds — with `--default` forcing the built-ins past everything. +- **new commands for the first hour**: bare `grapharc` orients instead of erroring (exit 0); `grapharc start` is the guided tour; `grapharc init` scaffolds a commented `registry.py` (whose first free run reproduces refuse-then-admit), a `grapharc.toml`, and `.grapharc/runs/` — refusing to overwrite either authored file, with no `--force`; `grapharc go` is plan with doing-defaults (the stdlib tool-using registry, `--model` required); `--registry path/to/file.py:attr` loads a registry file directly; `--workspace` confines the stdlib kinds' tools to a directory (refused when a registry cannot take one — never silently un-confined); `--model-arg KEY=VALUE` reaches the backend constructor. The stdlib planner is now *told* the deterministic completion rule (end with `summarize`) instead of discovering it by burning rounds. ## 0.1.3 diff --git a/README.md b/README.md index 6cb6caa..e29a40d 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,15 @@ [![CI](https://github.com/CodeGraphContext/GraphARC/actions/workflows/ci.yml/badge.svg)](https://github.com/CodeGraphContext/GraphARC/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -**A governed agent runtime built on [LangGraph](https://github.com/langchain-ai/langgraph).** +**The admission gate for agent graphs** — a governed agent runtime built on [LangGraph](https://github.com/langchain-ai/langgraph). -Build production-grade multi-agent systems with built-in safety, auditability, and control. GraphARC adds a governance layer on top of LangGraph: a planner *proposes* a subgraph, a deterministic checker *admits* it, and only then does anything execute. Every transition is permitted, every loop is bounded, and afterwards you can prove what happened and why it stopped. +A planner *proposes* a subgraph, a deterministic checker *admits* it — or refuses with reasons — and only then does anything execute. Every transition is permitted, every loop is bounded, and afterwards you can prove what happened and why it stopped. Three things this package does that you will not find together anywhere else: -**Status:** early days (`0.1.3`) — the API is not stable yet. Known limits are listed in [Status and limits](#status-and-limits); closed ones are in [CHANGELOG.md](CHANGELOG.md). +- **No step runs unless a deterministic gate admitted it — and it tells you why when it refuses.** Five checks on every proposal, every failure a structured rejection with a code and a remedy, work discovered mid-run re-entering the same gate. `grapharc run --check-only` is the gate as a linter: it can refuse without executing anything. +- **The worst-case cost is known before a graph runs, and the exact per-node bill after — even when it fails.** Admission prices the worst case against what is *left* of the budget; at runtime each node's spend lands on its own trace event, error and cancellation included. `recorded_cost_usd` is never an estimate. +- **One append-only JSONL trace file is the whole truth.** `replay`, `diff`, `metrics`, `viz`, cost attribution, OTel export and the live browser view all read the same file — the dashboard cannot disagree with the audit trail, because they are the same record. + +**Website:** [codegraphcontext.github.io/GraphARC](https://codegraphcontext.github.io/GraphARC/) · **Status:** early days (`0.1.3`) — the API is not stable yet. Known limits are listed in [Status and limits](#status-and-limits); closed ones are in [CHANGELOG.md](CHANGELOG.md). ![One English question is decomposed by a local model into a nine-node graph — four parallel evidence pulls fanning out of START, a correlate join, a hypothesis fork, and a final report — shown live in the browser: the proposed graph waits grey for human approval, then each node turns amber while it runs and green when it is done.](docs/media/grapharc-decompose.gif) @@ -25,6 +29,19 @@ Build production-grade multi-agent systems with built-in safety, auditability, a > *Graph engineering*: when one agent loop stops being enough, coordination becomes the engineering. Nodes do work (agent loops, model calls, deterministic functions, humans approving things), edges decide what runs next, and a typed shared state flows between them. GraphARC implements the discipline that makes such graphs production-grade rather than demos — the ideas emerging from the July 2026 loops-vs-graphs debate (Steinberger, Ng, et al.), the "Two Graphs, Two Jobs" split, and twenty years of pre-AI graph systems where every edge means something and every path can be explained. +## Where it sits + +None of these is a competitor to be beaten — they do different jobs, and GraphARC's [design lineage](#design-lineage) borrows from two of them. This is the row-by-row difference: + +| | GraphARC | Claude Code | OpenClaw | raw LangGraph | +|---|---|---|---|---| +| Shape | Governed multi-node graph runtime | Interactive single-agent coding loop | Personal AI assistant gateway | Graph mechanism library | +| Who authorizes work | A deterministic admission gate, pre-execution, with reasons | A human, live, per action | Configuration and allowlists | Nobody — convention | +| Cost control | Worst-case admission + per-node attribution, fail-closed | Usage visibility | Spend settings | None built in | +| Audit | One replayable JSONL trace; replay/diff/cost read it | Session transcripts | Logs | Checkpoints (state, not *why*) | + +Claude Code is a great agent — GraphARC's default backend drives the Claude CLI. OpenClaw is a great gateway to agents — GraphARC borrowed its policy-before-schema tool gating and put it behind enforcement. GraphARC is the layer that decides what an agent system is *allowed* to do: before it does it, with receipts after. + ## Install Python >= 3.12. @@ -73,8 +90,14 @@ grapharc demo stage5 # verifier: fresh context + deterministic evidence a grapharc demo stage6 # memory: provenance, supersession, recall grapharc demo capstone # all of the above in one research agent -grapharc plan "look into the outage" # governed loop: propose -> admit -> execute -> replan -grapharc plan "..." --approve # park each admitted round until a human answers +grapharc start # the guided tour: concept, first run, live view +grapharc init # scaffold registry.py + grapharc.toml + .grapharc/runs/ + +grapharc plan "look into the outage" --model ollama/qwen3:8b # plan ONLY: propose -> admit -> save +grapharc go # execute the newest saved plan (go for a specific one) +grapharc go "fix the flaky import" --model ollama/qwen3:8b # plan AND execute, one shot +grapharc plan "..." --scripted # free rehearsal: stand-in planner, no AI +grapharc plan "..." --go --approve # one-shot, parked mid-run until a human answers grapharc approve # answer a parked run (--deny to refuse) grapharc run graph.json # a topology you wrote, through the same gate grapharc run graph.json --check-only # admission as a linter; executes nothing @@ -91,7 +114,7 @@ grapharc replay # reconstruct a run from its trace grapharc diff # what changed between two runs ``` -Twelve commands, and every one of them takes `--json` — in JSON mode the failure is the document rather than a line on stderr. Exit codes are part of the interface: `0` did the job, `1` ran and the answer was negative (an agent stopped short, a run id had no events, two runs differed), `2` could not run at all. +Fifteen commands, and every one of them takes `--json` — in JSON mode the failure is the document rather than a line on stderr. Exit codes are part of the interface: `0` did the job, `1` ran and the answer was negative (an agent stopped short, a run id had no events, two runs differed), `2` could not run at all. The Slack bot puts most of these commands one `/grapharc …` away from a phone, behind an allowlisting gate that keeps the default spend at zero — setup in [docs/cookbook/07-slack.md](docs/cookbook/07-slack.md), and a command-by-command session, refusals included, in [docs/cookbook/08-slack-walkthrough.md](docs/cookbook/08-slack-walkthrough.md). A tracing command run from Slack is narrated live — one status message edited in place as nodes run, with a refreshed diagram link — and `grapharc serve --live-root` adds a browser page that redraws the orchestration graph in real time over SSE. @@ -130,12 +153,12 @@ The part with no prior art to copy, and the reason the rest exists. You cannot p Watch it happen first. This costs nothing and needs no key — the shipped planner is scripted, and its first proposal names the policy-denied `deploy` kind: ```bash -grapharc plan "investigate the checkout outage" +grapharc plan "investigate the checkout outage" --scripted --go ``` ``` goal : investigate the checkout outage -model : scripted +model : scripted stand-in (--scripted) registry : grapharc.examples.plan_incident:build_registry kinds : deploy, patch, triage, verify policy : grapharc.examples.plan_incident:build_registry default (deny -> deploy, otherwise allow) [registry-default] @@ -151,6 +174,8 @@ state : goal='investigate the checkout outage' notes=['triage ran', 'patch r Round 1 wanted to deploy and **never executed**. Round 2 went through the *same* checker and ran. +The output ends with a `trace :` path — under `.grapharc/runs/` by default — and a `watch :` line. With `grapharc serve --live-root .grapharc/runs` running in another terminal, that line is the exact URL of this run's live page (proposed graph in violet awaiting approval, amber while nodes run, green when done, replay scrubber after); without one, it is the command that starts it. + The `policy` line ends in `[registry-default]` — that is the **provenance**, and it is on the JSON payload too as `policy_source`. It matters because a policy can now come from four places: a `--policy` flag, a `grapharc.toml`, one an LLM generated on a first run, or the registry's own default. A generated run and an authored one look identical on the command line, so the source is the only thing that tells them apart afterwards. `--model SPEC` swaps in a real backend and changes none of the enforcement; `--policy policy.toml` moves the rules into a document. diff --git a/docs/cookbook/05-governance.md b/docs/cookbook/05-governance.md index 0ff362e..86fdc04 100644 --- a/docs/cookbook/05-governance.md +++ b/docs/cookbook/05-governance.md @@ -935,8 +935,62 @@ print(system.split("Available node kinds:")[1].strip()) Denied by policy. The admission checker refuses these; it is deterministic code and this list is only telling you in advance: - edges into 'deploy' are denied by policy — do not propose them: a deploy is the operator's decision + +Reply with exactly one JSON object and nothing else — no prose before or after it. The object has three keys: `nodes` (a list of {name, kind}), `edges` (a list of [source, target] pairs), and `rationale` (one sentence). Example: +{ + "nodes": [ + { + "name": "prepare", + "kind": "" + }, + { + "name": "branch_a", + "kind": "" + }, + { + "name": "branch_b", + "kind": "" + }, + { + "name": "combine", + "kind": "" + } + ], + "edges": [ + [ + "__start__", + "prepare" + ], + [ + "prepare", + "branch_a" + ], + [ + "prepare", + "branch_b" + ], + [ + "branch_a", + "combine" + ], + [ + "branch_b", + "combine" + ], + [ + "combine", + "__end__" + ] + ], + "rationale": "branch_a and branch_b are independent, so they run in parallel" +} ``` +The last block is the text path's own format contract: a scripted double has +no structured-output wire, so `PlannerNode` asks for the slim proposal shape +in prose — the same thing a local Ollama model gets. A backend that supports +structured output is handed the schema instead, and none of this text. + Without those two lines a model reads a registered-but-denied kind as an invitation, proposes it, gets `edge_denied` back — a check name, which says nothing about how wide the denial is — and proposes it again. One real run diff --git a/docs/cookbook/06-serving-and-ops.md b/docs/cookbook/06-serving-and-ops.md index 07ddaa3..fae91b9 100644 --- a/docs/cookbook/06-serving-and-ops.md +++ b/docs/cookbook/06-serving-and-ops.md @@ -1374,9 +1374,25 @@ token, not the token, is `HttpOnly` and `SameSite=Strict`, and is scoped to `/live`. Sign-in and the SSE exemption both apply only when a token is configured at all; without one, nothing about `/live` is authenticated. -The diagram renders with mermaid.js from a pinned CDN; with -no CDN reachable the page falls back to the raw Mermaid source plus the same -mermaid.live fragment link the Slack bot posts. +The page draws the graph itself — a positioned SVG the server computes from +the trace, with per-node status, tokens, recorded cost and duration on every +node — and makes no external request at all: styles, script and rendering all +ship inside the package, so it works with the network cable pulled. A finished +run gets a playback bar (play, speed, and a scrubber over the run's own +timeline), and `?replay=1&speed=N` on the stream URL re-emits the recorded +snapshots server-side at recorded speed. The raw Mermaid source and the same +mermaid.live fragment link the Slack bot can post stay one click away under +"diagram source". A parked `--approve` run shows its goal in the header, the +proposed nodes in violet, and a copy-ready `grapharc approve ` banner. + +The guided pairing: `grapharc plan`/`go` default their trace to +`.grapharc/runs//trace.jsonl` under the current directory, and +`grapharc serve --live-root .grapharc/runs` writes a discovery marker +(`.grapharc/live-server.json` — URL, root, pid; never the token) that lets +those commands print the exact `watch :` URL for each run. The marker is +validated with one loopback connect at print time, so a marker left by a +crashed server downgrades the line to an instruction instead of a dead link, +and it is removed on clean shutdown. --- diff --git a/docs/cookbook/07-slack.md b/docs/cookbook/07-slack.md index 3929bec..c97169f 100644 --- a/docs/cookbook/07-slack.md +++ b/docs/cookbook/07-slack.md @@ -95,7 +95,7 @@ whether model flags are on — the three decisions that matter — then blocks until interrupted. From Slack: ``` -/grapharc plan "investigate the checkout outage" +/grapharc plan "investigate the checkout outage" --scripted /grapharc trace t.jsonl @grapharc metrics t.jsonl ``` @@ -136,14 +136,17 @@ edited in place every couple of seconds: ✗ verify err: citation not found ▸ report running… 6 events · 2/4 nodes done · 1543 tok - + ``` -The `current diagram` link is the same mermaid.live fragment URL `viz` gets — -the diagram is compressed into the URL itself and shipped to no one — and it is -refreshed on every edit, so mid-run it renders the path *so far*. When the -command finishes, the status message is edited one last time into the same -final result the bot has always posted. +The `open live view` link points at your own live server's run page +(`GRAPHARC_SLACK_LIVE_URL` + the trace path), where the graph redraws in real +time. With no live URL configured, the message carries a `current diagram` +link instead — the same mermaid.live fragment URL `viz` gets, the diagram +compressed into the URL itself and shipped to no one, refreshed on every edit +so mid-run it renders the path *so far*. When the command finishes, the status +message is edited one last time into the same final result the bot has always +posted, keeping the run-page link (or, without one, a final-diagram link). Everything about this path is best-effort by construction. If the bot cannot post the status message (it is not in the channel, the API errored), the whole diff --git a/docs/site/README.md b/docs/site/README.md new file mode 100644 index 0000000..565e11b --- /dev/null +++ b/docs/site/README.md @@ -0,0 +1,20 @@ +# GraphARC public site + +Hand-authored static site — no npm, no build step. Deployed to GitHub Pages by +`.github/workflows/pages.yml`, which publishes **only this directory** (Pages +source must stay "GitHub Actions"; switching it to "deploy from branch /docs" +breaks the site and publishes the whole docs tree through Jekyll). + +Preview locally: + +```bash +python -m http.server 8080 --directory docs/site +``` + +All URLs are **relative** on purpose: production serves under the +`/GraphARC/` project-pages prefix, where a leading-slash URL 404s. + +`assets/tokens.css` is the design-token sheet shared with the live run view +(`grapharc/server/static/view.css`) — change both together. `assets/brand/` +and `assets/media/` are copies of `docs/brand/` and `docs/media/` (the Pages +artifact is this directory alone, so it must be self-contained). diff --git a/docs/site/assets/brand/grapharc-favicon-180.png b/docs/site/assets/brand/grapharc-favicon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..4e81f7f165261a2f17c4498e2b46e0e4707d21ad GIT binary patch literal 12043 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 literal 0 HcmV?d00001 diff --git a/docs/site/assets/brand/grapharc-favicon-32.png b/docs/site/assets/brand/grapharc-favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..b0efff16d235fa375f1bb2f8be2c669713192131 GIT binary patch literal 1675 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%0A5~u&7u6T7J#;G4E!_wxpdiZ7ozmTcbVx}I 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 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/site/assets/brand/grapharc-logo-ondark.svg b/docs/site/assets/brand/grapharc-logo-ondark.svg new file mode 100644 index 0000000..81a4c6e --- /dev/null +++ b/docs/site/assets/brand/grapharc-logo-ondark.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/site/assets/media/grapharc-decompose.mp4 b/docs/site/assets/media/grapharc-decompose.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2a6fc7be9b59692ca32d5ad4ddc9daa2ae29e1ea GIT binary patch literal 353611 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≦Jq_+#(MEdwQ%eRxQq+p- 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­dT1ZjFM(%HD~_Sl_wLj-?AK-|`Sc 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
4($#RMV_+uU^Vd_r8W|i^(IDH9H%T2Jt3K8 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

GA!8`FF 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%0DMbWtHqh0+oM>{ZXU^=x-tYhoL}0aq*d0nBac) zM*o{%`BzMT0Qzksn8Qw6Y`KV^9tV*4MYjAaq~8+%GLnfO^hZkaZxQ{6k$`l`#Qj<_e(sMl{XRGNS3rL?(rhcXPn*vlLcb`Pf5r4yGa<~v zt(tZMgaC&6hcku$WT-z*75*)(e=^iBzR6#4{ez)?u}}UA>K_dC?-f4&m<|40CV{Nd ze;MjGujFqL{r86YgM|NAK>ui{Kk9A#71KW$is4UT@Yihc3ziR%F#BD*cOztsJ-ztv zUj-5Rmu>$mp#7hwy?=R=89tjPx^h(ETZN{X+B8RA~1r z2v*2G*wwk2zeqfQ?@$19HNcO4^HTn&=J~}?`JbBS|1v&74E*w=pMa?T6rKJH(0^XQ zQUltu{NIP8KiTCMh2^hc{?2)P0387=`HRo;e{UB6jK7jw{&1mx{|ie6(EdM&KmR$k z{VZDscuM;7-Sv+i^Q$D;Poe+ABg21~r}? z|4&C70G7W-o4>*GhnM`WR`s_Y|Nm=x=Vzm{uK;o4E7@@$Pv9^knoKbs4y0blq@{%g8Km;va3U{@OF8AkZ|j0h-%@w431 z|I}&zKZF@-!4#4|$~d7(=>p33{|APDo6CNA4PaP;KZ^kX+)_^NZc$3OckrhGcQKv| zq|wvHzX_u{8=NM)2m??N6&eSB0fKrPyC>LgG3#~wdJms|bT7Ggs(#=M$yg&7;OaH1 zjBd62qp**B^7(JiC1f=JEzRfx>tl*D?((J20shH4WSMy43PYR<= zel6Atymp``FonU#i>^6lU%VN1-FV4UW@K-=)j+`DKAA>Xt_DGzuJL0ok)NXk)J;kg zSzzg9g>l?u4Knm}3Q0h*E;?Fskz3b9?K)F4_ALqad-*&S>Wd8beRBiWM2GWdspvJk z`c3e43%uD_m`4)g75RbsqW zgD6xEA`g&37PO@X}QkEFJ9M#*+j|q<1#{Q5NZU^23&$gGpwl%7;ZZyXh_JNx~G##j2=#05y~R z)-p|)rjHQArur^*)WWe&!@F|oEQz*z_9uQ>533?Z$gJi9QCq5WXf-KBL?>HgE-xr! z@&(k9CineneUppL$GECga2BC;bLY0j58GZTC}E3T>NV{jl?HpHi~7q0~ZQuoyitrkz}+pB%t4c#DIX9NNmQ$i>;2r-;TEh!O)A*R=pn)1nN zj@bhO3O=hD@kM@^l=*Cgq5$iV+`>WPu{J!^CfgyyIl<<#hFb*X=x$Orcz8`dQqIvm z91w>Bf-$6)r$REJ5u}msg6g=dxS|rFt?Bn;bu9|on_M*7xu?Uu}E{Gji>0Ih040~9) zeN>1^7eUZ*JjV@o8M5kbGL%oaU1LGlHla=HS^y5L&ht|H3I!$X zA|EI&m`!R6HBBsmS6=;!X###X%}mYKU9SVFu>6RLDf+rjLbVUV=Zum!XMn%D9j z#wU={d1)oPVW?yQGbT{mmNeonD==+B=j7W)Hnb;Xemx#3@CfeNs58y#}jB5Sk5G6D3UZK-o)YLFlfHWGVrRt*C?95FHX$_L-1zS0IjqM>(F( zQN=2=9p%+oP>-B=;s$3R4XbsV0DIFN+u`I?KH<&+&K6E&cqs|gD#;Yv$wlI-a;=9K zzr#CVJA)GWJK=*DNYv#0tlKeepZJm_ogS8tNAe42;%ppeGpYq)l#(voPZ>+zb6}wX zWLm{1LWS=*+&3ZzpFS2g)EZ^+B~%+2I394}=a?W*?XOiP3$SU<+Sphaw7T`D>A=tVwm<$q-!Oi=$%TK-iYoP9JGB?!PqJqi(paU!fWbkl`i=OQm6(xAKW;`JnG%x#RLypggai@d-M>JZvlb1HVN<$`2m40LeDV zSlvMDk&@niB_42ws|Dz$+0EGO7`VMQ0>ned$Zb_NTeEItJcVg8d56LJr0zjg-PKA4F`+;=UI{$qEgej3sKTPa=Q#5 zoP7vfmBv4J|44^9`hs}gHb4ob*?1x){a}YuQ{edBnT=(c`HQF})Z^$Vjp?gw`v5qKkg;y$I=W`Q!L44uc_05*cai8U)?2RBY72 z$ZTCWH$)YL&tZ}gPH-IX-604jf!#b2gs0I8unZEzP^K^QN;M`a5okb#T~@;iA?x5Z z314Q?6ldrS>o*$iMF)53nXM^-=%aEzi*gjZ`Z@KLqE-9ptJtT=g-W+Y+M7@U;{_#b zngOk*RD%{E7)*_ubyKEg_pa|1mPpNfuC62?m`M^IU_K2IftzE^D`scDaCv!4s%|Fk zO-V}zUMakr&=~Q)7hfwa*EeLk)d*lED-=-mt_wqwFp5g9!62^U=@&K1y6%d zFsr58nPaQ_M8E#-Yud{x@0A)D_#y*y`eTtc-pN$)kGXr1p?T)`w>l;MUi4FN zjZfx$&`8pJoH}25azpA|?lWTBtDjTBh8=CDG?GjyPU#__UbMCuo!iNLGBaN-+RUrT zxTl8888;5{C&tMGBc?+ha$&OR=ZfPZZYQ*CIfDB@UTv~6jn6#oWx1_x2;)_hZYCFB zttgYaWRut(P6!Qyn<3|{MMV8iC>`xJuz(?Bw^nf_Y~dVK7nIU-1N-f|-fw4tb7s1g zg$0liQ86E?Z7TJ`pS6>Dv#$G1*;;SB-(7yV+gnPyh6J@P)5EK5V5r_Y`h1^UjhDz2C!xzKa-vkdy*>9E!j`%UH zlA7UwSZct`qCGs}G^xqC>Pd`5R5jKkUb7Q`Dva z-*43T`}0=|#BQo0UK5RM=qRP(v;+oW+6xQuQ6>#?9%UT z-=_2#=FrM~3=Vfgd9r|<=uNd~alX7yi|*p#87{_M4hbEKOO-%8Gpa@Sj((46ou`an zBpUd+cRH1XB}=J;`Nto}r4 zEkB^I<5rhmKwOl5MYh!iA*r`?yz_*K-Zt28%)a=R141`rO)t4M!ksxj4HfE`bb&D` zh~9k^nvYNEd(}ABr6%htBRvGs9g$<-zaQe-WXwwHl`2P$}D4- z2Pupw~x6O@El^KmgJIX{WyV0Q2(~904zv!Fl(5m z9?!a@t)G^I$OO2FD(ugp=<@0`H6nD+Bd&p=TN=7a&m5^g7KVhB{WtUUh}#Xbv5|M(l1JG8yIDxE7EeKM2gs zAU4wl)Wi1!!;O*=%2=(iT>pskgpzFS$JXTWa5|j)MBO-AtFj{i>0XF?p1Uv^?kBBV z4>7d|d~30kFN7huQvgo!5^Q_lcqv)vs%Jus09d~$pEh#H~ zav&K7KJzYV;|DZEqgo)$7B>ffrURTn`jSiBHHpRT zfIdU~_b5wVVOQgOMHSqM3un!UObl(GUFA3#v*y{vKa~)grKJlsxlnYAhz3;om2}X& zz;4NlWzAG;U|Qp4=@2exIXhB!490vd#AuQoHtt=G#2R^vklsi1Jc&hR~pJa(+hJSg|iQzY{*nsU;s7UMe2X=xxYN0>VXgne<;;Y3{^A5mEeE)}U6NZ`W+C$lC<`YU$STxqvR$2UehVc{=|Kk9DBrig3o*nkRv3kKEK zo;j!Mz`!5P!+G#0<_ofjwSgN+F+7LJ*z4bRKsH+nLd5#(iL$ockI-VJ){!d-@g#zE z&?kXX;FKJdUL1Qix-&8C^K#QA6Xq7lsNXvYHp)o~)y}h8t^^E(-zE18OuJE$u#1-P z>ITn?S|H17a7mSx8ZU_rrqzDZv>Nar-HW*6ReB}d!ZK(l%A`?tf8yo+5+Li{cjf=; zFc2|Ehs#-z=U@dlCYvFfdu7tC)SL~8h)x3wOMTVX&Jz34aAdrQnrgaWOWKYnW47_h zPm-{iq}vx`Ah-v)uea;VAlb6Ty5todt+6wvjzsLhNz4%7eV4;;_Xj(!+wHF_@9{A` zA3%`EhpEfiT7@xBoT1ncsIx_l6}1;M)mj+up_9(7hNl~t-9!B#-=Y7&FDtf^BP|IR zJ{@urq0@|74Dov%v}*REpAbE^oVEOFq)b(`?z6w9po}CYP6f(tcsef+4>{g`yaoI* za^w58pJYi;0Zr)3o?PJWiDc@6aTtN{Ln=PpRoi*K{(3;HswGq8Bz-Kp!wvW)$-63b zxTa4$uP4oY*9-2hls)riD31@dwh=_7_|9@U^7Atr%FZ+H%P$GWnsn0)B2lqlKlN9` zTgICu0#izTYj~B{dVP&^oVK~?OCpXBu#((SbGcd2)JP=tJIQ5Fte!7An?$~ur=Ma^ zNt;>4@+F~SX!JbWAH{~?E#c^S@KF=l7^zguM_hg+#o__SACELxgi|~ zjs`9#Rjk?7BKm!gYDWKk9^_kQ!*L%hC~63V#1jLJeMefU;}4$SR8!FQ%K;Ukw4hw7mG-9b@<|XX-?BFg2LL9R8&KEf#=rr{wkcp zMz%WH8)@WwSM&tODKz)CLacU3n8!B}aiQV+?6tK z_Qrec=5ZBmksEnc&ioRGj%P=HU)%?lP7e9&=Np@WI#tqvE>f~U0%vQ}ITwqj^Q&8e zgLZhUb40Q_8oj_vl{E5LA-W8_o-}k*eZJ$%K+KyT@xx>>)+@CYE0?RD=7>L|s8nB$ zSOU0DT^)GspKk5S_u~{4f;>{yiK`hM7gHwjPBjjMa6_5O?9XX?y*lM4`8w`yV}Kk! zVCe`u?arP>qbsMrP6JPP!8VUv>0*Ktusm1O$!p2FbZwiOE@bWOyhP=^1*d}ggLF|K zGAM~-vnFmXAWl>az@1Dsp?2AZ!r-*_Ax(2Qj^tp6WeUh^TyT-)@g9k>=b`MDl?s3M zovr|_{M`LH!=Fw*tzSL|F78mee>FX>!$%$3EbZD{S-5A>)J^mpe0B^t7RpMUGx8;= zq{M={Z_QIleKyb@FmVzvIqb-an2po~X?Su?Pe@XEki&i?uUka$kDx*=+9cq1?J%c* zI#%|2;`Ljf*x>LFUMIY-uuKHC2}U^El$-Ka`^-HGEZm*LWCaKGHK!(_nZ@LeuW(Gu za$-$ljT!HC&@<7R;nA*Q4(xlRnjf$ZJiaBH>2d)(`v0t7+$_ky`;4U6@&1inX3v{Ue zRJ7KTzu7M2f?1niXO4-dUkiAMN?RXD<5{*f#XpyQ>Wr^#JTB2xU5X+O*f3w%l>N$F z-m+5$^%a)O$A;BMUBsyLL+)9OOI*j|%GB|#xwCJ+*=s~4c{{nAKhsSV2;GWUwS!z? zY&+) z(>Y-i0A-ujeT;^y*rXX8_dLWL-5It(`H*LaTUhcm$*+Mg%NLbX%a)(<$l0B>VR+v> zwq=QsDhuoZ;!fXsc624_R0V$K6gTZI0;~BX2iY8XTN7u8FWkw==hGKj|7}uP&cMM^ zw#?{8&%%79d~b&^uNb_lkp48!QZRF;`o*Hl zUMBDu5#;jWtj3%!UNW}HV%^aT65H*PH9&%%yF7U7>17X}@?c@;Gf&2;zGtIyq7)s{ zl(-)|RXUU3F=6Ds%c&=(9}y5`Nf=SSlXbCioK z8UB023A>V(YRyS}cPmmsDu@7>2ZOcBxIQ^ z{7QR)R~e#K*e9&$no*uo#edU6F^Kr&Mb)?Np)!-XK~?#^%tBB-mY?-=6T#Nipgl7I z(5p&TC{eN3QWZ9Aj`f#58dlm+BrjLF*6R-I)0cBLT|8-~B>a8fe>cl9Xsruos3=7Q}lqpGs28Wwm?21{N%bvaGOZRVkY?=8DVp<{Puk? z2qJ7{sl+Ui1E&L3gydQN%#UrY>j8B3d~FM}%J@=qn|d|UdBTOJ6?-yN1e$XZ966Ux z?Z5;=LD3J^=gUBH^&H@zJw5diK)afQ1RY4}YwdFuPoo|C`Qh3XoNt7U0t|d@L`ybg zHAEzdWCw1)FK#4Ldlae_RS%hL`fkwj&N!ra#{z$Bc8gb&X<~TNJM4APli$`gn! zZ@(0{6zhAu+atQvx6#E%WkU_XSbBqo|dHuF82J`>|+-js&Zr37mEFz;d0^t)}j z<9|zzf*=o#Bep|21}p6F_r^6&2S!6FW$VaTC!d2pELuI&c~>J%E-Aj@q_Z1O4XxtEt5-M`-LBhZ=~|M{oF_#}5NU2d*zqBc-JYYHaNjx9t_&TmT9qG5 zsEBD_U#D#ckVd~KJ9ia}~BMD|GvS)<|SjF!)sUX~o$xGDzVJnW<|3wLO_8^XY9 zZ}iDkC&d1ri#AExuVNK5!5<*g@tmVM)@3}3%+tjD%SA*NGb&bdTv6VgeKGa3J!9e$X3Qi@7D{GCVRZ!dFQv{jXV$C_isu1yp=w}OV_XQ4S6 z&vm#Gu882E68unDOutM7sI_%{kN2Nhp-lRb+ODd@h^qL;r5Kpvjjxcla@N- zzx%I~2eAjZq^lS3JkyKLdNK|3q2@p#dDQOH*llxlNWhA!YYd;Bj!}Dyo{03^#l?Xs z#a9ole`g@jyrD8FhMfI6f-xkWbC`qNwN?rLkR>o~)}lmfN9iBl+u^(gmpIMHA%0?{ zmHS4iO{mZzkR2Ta3Nn@MsnB<< zj~;cjh_sz8s*-Dq*f+fu#%!3L=uJgE-3sDuJ~Mp~u3Mg1$i;3m65KhH|C zbzf(&U8aFsDI|stRP|NQW?b;k3wsKe)|)C4nyGV`@Q7Y5mGH6AA?jWXYcbdwoNntj zU?wk2$}uvz5+!ga?F&xc>pLGq%8}+HpophdZslnuBjT%=7sY+%0^`P$jbu>Y=?i4s^VIw2Fbg~`)G#sAz<4caRU{`q24yp zXi$+fRsuh`xI3OF-O38BEoiGdgqSZ98h+BYO^G2?_v8rDfSNB4rS`s31;Je^G4dI; z9sBrTlY0_FFtvPs(@rm9`i$Hx8+quSBBcozAS zDes&<$Ys#^R!?ya=N}b^fQ^zdRyAN`1Hy;ovor*P6p-N?m1YmB5gtCTS_ubQ zTnMPr3CvWa7-3l$!5Zm18-2*v)SDCE4m16Ah=2upmLK|(SKkD&kj5S8dHgP2Q(K~k z6G5HjO3iq}PiZ~hdh*Bc$lkiOh=;GH=JA}O3Mc^sUKN7!;oL$oie2dWE5I!vGNYNIo8{f z4tgmWnHPtt&iOJ|N|MR^TDJumlPra@g1Qy@AX1t(*|Eafe1+P1nerQRZS}Tum)GN* z8ffo))ei+#$&{3h=}NB>OdrClC&4c`v^0D%TF9W=7fKJ>Jjg!vsRW<-0E%CR0LyTR z|K}FmztIBzJG#LC>)#_9;5v_1p04v?PPpFuL9U{tq1k zhWC?%_s^|x@&3fR!%yF}sn$D)H@LSRM(v8v{LVhr-Q&K)2m;AGbxA(J>-`GPn6AGJ zoqI)E8IskLzK>aafXTLs<&ytHsqHwE2FTO&`MqLrCmT3L3}6iklK0t+WD$ghRa>V= zB?`Qj-OjK?2uMR}H-AM0J;4S6A`OJG870JW=P~hKbb6qQC`jcF!Vw^g>IP<5Lk`mkxk~SU|y1;wzg+l z=(Nx|ohAc}&)9JRv~nVRNT6S9(1t1oUipKCHo#8)=ZEqygTguKVHlZL`JpLbdd$8&w3LVzcHdwUKEpyWdnHok)l8a&9m8PyLv)MOyw9JI^2AK8h>~Lsp)B8%P{Qgk=7nU)NOL>=d#^r;Raex_i0CbSD58bXBrFL1 zd{7#1#yKt*v3Kbx-GMrv-)qcc}$L2iG%YjI8SDk+Go7CQ;5XQiKjj~;=NSzR2=k` zw9!?U8e4zP;96Pr=UUh8bY=PQDcd@2J3%XU-?B;qzbTZ=30~X8eDAnf zbv}?QAUVFZd`7+PZk6ZWt|{(Mh8q#mb#+IJ|9U(Dy`1R~D-A5W^t2zNY4SF(u}`o! z^uwZ7%J7S8^r`FMkAt|ua~oGB{kB_OcNt>_`gmAaSAp<#GLM=74Jl7UNU*Pdp=;oT zZG>gdR|29@w?WR()Pk|iIeI^)Jv|G1Z>Lmr>ZsBy1E3raaR5632p8r4P&r|H2B&$_ zS1FcWgq~eH8d~3$GM{w);M$LXzz(9CpYA4pXv)I@sVIKw@?ymBtnr%M&M?+#0w;x= zHEZ~aq%t)CK_yNw(|fB_-y?GprvZwBO|nJtq{p~_UEJd(2Xfzlc}ihsNzBQ;l)DD+ zJ-0wEGL>G@ZSY|i+UMv2^VLtXpFj3H|O2o+kt32+|n;`UoK@VVi2 zrNc^?F&V!M29x>LCkARc>!3i{G^I+*l=G@^usw69yhwHduOtO$$#+zE!JGHd$7Ck6 zD=rPKc|`2?>2fjz`7bd#cRU(+uJss#Z@dPY;2#UC?9I^;pdAnCdLvMAoS6Gd{Vk!F zS>yDlrI9C}XeJ?U6B@VRkooL)Or>HHAW5#Q%oc-73O9NS=aFfU}JKFQB9 z%|LKg%>DlP-etEZs{T~#ONS8}kw+xq4SmA9P@-gc7qzO5;r2TotYkL(U{KHZcj zGjh6Z4uFJlM(bG+n5s5BkIWBEUN#PU!6c;cIMCy{k_}3G!XkqFRJ$ccE!iGYd zM2!OhrB{66^QENl+^15<8T*7yE>AoAh~#LZDcWt~dDM?|Wk%%0bUxgfzd@h7>rtY% z{s?&)je0+rHFw3}3$|&TA9`ZcWdUz3I zuJs2$CYmKBHXSsc=pX0DlQMhROh&*(M4};tNJ`azuXI- zI|M$9dF1D-bWeb8Q-tt-6?;K|7{Zd{wGoE4Jtu^Z6z)xyYkz~di(1%`OufnMmDc$B zy?g z+|*$c^s-4SAW)eO7SfFmOowWTZpFu%eOYD(Qgd$HJe2vZQh|0Kk*A$;hvd(M*KNIs zw2xtdRd|=MSRYlXI}mo--1><40q6)UrSSQhqswwCjqBvs!4{rZ{nXflMsTU@j|NrE zkzg5tJn(3inY{$4Xlp#J&46;i84&7G<5o50*fu{=Onx7W{ZKyRH`pGf@7y_jQEiku zZeF%Tgcsec_?hfl1y~qT+PLw0YCo#8BQUMCO3ZFl??wWJ8?-s|?T_SP0>MrKwU&Ch za-o6KJx{hwpVB&>yOHkKr(j@Op2yr|rZ50AzAW_d*MrH>W~U#9ilZM+xjxptQ8g4V z_IG=i%EA{`ueI(N+tj5B<81GAFO_6qqDA z&jhAmZmTIW4T*S>PSq2k4BPDZU!Qx)<$>?vK1cPEk^ zu{pa^(j3>l2r_fl0$=Dbt%dL%y8@6RxQwAlIq~ka+=5_iEqfjoOVzuQc5W41q_qLh z*+kC}{2zm`CH1l*T-q!SU9He#NR)$NpGsOkEBOmC=Gd6Q?!@JA8D`3Qgut2X6bTyE zwcZ*yiAjHHa)qSui|=xgS&^(O|RC0yL&@&m^!FObuKbf80 z`Y}7?DkUfMcMX)cpl!oQqv8d7HTqqdqHo<&>*^)HmwRyrDBy71w6Z5+btD9t=E5wW z{D76Ai0Q6hx9m9r{ zVHyi);jPiKXL*1Ub%Mhwy|5y8$8cQj`m%KM95saPi&CM zR-;nHSi9%)1cIVrDjul(0w*;(joUhp;R70PywW{xLJUNoBJBY_cb(!|T@1v{&eaZb z$p;jMRIu)dO0I26j>_qpwFy;oNt^`6dsOR}!AAtDPy$0MC}P@)>239wWKc-%d%-4S z3-9t`wBcQ6q==DxTMt%2vOPCMN`KJ;^Sh?Ig0yQp54bUN<)j7Y((miJ%$i?m{PuUW<5>aN>o`(zwp~PiaZJsg({xMNGo$zfN7s zKfwyf{Hd{bbr6k6sWbSt^$vK5s8s;HI4A0Ly8BbS&`|#JEnTgVVhw!#h~|eAOrDBR z$U)@*5Rf+2JDwVeiMcTIYp1hlo3XYXw_63M8mvzD?VIE~z?MZCjJGiq$IgNUj^mU` zu}aV6+poC52EQP9(3#@$YIb_ou9oQaR}b8qxHEi-UXLqJ4DJu0jGq2*g?R8nwC%d zd)-_T76|A3Yx(=lUWi&`w167{SOA!;!Yx?!!>=Q#iEjK(8_5*X9N+`Ri-u{1&Db}X z?N74D*W|vMjPNgbE?jzgZ>HPV!=vMg zcwTj6!%HW28xUt1G0JhQ?>)^WG#@?ul^E`s!wv^Yuz)r7=kV@gFRJH(n*7Yi&*>hc z4fgVw#%s04;qgIV*z9ul2cuBpuTXMNZuaE@wy~L5xCs@L9QNd1kFIhg#S=Fv0qdFm z9dxM}D(f*4kxNeGo4 z2+pnM_&^@pn#(-DsItRQEq2pO5N2mVURZ+S%t~DI-V9k1xLPj<=q^FOmOBE%!m#~1 zYI%!H*B4o1DL}%y)^hhE zC&4)JI>NxyQE%hg=Y0#|Ps;>#$9h+jlvSlBjrCL1hqE$cNY(%y)L+cb4Cx&L z`kSOIdJ{b^ki=5pW>($4>=xPNG5`=}9c6sev(&28R0Eqfkw%bIK1F>3XfweQ5wU9| zv*ECYxil8Q*A8ROAU%QhvNaS=9}O`mTYyewMqd6RgP>o15(0*#Zrsl!cqA5?-%iBm zHm=@UPS__2b$M8`l`Z`@NW~S(qSx|D`QYHRu_>B3ax`kJgR#$NnMc7-%qqY-&thdR zwA{XM4U0GuX7C}0cgK8uSj!)_{CM`SeS$Wo>PVPZq4?HDs#!bR#_l?c4HZWOdBh=`rv-_l{ z-)Hpjc<9qtq3CInCHq5MQ^=9-l9t3fB3u+up92aYz2eO(ISQY3v$_xKxNj(gS>UzIUuT zEf^dtZT%8jW6=r0WA5aPw1uYm_i4vI+k?Z0ov4Vz2xl0&W=^B+KyQs^uFLki5LMW3 zO)=qXgw1;AH2Z_tuNP3BDTiv$_tEomDxcxzk5}fsnC+kR#gc32y#om!K+TJ*>28{$ zo>%i=w?@7$%MT+Ihhlj2`lu{14-&ZT4`9(?BS9aeV43)jbpgMH^1f=dLA5EYkuhmP7;@IxpAHv{e|NQEogw#?Bd_keBls14>1Hn{ ze~Yw8A|xg)wTNO2yC+v*Bc(V6M4f;tvD(TEvpqT4O~1qzi6iq@WTb}(Nvp>N2a+ua zmnw3Ru8jI`gAx~>vdTA$b5NxqRg(!s^{N=9$OdN?r|1i>=DHP5bR%$L^5TIUmZ~sc z`RM|4wH%Y~;-KS(9#x`^En|1`FX<{#G}qc{X;qMpw7zBI3_>h~_C+Niuh)ojBYb4b zd{#Ru40NbKH+5*Sa6Og}urm@_p0|Fw;S~LNy=_9>Q=YgM^FJN}JO|GWWd-18;w=pi zvrI?(SsF_tiQz>^t($|~g-7uj)cBnDW{oyDbieCWyzRNW@rEf9h{i6AV_iYw5(7AT zLp%tfsDXNFMw;_?rK7m-mfp^-XsNi;yhNolLlsCf-d5~4Z}Pd*n7j!{?grRDr??)* zB-1_F8jX@Vv_=Yx)n)I17T6<%8}jH4UBO7CBqYc`Ep34zvt2;EH(l2+i*S;O4f)rhntmc`&YFBH*;f-CxG zHVmDGpSB71rpXETw}o~)UVfZI3TF6_o(0>5@M&C%WfMf@PJS(+*<%<3sZEbKF{GST|Rb5HP{5b1_Ye#_& zwAm>ePSzo=aya5k+mdjj7Awl`ngT2GLo})Z4MDPwGcs4>!%07n_5gna7|L~dn|$3c zD1Kv0WkSHjD5hC3OFh4uorPuo@`rJA4dxt#F~NG1tEbFo-Ua3yaV(Hr4W9G9&r$jt zaSuFr`be#%AO-y7R}+b5d`oJLG`SgK z)fuf7ELJko-8E~XOKbGRD52ia=jT*nS#!#A<|EkK?xM&Dz28z|!1Pt#0+SpH@o9V6 zWN+#8{r9*Z6$}D#;-+FRs8L0mrch}L3Y5KK&T@lC-Y8IKk6G&xCp|2Vs9e|!Tz~u6 zxwO;KzAF&2It4H63jaB4?~^*%r|&}VkxHdq5wQHrAN36UwVVQ_$J-^GaXAP^Ll>0M zFo|bhgSbid9rx1bfynZ1lUn@sPGKl`nE=XNO9#t<<1&cR8)95faR9rKes*dc4EL5b z^-#9y9#=Q}3KBpg(l(*~FTIpHp4B<$MRXRnHQm$WY&=ed?BixuhDr7n_-%>Nz7udpC=dv*WYb z^4k5TvaiQ}F6@4IcFG_oI$9g-9<0pdu7rHG0MGivlsUIIXqmrVN<$rx*9ZqCTtV4L zccLW~eO&Om#*7oU{$0DGh#zgk6rMyt3pp4OvxN$Jk*LctIeN4qZ;EM#mS0tbqp6qS z>g^bTmI4?6)p;n9@l2#{6-W@sI8No(Y9ql!Ayn3r?Cs8AR~ zUl?ihX3q~izkJ#0Hu2az4khGprfMe@L(0`tN8NI?f;FIdDA%hbIRmdo^M)EHcY{Km z=zn_FXR5QMeeYFdXyV)*6zPFyiz7O%lcB{!++Drr4C4i=b7|vdDG=Njehls>FCMJI z?VQCltHW!THVKhf8iCG}J;uK5tcE3l!4tC$!CJ!Iv{nGXMtPhZRHz9bkOI*k3*IO< zjAVA)XwinNoaG6h+sj2m-uvlL5BF@-Z2^C-oDW~g+w}57;w?K*^!8}W{O_Y7QIXXI zt9>|ZMJ0!mLTW-ikat(X(_stadshD#S9Kn=waMm>XkyQA2)TwQcYa2w2?o5fSW#s_ zK^e&rdc2-KZ%84?@X=5^JzZ&Xc08_Kg*wX6>)e04>P0XKx{*}v7YGo|q z_NVZBe{q9_l^C`RQbEOe1GsUv_&~z&Ne2cR5sBxc$v@c{Wsr0K*+@O%#^x!1FSr4C zgA*fyx(dGJcyFZTD`lCkn;`$e(-uMMxozT-WyCbf+#{mYbR)Sf#;EJ@`>u8j63T!7 zcwuF+Z9)tYPBitD<6W5Si?fIzkaK_ghK!NxT8YO9+SamO;MnH4#3U!wC5vU5TWkpcHoIpr$9n#af`5XgpTh1l&feq9Ok zB{v?wqA{zB{N#DD)8#hC%C@<#ZrnoBW#9Vv*{s{y+zlTPTjU1iNbfK6Leo8skIs&C zjfKKhC%z`eAQnYENoadOrz}8dlhpj0j%jL!0;q-^7qi~3>F6UyR*jqREc*jJ%)U;WXqMZSqb~D8Ru%>;oax@Aukf|;RLa{ z4X}MlC5r;K!R#8wt3umG72UwCoQo35s_Y|UynbPC(Pl5Sg^Np%+Y72Lcf#ItI3xiV z35?%5yIxm!X1l{tekq(t)hKItDfoq-VAs-!5J~0IMPP9DaTzxO!I-*Zs`bV`#@iDH z(S)@T?3blBixdL~W9pXRH7Kixil)3ge~1^hupQ4tCer*&3#`Nv)yjcPFKq8S_8T*$ z#H8=^3d?bp-y6=g;$uYJGOlW1Nk4CsRZO;&n4K3m8@0c*MrCai3U$+!;5U#FBJb&x z40Ot~);(HdL>h4=edB^%?3O6&gW38Lh54mRc#t^#py8_8pEgM9EI}JUI;~HM!{f4Q z-exiIXt6qi2{*&Ag@}2-iP{=SAedxlf{_w6uZL~ankd8q5<;-;S3<94vl;gOezT#J z$y3G*nKSmV*|aY=!dw6UB(@>^qqY6DwE9>6<3Ey|-*gjkY5rd`q%i*f#zREDpe-@> zq9gqG6aR+@2|WLIJkh_+kN#2NwhpuZ-4gtNVE)R1cnN=DT>ti&_y@L|WAJ_Z*O$x& zi52R}=bySj!qrSQlKG&6|6vk-&6YjJuQ9-fwXmWB#MSIQEK4WyU8KGUFA*Y;u^I1#?7<(ZC#Su9o=mNWz}-Z& zrVM^i^jSG*KLWn~7+)^?|HR>6mq7PlW=j98lrJ}5aoFJ@YOd9J9o}Dp|MwOD^HAy^ z%MZ$bJ1PBZabPe&}AK8NQc2uUR#x`ZN77(Q1k94JJDf83mIuD9grSUuG z6Xs%aqsj1Hj{CkX!~PtgjV9qa3SV~7_r{kz zmhQO%gZ?`BFYa?6G4;NUT2QS?QoYXr30Os}B4cdddGYH}7L0 zS-?$5z78%CM>{XK5N+F+!Ch+^g_tp&3q3IH@+t(5a1AtsrzoH1W~ z|NE0Qnw`mBj%8jq;Wn#!u;;dz4he&bYcGuJ?~nS^N?xTlQ_T!T2%1wjPhO`I0->AS zFN;;~x08>&RPHN_i)46?*%pKrjypUYaC-laA6=QT4&8n-NCF4p6cmKL--0jpF zht5f}!|oC$P0!l4eXQIp`0eCJ^GzW<$z_m9A-V>IZl}#!QZx|>DR&2Vl@&F)53pi? zRlCMVhOzP#h_xow&`h=y562Ujxru_YC~pw*D(v!9pygtIDqJIqxRkyrQPA9aof;@n zq;i99*XA2+H2@s7+C)uET5gWb?!M_`4y<4*jPa?kUqtVzJb68{1!eDA$I`F*j|!d{ zw)r@9UE_?hxY_QWlid`M@peFL*KuI5N?yY{%^HEl!6*1XwzT3@H8bP$GpW_AAD?lFk$T@t5w|L(x$1L?k!ycb9pEgrW zx;-*NZVj1!&`M8?VkFJ1cYfI(i^+Sp*t(t6%4i^d6z1HgaFB|!2CuCur19jbi-weC z%|cXza2cy|4~K+i3px}n%OB;>5WA%$k)Jr9r!#g#k-TwZ&%D#DVgqgEOZ*3%yCtWmxO8N{JJfvYR!z?FE7oU?VzI5FPOthcsvEA>uIsx< ziq(rRZoo=9wrPq%(w<}D7l5J`kt`{sCDW4md7^4R4P{XCvvs`hy!`XuMvB`%-M*W-;#@jp&~!+!Bc9Wgj8;gbweL{|(4V(F0DJAbhM zh-_^yzTvt&67WlaI#Ujww6G9EdTPVDQq)CWLt$BCayM;i6L6{z+_?Q2i|$$c7m!}Hw*sf_Y}y-K;d zA8kHtj?|Y>*PxOA{K8v1E$r1%AT)bD)oNV%dkpK(-U{|{jGVTM)9k}329>u5Y)@gl z-5oL33i-@}9-EQ&-mOU?*&)qOurS+_$JlwSy`HZP)~}_jwRsh$5aO~4M(6-aclMpd z@j)+!Nx?{AUY@MI9>w8~siIu%_8be~3^(F)g(FSHU(0V$C?o3-8YHgLz&i6xhaS>v z^PsEmYYZbeyqi=7J?W5aZXt3NpvzppnI7N!-#yYkD5(uSOA=|78KsKVM0HbTTu>m4 zvJp&dQkPx{RAET9A2EeUIw@HfwhCVOb*$+}>f{MOLLkxkhvD&REV8d$d8q7KocSfw zzFZ2#zKVgDu8xtaPP0!D})U!b0(28o?nJa!M-E#emwqo3}}$&EQtD3{*_h z&bh6As(+_DPHOiZ8-H$Sh(^A)--iQ$ya&(R4v~S90orw<3w~087wkcsq53@xOAv(X zHV0WsP&cnnz_kkp$gg|8?ig4&ihZlw3*J8WjMX70x#tOy+i(iJO@gXED3gAJ-vsf) z25@Jdx}&%mNf|J3PS@K$2e^l>DDED4#g?~bNB&CCixHA=Sp90O^| z16WS0gV7mTbI}Fgobm<2rj>N-TgcU*5n?MO>-Q)DDfUB0w@AcqtZ`09ydSx&X-!&w z+(=bLKQCN6b9Zw<{>zeZMk z?Q%{k|Eu_&9Ozg;@O7Cl95ir}S2P}AR zDoJcTIh}=Arbtr2?1y^|>O3QMB2*`mn+cx5{ShR}rMtFn^rP)kWZolk)F)4ZhUVM` zouSDD!Th3Q)`S{Pn4^bUQp53g5!*3;2kflpJA-bJjzZ4wZHlI{Nrx5fa;L+2#RW>< zQ?9HqTrHdWV>q#IdLIw%@FC zP-#xOIIyL>M?u+%ErH_y)Gi4aqY$OcMis%acTM7h$}PJyaG{0~$(Z;Ru9ku3^xY`= za)kZ;Y`-4DA~mP^Q{w`etY(B3B`3#!2pgkYV4Y@=?IRcBmI|4J@;pW1aQYysd-yl< zs?e8UdsC!FE3>9t92VH8)Y}UgyO(=q-S6K9_OzOn3K2A{h^XzE4a|w;zmL+E>w_BCqv$( zB~U)rPZ{fBzQ*oI*SZ_)V$X1tHDQymU~?wWcw~LuDjamvF*1MZo~nV2a>; z6*-k!WC*PU9Wiygw|RO-@v*#JKy7dkKIKN;1&XAogXjWscaLvO8|sxRkH@PnQlAK9M{&JO`V!r36r9XF^8HRD2ggHVs^T>6uZzHAR7B=bQe_?yt7Sj=kqxHAWdh`2 zEmYF@c%2px7}n3FR;zw3fx>SxWQq19EAk{ybBe9Jv{R+5&gQ|p}g8A*!R|W7-{_(K62gP+UYEe7^>bM^B|I+JMAq< zolysm&_B%t2 n=a6${dh}Dcm@u-zu9GtRj*-G>@|zn??zcl%m9h=aK+IE2 zFPTN4d}>;A@Xf{ez+k!=VR!i+E;;jiV9KB$cG1u7tt17}Rt^%;otQn@)MQ56n6di; zmcn%?^D{gG4#w6@F;HANbR;u8&T3x=1v#~fF4V4>90d9#@91;2al)ReE6rhA%@m-2 zxJpBM{IU8FNJeV+D#4oFVTIEnE@TB4BM7gbowpwb|Bf9sw1DnRqws zE_0&tTxgbjLGzMo*K()1&P3rE1hH`IJ-;{R3AZ*_ zMfSoQSxMhRd`7Fx?%ROnsXYJlUI$h|w>dEqbFyPV{J-<=21rCo=CeHDWl1RzilHOX`RAwUak-{C@z`Jy=oFd{Q<5_>n z!az*6OuE8Umzd-+_eI|DNRBcV=3L|ceM$G_^6KB%cmmI4YxJ&jfOs^zhPcNPUR}37 zRx`bylmP%y*3p>H*SuQXLWl&E=Wf4&WvTbN#6YVUgB*j2Z=FMit67q(1gcQeIlHxSVd3amp83B-x2nZQ%Xd`v}h7IWTaN+Kd*akoh1R zk6Vw#H-{7f=vtP{bp3q>OZNO>du_6)+tqwUuc4S51l2%t4;U@{lYCj~gQKv1!Fj;YmfkS-#Veazz2*P)zdt3<4=VSz^B0&BzmeDKN4ln7V+$YHT*(p2bc73#YIez(gfy}UL582^#wF_L#somEltV0iD zX3j9M2~-{c5c~3o2KYVL^9VD&=9d+t${%1_?q>jb`M$!9j7#N{yX zf(PRYIup(-6*3PKZ@`+y3Q#5u-DMRIRG0(B@lsr+>SNfCvt%j>7n09( zB!xy?CvcE=;&OqFI?Q2!?Eu|}?6$cYj;|-afUm~ZWXr;+&hAj5j*1%sDe!H^mN~|W zI}|<3c%~&~mY-bMH!Puu-xSd0N5%GB#vE#UooGp=mm(_*NhVl`&OwM}Nl(iznc8*a zR`OakHh`Tk`i5Jv4LduMUI76_c)F-UfxBMp?{F(tm%YrRHL<>5KL~Kin_doC}&W*p9*S^cEZV zO7T1R>>^ETH1`jy72r@1-@B`w2f?=?c<`nw_k?X)wX^HY7=FwHtWoh;KH3$!O+VPy zj(dNRU3y*3CSj)t9JbPFMYLBY_mDZb;d|j^LA|WU+($74h0!`xqJFk~^2FJ43jaF{I(ThS`YudPCV0Hn(C#o$&S z7_*ep{c@>_*%0-+*kzvDX)Xs!8E4{_LHcq~HenI&yT!H$Z`GcZk*Z-GJSx{h9S`rz zHRzX2G-+-X2wUHM3KB>!0ZRgd6E09XdHtH#AE?zDj~DkOrdtg1p9 z2yKVkRV3C*O?t@~;5puW$6n@IEdA_SoeC0}ylOR}mW%jj#k(@hmC$0%qlg+K4D`<9 zxK*n7FH>`%hKWne%7lo)5-nDTL^&ToY>F6^)cvE4?ezUKG;@j}<(;~!;Hxpe7)@y0 zMqfoXF-y98BAm*hJRLG7^bp@Zt8L`p6YY-(zsx$=G&Q0nDPwuAJ|bIT9V5?8-f67A z@f^=^;UK}28R1Gt0g}1Ri4abk<8e}VL3&)B$aRZ-cIdvMm*hN*TG%2Gw1 zq+Q2y3Je+NO)NjyZOVY{wGq+p+f^lNitu4bFJ1-SI3M7RnToZ8KH8pn`7VyI@Y3UO z12b}S7TF+;cT6jZ1G;o-oNJ)kDYL3`ikXm1rS^(Z3+?1H@y2HzC@+d&g2_`ZXX(k^ zB=R0&_u=d7F@8}i>C5!C$RC0yD6pE=g#5-=t@BIsR!R~*gC$I+i!^m_@bu1!iBOi|r)fKqs3xvw;{wKCI0e{&F{X37~ zU#b8xOD|xj|KEI&`%`die`uGoqQyHWKQb^z6Cj%->hZv{1pWU|2hiqk8Ck%*e-aA* zRpyt+d(@euxNw!JdgKIeQEM{U+lj_Vb zULgnV;}d|eSA{53oIS6IZd5wJ&S@<>fe|+Yp>pNXyhfMmX)MmVe9QbI$E(;`SkV5E zPf{lDzgka5@Gm4|PwUU9kxi4lNl{aQW9%(WiVw%gj0bu7MbV;Lw@6A3h?C=_q1GT% z1=rA0pBFx3-i$w$7vw-rb3+SI<|OMW?9r6L+uP=b7UG6etD=2hC~!kejt)4uuHBS< z1XEq{gctG^Y?>k6&P`8GSrt36`te04N7-NTm4~KX>erhfbkC|`47ykxkhcHS2bq0w zsobrFFsvGRr*yaz_tZ8OJt*0JfL9>OLfd+&Thf2+^K0v?C6B}Puv*YDc;KAyICfpA z|KO&u6K^6v=1-|$7tnpKyU+>a^wOXUb`??IF7=M>0lIC1dHo3mi6dY}H{K7;R5V?U#Ak}g$f*}H6g0An|CWhh(l z1SA^pZS=Dvz%x4!^w9GWNHD*s&hWS$^5@jYlwyp7gN!i#-MKUesd88f*)X}D41Eu0 zt@Dng>`QYYOa4iUO62oCxkcrg#ua~DhLk1X!bTk-F-MgJKDqa@YGw4T8QM2bCwVM; z3Q@h9IDIJtbbR0zsHKzeC9iG7MwH44%Lqxi>&#Hk&pCS+w~#o3%_xFjK#GJHewfV4?H z$rKrVg!crS2GYe>yZ#$Z+_)3p-@V||EMSno@F*KkUjYRY<3G&?4E$F<#SH$xz19D& zl!w$MC=LMCPutzfZQuv$ zh#kHbR)8iMsafrxVNIn~Azth&07@2+I@ZzPN&27bqL_Y)%*+c>P1<&_l%K-iq{P?6 zaQqIt>TE``WXXhl#hME2iJwlfqEiO;z?58C8?AsI`U#MUfwXlN$snq+ZTz2+Rq6`S z6X-kEp<-jNrcdd*?~g?B-17j7PJf=Zr!ZK@R4)$OG_w1p?j27pD=NHC&^>m$xmN@I z8q@sXz8$?Sep*)1_JlKU)dWiYRsmI zc#s1eA7cKXrt(eQjtN*DfI;$I#ZfTpiFx^B-Cn8K3@IALL&B`M|urU<1cX(*C zY?V9qioqzx@LjeO@0#w*4@clHW9t!$G=H4e_{Q-U`6EpoDFE);Ps8Y4 ztMq=!S@LZ<(~wD3qgy#E)}*Ai6QlXY@s8Nu9%tRZ_kPihP3V0^tF)yJOFEvukJ#`Z zb}M245PFDgXKuUGY(QLT=+E-H2zibXg7ux*$@O||oH9*QbVA2$oXq@rFOXiu?Js(f z^DHg^af%qye_;hNsE`8Q@g)*>k|`8iX~aA=r0>+1Mv^M6&_3Q=WL_l?C39z^Scf5YKJZ@HD#bJpZ})a2ZFLDn3H zY$)*>L$p$hCwmG6N@;uM&p-I1o>joQ#G zJ-Va^CMRloGo~K*>C(O8$R3Y$1Jt>u`wX1?PLkIv->0Gd#)%4GSg-&J%6?q?tPuAb zvs19GJBLqV$96;MqA9i6Y(IB6FCyi#ZnsAj_PLH+{@@o=(6w78t9dRuk&9;2DHqmW zc5|7sPCNe6nq{4rKa5E$$HDj1=`l z@CJ%^_68vpST3OoLrTF|9kzSlOilZb+^9X4i)6{g4shyd|J#tKWe}nO z*4%pharXok0$$bd@T{~#*4e5kqmfl7;j5_s*E%+G*PS6Y^j43)z>;xp;q}A?5i1?2 zIirwKoXq7kS*7XNs`!IwTxm;L9m13IW?LmBQ+V;2(jAl03O5~F!^;}$ANV5%L^GHq z^KUO!lN9o_&*#DDysrG5Xt4Q3{g?|p_wJEbp1!9+uhOJI zcaUV42Ib{#zcNJhx)mZ}Hb!3ss#e36R^nwIOx$za!qPeC@e92{k0wfnn9cfyue&dZ z>A!VcKlpSq++ao0EdpOtI#Rz9sp|_sw-{gjV3N;Wg}k;Rht1L<^De*d=xSmv=tmUr zdnr?G{#X}^tRb+Q&k3^APy^{+TjLhCDLOU~3o#%!ixIPbG3ijXM>TyrkA;uYFNHaz z$-DIPSrULO1-s+E&N|0oJr^f(qcpsUE!}?RgjrXDQUyOjPbz#g-i1+f`8Bo06FJ?d zaeXcvw-aGF35dd=GrJh>DAd>;a$@+yQlJP&$0eSJEqW6Yt{-WDU%l<)XDoD~pge=$`CD zF*P@FEFxo(cYw~F#C7nU*T-W4I0tBqN{LCn<5IIe*VUB2zBF}rJ%Avg>TDr(90&ZK z;Yq@m$)`EcMk7CpNjv!4c~6eG7Lg)+BHdKT z2&Pr#J3c^;O4W0aEaks^AptxlIwQYtsqAM`@iie$_TV4qj6x4*O`U%>`qy>mO|n{KAE z-4=ALd{sw+Z`ev&ByyWwlJ{qKyJ}+*@Iv0V0~NUF&LvXG$Y?@IsUxH%f_S(8ujH_h ze^%;)DVICc`^UD!T#5o!YWj3jnCula=QLLVQy>=38*l#IhO=rt0f+YpZ#JhTsu6=C zkCP}ke2;FAY@t>&&9n4T@yU0WKgKa54a$zijPQk^F|1Q2>c;?++B~O+^%6a~BAYlv-oo5sz|U2Y>ta{0X5~uay$7&_O%~W6Y-(rEjeXAY!I%(f z%Exm}#JC?EGJY$H9?B0B_{(tHPNUIU3M&U@yJ<((l}Su{_6^0SCq3#z`0`9qdK}Om zkbbGK?Z=gay9&YceNf%A&7ggYD3hCJgYwYon}CFyKE395FsPrT7d}v0$@4)ZcMLEC z+6VE9LTsoB{I-H|25cwS;i}eBa!r=klX;}YaxRBEAOt4!vkCPwmceSTGBi`g^Mfn6 zpEfG7-skIGP&FMbApVDskEkt$?qRVqU5U1DCVQx*a9vKIws&2P1#f?$!(R|JemrhI zDEgZ2k3S-r+kQGL6A17nh=MtC6Nio z0&>&x{xvtQSae$G8j8w7T13ApYVzrk@kQ#~$$yxJ^L0~wBD6`U@g2ZqDIY*l_ZyEPqjtnPKz^2;AHMN-u$${FxF{ zeeb_b)^9%YHC!{KYv)$tF#4Kx*(P$+1^zXoK8#{f&9@f_Lr{*7**JdM0ba~`_PqJx{ISQ51@7V;QxAK*f@G&d0d2m3fn zaXStxuje$LHQw({G zw7tJO@>qJYJ$$&@<SZZ(0PLej}j2Bk0!zoZbV$wXMZNVu8)h)5zyt~*9NbCgFW0p#OhrLGgwkJ^e z{vn9oTt-Gmb{oIK(@Af5?QSBML1W&tDS}KxrCPoySSMP;OHIFXiJvw88z79;vARMe z47yp`UIMRQg^xisth_sY9h>``%cl(bR#Snk>hZoB;@j)G*I zWG**g;j5Dw7JHKOJHhoALoD5kEZ#zJxb9cw!PRk2`OK_ ziFg}0>~f1It<3;uN-;o};Lh5tW&E&rjXWHhuB6{pOJ6ox5ZX4>CBwiw(3VgdUdfIo zzZ_Fdzo1v6zinP1->wYBxr6Y%6y~0dkGuvd0fTP$)dsU?S~x@iniCtN7ZYQ9h~~sx z?8@UAv>spNJ-e)hoD>yE_DPw1ZxplnK3?G)U!#n<6?*W(Kk}Pm43^0xwfkJGl(wGF zsH|`w39k_{pS+%$eEG_XjELfJ&Ni>ES#c&rzdV-7YPUo_sxerc8AKa!n=03{AWHJ_ zP51aMKHA<_E%ZTeqUu<Bwe9|9WQ7Nh zzy%i@=hF14LT7Q`cX)eo?;ZMHG_W@K=$%~`kd-iZ(qdveXs>E#WD>Z?OKnmbY$^9n zdsC~}N6kBeK(O5q4;i4PHjY28@O~KfKf?|3bV{Qgz|6C}R(KX{98cV%+D#rZ<8=2a z9*SpigmDqyg1LCIA}Byq`E3qK^P13rZk~fz>rLq6RHnA+dn{;{MEfXlkQTRCszUFn zNUA?~Pz5$pYO4QOB$kW6&}HIVXW9p>ms)q;WU}n=!?tVG@p}{`L{&-Sle_E{iozV% z29Q;lu;W;*iyfgLZ4)*jFcy?3Ijn!rL89S|eG6;H1d6f_SPkF*a{%VA%iI1u>j_hJ zR_ufvv<}Osh2Z11Bp-&Jxv6&vY36k6@s#nO4U>rdkz@yo$>S*2Zvd_ZFP|A(7{1zJ zsl1gb!I^RTOdfqSOd#d~4{o)&%TJrOIZX!!Q^F#xGCNyVIAcdI$n_E5FrGR|uP2Bx zDcb*!v3CygEXdZie`VXYZL7<+ZL`aEmu=g&yNoW|wr$;>nfrbBoHG+~@4p$DYh~n) z*gN)mSH4fEK&Mxly31&}chcE`O3V*1z5`>ufb790qMhB1KM5ymviS3}!XIkdp39ve z6Oix=YGlPX^fLQ)fTG+0J{B#IDkn?v2N$jw&Rfd%4G5j>T{iVP_8dm%T@eFm>rd!$3#EdzTkWYfw z8TM(NM`Mtet7P#=38_BiH0a-i_=5b4YT6t!tLnmS3o-2|+Y{hqtN|AoN?jFOzl>QH zhbCii1vxFlNQVa1V>s10T7-rch!sJoFQcGadbOQAM%R#eFH%qtenyi0QW&|&ZLb;& z$k;K%BZK65_=DD-cnXR%b6{z}KgV{<*ci3aZh!}RB<~uq@>IXz(<{=2$>_IvtW%)w zw@MZW**#LS8QWKPumfppKU$lmrfLC!TGpiwEJPoZWQ^2Xobm%wQXXcmt>h$xJ}U=5 z=3{bfwNGbFF+$85e$Qq2RQl&8;YWZNZz?haGY)YY@dj3kMPzc;?ypeSwa&rt4}m_=b)-h8S3!HfgZF0HEO4Xfm*_D~)|ehCz451|XY^ z4(HoBWA0|MrIei9vB6|iIsPw9Q2d@??E#_sn;jdmj_#NuCQDg;MNR#8kz5}ln z)7OZ1ImCSJa;XpP06yB2;L2BE+|^vUW0~-sgc)}+mK7I`&kK_rl z?HYy<3DDpR)^-BtVQTX+@WNaJBL1whcI41xrrrq6=W@aJB@6AhH9rtTq4}vCMRPYy zhqFcR5z-T3Lah~$4fhld#uUU7uxBXW)T-aiV!e42#tV@l?)cY#U$qavfujg5FeCHR zSPBsR*cKgY6#B8E7@k16!eMHUvH~f^lm@W3^pca+1AK%E2fmaVAVp}Ttz?fnqn3aN ziI_1ChM^h6v#i@zUH5?lT`WGpP$^2-^7n?RVGHvsN_U3MT~lat$0=aE&1v+ZOXZ;P z_+Ow#(X%xr)=wrw!GEPH;UAWA2Tn4y!B0C zc))9Bc|3@KobGFqdF2se07A>t-q8R7ZNGcw10rM zVT)#w7(`!-QVg=)(G<0emr6$*w~&NYuVm0&9U-=Gs{fNi#`0?)WO`zJKnF6TY6t`# zmNJn+1kO4CjT@81y zLHxIJ*`v%PpQe;51zvGvJgQ>5_yr}AfaFeTe+;zQ+l3x*Ujy|qLsR!JF=dr~f}C;F z;9R~ef8n&+@(&-ebm@fWSxzOtO@;f3>?V1}6(7dP5%AoT?8e;a^g)ZaBk;*W^I-_F zIyLB;%!ajxU8gw;NA`Yj*6jl=U!#m{-WY31=HGGSGIXX55x`FnW54!PXj)|Bp&ZxD zZkC5+(tIIAJ^r9K5uht>hKhbS`$fl#$L^0a8N4y5?4hAJl0}M-Ddn<&ZXYN~LnKLU z2QB)yO_S&8pZd(Nm(Qg0KLuWL)Uv^fG35^ap+4UnVk&HDDA#mT>7f+dHJ*{gBqPk<;8TOyQrtNZj}9gdiDZ94_mQK{6o2Dj2b)k>bIB z#m+0-O#i@lc@6Hu<;(Y)_Qfrb<1^GA#1uiV=Z=)fTc`pj3h}~{lmUM5)Utwg?+ks$ zx1GHUTn3s1Pws6KRwP_&`+!2)vQt$jfZ%tJwaVpK zOvynRu*ixBDrwc0?=iY|(s6nvkJoII;vxDYZ6o%Bod@_{$7r*zntl1DnER`Y#m67v zHIQT-PVxRhGfQa)7T0bUX*0ws|HD%oq*Y8Z@FUeXyY1}{e|o5jp=seoCkP!E59U({ zIgcqRk{1InkPXU-yb@m8bV-1bs{;Ia$4KIFI{u3%^6I>41=)zizzRq1Yx){HpgZrm`V$FU3)^Pfzqp<0hKNVlD&ZCVNIzj_+Hj7{1sy9+`SYFdEqqj3q8&mx6!dB z-f3qTn$3N4Uz|~j^$9(hxhwHvQ6vV1e_8G#B|fc7P=TOznrA>BJ6&QF0a^6x(6B;x zdrCI^a)CQZP65|i9JrKYQ}2hP`GjcErzzs13#DP$jWup6EGlT~i(F`_oV4;A<8JI+ z5$M)hyH>RbPUia5mA=USg!-2v<`>*X`U($Fp1pdPfiy<4+K8;&=x)!DXEcpECv zle&(d$*aMCP+}l(|H?iF{~J~NFX`$b=T3+Mzqv6but!vwffIyN>-W@cKx&9_CHWz! z%|FGXBKuMVlJBq*$}*3ho&4o#i%DE~FTJwYI;{VY0`TV_(vthXu(tn{mcWq+VSbW? z9_`7P(0gIF8Ne{X5H8GCda>oj0f5m2Ks5j-9R*?}MtZ(b6YoZD(=UKheD7KZuQ1Ic zzOE?a!u~`ux%{V`;P-8urL0jdEsr=J?UPUqPzBz9OOHA8H}?;qf6{UPnVZBVVgfs< zn1lzFiIo}xSTm0F&=?4pN1h?d-)>+jkPm(1-(r9OKso^cAo_o!vU|gb6oR2@*@E#* zz0S>!sl8 z$=mPCZuA;XrlozwWH;RukE)rIzfk)LyHG(Qa&*W~fO}svI0Jf9RD$yzMheD^KZG7! zY7BoC7=`JE^n6X4`SpcYZ!82Udk;;E8p;sCT+yO=+_NdZ#EWnBi>-2yxZ~#8SpvQ+ zzj+z1OfwAX$kBg7O3NFIp!Ng-NiEX+%A~3v8+3xwjp$_`$+MIIXfNMDv=&pqo{G^P zo?QD>V%dS{NC=BS;*wU^;fW}3;epg^FX4F%RaYTf(%OlTD2*3kSh`WLShVp$-1jBflleyfL#Q9U zeK86aR*-*Ll2v`-CC^7>Hqdsd-)6TJN+c(=&1tot%8<+aYfUUQkv% ztrd;xQ*=13BRQXF;*yx&nrn5C)WY~(<1#%H-0Pd@(Wb^45DzO(JtOiOgbhl;xbjCB zBnVAaserhA5gGB-kH$=&e{`J>ll{gWK&00#RESRW#$)+hEz#S>se73uD4IaqvxO@f zqPR^N9yYj>{z9Dj)boU`3~zccTyXC}21$UIbFe=;rL6%UZ3`fWcryf;6*ZT$06$sN ztYe0re~9c)no2`YeOID~cVg$_`ng(*c#Ph2#&ZT9kSLPkoh$oSqDD_?+-_aNr>Job zHsEoqj^8wg<4t;=j|P`Uco|1;-xU6sXJ3xcQ8Bvoh1I!z3_xqxB3}0xUKQZ8iIOtbOv3%K zz=syK-2N`IWzJe65CH%iYLJ2EFw-cjio}jj1i86Yi$exiI zr@v7Gsnv-yMlW`AKNrA>Atnvg>9rLCn1JJ7dz|dZ`x7uE6beeD_3=AYeitOsI08$v zI=_ZX!Rhoq+-{d;amSxtztQ2zh)GO4bEcw=@fEgkAE?A8Ff#iMpgoD-FuR?WwY##2 zgEctxo<>3Q@ZK`0vVJ8#M}iXl>dwtn%i6N_?=J>YLLT-znG~cX2UQ!3_Sok1Bp;j8 z+$ObPV_0Q|Lx3285`oOV?%+WQ%im2;I`Dx3W;e1U#0ks)h)EOE95Ai^VCER~?J`ZV zME=QpIYWW%z>VxRszOS0Nw)svB#UIy&Z$MsSjaYlX|>0kn!F6n`%6@#=F!r;%hN8k zF><^3j9tGVQ`+! zSa))Zzw6^KsYXeqSldjVJsj!m5GZ&+&aIzhy?86a`u0%-+Y-jYPs1muV^JSez$UeW zvxh!f=}KO9dn9+bE=8$pKQ2HCeI*Zwlmmeg zR18T~82(HK~lZ9#qx;{q%x%`KL8_0la^V_v=2vSNVj-X44yp?9mWj(m_6_7S8n zfu^M-A95Bn5l9PG{Kr#M8UseQ0gzMc`!n668`!nr{Y@RY{4FzjjCso@5Og;+zxhHU z4f~Xmm&l~rL{ZUr>FdgCeHvZ-(&AwdIwZWAD|XwLMT@a|wb@jxl7jQdCyN`+LDI%H zC)UwRqxXc3or-{__g#}k#KjHSn5ExKcG|hZD5kcg%!CrLC{tkjCBkP|zm%uN2DuQv z!a&{>pH)T`97kct>}`S)dFVud>>78#0sOYic3C`Lz&Cu(Qr)?fu}om{YwJml-@jnU zZEiaBj}e^bUmM~pXAb&$68Sa%`pRL^>cPj*hLPHg^s{NH@A@B}D@4hWWU;T`O@|1+ zp5p`3KKt2L0l-V8x;T_paafT|LySkSB2@i;T)1RDp|yRt;l(!HMC zCYlj=1|Q^yA3Vj=&~bu?t65VjUr@w}F8moUCK`f7oiyM`F0XHsQEbpHU%CsE2fqY5 z#&8I9sHLYpgAa|{>r`?c%CT-yRVsZhs2JQ~)uXb&reLl81&uYVKZ{I((9g)-G z&emb4O-H1dk5{cZ1FGq2RudrOs#kzzc@R=RD)XeJF*~009NXrWBmAmf@4Z_>P~mFV zJJMHEru>>9MhW0(@u|NKpbRjijSaDLWzk^eJwRoaza2bWO{0E{e>W&uqINd+hQBKo zjy|v~HZZ%EGNP@rr^v6+8)RS7AJ&d+({+I%$c*|@Lej|?6j|u0t3V|Tpxckay7QI* zcKT5}Idvo)&q2|Bj)VJ(!fws#%aKfzcjKwTf9DjoaDB4Hv|NlbGz+EtfR{xq1%CT* zImw9wiuO~znGgatke*KS+sNbj_EJC_yFV-Q(q__U!r!kEK?z*4i^a$xQC%&;pO)S<{7>(9!;MVUx>G z!R7&zKGdF7>n?AvWyCQJqP)m;t(=O3*snqN(ii(;KK`%wh^C11w}oe$^nUjcp>fl4 zY+*xIve0%Uv;U!~ZbM9^Y*WunV(~Yt0VcBH4_x8Bv_aUigoxI!CWE72l?T*6&XlBRj6eLBSMlsxrH5A zL<5Ro#-TpiuXD?`#&!k2P`%=vW>X=BUq~cXs4|COi*8psU1 z+PA8si$KI}S6S1z+4kyJNj{D7a8vN63}}fhBb}|-<^m}BS<7}*@~nK#nfF{olKtCx z6XkhtY?O;-a}^m<632(Za`0S^Uy5thgA3{!c2+m;XClJt7vJ7Nh+Hx zmiSd#5k$KZ=O&-)c;v&J?a(0f2n>w8IQ9Bfj6Z>=Sx7jLM3a=x{M2mSnrcU{U zbG5kzO+FJB^iyKvkxI4_6h7af9#iNP zE=|C{;iz3YTHqb$Ba8}Ny7#5T^fA?(SipkhKsKx9V`LzRlwPm>Xx)F=8D3iHTuwog zIQCkVH{nb}`*z88yFOA`3y#9tn#a?x*U18i; z6gpK@wS?LapndfGR1zXY!^Jn2_x=;?1OeUwbn*s?e8C%t;klj@Mbi)B3hq+PY=T3N z!zeaQl}cp{!d0znB`-C-Y%L2*gqvHK^Z2TOL@ezMYD<;=yJ|vEFk==?aowj5nq*(4 zx2nuLkV0q+mdyQF31O69gK|OEX=Qq;RJlhE1fQ)8)X6!*;=TV*^4&D`J{ECNG;Y|T7Sx5&og*iuI8&%&IXxlkwivi=E0F=R z`tLVAOn8n2zgK0CHmgtV!1c|cyJzM~s$;&x2Nq=Kd;i*BYCnnhL9j_q0OTLaf1YU} zC2AYAh9oz&C3>jWZoLu_8TN#Dx&~o;b5X#VDi$ju^S!L-VQK0%ebaXB4gC$o0w|^8@PIZ`Dc`N46$5@V~djV;X zxRiyXgy0a7#+$DBZB4}_y6vjs`9(Ert|TmNm^}+cq^m9Op9B%$uuX!ixrhD~5nt>d zEKxk(PJb`ab1d+&1C?0k0GW-a+V2l9M4dU3{0+{B=zMx!e9HpVZtk1w(aE{XuGoRD z_H#}h=foyiS-A)0X;U5YrpSL@kQfY6{3?BuXqro-?)4%ztm|W$J{KgP4L^OK-n`xL zMh#qWxb9-Z9WAxz>Harj_e20zZePeM%?*JWv2h9GU*X)Iyx8c)9EW;Dxtwj~rRfH| z0Q5lbEpMVsA-reu9K1!Aw^b00IUA+oOs$Ste?)zaczVTFtftT%!g-MC zbi!s3*Qp(%6G!_#JBdj&D?_xqjc0@M7b^aiPhDv;-PvI%4H%{*hG|1 zrvsRlc8sx<6E+S~1^f=5@uNA~VT9t2n=FP)>#vp-iU9HcGVhV!L^kfMPrlRk z{1RLz=;ePLcL3}~=2lMcQ0~`*Lv%TEXVCHUhirxEWsg*nlHgW%!-1M`Dho1D#(6UicS<hM>6WgZhbMug?TEAuwAG|xeqeg?(0QwwKu$?0L=Z5`0rdO%J=axybE1`1+ab!CXMPr^@+_es0W;>*p;W66P~XuhmczB#YxfqaJ3%y`;>iD7NLu{v*1{GR4M|d&67DIS<{mo)?VwRr|CIGrL7WEy?hD1O@~kFuE9YtC#MHWVK9)z8>LD*DxQ z?WsU4V}U&2Gg`5`hiib*c9EnE$b%+LG#SIZ<<0|&5o-T00b4*_ZPw`sryEIJpl`Z^-RQnrjSo>N61=|LxN)v-V< zPMuO7V70g{|?i@&n9#F?2nr14%e%1V2nB zH0Yq8$4z@?@2N$)3|W0#f}HYQ z-b`ByJV+48Pp^$rBQ;c0?E_yo{(@6@ts$kSX~kJpndw)%ctR;VG$oQlM28fn1D)F@ z(IEa+%%1M-p*fYvE!NgQs-Z*m|g5nNu+S9 z4*&r|WSim8E}v+J7~2LNbp3diR11r?wqxXl46qMVv7nDL#@U68T{wUm93t|{QVk!Q z57+<;!)RzBJPT%OLy<#kWT}a+bmiLDZi!{vb2_sHqL%B?#L;DLNRk7{hvq&aWt^CL zh&gSirc*a~k3*-A)f2vHrQF+6GNmD}wO8TUEY>ll<%Zjd$o)z4XwYEuPww#TbCSLh zZJhWCh0DjWl#&g2>*lV{keZaS)RL_JG%$zGv9gGsXNHEzod4`gOADIc!bx2VfvU zSZ>>NzQ?<}(;3DC?9kTCG)ID&0*m^38ReMk&OXw?v8Rkn}M z+DFB>Mi9Tnfz)MQQsBOb5@tcpR<%tg&L0OTD|#4acFD&!_3~8CFy?hfr0umfH?+^@f9h8z&EY zB_AvdaoP#peHV-BCUs*z)#U7@P=gv5;Y)`QxtECx076R@kB1DDHy!^2j0V}=rn5sx zpLwg5&y9;3i+S9-NhK(#WKhWgkDf`R`ocrtx|pQSZ6BIq!}fJ|?^?IV%`cKMNS~bh znTj^psL}@hMAt{(9oiew zu4UDaV12|{LL=>UL4{ux9VRy~-O%m^9~Ff=dUN%MVR!@NuS&lv%kTWs~U0INvD+mZPi4fb5I( z=jW7vWHwQC#~Da`0LfW14jjHeJpYNT%=#t~VX-MGuEOkThU2#@rbM|`1|$!r&OGO z2LYx31NsI6(Ajt3`i++AtKV7N;rtQ$P1s4NDaR37n4&U6!1F6p+{Q@Z39uWO!yY+$ z$sX&QYO6UktwRzwP-7&fk_dLVPT5FG#4kdt(uuTFg6*nA>O+De*&6{ZE8sUG+uk;( z^6o==C@Qdg<^1J@pMN1kSOo<=V}Ae5^*yjU3y3t2+W8NsLbCgJtMvcHf72U4I!svO zHMv*DiBdD?|L;qHGybli`9F@ED*$C)W{+p+ycsZtf1eFNs{cFv@3Ei%e>W=s!yEv} zKdzzSR{sx!&3~R4$4O?#$8*L_9ZIND*XI6DZpUWe0vsk)xfE+$1mpEIajxIf63y*r z0X=w)yWLC9WOxX^mp~CHAO7L&wJ(6cpD%ySDk`?=;mC$;qf9y zcyOzZyF+$raR-pml7zsB(982Jxqh-eO{lxN$T4kdR7$D>Q5-zBbYJ`dNj@4=hr@k0?nO!a zi4(61A6CYtMoeV&iyZPNoGeGFq&!D-N!j+^GQ|=Z#a5;x@A`|7r|1+9Z2-2c4Z(Pe z732QNOw`*mAy)ycm6UoZa(4rv9Bs)x}UKwiRJ4&BH?NAa38M>O$Ce(9*f+Z&Q>S)x|VO zl}|IPI$Wg}s7&szo`6_oHm_K_dj}8lBt(qK)fZ#vAI?6_yL|V2UiXndMXAnhHLiK;XH+`H$K$*E$t0`Cq6DZs2ANId#R zk^Jq-tUF|xe*z})Td$d8#a81|I#?N_!ma6ZrX=tgv^NRLT0v3&!pY~UED{nUnk&r* zkY$uCO`btQ?Dhw1GIKbZds(umFX&c2bId$*_v7-aT)(IP|jA92ETplFA&ToSBkR4uNc zA^DzV1Cl_ldaFGw41&uo&qD1~(2dQVi;^*rx0<}-BWf2BFNH)w%O*n_E!{bZtZ4z9 zhyRc>6~Pfue_?w{g2+oqa7EpbaeLcvCgkAJ{`mE|_3&bVeZ)H^?+EcGoZAMvNE=&x zyaj6n>LND3Y&diE4MX8N5&V)ADyh2wsRyc(ZB@&&LtodbZ+pL0E$t9z_o@AfCCafuMZV-*1Vs4!!+zvPsNT!YXow^W$XbU=KkBwxG>>xpp! zR*^vn5;rrcC=6bf6J-CH1N_F9i6eKr491tNS8pf>OO5WHyhq#-^JPPoWtLW>G*F<7 zhOP>gN>-zBGz@1-4z-J||Fh;edKp!QcHFnV@uAw1*U{jYQA^g0&S&@UNHI2y^*%6U zQ|wRZ?rjn0OnD<1)VX;hX*~Am%8dz&$8KUJ^O|4tli8an6ZqqIo8Iqpif6Nj8AutP zdM>2qbFPCPw+BD!pVNLM?NZj7CXhAF9|+E$_q-(*lIn5N-lgSj5Y2GVrc;%3C{y`x zZcO)+oJEk$300f}XiCR>d>EDqNMT+E!9)f#NPsmXnqo89ymodQBy4NR{<`76mx79u zR_pKndWwo)Qvja-ZQ1FsbfMjGabM`aS?6dEeVj>} zB_b^7&_^+g=@E+vye90oHrV2l$?Fi|auGFw#?TnG=?NS#?9S!XVjPo^&osoJ3#2G( z|0AS#WXt-Y&nI(VKxpui+3Eo!qVC*~WYD{8+u(s4yZ1DC;WBRbm&*X{lOK30mc!vT zEDR;>Il7n)>oL4HwY$spQKkMfV>t;$^B#6ZpqFT1)qZ@%ohocjORr=1MWj?{o;pW^ zW?j^oAx0Uh59jyh{Ilp;*uoO(57>OHGyot+rpX)?B_Wj+M=b}?lewRr1VbDt3|eCN z1GutCCefx$vC#N^zw2K>3-9TF#Ee0Tz7_6TR+h*wI8{;#+b zpFlekPJ$N~0`1)li78zYc>1Bdf@}TFOTBbnp8L2J8$9zR>i%gup<1i@Ikc+S%i(LT!=a4EB~|i_c#!{%}l=)`G#+TR(Y-G)9NB=0&c$Wb>qId`Fs4V1Q}J&;U^b^%?a@ zHpUD?5cH+~Ilxu}3m$y{kb=Yo30h1!EJRJm0m3HMA?th@oNZ2;_W%?{`t_mY##L zN}asa2`9jtdWp9LUy+@tn^kk|KI%zWJDyVqfF7zQ-jbDb(_8mfEKNUL>uRFsqm^Nd ztHN7@LTUZ=A3T3$n|2pOwX*#L3kWuSA<0M620xge@F?iNLYOYaZx=LDLro?n#v)** zrgG{W+V@>q?BOVEIIg%-YpNItuLIH_FtFe3(rEG4?$>rC-pP%nqv{44u#DxuMfq#U z`jiEG*Hs30==hX)yX&%Jrv|4I8%d+_$^MN|(=!@f?$)BY;RdG<8qc$BE}dX(#KU7i z*jxMbP+hS#(>9no$1Imk#hkU{j>|&PMxuJgO|q@5%byMvcrqENDVMy2UWH$%^LfTS zvo>BZv5A2T&8+$Zyr7tikR6@g2B)u?0ji;?RnXoNI3iV;=9B~Lq&vTU;ag^S0m0{w zeWZAHe+m*f#IP%lNzBR}+L8jY=n^)h1qy4qcCke|@2x^(Fzl!>96vV2PUq5u#dw06 zf~oD6bCu0;X$f*nm9++ow4K$eJT&u(@1~)G$R+Y^$Rs7|1zbshTt#w zmW_yF4c<=Q2JsQ$4r!RyCCG*Xq;*ukrYx zRWiXOHjxO60g^H(pTOjLzvbfq>z6f^sow~SU&+a>rrUArXKDr0VfEz0^ve#=YrBPR z7qwEA>loSwK6)C`=hK}H$Iu_F5Ogd|o`Yl|5Gy9NFGIhFoRKf+-+$-4ra;zY&M&j0 z0a?U-VyfFD?Z$2d`#boa6%M2kK_JYo2d$e?<0S*zNsV@~zzvcKN4pstVG6zOfZ?v`(spkPr z^hv*B6jiB(YIg5}n{2ks#YS4s8pxw8j~=EYN8P!tWmOS135#I?=B)Apr(4r;-4yGP zd~sE)*y~`}y_H!w6`yYUJI=UOba%~lDAlF;+`Kokze0XJ@LEyvA5bQHL;(p#*HRoT zoo+r8KU1_r&!LnCjEYxgh@U+9*oa^>Cwr&AW!t(&w=Kkhu`_ur(bkKI647ww`|hLI z!tY^1&PQ}~%?L5%9DUlj4bdA_X0cz6y*tQFJ;l++hT|u{O$(LiEQdD;4>K0^Ipe7JFRiC=AiI zmLQv|&@Pvg-gBQ=BXUi-q9o)7x6O2ary%~}I^Tm0!39!eD*GHf@1_ai=ZbdGktExP zn0ouwn>SiANmo{`?}3KVN)J61tzVWA!7V-I0OuUI(vLej;$Y_%8F>D)pR8jt`S7+z zJ_sFw7y{f0Zuy1eseup_6NB6}Ud@7DtsrG%srbQ`d4 zB*%*bu1(;K2sn8l=v|Ez59j50Dy-o$?;6bdr`_y{FqAW=C%|Z^NO>gGG`tPO%Tzg+ z^OCjHx=QCK$qiB*tWFO(tjxTcc)j!9IuVHd72l@+w&Cwi<1-Fee23qft~`zAeP!}0 zgzo3>3}~v8*-3%oKwR1rM0iSgJgPnm!e7FgdRUy3PD_hZC5aP%#lb;L%NB%=tZ1Gu34&)cd(?f&9j=S~PFQ^E z3HCkP`Yo3XF!D#NNo@o9RSs?{Bc#SSa(}@L8X#nNAthRvYsWm*iWz5=m=J&`8eUb& zOo>5e&_6yW`?z*X)wPPtwXC+K5TFgwB zXJj0LO|;6@={FZ1Z;6PI+ZL}@Yf&^ZO#DbG`MF)=O^Wi#BE6vmx;}QpRvdFMe^X)7 z9ZAL!qORi=M(}chx`mg2yk8blFq*cIVbVN&SX;H?bk+sdyKZw9<-OmEhLlnVunTtX z1lrO-j2Cg%{V;L8?rib->REw$TI-8QyDh!i;sk8oyWQFWCKctM{H5WA?#kcnjz_|H z+fZu@t)p{o*EreZHIm9#9)YnA$I*lUB&cDou&jxYZ`Y6Qwcmo^lQ7k62{S|v z(3t(GbA9s0tkk)X+O}3-8P*GJ%L)mSl-p;K=mjuZ2J8mD$Kmmc&M|iU35!cC9GpjU zWW6`CT;*T(xS^`_W(fxgttsWkN?b2T2qk8UJ5t%_a}3{}J@Kghi)(m#P2@orR6tK4 z@rs+^gVpN@r#f9C>4vXNkU-!<#$#_4HXJC09kxTKr9iU3sm|APlTWTEJk0|w+41vk zjI3YXI2%M6?6hZ`wF}M_kirHlNKXKWn4M793tJ7n*@vPfQbiZAK^C>4gg+Gqlaji;Tdc#EUXF+32J!b1iEGds>a7g?9 z{=;qM;oqo*uf}9R*Z7lYH*{jEgnhFKAWK3=)xdQpwnQMJ6{^Iue)A0#>*ghDi+tV1 z9W@87h@bfW`l7+z;5LCTR?F4HF2;6Xh?q6sqUowyr%9tfp`eFyI>3vhbZj-ev7L{T z)95?^U3J1i>Y@xi;1OCT2)tS>o1Qz5$eC5aANx&~+1g{s_uB)dt5Y=}y8e>K|K|c9 z#bh@XV;m-`%;&KK9nKf_=L#*`IiZejXu3ee{MW8}#&EiR`<14Idk!%P3*!$1Qo~mF zagLsb7R2E)Uc@*4Ug&CiTGc{li?7Xur%ZLM;Yh#n=`(a zG@oo9%-6FV+y03TIK zQ|vTbn0?KX_dN)8R6RW?+T9L*Jz?K` zzkuk)ir}2oOkzE@)hRmr`^$ra@TOc5hcanyohl9Uu*H&p2Z~DXzqdNfhi*r#FCNq8 zvvz``>Y9Ssj1;`|h~T$&#Pk)hgZuugCSLdOF`Y@HiObK=w}QzN4^&=-nw*|ow)AS5 zcGbW-W=pB%g1JQ+&>lfj5&%=8-OnLvu()40zT{0mRA+@hnMf{qUD~*{O&DB5BKB=OM&ANay-IdfSV3}#$W%2 zsV&8ED+i<9k*8*{nwHq0tHy%xR?)Isa*C%JzxG6!O~{O#g>bsR- zWLSC6P=o-VkQKjM>%5i-YNvQ2@e1pOpD+dPFR1yZU)m`JNu7n~U04R#C5PP-i463Ul~qgN#(l!xk2r(vFAL5P+YTN=@;E8Vx<%*b>; zx1rdG+XE9LX86DbJuP4ZC{#$E;dN+>Z-XJ%c8s#5c~*0;8$7>1r_-7ag-tiG$xMIPh%gas7^VCi0^6_bDG_PQd z=-P8@r#at%i0EwH2`+eC7d#G6GH|gSn=Z%uHZe>QDxE3sXBpaRGsc0vc3tz=PX;^Z zZ}2u;dE@FD6Uu3#CsHjgK8s}fWOfFfaY%*zx&V=-j`S7P(xUol#sI^(1b~YAymnl; zAT0Bqq@uOv;Q|iSI=K9)cO`mKWor4^siJ~1=eVAcKoq)D`GVWbq$wt8d`N7XGd_sC z9#v8V>v(nov%?jiJ=S?$=2pjIL-=tmR9(PocpkdK{KN{C%mQq|UvW+HA`HhFcn5gG zrF1-%-PqG5usU&a0dilzp11z{R{0p!qLitYCy2CxHrCVHP zU?K9%dfOP7waD=>(|rs1TajP)L)l% zPdwm^<`&LN$_vjCpHpaA8DxKH?1Rj+f$(0#B|pi>Y{GefJM~!M4(7k@MlsFQz;I9| zPNPrc;uB=otO@!LP@4r`HKY`nhb4 z^n)!2L)IEyD2zDNyENY6kpZDyiDg~#ov@V?GC~f<*j$@7&vGdIUBT{=ikhtQHANj% z2NSV=2&+pYHU<@5tYm;aRv~sP5s*1GdzltH*E(ufE+CmC|4JSKsnSQ#C0523CSS9M zoo;6_@`YTIs;-f}&dJU=ucW@^1nhuzYM8%mC=xN~b)_R~-maXR3w+YH4f znV*Qp##2x&=BltzBu)24!ZWPsY1SiHSs|A$%5ipE4D@O&BP25(v-KRDVehWQT-A58 zyouQ7Y;1}k-3;PpYy#wDht-W|3iq9-H+hBE(sDFQoR8DxWNn`EWGTjT`Q)mN(Xr66 zy5?q=%wDf>516bffHE|@XFHaY#RznWp$bB#BW(?DCtalX@Gr_aB%B|<$`*v*R_8fR zV)<_)_|{~Q5W`4mduaRTSR!}qQBr_TCk$_oPfM5>ZuX7NI$lG*C|FyzAI)!3?7GHw zFFGe?)~gOMHs>^2`ljJT7mz6NaQO!8KYGskV`VwvNj`NHb?8I9qPdK;|5LSuY5yy0 z^86PXpEm%=r4P*(@xM>hG3>OJ$^$0;@1uWdgZKYbMRV!?@2ETO|4QTo5ii@Z7yNJ0 zKm7q^+q$N~vTYluY@D)f*D2e!ZS$0E+qP}nwyW!TyQBNw z_xbwIj@UnDj=ASpdqu33BQsC$(j9yplUc$LCKH^`h&w0@{+J(<+aYCbnoeIwhbTI~ zEp&aLxcTSmd)4qgC9vFe&iVZD@L8zD(CCeEK&q}*W*Krv+0z_Zy*KRh)f2h;!>xe~ zmzH{6z$eP_iCUE9$blr=&c~{)%*(brtaiAw-{YRe-8$T+Z`cEG-o)Id`O>uyYyU)l zIEu9nZ*!h`@|RHp_S6*oJtyFB!@E)15D8m1!KWdaz^zOJD_kdCa+yt5qb9+79t9T; z002yCKZd3rqYkX*f42Zo{HM#}{|G+(xk#La0buVxOL9a><jmmaG_9nL2K_Cikn zmRZHwH^|&YK}b}K{qJ7>v=cA(2Tbgr>4=DY9kS6TmN4m{;X$3Cj-_hlI})49NBaq4i$@i$60CY{$CFB|%blXGe4&H?s=uxhw<>Y|%TD%vRRNXd{(P zgN^p5XbCM$Xu1+LPUg136HVZ@|BVkI*z;3>^`HLJKZ%YeEl@6BOhNml`E;7gs?+ms zat8msH#qg5m9+oxf(8I!4hH`P)K>wQHz(&D$oF5>kp43pLiZ2b=bshL;nSp6PEZ$f zqIPY}|Fr@bkQe{}Yw^E)o&PiA@!$WhdssdfPqS&E>yRRo%w!~+-BSR4c5@JDGKx{D z%R|b+`#+Z{E&R%5=A<$iRzVC(5=3>brZ0qm@b3eJy!eO^%B(=jQBUAdI8k0Es>VTr zRen42E#=iggkFt;L?i{9z%d>1(j#Cww@Qa1UYA)jtV|zgdtqhzKHNfwclZdvHtd2` zF5LI7hCwU@7nf*<3KhaR1Zv2VQTv?9Xg1G(KFQ7QQQGjM&4d~dx)K%itgX*Z_V|BS zrNE89+HyfKe@}*Nb^)3s=XEde+MIKp~FrSKb-VIpi z*iSVWa&@Hd&OX2saz@PFL*PD_@J@8XzLyo^hGj)QYcnIv~^;iYuELV8nn zQVR76)&34+ua8O#3IWwVaz20YsdpphdPSxG)rsk~@D~^@smuJDJgK)E3vTMom)Sak zhyxY;+ggk3(&HKw1fI58g;^tUUX9PFM!GxPk0{-5alL3WO1qG16LaNe5O9ZU!N)oy z!X3>&wgPg}45P!|p8a6SK{YHjiqS20HkBK6Ulc+BHV^{JW=8@THUqMegtKeSH#(zS z3C`T%h^c*W>6<$Gisv|A1Blm-m$#t4Mx~O?gx&(Kw@LmeObkdvZkBe|`Y(5gL)Ekv z(MGWvwqzg_Bj4dX8f6z)ahz)^;XaG*)llf!t{9LsdkT&TvAW3>`t$+J04H;V;>MT= z#uQotSb&p?maED@q$dj1wBYWDCXxCGYU90ysE*_kI2pD@63ekMWc`&2b_n~>@%;2z<@6g8Pms)E0600F(wqM(WeXsHCjO?_h#4^fyMN{o< zOr)U53f12R8Y`9O_GhlhZ2-?ncd1diNu70Mq3haw3XlY!{~eBT{_o?2C8KXrYn^Oq+W zQswS|dmQniKx8_yoVNBYW8f_I#V-6rU9B>OV}czc$31b#;hb~jSrb;nn@Yr z+o;|Nfbzl5^ZK5c4W!!n$JO<>*>q|N%oMMzICXY1~=TDKmKRx9wGNL+_ zPuqzc^wZ-Db2)-k_|9Tel>ezZ_^6yn^hwa5uT}xo`*u9L8})H}kzl%^e_#Hq22Gax z88+H|uqk*K+uEPdf-c5p!`)Nb!$zu+W;U7WR_WpA{n;iRhX=-FW28MCk(GrUU>`Yz zEZ300JY*-k04m_00e~7C!R+!?l@WCNV6U7_p5&6o-Kpc>J(ql%ChEq7+U?OCJ4Kir zM8Xo}C3Jo!>>bCWxB&CMosuwDQG^zgXqEGzG{orl%nN&RqCGpQ0&X|~rmkmza@0Lu zYY&UA+fFc z05mwx<7@L97XKAh8v%U5Pl1uGN-kKV2Z?eM@x|^z>6lo>B@8?u_(Sz1TqOC_`jJfF zl_o^$QIwjx)L1}SJ+>HuD~Vkf4ptlG(JT&m`n zvz>1vsiWvTLeipf`Zx$m;(*^0j8LqZ$Tfksu8Lv5$^u8*{j(R(w@o7tC`6E{*IMLS z^mFReRih*Ps_qE4FxUXGLz>kWI#g6gL$mfA_d%j=hW+qsSX?!v-e~zOWP9#SWi(q? z=mnK7Gn>Q@XOjx|G*8H{?YyW4#JgB;CI_D|Rv%cWTXnkglcBhfpPKweeR(PDBOEot z4pZONwI&*uJw{6XY`6G9R{y){7b&h*7#HI=I`Vy8t3|a*1P%+hL+A-p^xCq*HbDW8 zb$8b1ZAuuAX(!7xucRbCwU6xgL(SaJVETQ_Dz2Ma;Vgmm*`duLyp4tjXXduv)e~#Q zTXqe${*s;5b3ZUp;Z$+%@_rg=oe5hgiB*86MYEt(7-TWzlE3t1_=cP{6~a=L;xcZ9D#!FH?^g4d+NM_YG7HW6KT>Mp(8qK|VE#}W2-Ni3*`NVDr&HU{=S zxR9U7t_OyhLXh;AK{YD*W0LuL_PO@@*iuD$q+@*zE*sJ5(x4w;z-w^chtE7S54uBW z=qNcPoe==p=i1gW(@_1Oi0EEryzATOOzAt+~^_#mpV_u73Y#Y8k-a&OC&e)|yj<^mk0Z3;IB`D$G6B%ep^UIwD=Ths~0%b9H; zHAWR6-?xvXRP$|B!0k1<9(q!D&n1r{c}6^(!o%wOUyepmO1;~_mm2qOC|;r3LPE|d zsyBRNcDiR_8g;Q$cS|xT4am^S=_XZzfl|w~hAoGS(^Pp~pZ7OeEVgz^S8_xJ(!A3} z(dfV@`7WlU+_d)IZRV*gsS_WPQP_E<8Lz<~i=l%br^D~>3AUDzsy{8BSOFo$3E<81 zL~;y7Q+49C&B@!zV&LCgqOHyvd3c}VpKY-0DJ&7YRJFm?J4zzz80~Z1d_o}o#9_e- z!TCWT7QVhXLu`h;99q0Nm|XwB4eV79J)z;#t?_x!6m;H z=vl^SR1H-82aSoVCt?%>k8OKy^cWlVLJjcz+^dA#ur1JC)zoM!h$_kWWkePg8SG0A z71s~7oUp8CQ&U{q1|AI-da5>mfg5Ai1qjRU?EPu^ltE;2! zEA+q$xuJ`&8l~*#=HX^z3Q4Ev9I2zk4w`XrIZaqQK;ddz@n=vo8;|Ix_BQyOrM% zbUQB0YwqjaNhPKbmqoppA4Oavd6Vj!Imb+YvW@i!GXvI?(t11j%9Zz79M@b&U6aZ^ zt9NOCDS{Td)`%N9`IxWUHv&4fcf#uj=!N4x zh>oWE2smGUoi*jx-0Ru_@IIiJXuaFgg*hdTdEv+9`ZkwKw3F{8ic*7~8g|3kuMHHx1Krgs&BUg+O?bVk-zFu`tkblg zdX39Dfv<#4<^ZoPeM1F$vwXR79CvO4FGgRni1`_Hc@kq;H&T60ZZxRK*4k{%oQI~Y zzIh)4!(6^nBrWNR)j3EObe&a}6<1XLaV+4Ij1`XISWvDU;4}#|T8Xp;#md|4r4HL12i^z+h0oB(Ul*Qt5 zAFBeEmaG~iZQ+%Q)f$U;#53sM@vy=Z_T4kR>nm2(ffJGHhmRnv&qpg0iLySS4}CuB zgE+zMgNm`{pNS59+twl5`y9W2q3m^&rS&uSg-fez2Gm9q&Rz(d4=epjBfN+W??Wir z@|}x@tv|{%c|?`>(;rWYzLb%!4w4Gqm;+aMr?sMn%fYG;Q|N%l=rEHZww96ZWTZt7 zyS(vjmTR^KsJ3x^q&{Vh*AIisobDe<a*v4Q>-K=efm|ubh;x3UHoc&uozz(phmZjWY73%kS#ekz%!qVPOm^!OGLe$Ej69(nP*r=-~D#$Sh6`s}wa%@&a=$8vg>Ou39s!8Ez zn)Ez4QyGP>-?Agx;3|t(3wFj^C{bkR1%^+XmbyQ>rNa7W`RERis_0_OCs$sCxo+5R z;W`lSmx-vXw;)pDO-D3wnaU1;24rJIh=@Oro{uRHSU3}=hauji6>ejLYrT;8?Z3)7 zX*8;nIrPB9Nx$V#j8g0_X7Sq}p(SoNsl1R+6p7!^V~}d54u{+WS=VANf!KGllHo|x z7;!D>3CftN=rP_|u9ghgvZ<@1>`}=>ZIU{!{HTeKw>Xll=|Pm-u|AP`+(U_5!#NL? zmYp=4z1hcHv8oO4`Lc+H?!I?Zq&IzN2gB9Sg)7lIAmf_@w+z9qcvn$-;0R`%2|_p6 zX~TiWA7;F)G*pdH!pljtw>e6jrwq>>a94Z$?L%!%HBwY^_yC-1IhtwS4vab1bG+Ne z+&Q=6nJ1d->trI$my)%MbbmQ+6g~B*ySO_zKDV3|lL}=w8BzY$Y#F1zKMq+EPMu;*!EhVNVYcPx~2JJJv1Hy@%9{z z16s&EvthdPQG=-Rbn1@KGY;mbE9v3m37>*>!*AF{qSZfK1@EeZ_v#SdbeAj9GQz4b%}h0(?T2frwiU)*EQpJH3;^1q*iSzv_#6- zQho~%#jnk$p{fXI)=fL#S>V_{=#O2(Pi6Oc(yT1oIS5=~++KIKlMV;(HVwO7Z{I$h z_z)c6@YVq?5HWjW+(@nJa{#EOgN`@d0f;L+8wjtKOoXUW5% zag`4~$qlY}Fn@11+(;Ugf33|5Z_?4Hp`<-&{?d7TlbRc`6xBj2S=w%aoAYg2#nX#! zn!Y@KE=RO);b+1#kRmRuL7M`84L{`I@T}wRF`z;i1p-%ZLhA3M^@mM4l;dn^H;Z;i zX6262Rkdv4;b2G9e!DoAxVODRXbXSY?x|elGl01k+L-o>qaR4E?l%@~h)x|6qp4K; zChCqS?jJLrJ~BA*C{P|-bGL#{P}3(@UBL-ri1(kAf>89zEg z?XR;zI*@-sTWUQXS@8L7DB;c5))G=XTqPwv;&7X)^I0X^gkPnW&B)KwWzxz0uy3ig zq_~iYZN~uJLiu}bl~&C$Md6(0ttjlH?!_}Z1vc3P-AI{};4=*5Hom$T+;xaz!{^~? zsae1t8Kf~ylgHeCSqDBm<8&5sR2j)V5a){%OZO*eX=$-ZUD4ky@~AZ}l9Jq+AHJuM zodDMrRMrbU0s{D|U~E0|$Ged)iyGinMsRDs6%F&lw}60WbaYB>*8+A)tF=Oy4HV#} zls50t#-)DF@6cuEV~0grW*DH>(!jTmQ1RY~>dqsGQrek!DPZE#pTFs2pT8}=9=NV9 z%7e7&<2hwYlRR#mx@7REk7ByRX}RvgdDJh|0;@#-;DbF9XvxB#6uLfm=F#7H#yU|H z!{s_lh~q?I;ks$_6S&_L4+K zhSTt25y7L4QhjU|$YfGR%i5*ip{B{pgY}sN@hw!zC(7{t6>Ek7v`r^D+1VB@d`zhP zh+*_B%@;LsQz9p@GZ>@XKb>eyco&237ZY?Z$YW-HT*=BB(x` zHm5i*-Sk`aEa9k_QSurX@KY&t-jm&j^d8;(E34Ago~Bp4fj6juiz-Qgn{=DlW7{VH z%5I9}f|yBU3tK|h7-M-|=vq;!(r;k?PLbBlb6ni|MRFX*ftaJ1$afRsVxm$epa>cbfrS6 zVkJKVN>8pSF}oR3=3nV3chr^Os$n$q=;4q}3t^fhd{x;N3prb#GB3X|dCv@A&AAE} zi?O#muYMzHkuw(sb!_Osk!O$+l$Sqg(xpXk;R)c4!Q2Q@kho+p=-ki4U?#LKGi9p< zUJRGqQ*=3;fnyDrw)()?g-!>366$XK9$i$2Bg6C`eAu}HbYBohVnedwSpYZ6le0$) z;Go+86k6#+>oi9#Zc$9gJDV&pUkG!w+CmZAI;5g4hP^B|-Umn1APbRt0guqg#AM)u z!a-7QkGr*+LX99ILoN*~5tEyPDkrbKip@t3E#V+vB$pF4e?y+WC^8X` z?S8h?FNLm>020Idv(DcRdlRf&>uvgsjxg>Jr23n+Wp>B1t zUo19q@U5Z0nSM=qg3K}V?g1E75eynWebJaFZrf?-)K3&7{)*SZScy5EbJB5HD9<#j z@Hc)?uvhFtx~!b$7v(NiQ0}~$L9M0lliWM9nF_)D3;V`zPre|1HQVNXq&iFLu1aCf z0e+}QiQydqiBflq5&hRf`|opb`!g1b)8AL_MyaU_D6^grO;nIyreA}U%N68E)(M6U z61g1>wP2Yf;3-cU4BtHmH5*?}Jwnvc2G!a@&*rJR|HW*P?)-3Jw*O3o1OUK)TZHl8 zCo1MXA{5kXUpL}(IS8LO$E>cmeg>te4UFi<>FL(I9_kZhXrWo}6X8`c;tK)d0paD^ zm56v;QjxHDeuJ6Hg-cF&`QyPEM48O7A> zDwUZ9B@X5?93vWDYDRkvkqa3X*|#T&%0V64z8^9yze~&j&$?c71w0g32axYyB|x-( z@a6uI5cxm&W6rfbY7pc6YunV}6t~Hlu%s?F=Eyr|Y(pPs!wx)Ip&KeY?=7;%D(G(@ff{8o`w&6gF615B6F1PC#6@u3 z@#CL8|IggnP^Z0!?93=(GIze$@#VzKQBX+h>$HRCMTWo@jSlvvW@r|GAb16O9cD1aZ_NT>}$Ap z%0kRea*G6IjAM)Sk`F%sry9}>SJOMUNJ#C;?+$}I#M?RCc%Z>}E&suelC_{sQ!cO* z4ek~VCLY9L*OYWrV;vlYUCe{K<=jJ`*F>h3EdeBgA*?o^-v25YsZZV1L7s zdNMmKwR98-^Z5In+Y#{lqE4iMi;iR+si#h5mYI07;_jrtd@><{aEQ@`K}Y6u_4|2} zDWrcl%T;H)ZAVAv*|R@3xwPK;&$K`-8q4-Nbo!KhBNBX?N(T&D!rod(0xp<)<9L2C z3xqOd&H*@lYl1vSA6j5G-R1#?$drBDy@Hz(%2Uz#LWg)+2n5CUtg9RgcqDRo9F9Ji zb;P9B>jXqHr9xW|F_Pjlzb!f5i7B}z9o(o8ei3_UK52!A5q19rKm_aF$Yu;bJNz=? zW&8=F{s8t+&wyyR%(fNn3{)+O8SkMy-tJ$4wIetk(On0QnQ`Iu91L0IZ(sTzK75Ds zuPw1S(~FH~uLDL|%t^kpd-GHVXFz`?@+7S({#p&&*1BTSYfIXBY$4^n6MXW54nCdP|!cb*X_}~>G0!C$kUp%jqe|{OlBs0DBnm+iOuN;-TQTc3tJC@_%H%AEWGM!A) zdOg&jFzLLjLB+Ta5gj&Q#`f{-O9uxZo;?*F&Ky7*a$e6Ih@aLa(>j=tI2i&D?UKeg zv9I8?U$p?@+L|^ySh+G&A3dmt%SfU|UE0P{QSU4?8X}W0%=`q#ND@BjU%Ozb=WRK; zszk`8!_#^ZX%K)N;shzymZa0~<Dj5ULPH`s~`#c7!I=7w4u# zf7X3w!CayMRHQz6hERLTxO~Gb)n?t)CzG_>MFggy0Z-pMX+FN7Gf?-%{?ZbT^en!DRON@_lJdHTGNM7(&)-`_eFF!Bsj z*ipq$lqcxbdXHyPq8&H`KAu@Y!&pR;@w4@%Ry^{u-kya|;K(gC3r?_}Kp^b{#F}R9 zyQ5p&b~e z0_TXdq_pZTG_c50Xky11y@H29VQhsuZ(w=)J!WW8JqOI+@qtAfs<~>kQUXUTh}a#X z4Bpy?=>4kx^9~PDPKNKsYDU6-_f`U$1FN9Osx5`*==|&^T>)-B2)$XeM)`nr3yS}WNXLnL6w*Ll6lH@O5g%}RNq^C$pMNp_n1o(h3aY*q zu*qzu^5ljxj#{)|$z*Y%JA+WX?|KP{`1E%o-q&e?lK4aA4zs3jrW!PW=fR+L`{H-Z zBG9uVaJdc;iujoy{EIHGK=ATc-9S6p%J{Lo(!G)|!FCd5Edd3e__3+_$TRuI4Bf-G zZEAG!F_Yd|fQ5m-Mn-Q9E*t{R-uo%yGPBJx88}VEfk>0+_<)NUGD(FTvtfmI_PYpY z?1*zYH%iq!+#x5j>DFWH9V(xz;ZGzThgdf*BBN88qxb^b=~j&btO$>y_6VbNRa)Lr z1`Ru}&jFRb0oi%nn&VX?LDIwObs^j54_AxOpnh6XdcSu8*DnE_Y%eVPwE4Fuj9;@F~ZRy;FBF}mV zi)e~uFi`${@S0TBwVJ1I5wo$G2hh*6xa*F?1Tjx{Y^ej6rlvyhk6QFsaY(1C~z5~n>Yv9}J;9&gIOf)me2Lj2*^lS*fh?<%>mN!4#+oNk^^0m55C}o(}2e-+rwEx}K^(Rlnx75t} za)ACaGF#}?PGB<&+Ld&uLz+SYwQ0V$0g~i7RFw{UW(g9uj=k7)-HQTfw+j5ZQ;PL? z7(R&rx{xGb{6Tw`NblKNxl zUAoTYl!zzj3^-MNKZ5B1WaXc0g7!F={_Hc`Z|JdJTglkn01|voRx{Df#l0a=2z)`5 z-F|9g>|@Na$uv&Ci4yf6&yNGqJ<*rQu;QyZ>+~uOn?iy|h!2WFfb%Y)V;?;(bah_8~49$nl^2 z6&I~qVwWQuls_va8Bfm_n<%bC}Q|GlBPFnnvKf%NH4lS zGOt{O6VgncwP*izS@)A5n6}RSW?oZSYutMlI^s*~X;3#w*~kkz!e16#g3VAmTjuwm z597O(MyBb~01e~@43zY8Ok@dNLP`2SQm()k8DV0NJSI=zY)i^G?YRUq!K3}zti9NY zY*(35eUJ7tzu%q^Wz&J{;$mWUff-U<$zd~{j@QaKQ`B8SZ~>z_Iwvw1Jb4L4cdmgp!8X=p6-OlHdvGZ$w&j*NqJYSi zIeEPEc5t7$ExB%TT$k%kp^FtiZs4o%@F^qc%AJG0;N-8s;}ZIki+Ynes35uO>Z1M_ z3eQm@)!N{dxb+ps=r)tmDkTk2ci?=B=`snIIu>-@s5r_1@0oZP?e~E=M@HrJ1SH;% z_CZVfr?2QJlYkMzC&|D9Pj&&YCTFW8Ta5ymP}2}JnqSam^*)222NrianI3{pD7mqm zj=bnnHU8&c;?4fo`$uerHHeGZ)Zf{A^+e^}H2VRB<~-rn-0XJ3{-9iIj$iOz?`w3s zl=j6|B0gD9r>P?1EJA9;X4e#BU#lkV(G|kyo*0kbUcrTn5&C*4=qCFPiyxUpCpd-s zY2V7a9(SrY;tq{@ip)sUH}j|#^>ONA99QB{fBWmP+mgz8)(g-6rfLCz?f%J=ZXPcz z^|QW4r^O3{IpPq4_pAPOn*+aIWwJ&M#Ln#Io&ElNQOQI zF&=1b7N5Nwa-&Dwwmc_uYwIp6;|^cR?})FPYHm!xnd>G;SCZ@)h&MMq@r$zex)rwR z%i2Y608!Tg%fRXlXMPlq&N%l#8|}5?NbL0b0&Rc`BV`KLxIF{c8mfpi?srR;Z#H$L zxFBvB%McLqvOl!W&7gzEi@{4G9L$A`3;c0Ab_vs}zbP##(3^~6_Jyof*6TjeqjE#d zFWQx1xgrNIcisK<@XJYNu(tk-ALDz1$B zV|g)}L(D&WFOHuO#K~?m7gOMlL%=`?_W~va+whBi`=i!|lROco@=K9d;)cq=K++M8 z?L~qUIYW3GDV9r@6d9oUrL154a>swA=O>pvyABRdHK7qC3!B*@=ub(r8EDDfYTU0D z&hn{RH3@Tu1BDnOyI46pPUYbgxX9vYTzs#0Dp+t+HBg%03GW25&u@LH*b=0Wx+D%3 z5p$<)ZaRYQ;Zrz-s`9`loZac65rd=9wBTQ1ced;qi$lPu($oxJy}&~ z%5bluEn3wW*hgUvs1o_f%cA6qE>e$<+%KW&?|J3;mQB`Uv0|6_PN?9#p z?fLtoX$Sm}dQR?~lX~Ne<}M66iUH4ap`#P3PlQ1r^-c7I4p*$ccmGpEn19a>?;tp* zZ24NBoWJ$Fo{_Qg*@fG2HX4f%dsZ~Jyn3;l*k}MRE=uECsUl%L&`q}cP@5P6a*pjt zrLEGv-y3-LHp<7d)HV}!0fgz$kv8urXhq3E?VBf**L=B5!xm1f9BC1T^s7BkQ?u{M zP@DjzM@}B`g%Pni3d03bX>R1nQ~!gHz=Z3|oIelWmb8Cs7JYpc@$zroAN0Z26J?0I zx8`YB4T4uq{xIu}{iQ(S@$YZ+ZqEMuG-e}YHD1-La4cG5cOlyiuKbElsg5Esa*h+l z2!QipohX`(s`?%w1*!v*b%DjaIep{=4yG}DdKbyx3<l*!rlb^3Ip+e~6{Jc#-z4BKeKX~Dz(&o{{XG$HfBvWo7eiYc^0w&-O>a?VD@1!O z{N;#WB{4F+dFsy^BB`!I@ALJ`X|gwmN0ig(Nv(=7o*`_hh^HUfsa2~sjX&xD^zlaW zZZ`oYarpgKO`Th`#|sv3eGB=Io|)ChB5uh$z=3~P_~CP80lT?^<$HNYTNN(bZ^1SB zH7jz^aX0ZP3x(T*U3KjCYn=FG4o<95I&K*c--A$a0xXr$G_dq?J^u%}>hviX0^5wy z>iKJ1|G`e+K8c5v3_l;_Zw4fEawB;N7_5C_U0l0`Xc2eW<9>G-EhsxH)Yhx&dN+m} zRdP;nOy*XQz7yVPA)(d0Z*%oGVs&-;L~xlzyg5g(H(VMcwcc;L(KQ)Zz`Nh;bajI+ zX3Rb{C-XT`0e@_yyylfcc)h%ZX;WQBOC`=v{CwLSLtNhB=<`K$cok-1*R~BPP0AU3 z`}1m)oJsoBBpMl`*;&#S&3=XM->63+HbH57$9)|nWOGU|j6gwLAty7vNN#%!E5HAi z5W}PZD{_1a7C(jv;s~Lu%%85SWd0-n_2b6RD>kg`L+g1tW~6ZkW=k>s``U|E17^2y znQcZW@cJUzJH*m16q3?w;CfBC>#FC9B~L|>&-eo*Y&u+8h8-19?_=8dubNo+=@}?{ zkyfO61w#;zS$5(yn<284oZbj2RRvW6ZC5I^5=J@G9V#88;)?T3F}E@ zZjDG)A~9Luq7Xk+I!j!)X6TR;QJe!OVPeSl>|ZB$&BRv}#j&BZR)m&c_0}3J?3bDDwFdt^j1p^Fwdr27`OW%*m}s&7de2o|q7&rL zZx@7z7HIiO-FOHg>!^GRA+QwGvEH?<@0C5hC0HroZv%(`M25d91&d67huzDBb&rZ~ z@IW#3#jKX5;P4bhI!YLFhXpITFVKTZ#qJLo!UU0O6t4uh&0q4znhP=VPF*~9_NbQ# z*aQChHg@+U$@xC0$PLuB@{Q;jdt#~PE#jux9+Q&W6}Nu6x!gM6%*aHL z0{a@IZ85YS=!s9yHX)ScOzffpWaZsAZQ{xru@WNQD7|sxV5bH!UKH8rcjba5n&eCY z{18vWLgzYCGNHyLKMya}$aPkON5G-~CHEBOTBXmks8rJBmzfh%6Aj+_ys9~#J;Mjq zMN;XNe5wiDpKDf8f8Y!Ji|HQrNKdELByxfA`n~TDwjG^)qGZkPnZy-U>Na_JpYqk; zS5+2o(E^oW0udcRfCPFz+KJoYL5^;W_1{fbqtVdQIJu)_2}gx_pe-BGa7O+EoiX!36+Z|c*&IRSyM_uBI} z_b($T>QOB%;)wAZ;wSs1qX|GFKCZ(4+Ct?z8vksIEpk$(r>$5mtfIH)?`}7pJ0X8@ zbUz-@;+?xQCJEcCqTRKT^Y!Z01&86S99+pdV2a*W;#uH5cC(PvC~NV&(g>9J+aZH_T3D?1{4V+M^7lP$ zyfWOe?2CkdW;c7fsCMqoy83BdQ*O4wgJX=7y%w2ai|8!1?6vV)4CG&Ja7Gaa0Iwuu zV(oLjy-QzFzDF6&#Tw9B@W;xM=v#v(LTj-bjA4|f3->bFM7%XZ))Kwn8ZWJhr8S1~ zV?mr0vv?jf5FV?%->KwI{Ux5y=`pgp3?q1^LHFpXY$~w0r_ z7K_;y)FGl@H1!Qh1}a0punifE3UZhbDz$%wv=gZ1F@>i63EPVo5Vm+JSAG=^5?H^7 zUTK&>^X@yJy88(5x6%*;e=V}}?5qj+Zc~*g;pOqm2vFo9T0g0bkCnF&nI&UtEzjtJ zc7|oG`XF^ev8u}0!kc_;7F4c-kh5#m>dghY7PU8yNNi044i7WrB{8pd97 zQT_fnr4k)-s$I2)8K@b`yp-mthGV(G_HCk2ojtGvi+k))VDCX$3VCewzO0mYQAK@)h;tcJS{7LH_r-%aUJ@O?mM?yop?NZvs?xHb zU#XOL#;w}YKvWAnYcJxGtDNVjAW8$R!K>vQkc(;qg$&&%5ifaZx?8j(&y8}}3Q)Po z!pt6pvzpPC;fS9Jh^JIrbz`dZrrumLmAEtuk4@HW3LY22^yj+ha-}iH1_A`!U4m}{ zM^sjo89ZKRJ9@4g?bo)CEKWwKLuj9m{9ck-EB3Lg7r?nQ+(FQjgAX#HIR zSy6@L`qKQ)-Q0h|gXK$r{SsM3qC=9vdDE<)ZX38OxO`8mxM-|^o#|A2>OUc@@08A} zSG0(6w_naWuN`BZECp-q$oUWk2(`H^ZFFq+J6Kj9s_kd6`ByGIKyHe>K?+{+Om+t> zPG_zoL0;KIFrGGJhe4!P361m%J^(e2NGk@XR&XVGN9BaK{|XKLuZ7M<1jfh^KSAM< zWTX>qH&bR4#X(cfE11Q^dkCcFQu}gI*9`Rzp~0mmsOYKD3`Dmn zl{y4Kd59t~*Qh{Gov|R7-Xah9@+xoyLiu{rJwlHav8L-oReN0+=<4Pk#nO~X@3aCA zDm$=c-t1rf)A_mn7o zN5pLp2Fdttm%^wA_fy!=yFEdaN!ybPFi7}Y5>%AA_8paCrS0^%pD~cCORft|HFhl> zd|k^)nvHwmJIoeT2nPsRIm@Bf*u4Z)uIhlbo6zRNV5@npj_OtiZFt!1JB&UZT^`o2 zwn;cMn`Go9Wo>mj%3RmnG*x{1B*yd6$k=eR^e78nsskptdMq;4MK$x+z(qhRUDgOn zb5bx${9)fivVGN7=3N55_`!QLY#YJ7nwL%!N1++{b7x8P4>TYYVyLCoPrj|hzA}-( z4T^T#G(WzgKNGTSSFimNi24jaFR!z?HJGiiLlv^IYM;}p3H~%C<1GM?F>K|+@E)&9 zXsIq)S3B?=%*_KL!k7-}}S9IsP*@^Ir_2Mq+kv+5u}Ne1J>I#l^XFPeCWO zUi&q~7Dgh9X9=vS0e0W(8!*B$0C>qp;9Cv0p&uW>mn?!Qo#>~KF92kERYM+82`IeM zIDmD}ze)o~|I{`3N6O}Zk;iqYY&vY4GnH?|V|qwy1KiS5al@Z#P7Oes_&67IBN#vG zZZLsrod%>KFKX&J@RyidRK|bT_#qe>e&o~uQvb@|{2?#@Uk>?I@M8yMqU)&)dXvvO zPeyPa`y03yV}cC=YTX{H34v;y1odCL27p)l!OQ>eq|X1dfcAg*V}jpa$RlSj;LFk- zza-Lf)PoN}d?=0NI=ilgM|VXGy?z8dpaYhv?_qshXw)O*!!O`${%KO%iDYKEuqLn_ z7ZT81vBSEY*Z@W!noWNqPJGrLEl(TeWn&6w7zRA?;WB&hf^hzq78mh9(pa^B8LD}XBK#v;*+D)|q+G26gz$2agPvnz9?vA7DmE#)v|9t?!0JyV1 z*fRgJWd8>r41DkO0LrfDc?N_vyYK{;Kz*Uch@TTp6*+T)3xMG`{?$mu_`eZ>kA4tE z|7qX;A0hAmH0eJKa3`|Q*r3UEchyJEajc39gOvk}DK+@+ec^r)z^|TvgMonn03aUy zA5Gl4R_=os3Pbi17OZR8(~)!OAJ~<{@7p)YCpA2+iZs6-T0SWMdcou za%3xpgXaQ-FlvR*plKjYbja4WZmyQWg=myAHnF0+QhmQ~-6NC(m>fkwHlor75X^T|dBNd&`Yl9*Qn)$u zI1OHL=yG~W0x(vV4@p#^2db;mu=fShjZ#)?<#10w1Umy+8TKy9K3+Qg0rKN|S^}upcqys6fm1K0t(Lk`k7EhGR3g8Ud zW5xR@^2nlvaikdoXS3bihg?k}rS{)@xC6?Y(e0vs>sm0pHZ;s5z24_go?nlCtp^*r z@5uf3eCxcJ#PhXmq!1l*X_t4kY8iBr*X8%B$g?gN!UFLgYHQvOF5g;}N0ck)Rf)5A zW1Hdc-VkMPX)~2Zz5$|!B{)>c$3>O5z^rbMH|CrM)%x?U8EW0!oE^H4bu$<&uq6E* zG7}A9&$?>j;yZTwp@~<6?mojS!pMG!xyigt3t22wBF{7`6E|(2%*R>#QMm zkAlT#Aj!dK=S(QiV(&^2CN}CyC^9l2M(IY<{m?`ex1@$&vCncfFf-K3F4M=T`I1RA|_d&meCb}U;h|i7~iq8+(yIp&}$vRVq!(K z7Dl-N#BhKCtUaOX#Xqv3fxbdK&feF+H6;pcZ`>^N$?KjA3~B6Bz{}0I{W9qN^|eCF zWB`nA-8>H9mTNDZm8#mxl_}H1;_J6BN;v%U7!n2t%P%7oF$r-dejrh+J_DoeNWL`J zCv*17QHSJ=c_3C~p{H5aA=ZvrMW0h^)I0~knhic6NUhQSIyy{Ds4V(dH8tG)0$!I{ zkSqE5!o#ph3d*&#b#Ety7~mds_SBWa~)Pvog01#VO>B;wQ$NkWD2oFLUHnQEIta zq*p3haAWc<9CGqjFYKlnL>5(e7NjSQ= zczsZpFNVrb>_D31;`!-FOiRKWeQsID2IL(Iv^bBT;>pk15W+gTPsp-=~_1UcQuGUjWXeBy(PDEU9SL}fE zMKz0A8tp{KNEbERC5gM96ts9*$tuFaJUrhZ~JVo1OP zGQU%LPg7Hz9e;PR&~rV9FkXISUQ6deOvF^>OjUXWN=1V1@?)>j`e*j?JoTS9*`a=O z<-Bf8T!~_^C_*_Y9($v(gERItII6(8r(OTND>Tw5a&l&#-#DyC8@#(Ms3%dgVyN_&Z-2eVY?&}@!9xO`x(-IVnYcwwqN}K0%p4Xm8Pe&YGUw$i!dthJR zSyUB^p7rd{GY~{i3wE;h(D@>;F*G2AVM2A-9%5TV%Zg6L*tr|7;tSA4WBYB=%aM@I zxzFv;+QV(^N)L#swLsI!pCBE*0IU}>CL2@PE(Mhta_DR?kO`=z6aRnGbQ!;IazFThAwXMf{dx8y1YriL=<9Afk%sg`UH= zqlDTOG%Ur!Eo7{rqW4m1Y{&=P`8-`MaIbvFkTYqFCw`3960Ez*aJH5P)ztIOkJaE) z(Y{X~8&avguIe!n7exyAg?FG(wnhz`{B^`suGer@W5rhF?4{P~Q0Sp0!C}IMf#WcG zuOhT{sy_?w`4Xxu6bFkm?lueEj-+q6dCVm97X&`?o+Oq~6&X(p{2gwGm!L{G5?hqY zKE^~?d2aL6dqu4BpEwI^%?B_Xbu}N9@0KI{f(ce1<9+RDXvIACWZeqFP|`0E12_`U zI{u?E_BrYZaTlJs&@n1px$Q@$6yQxSd7Vx^mW;A|2c>osd6#n@)}^1pszXTx@{ARK z_q-1=Gqfbf^`aJtg@(Rq`cKMUC`8+&!b7o>EgE$ibpr8Kxiq(*=7O?|rAvXKFu*1T-9K~uG-D~sT^8T}~djRRkjuw{T0t`k9h zoG~?kjd>TO6E}I~8`;TSe$Q%(S7ajs5t?0-4i>m*?qL_*9^GE+qQ) zu)$`l-H1vx2z|LGPmQ8!fO*D9DDI@Lp7%J@sYFr!odmif?) zHQBL#B+HG%B0dgMi(HOJK0Zo76xpsBgQkE+Ag_DKr9XjD9GT4%+?{cS>E>g+^2GQG z@64X4)jO|3CeF0{`LlUH7XGJ)l}D7F8cBCb7tw!Q4H9Jw@Z?rScnhifz$1f zosBNA4o$0le#%wBj1iE076nA&pvnDsRTOiSK?VX2uKQ#vE$m+DK?(1646FWWc9Z}f z_g#~u-(SBHsB)_Kv5KCHJIIGAU8i=>!Py3Q1xqD0)^D#b*{TZ0Mr~VG_7xgRJYt4~ zaY?0@3Ttv>Ty!@YipU1B@y;DHUcOtkUpZ{=>xji*^JLLn?+oLDMb7o3ChqcmVjCfq z2^AZVmR$&PZ_?e}t{qQZ8l_;ub7b&$Q2%TsL5^7Gx5bG^)p)I3M4X(>m!12-7QRZN zq#CdfFUdJo4HPG+@z4Q{BKD`=;S0|>x4u%gjx+d~>R_AS5{u_qRi~T~!H5DQEG0#h%@)4vuo8QtxVw%RG+?Hx+Z%&LX+oB+rQ!44AM=!a2Z;k}h`)VAE}rs8~7G zDA9CcI7{txZJgdmq?s#|(N|7)9Bw9r8+ZkQ;1|rp62?DbKuSG;F9masyHh@2v7D&~ zoA;ZZWRixf3MU#UYZ}fVo=-%+* z!8`4lvvUc$t&CpWGa3pP;RbbatAfbXUe8QJZt0-mH`A?BiXxI_@V5knd9-Pr*_tP2 zBa}iQ%q|_)k|lc@@%&K|$*ClV5Dw99J}?%zIO2kv^D|N~BI{LTZ&k3P0COSMNy1f7 zBQKTRIb4~*H*o<>bWC|Bp2e{ibIq)5Pp2XNwWCw{Pi;wjWbaYj{s_d)S*YlrR{BCR zYKjzfMD7Y9X*M+~y8WX)puja@E)pQRsSW{x~#&$6|8#b0Dk8hDdGF7KW`n{mf#qn*(vyHb9|}EP+{|QHug5+D zfYI~c1vZz!!70c9Sy3Q&onyN2%V3vf+$<1_Ib_stbM7Ouh=Dsxr9W~Z$4yD{%6ib; zc~B40oEc7Ht@AucO^f^$L6@PI)QO=(rTDB2jddK~W65sfJeIe+^p z`bCg|0n%Gmu7{T{E&p-K0D35EcK2tu-b2+$-#>Jq56SEf%R;;L?KKs#*gE79Ubi8$ z-Y`iGV4gr`E`jc-*{|yvIc;P zj;Rbui>lq zAw)29*djG`#M%AGB~#4;is1yesbXXLh}07FI1NeW4e~|sN+ryRA)~ER&3XB=_7hga`!48<-^*qUP~918PG%Q^s^&gEptdVi`+!Gyj|V_&`HL8{&w{!dcUXnq$U z!rFrv?-7D;CO$R8P@q%+&;^M3lq>32Q^m8+oaMu7% zDqm2blNR4LY2;xCDrZ~GHBwX~eO9qFvkodp8r>8M2pET0QA*^=f6WTM9~_MHZ~jKas%Q`@_VReAC5UuOc~*#CPd zrGJ%e{_}$VE1U9P8YBSvLcS}XRu6gC^h40Zn-AS;j7NGd%qPh8)>rzQvdohkeNb%3ft>5E(#2D0|6dzB|3Arb8_eRls{U_K9{^;ts*Z$y{KWNx zf76sHctbMyy56W;g3o)2yGS)k2-0LnwdyP_1g9&GA+rwOHVTwM_i{alPp8_9o#5zSJ9Rwm3-rIg3NrC+3#@`=aX*~z z_j#h|;FKQ3HLs+l$T#al0zC3iTqJ=Zb4GNS?2}dr>+RA2=ic9{@BCf+9g&VPO{Kw@ zJ}AQ$*;}PbX3MY1OKlgM`HNB+o-Znv{UyHB_mO%9n}A%<7l4>Oy*Lqf+{yLFUiVS%k3l_W9dSCb3soOfuxq~;izGSvKmDu>r6 z%TOum@p(lA4h$@floRRNDi%r)^~lhN@|_F73m;0VvXFF3nOBZ4^{B?>IQUc)bMu1$ z1Gn_W&WY1okytwMvUP>!DWmrwx(Wb608D(==S1D@|%Q-hyLNAHtEjOShBjYcz zqOopgY!>OQapa-6U*p~G32#V4H>Dd-eI;0^G8TOB9`qDxeRwB*<)VG?V4~>>;_NuP z5+mjGGlhE=g}2ymaq*HMY%hcj*HT=>D5|45Mm;?F^t{}y;oCpD^l0INw5m0-)ZKlbs)dek(W_jXVve#2|Mjao z!2Ihw{b-9W`)Zx}=N3zu@G39b^2p}q6%KZ4rP7uQJ1n4FJ16FW+lRPRm*yev{gh=2 z2=?f+a0!W_5e>tSDqi76fO>_3W7b9#+-7nl!}Rf3Q0r{%0|G=Qs6`LicD}T>pOSkr z;+0p?m-^2hy@0oQ8o1~mR#}z)CX#E$>T)B_CEJnxdx{r0pXFM)571kEoYNdkld5#V zwf2w2wqNEGy4=f}#9uZPvR>lGlC!pb6uS#uT#%xG(KZ=!J$tjG*JXMh)dN!Ihw$2( zZAeF)T)-|zmeJ;dJ;d-RcvDJ~9ppmFs0-42m_tPNy{G%DN$D*XQuIu}9`!6JX4cML z6QrbnuD29#(3ITsGYR_DZW}I@kNBu;SX==pgvUzv#K^^DTu|ji#f-QZjr%!#-E^vR z^}vuVu)%?^JuzrYeCNml#p}byEw125ATG!e#5@gob`2!SX*;G=Er!vIu~k&Ltbp;* zP73i>W%IJg#gKlbyv4hISQX9^sf^=#HIP>f&YVcU!GLQK`irnXJ<6@VP83vU1!J?S zIZdxa4ypjn0aga+4)88J>LOYST&ePwy&TKKtS!7+Gg^P%G99F~Vi`$z) z*2bHIi)?&wB5$e1WR1vU%mXQA?;g1;mU%~4ON<2sEtR($Mi9=9o{A&V>?9GnLE7i>gxNG7KHidEr#YDWb`{7vq{q0Il62cujE$uT&?Z zIOq3`5BHZax#b$AX(@55W6ELp-qJ@h`RJRqLTM8rI9M@3GLz|)U++6?0& zNrV}6dK5DluT0{X*O8xaiy5ikci)E#64(GB(P-?7$zk((t{79)xTzxD?<>a@v;Jcr zw#?1Yg4Ao`LofP}DRt1HLr+R$M)&6y&op_wK9DId+mNl)r6LD!8=l$5#FeR@DvZbU zOp;zr*Uh|H5t92dL(d&YvV)AhMbV`A&&4thv#wVJ9`x%qT8e!~o$aq$-x<>~KI1cI#iI4kWPBp2kqWXAf& zV(Q>9{WnMN$?V(+V7;T0pPFOTngKfjhot=UXIvkwM1V^}Kr%h7Jj$MdYYzly(0Y2& z2VD*sJ31sU?|7hOb(pJKr-`HPo^tXT}UgrS*ZyABb``$F8w zMuS$XDx_VRkCT*q9WlY+M&)b-kE~O^EBB|son#7$+fKq!q4^HV0Js-EJ(tj<_)$+l z$E&la#_CZ*W3-bRslFFJ5`*&5iZHtKH?^wp3`9C02$={7-wVWR-6gSFpE(@AIb9UH z-*+D2vwYGDXE2uZbx*C>CQ z_tA>GNip=V`}yiSCzD)1oW>5i>{nEpnb9fE@iR#wc?Z&$`K9jey|~d)oL+6m0rSKE zd`byue69^o2(HLVutQbA9{XkT53Ws-qow$S7AUY>MkaF4Tn0 zFYfGSYj|D)v^el=4BGi(-ZH3QRUrmUR9`Sn^hlC5*{p-){pk4^mRD1QpMQiI2|&a( zzWgRx(gvN=+33DKCzz~K6{K^NwSESxJ@pOSV%C*Yy$;yQmI-unHOS7-<)+KZtsnY^ z!gHw?c*)2vm|rnMu_xcg!&31D;|`Gw-V$l|q|=(ijj*YSx1Z|;d*K2@U2I+!E>DKE z?}~a(to_UZRKG4M?fp<4qyWH2h#8~%yf{DP&%w(v6$wgmzkoHn zWU!yDSfE#3boJH>Beodb9?j9SXYMuFv4|<0sZ+A`-ZDT))|k)%gZ^aMMZ>J;jIXEP zq6*2(-vXh~w$4jG`K-r)1715lVpjAmNqYPu?CVOOhFPD(#oMr~ZQ^{p=+g|z3p}66 z_*J>6@y$6@8$9*CJ@?$(vnHDZzc%ML-~uHwHS^gMM?{JnT0;L)_(&!3i-fmV=Fe}P zeuDyYHUOAMWJ`y0*gdNxbuxvLyU@2$cKg;@YQ41EPQtm+D-WYV%pxJ57 zX&HjS{aCXAkkyvj8PZp$o5v#pC7`Y<76imx5$UM&kMvV@1)gkcWsxOMvme zHF*Alt=)wJ5B&LgIeI8%TSv+mxM~HRIIC!GquQM#sbSX-y$^%+tZnkSkpR8k_KB44 zGyFwTZ4!xhwZA6p`q@1_tL;4<%w{dSpYezRy{}UQ6(xeAOI;$E44jG~XB{)o;KCB4 zL?ffxmGAPtTTCWg|T8(Z(Lz-qVprwtoLqYb>* zimpJoO|*-D`C1u_Ci6lk;CHs(pe8nZ?Opd2XL4Rm$DS~hN2EOAWf)c_K;e>h?yQE8 z7^ZRb4|4|12xrWYON}NIACeCYMgGoD&_CxA%*5yHLsajYGAjkD0@rA?A(sjgZ;25~ zjqgb*BFf*}r}Mw?Bh6sbdFs1XvsGHlu^Xq+T1DRIW$uBp@Bn;jw%OWVrS6G3`YEwO zBz&gTsX3lTWV4UqYY^(WWkZ)vsYPwd@I!V><*j!m+shkegjzKi3n%>S22O?0Ow5dm zCd-RZ>WO!{6hmJ%sh_6a$Gf|*ra40)48v9yf)--~d7#NKD$&5Mq^vPaS&@;4&g$?S z0nK2+yrb>9B=>2bY|`j{s`)KKtQKzE4IVrDMhWXCfE0qe;XAUYbl~a1l)C^zJad3L*n;prcsxLjIkEuRvUy&%-Bqj*# zm{xADO{Ke`)hje`cWHjS5@>C>tdoeJPvS7Vm-Cd;l$QC8+#vPjpGtoOA~=|UL7fb$ z#;o&x9ePBx{L^tx#cTftFrV%u>KKJb6E}w;=tElT0~eafkeFZt-4$<5mIl2 zGDK81ahM46im`S9=t)&3>*79Gb4qp|=|xV1g2`y0=Um#t4!xC$)c1UI(b>41GXsiMV*SVb_GEVNTj0)})%790$0nuxX1RwPP(iy=292wos1&jnk# z>I?xswB&B%*}oVi5|qI6DV}S@+7YV9$iJgIHi>Q=N7(V<)Yp%aQkc;Cz>*gO7-hv+ zK8clrejPm=k{TNuO>z|Jn)^^T3<$+L6BgOwd0{QU^k(jll}AV}BfK#v?7g!>eKkj_ zgnOI(jv~Y|H$%ky+ocgSeeYwF8zBDF^{JG(JJ3fWbb3CN=+rD;=7#2L>Uvctlbsd~ z&9?I3D{_(QA;*cBURJ9Em%JP)wGUR4!3Q*Z6HQ0t2CSeqc}kYbPMFzyyl)GfZJ;;l zP~s`k<3RYT63S%*WDLqr=H_FSmH3f5=$`0yIB03vH|`7r_>G)5gZWovIVD1*!vpvP zmapLpd6xK42sNn|0CuqZRT+g)M^Y!!Yp*6?o1l5Sl3H(6?&PuC{IM0Kv`-`WJ3h`t zV9}$Yynf`$&6#50Elf2bTd}~U9d|(g>`NsS^30%AK~L&9Ntz>OimE}Z*BI1TW1OM^ zHu7Vm0E(y2wL@dM-1xkM4Ek?dlS~_IR95k7Pqb2ODMA+i^Kup{`WEj8($$dCSpfkh zrfsm>*Qj;Mu#=SIqDX-xyiSf*VUkd}r7Jso)PAYI_G((^TzTlSCub}Z@rkIr)F*psF#J2)5X1!M^^35yVesI?z0 zv64W%8SyKmd1_1g_W@-;0G)ioHS(}0%zWZP5l$aEC1o0ro@}qPrRw_`z*=mbKOOE> z4P#{vgMEfYoB{K;MyZCOA+JJ|=!CIUD^KsveE?@cUqueY^B49-K6p_D?ZHl=5hW`( zyih#(LAPcuti)M*#{^8|h3jA2bfUc7nO=F=m(J_d` z&-G9h+F(Y#mJDFu7XfOrVV(D$qSuleOak(geAuw~m&&C zyTN0!vgf_HXyhWdqY{^tI5uB3^`Z{5VK`g@dhLw3M=zC!TG`USDqU@qi=V|cAD`e{ zzWVKLe>=&1VD{iK&GHM8T2i;xH&GDyeH-W`jddz@o#Cqrxe#be`|QSDi_CF2IZcze ziOZ>SinTd|z+LG%{%su-LUn4{d!ymC`y*~){ds`Oj zZYF4`)pThyF?}h9=9YW_PxGj#OsQWP*|kN)6cfq)WYPl19@dC9m6IiX#M%;-;@;EE zIP^3Ka~EW-aEc_TA`e2RfKI26^f(jO>VPtn$)2!8X7z(Q{DgtkqKM{&hEMf`1s_|G z>^=wBfkE>td6fGe*uQXYz=I%odP@OyokC4)K1+dw*}2xM)y0tH?PSi<9cy`ism&j& zK&p7=sW%FJpEwbT39(p8UbuH~2Yl~B69w9B?IBq`zCjR^E6Xx)e{?)HF=nJv^t}^C z$h~ul-~l)pyn=Jo$+bTqUklt#810+bGRSc~qc?xBhZx*osSxvyaU3UGB*?z_w%eZA zmNr&+T~!N0rsgql6nzw1`!6+u-ZJBWj_$9qqJ^^HLL z5Zyt6Mk-C2R&DS@f7csoOMa`7MA55EB`2gu%UTx7=*g`3^IW) zo`)@EIXa-+pliYB@2PRxzG3aDEy}wgoie80o;Q^T9orHmiyZlq+bbjVML9 z6ukP8NlQ~(R_EfD7UOrZ;$NP90%dJGzqJRuFEw*In*^*C&gG&RyWKLd9w{y(3u(Hc z7+APg!U?BO!WY`OX zTQYQr9A61&G9`A7rd-%v9H*T@2*eB656gItF)iq#rno}8h^0t z>sjC@G^V|3l4F;Go{;vKp7THOBYt`k0nD zqw9po!Q66S6{3gS9Mi9(U58W@J#eyGZTo@FENy+b4h?a?tH&`g>-Celg zc4b3Q=SO(VyP`LhSIax}yP>+EC&edYUUDuIS4j5+*{hzY4RDX(%$>n8uaWMbi?{Q` zX2ye0SWgvK%+>QH6%|Zfql7|zgTwedn+NWbR%+Sn3-ASV`22#olL9i1^HYFH#$g-v zfTgfD2W|(lRMrQ?D}v^^kY>`k7E_Y;d_bO3`h5kprw{%$(g}w_w5GPoFk71lZB`9-C{Qz8}|a!S4U6-bF%VU+31R zO3x_4nKkSY=+$j^5%Py!_}%%JtgJ1WZVom%x9jNA+*)TKN}E&6oBd#qEN1?O6AF@$ z29tLng{+#5fbtFW9BOP|(xJ7~a=o&czzXF^3-Y%C#>ebU6sLp`8$|HeIir$k#JJ*b zrXFVqL&)%oK?|#9wevberRR`2D)DESphZQb?T^;L`Y~bYo<2R(ji4@9NFRqzXx95z zJ=6!Uw4OEK+)<|4PY!!1+2@!X55N6gk^DG2Pn)yJK1GreeZ9 zrFmIMXDnn~(#9Eky=P1gv-8+!s5;f&=pK9FBrt>wq1Umtuxa95x9Z*o^yfLU5LF6~u7e9eY4=$ACYa zUgqZ{mVu*q+3*y;03skJq3r9klUvuckz0O}-&yFtl_BYyHA}?88Esy&K-L?L+<+>3 zK1VpYrt732nIN)->z~5O9poH7%=39p294CQh!B5baelQ!64H{%itRXKb0J+_EZ0<$ z`BX~tf*QqeBQRbpueDJ$tju^1 zCOlanVF08!gBe6cFvsaW3C?V3BaSVdv9v9C>pK33lLMugeK$z53uwQ2G52{gbB;F- z?iHYzeGy>_LW9)-_q6Na0iYt5NRXhbqQh0M^X26)*O+WIk$a^T;5-d*uYMOir_Jth0!F$)qNg6V}_D4yT5f3QF+_ynXf89z3invxU96XATzc?AALJE*m8;dt)@G& zhH;EHDU9<7A>!Y&Q?x%%Y+PEJ*ro#Qbyc5}nMi(l_GAJ3olhbNcH9@rtKP5x;Go%G zC04Ti%aijjQd>s(hET_Xk!4ELe@~oR6eYVKV)ABJd3VWXt1=1lb5gF+iQ;VYM9AucvG6wpfZO5~MNUinT2stS zygS(gEK8272~@9X&`HV+9QD16EHa%6JeygFMPPZ@#w(7bL@jE$?16%Ig{zwi7@%L| zzOv7{%7cgLAbUa!DIp_~$=LV}r+$|5vU3{x#w|`nP=G=f^*k13_xFpomlP}>VyLQn zF*b%Cr z?xhv0Fq4{#QH9kjoaKxb^ zyxL61sZRyUxVgwtfhl=R_{sI9^?%^N=hhpVeRb*+m%(%1Vd~FLC@JW`RZWsAp-kB? z$&UVh4%dGin*mdTEo%+77>hn;wPKM$mu7|pTQm{l120hw^&`~t$737aK&ms>Z7FxY zJCK!&+CYE*{Lq_0z8>urJA09!%=^>JXnm@U*Mg@C4Dz1i!+JpPc39GZVGikpdTTRVo6x}FVzUOF! zM?(5T&KQd7e^c0{jkEaz&Nn@~#{E8U+%5!E4_vqFB(BhfGQe1y9CJUH>1iI)^o!Yv zfC4Ynf%VjLQG%v)pV~1<_yQvmAgGD2be0>B_K&w2@9yqO25B~V=#}JVi&4Amlhc)` zm90VNTXHrmv2haHAPn0A%&6C5Vr1vJJX5Ghtllih{oVJmw7jqtX4;VYA=^SHl+5<9 z(M)d7v5rVP{0LfqUUCU?8i)y;tpi=cDBOsf^PpjK-`RffYr4F4;xMsg!f@ISF%{R2pDrhBa2UR$QR`}Z zY)44S(geaPJy_irGbm|Ym88HLG@Z`Y!eH!bjos>)u;>fFcIzWQA7!n_-MXIsR9RDQ z>nUESWks|tcaT3UvsgrVi03VuwTNt%O&E$Ik3*9+D``sN!)|X%(wNTps#jW2p#q2Q ziHs%p=t2YDLM771`H^C((5w)hYgjKUTHHh-H*XftO3Xyt;@uZZbkEa}2bAoNY+DuG zbvtS`_M=H4nXfb6FR0o;GTKW+4@aWfrnAf*K9l;F9sKfNd{ry(8iF$O0Y#0KTI(LE zEugffGcI=2!6V+`RC>H{lI=NZD}Q=jdijz#awscd-z0v1va>&^fXh}=sjEZRzICQZ z-U~l&E4P4?4e>80*d;BB#rpoE{=NZPg^1wnmo%YQ!gQC`G^;9~msh74RNQttQ4^kQ zZ0lV9yeT$S>QXAH-?)wsyf(8sZ2%KT>^=PYjs3S~2p>)^2ID&MAksvPX`)Td^t%Y! zL&JiA2qSTDI=G_~8EALr0cKa_=QeiwC(S9WQY{7jGvnxfg_O0Z{2gkll)ayeA@HTP zAt%zwNPJHc*Rc80sE!KR6^@-cL@a%+d1~@scUI>)h{x zP~Pe}jF9^Z*>b94>KdRd3v(MkbIaMqfSsw}0iltg=s)^pn1n&bAaTpyc&Mo#qZ9^ zS5wIKL>h?SqCM%|)%y0_tFbQ)ICgdG%ILno;OPvhWC}X*S0wkk&;5Pk5D|m(fNQ#o zpb2Z~(%*j+u{rEZKptgqdqn`C0;I`F>~`I34o`08ZhUPm<12U&F;#^dBnwi8Jgy>( zJdT2)WLcee(S&O>`e}VNq>kxOtx62ziSBJgUOimRW22z34H71T`={ z3+PlDJrs!jITbm=Jec_BpcMvW5zMP%(Dwx!1#|-jm!_9KHGF%&0+dCfIiQXNgP%Cnsv#$~XSb>U zX#5s+UXh|}ZH4K-4T|t-e+`QE{{m6}(?I!ub-4QfB|5=9fV`?xfYfy!^kYcIna~&> zV)6V{yssV5HsSZ&@03H${$*zbbo;wf=%2Xi|AXO`mQV6_jNlD8;a?;EIwB+f=J1hSO%PZcm^o%hx2FNi$vxk$bXhwtXP6}Vn^fEh%BkdD8X+B@(oWxA} zaHW5HfWf>00EqAZUuo9=ZF@!Q7QD{{fNxdx7j$&hv9U8X*qj~08E)jutZIsa`MTvQ z#GnD}fS<^(&Os@052^poFJYQcdrf80eT8>=X_u+_Js8ZkNLyCHh}UIZ-p_XcuGFft z83uRiWfC!u=u`POvY^0^OMeF~dsp*7|6O3xax@#ciTrbxSxlBARyDd1kwNx$WT9yw z^Htq4iYsF|v162~bCOES*U%5dfudx>BrW{`G_%Do;1@VArY6Bb_2&Y9z(&tp`?v@B zajEC#K%!^wiqk!eecv+#?&9(Df^-`(c9z62Q*(y%)<75Jr>|1bF2W)z*DI~7$~S8h zma&T7KW(MTH0#i!wVv{J4nQmK!N4Y+n$OQR2g++Dmq~L;XwO#9^crP5d%7>ljZIvs z6y3RIRokXaj&V>zllLrly3i<7D?!Cmu@Qeqno6a`fTQ`yj>#9h`kDAJakjC9t~Yy? z)1`&pSFsHGm*3v8zOuY@iww~!K2Dq%bjU>gnmLC)#F6L4r9_Qacz72<2?8)sp*$aa zP=T++uU}3g+x41QMWX=HqK-SUb>wYG11XT)RA>Hz@G0%x@G6$JIywD)$0&rH^=&uB zR&Q9|M**C2lmyVxrLfo|2dGO&PVvh3MPryu0 z;MTCG&~M8h@G=DOP{rOJ<#|}eEo)wkWQmY?Vsh658B9>$5@v$S`Iik!NMExN-GF}p z=n**SrB}S3qVWrQC=vu-eq&K#d~$@+1MuoREIK@F_AplNcI}Pv5dQ%Vqa##RT18^0 zuMxVF`fU|Bt>r|4z%{k}K`#i7TR6FDD9jZztCb4zC#B0LAzGV<#Tie0%UV-7aEk0P zS-fWE1nTr&S9S3SeZ4lQ!gT=y8qYyEFlbzrd~)R2s~LtAVo1*oJ71>1_HG2Fn2v<+ zU`|%_TmC7p$=R967IQT>Xatppbjva)imibaj{}5bQJ#v57dp z0V>*($&hjCK2NWarmkG_w&Mre3yl_q02tH;;$fo5X&rMJ3$U2)2T>y6=4vZbbZ@4b zs6tv@PgCTxMeG$JfCLkg`X^4@hCqbXHM$YMy@(uGF}IkKTh+p6RbEWE#e9FA_$(o)}y9sLNAcQ}?A0M%}N2l#eGntFO3 zES>{(U%_Ex0NbJ{R&1mB)|eZKWnPX9-i{9BCUGRcR5GT(wxl!*ux0`QxxxW|CkvvicQ zlVSo!x4&z;Mpv%qK&Bl$R4LVU299L3(MINUFO6ghN|jJ0N!u~NOd2vvp>~Wn)&g$W zLrD1UN7TZH3&?fVX|GOXL}{s@!lB=Dp;L|JCXTLZH3g}eFkDN z0?#k;!|>XSCHLb7gOa;xT60%eaat=(ZdoQ*?%NlX0_-WLGqYO?HEcQ%y|}OPGAUk% zIU6{yTQ(~7=W`V8W5zdVp`)rgFnyR~D6*p~Ejd@;KPyLUOn86$D~0+E zh(?R~(zRPzkF|$OU{w_rZ}pvtO<^nUZoX#@F699MhUl;)(58inPkODvHW>bopZS>fja9EFe?LP^Kh*sBMc$3kFhiCh%TQg*$?e^-z=iqXlu z!ON|o0HXnhe=M*pYv*ftq))V_V%AeGKwlL55eN~NC>od-&9A)rr-($g&4utLuUzMw z!f348To+iH3_0caq7d9u#UdK%OJgj+$5CfL%~!@Dg<|kddu*Ht$OnPCPa;S{#?%Zhlpe4>oh#7EZGs2GHi3OnWl*d_gMg2x&Dum+LkFJB>j&r~}Jh z3ol06H|3LJ(liH`s8i^ubusuScx6dF#pR`dJ~hz%sH-P|7_j9BR%zp4Zh{Qn<@D(Z z+o_U`D5g@=tJrYMFl$Kl1@%IsgTvFR7a6m8(V5mI$*4VwFk}4KKfYEHMBN?l7KG(> zL>+x<-LW&jrs$~NBf*NPj02GpWF*Wc_km*fk&E zfTfuay9dq5Ae|r<>%#XCX04K}9CQJLWtR6|zhzNQoYSVVQRi%QGn)6TKBC&@Ufru@ z@|MyXt*(x`bXRt3QE6&8VvA&;zXhi+%NnbVgcPb}T9Mur2v=ed<-X}z7*(wJg^Q$)wCM(Qx|Z{%|^l|{?v=sH58s`Xj_)-i!sT_^kw zgdO%@p#Yv$rwC|2^~Wk{Y2*A1*A(JH5&^@oDCAK;Y=+vFb+k8D-mttz3KRlWCF&pA zO@!jh(UxabW{04aZ5vgGArz5^_scL!F_=H<0f=h)$na3RBv3q2u{aQ`J!PSt-54V3 zcV-JaXMeOu+tc&!#PW|OZm&eR8$hFcm9RmRJ%=?s zhSi-3BS-4zgI~nPf=gx#f)jeyB?o=Nn;Ukgi<-snIkOe8JI4B7?7d}h9a*w1e8iG1 zmMmswmc=YvY%zny%*@P87BgGS%#0Q@lf}&Zsb_k|eW!b%d+z(UH!9Yob9Pxjh9?;JW@1)ckGXhh}cZ zAg8PD*5QJvinJcDvve@q{hZU3HuBp#|41?LoOWzcgRV&*ghs^>-RNZT$t|_k#5q2a>v{!DRDX<;yICE-eQ-X`0gPenXbYAsN3D+JZD$SzfF?2+#xH z&J|&ZH%$Y#oMDW`r`c05SWGfFeB#A+%vQ@sW)YRqTcXLMoEP=fhFiiWvHpB{JfcRC zsDT(9uSo!XfVq6ae12lQ2pcC2m+vu#C>B0;bm*u^v&bxdp&3Cl>EYw_uHXe&EQ~W2 zxs*CFlvfo2$ULryJEBnOam<0~1L8NzFG4oMpFGC)x7wHrGC|XH$LvRj9lvkTKb(U5 z?VbWX;y6qZpKat2Z*B#CNi`b~9p-w0F!GfZkLoy~c1dT-*$59vW4FoMz~d;w`0Oe< z{6SBRcqv1CXftyvS2y+fbaBLAbXOG>&A2D|>b5ROoo~+(#z%M}P#oi+~H_3iu=pbV%qP zZGWa(?Hz-$II?^S??H)EB3_yV7R4bBUOL5qDwcIbnoqb`rWfs=+w@I7V4MD%hE1Cy zZW90wExatbM`n0~wFDBTb6oEROiK6Egr)GB-(_*stb64y2p>+-t8>P@9Yx@bq^|tvGp7)CN;%qtam-*|<5nKw$${)macR!Flbpo}tG{zw`!QgFw6ZH6=zsa89SP)x8tM3CX=_9{AX=W^LGMb9tEAM+0S|09LN~O z+v5>!-yr}nEFa?SgdJO~jLWr9qQ2#kem%5oo>v1(F29f8EJ&lJ?EOMT<4;`FhxWWE zUW~a0)I!0(hCRXQ^6*VuO}m&?<2q!j$w2t&hzXjlUPhNbQ#(`;1!8JX8*!qjP2JKPhx+_ERv)X{Ud5e&NzP0bMeYDBig=Z}!t>U(is+OFi z$`y|yweEf%(9aZyu>s6iJlc5-(#@ydxb7a4qQG8^{V6;8xRd>5vC)TWAAq$NW|#W9 z-aezL;`HIz>14%p!5{7cIcWOrA--jo;9*>P!71NHD^1_Qp6o9%$7VoMP911w^AT>Zn;3~aKkzaq;M z>6YLzBxah{rpB8hE~n%&ilu!JzImisTgU=TFF9~{`z2);%_XJ29rG6;0nCHkrO>LB z>EY}m+j>m%XnB3E`CNY|Gm0Z~wX}-#3*WkK!mV@mrO88Bc1FoH`%wFrM|I7c$nL-Yd3EGtQqa1#eEeG7&$n z@~Y#W*R(IL`b6ED1A*QP435(Zqj0cT(2Lr{xtb1j#w03h?NnYGwG-5uO-&AHDJ(sq-@wam1IXbbnDca1eWFnFZ5RyeVK6m$UKL=Ny7_1d|RKks1 z9~GEv2uSm->GM48F;;pe0B^u~_bnGiI1~4q|1+J=iZSofr>pg4$NlloUxd9{Q)MZ& z;&NFfq=x$yU3f`yN!WH<_??1uz)JYhO!Nahtq<&rd01O4|LK}sy21#>$So> z>=(+gp>2KJft%5rjac-?yew_|`YJ;D3c5$3n z!#t?;>IE;%t*(Do;Em5Y*67YNwx^Nig>ou6$33#KhOi-)U(3mgndn$w?6P$e-+127sQ_NrJvWZvUM2l#^H>L$5h6L0UaB$9Nr@*7e zwQ%H!>O(Ut9p^G(tUVqwh)fvjlXyOe{Y>$XAmoWL9U-WaL83oJbd^UMN~R z$+Vmv5liBh#bnx^iR7LtsSgQom+W2p3aouhliRQ?#iQhoyTj@Pw4-3E z?Fz59W;#u{6*P4$1C@P5l;X~>DnUE)BR-FeC%%1maP;EJ3gE{CwIm95yqp|0xh=ii z{k8#Y!>h432r}hvo0y5FZKuzY5?<|PhPAs+?JwS?*r3qplo!(bZ6ZLlruLni_4VkOKKBo3L9 z0{tX=12J%>d_D-Md59IBwH^hZjg|D;STo5pX`48h}wWB}tm4cDBSY*PK#*zi?TkSHG zcsl}&I0dTOw$GCx``){*b~ohNeD_3_nPPnzeVgaCRPt44_ziF9y>%m{@uf#fdV@}T zG6sPs>3D7kR&M_PJx&)|OH>|(~cOpbbaMQ=U0)4B{-P39adNuz)ckB_;1 z8gAqS6fjQv$^dMtj*L-nr0O?}G13tkC!2iK`_l)MX3RveV=c*Sj|+g; zA+z+x9$ciFxGPQgN4z71LUk&dPkj(8>(COmC*-I+0<2V|9Hv~e>B1a67)OMoJ`E?h z7p~kaF=i=T8kD9`9X%1k$Jrf&$Y19MdT>9J)?Qf)3w6b8;!5T0KhaNv@dD3D>%IhS z*4wLK+DN}esW=j;>j^>s-T}Uo{@Ocpu|nsAe0eJf!KM4ubO~6dFj63KU!n1-1R>AQ!M&j^!ayV;J8li&sExfo74nY8TncZ< zZ?vNVU}tV0D9+l$DDCx34=Z&0s-F9=4q=l4-_RQIY>iaY!J^jhX$^{YlV_Oy}QQ`wM`}= z-GnkdH=0GbqU+>5ybd}NxXiL+mJ0t~o&K4Thd(~Vr>5Ixc^U~%iCNoEFBnTH=dtOT zK-AL~qpyk-%i7qFk1{S5+2z8@7sJ=X`eqibHXm=n0sO$y-YIbZeUDo>BMh`;V1rX< zjo>D3`Mxr3?%=0fQa4y7H}-cM&WH=~f6eI;M4J?VE{pmeBj}hJ+{?IuqgZ{-67q$i z*4=!LxSny@{+xM(tQcWsqI&5gny<}+C*cJe0?bx1dh0BPSo`NHWSN1_eF3J*oV~sn zCv-qPf8-GaI%YRv=h^dwGmT zhTi< zGVUI_8&=n*qtqr5FcQ|nt?Q?xrW<_xn-#s;E~at#w}^MC%SXadMcM~if>1VEfNSv4 z9Vlq6YVwLzb9eXTXt?KoN(Lb9uV3_D3pnLhC_tIn01(L3q;%2SCjc+$8Of68ul+U- zbZ_!<#u0*8jHHEuuLpehoG2V_7n4-cPG;x^7}gXo$` z;F#~GUu?dv=gehYl^_8<%D-oSAjZi`GE?3v>bG}cE}~{yIUwyZ!csN|_l49RKux`y z5IF_ef5@H)H=WXLUR{h#OP>Jx_P-5lN1vBB@(TVgNaoJZY^lA#{ZQL=QaCVaPH+Da*6KM)4uf zO|(X855wJI&V#Bxc}CS*ey%lSRf{Gl_w9YXUCD5X{YtG6l8WIv2wK}qncC6~T9b>i zfON7>Fm!309`btyN!d^GfuAHMcrawirmp2OLzo_umwXh|p!R3C zduEv^VSJ(K#8|otA1$18q-m{;vXrQue%pbgez-)B0qd;<|}~e`-%$PNtbckrqgVE)JitM7U@Wmm$JJ-7Oro z!BXsL^9D=Pb3gPberdDrI|GWauQMxdlh#{H*5_hh0G@Mi;*AW)ptaLl5ZEzbh0s;l zbo?NAXhQM0ww~VOD}+sc(gW)sb9qYeRs%kCcc7Q%VlN~IZ~iFqfNK}@=|ivebv~~Z zMEyhc5KZmVFyI`n2{A;8iYfIrRY(X}88Z%(`r2 zHGL#xZ|ox__>C&nr^2`kf}Ho-$~x<*+!1^C95*DWsi$3#AygnV(fZ^_LmuW0ZCwjq zFhYVmd2k3~T=AZNri?5JvJg#>$Ld!H&RJ$Cv*PzgSurSBAO)!vgNk_dt(FB0c|6H) ztzL44;Ds?-yfye6b2FvQzZ=}9@w2VpnhBG%dps=*6fxLI@7pZyW04Z(W*tXRW^Nq@ zXE)GSY*K)#mcT%#*0Os()1wRDt+i5<%7kStl4rLYW|E1XW!zAzHRrPA6=Hwx;xKAU z>@7o68uW$p;6Ezd#Z>634gN~L@Nxym2R_03{qee9Bxf`5K~H`kC?uGYpo)w*ULBlv zQlc{4yhG!{@bl4NbOzHCnYGy*)Zm~~S14^93}huMZtEFRaP=E^k)~C^?d_>Il|_Yy z252*E9E(v96eV#!nBS~l4nOdWrmU06m;lJWLb5EkJSxFNgE=sP85B8@8NLvt#&r^& zY{{q)P+gt+AXOPTPC7!|lY8dNZpSyD5#qr}PD}c0tF|XvA%O*U{v-zbi7l9%dV)=G z{isaerhR%rMgy*828&X3$%>$9L$w6Ys-X0m0p7+Z`Ur}bK~ zL<&)io0F-H5_W`apLu3s=M;@@91$Z_S86r^!`_ibNn8gL7(Sc(5HJHm&JPNqA6dr~ zsQIU4@$XRU7ZD3kTSd%GOzS8HykyUANyv%JJMkC5ly4*y<82Z4_D`qtA+~ljs)MNq z=R1r2O1J8+N26;Igt_4o*xg1~B4A=Z4YcuSQNUFA2&o-{Rza!Ic_zt5Z?jmvtWuB?(Hzy@ek*fMmfOj~ z>GqDX?$$vUY+XW3+`j3_HWd*@aU|jc9}LaVh(~ACY8B2-3AnQ(doD^7h0RJz>XsVA zi0s}2q#TBR4xIvC6Koz*VtDwQiRcgW(rfHbb3$E55l3pLOFM-Z0|zC z=%3r@afH{Y)2`yX;-NHT3%@X|C(xqOWX2DD!l5@{gCi|T zrO1Ae>%%UD8f_DiW*6I%mQBum8vV(X{5qpj&d2%ZU z5XwiZ%fJTM?|TKI=GzdFumvHH_9kGO2Raqs+`@X`*$;5fOACgx1-5(5gq9!I)V>Z> za*9%qV?ajKRj3JV9_@ia{%((vfd(cd!Cb;t$QgL=Z)O`XLl_>gk#8Ecgi@(W*O`2{ zcEETEx5~oT3Ur&&@|0VoHd)idrSOmh4Z&>tfv2yI1le?~$fn^$v4^W6Y$4POWX3g<+D~)<)NzFa=^O4TOZNAf}hfh`cfVq4Ux6@8gj;6 zcmc)Bz#?t2)Rw$nexBvyfScA;AWJhsqM0i^&BBR{PXtqvh4m2W!|HY$xh12W_-`+- z{M1>W5& zov6Pkj1p*nEiQ88OON~Tj%pQ5yEXH*2nhFn8rtR8qb+{fR*ouh+^~rnfYuIR%duki z4ZfV?>qac(_dT~0R8&IGk_XUdX4P| z4~Q%#^Ve>H|DyTa zEZBpk6$fD6B+FV?eMT&xZS-rIXWDnc_0wLXnz(Fc4B1S!w*=}FYd@9%3IhNDIq)Sn zixlvgCW=G0J+1%rrP`(?+dmti@!HMqRNcx_34r*Xv%X3a*VCI88*iseASA=_grL+> zTy?3ny=6})04pFwA^XcE)A~ppj$U$7462zR>{7};56hiI`Whpe=M7TTMoQ~qrin%}$BNf~tOtlDF3?H2dPs&c0L1oxm>xXm_32ykr7!$8v;SGWA2VB@-*qGf z8VD}{Y!VSE%7RGi&I{wPB+7ODTd_8?eftj9*%s85d2JWr+Zr{42;sjl2kIs%Qd#4-%z;6E!o*Kx{&jV|HZ9;y}cR8w!y z<%#+9F|bD3^T+)Gpp{=Q_WJrf{F>hXsxA$Lt#a)(v(FXIc^?*QE()1=}aQ>1#a4uVUL&JTu^XC49+1Bgg z=t13kXz8xEo-ys3@py{i`Du{6|KqYKKiN=@FAe9npW8ocbos+0G00wF;&Hw?@YaT1 zdBytclX)Gq^Hb+g5uh}#Uq74QXm$C++W@`xcff|%fv>HS`2hy1_6jBnl*aRG+y9Rl zJHK)*K-E0FnAbr}DVXg)&>)>&(Ny@7h<;`1fA1xLI=(`=2cqXC(*-g{ez}w)F{K1u zsJO!WfetMBimv!?W>WpSwd+5;2$6tZ)BaptRe9kb$bcBnSLl!bWbD+x;`<3;Oj>g7 z7f7Q&6}i3+EzDN5mKjH=8yfIt* zfd$(9)3Scou&2N8Rlj97+27qw_?MJxnUUi~qbI(I`NaysUL6$R!++R7_=kIi_6V*7 zlJod+rvT)rSD;tk9{f(W;WNew1BWjE0txSRc|RHL^jEL$zjua~BE{_r2}p=o>nTU`57ul+m@_UC~Nf9kcrJI$ZZ27l_c z-#&=`3%~Zea{=h+TWu8 zuR7r`!Q4ONYyURy_;*S5*AMYOg^K=+ugMqqiCgV|r+qvfmubpm#-snA4KRMoXn)4n z{wUY}jIaHEuKn?Z>AyG|{HfRe)N8+VZ9ng!|L?@txPFxn1z;U@393Svosn4g^}}w} zetn-@1G&AH>-3sF{e{|i{rvcI1b6VagOUGj1o!{8s2bU?vY`K9kH7x=mi#Xv+H~08 zw#t7MqWur&nEwA8Hlz5}Fw?)=MeWMF?+0t%mH#g(tsOpxwI8q`y|1vZGhY8lCR0Am z$nyshgv4Kw{+Cjjg1_oD_OoGTQ&(i?O(%4~QcvNH9kNdHO|$xAyk|Ikx$6{>pcCJV zY5r|ZZ(ire{-4QXz8dV0n=C~TPpv+%=inI9m@?IajCql`Prk@Po2KwshOgX(9 z-Kpw4PH*jRcaLiM7lZy+D(g%MaH-+1F8w#m*X;s({1x_(RHf}7%W{H(hSe;3{y_Un zi^Es2Ka$B59|d4K`~nM5`P1&j`>Ud~pY1yL@`)Gcg+!#dfJxV9;pWC_zBm}nrNp5B zgckD0f`H&&F<#Sg|78vnP61-=7oZ`ppn|`<=sF) z?FkbG8^OUu94+1KI1fk*^LNrYG4t=do23LgpxA?D5g+Q0FMgm?Wd7BN|D_azQNsFx z`4@7(^6(Gnu;1k2A4zWhSk0@U#M@tQ@>PYM{H6*2X6V0K^e-w*lrJ6TRfQ@3TzoHr zq|{Nn2bSaM)z+f9e40mh=>G*%)P!C6b35FND~qh+voY8%Rt8B1z-*`8SFUQ?y8}z95maI%)j?oWc2C8f=a*Ch20BCsvy~@E~EDhA{^;N>t3-kpL zzKq=BY&IEo+lAc1r0(B}eLqs(b%3C&lJawMA67A96wd%Y9^kWr4YnfTor+5;c1C(RDT5`_-F#2N46#^N_C$7 zv%b(Iv99Bjm+R;+5*!B&m2reogR6W}jN_thrkLTZlX zyd^|zv#<){A)$k5RM*ctI38g_9<@BNs%U+2#h?iwQP`EhB zG+Vi$iw=PVQxheFg6o|$#j^?7V<;h@-{HsO| zE%P5X-IEYQ=R1_ET1DmhyPUxVny=F!AoxIu)Mr$%-Jeen0=dPt#4Eg2tIP>jwmvqK zjs<5^qzxj~IK}mLyi5j?Jz(OUlQ`6c-oid8Smb|Ec>@N|5=s3<4H&p4#1Od}OT6OQ zI`)#fAG$pp7L-0Nm>d`%+O?Vxk3|R7{Xt3oaQU?*%xiydCPVzw+)#0&>2O&drU9wB z-S`7_7|36=rV^t%E z>VcT+|2BmFAFo@V7^4}iIMX*XLpHcO}ZYCMX znLBGWAY>NrQBglWuM&{$0Mk2Z8rE$9)kEFJ13<-e)!fh8 zV9RnaF_}xCJ&(1C6#+(E1gTss$#Q%izq~~4?#XBG2FOagQ2It-_Xz?iaVO(?gx&K? zVS+{%qv*c$+_5k-%kh+AUMQ)UBl|PT>r>6 z?3bH2KOPq=C zBiF@ZM+QV})~v`)+fwpdg1p>16`LC;7K9+&JL=H~2{G&HEUcrwfaxLYmm+4ImN~{* zo&rphDU0*aAv0c86)xzi0O%)pfcE9<4{VXZhg?4zG|>qkX6o}6taJCnNdT`zp5*!2 z;d{s-(VWghAGMV@emRXJ6%vN*-5y$2i$MJt#>dTvD|a`fr?V@RgsM(Te+DaLMYkvJ zi8l^6sR%l(-DVbh+qvI4C~}i%!D`mH(wE)XtUilof)Rs7@pUe4Ms^+nlZhF}l0f=B zHDS1vg9|--P9w7I1ri}&9#Mi{&hC^osz+MRK15%`(r5p>bo}flGBkA~+>Lh}{uv0D zmvW_x=uB&sq2a*Hiz`R>6e_pg_0~vhPD|(koqkFS7B%futvH|9i9wSom}gBrqnkcx zxteS_zU0ZzYNc5~`5A?7WPac_&P%%DVD=Mh5sUfkZ#Goe-7CidMteR7)iAvtlNk;D zy#)tu&n{w9k=fF)69G$Rl1SS2==H}(=I49hO2A>%#j}F|{b)Q&?# zc$Y3)(vSHGVK{ZW^Svh-Vy7R)C$r}wC9bkNw80%bah&8vZ91<~-|TzCowW3kLaNpu zic8#CAy(zteRE)DTx9Te;lt^{Ybp}M#_00*%sb!SpLqz^po!rDf_>iBMPh1Wf+uRf zgv+=D(W-Vvd8uI8dh$HxA!4q~d7kPr7)NhT4xOS;tsz;v3GfD0<#N@4e$l1lTd4_r zBbSpC0WfNUh%2aPUAX`HNaK>>u@`hs&Vr(|zDq;Gg0_1S=QSd?#wL`fR)|~i#0d&6OyqGLHFjnMZO(mKQ~Jv zSP=`iE|rGkT&Oe#8JU;|rxFe&skAlq%QA(bz{Z(Y``n&o`T2onl`4@`O2^4Ie zmLc`NK%H(}Ljvn?;_KDSt>C~c!=+xCp>=+@pfir!%Y_!>aJhB^XB5hrPs`N*_h1IY>jt&@$TmPLc#?E zh*_~FR#_cw+0Kat2!1E?N#iBkDyXJ}m}0S1n1JE*I>xpX4h0ZS+3xwAhsqGvT+eXa zIN52jzq=`mt3-3DWa!NEdIsF0$XP}E^td`P=G4G;ot3RoR0(h;WGpK!zGS1lq7i5h z16SrdVTMBzFrQCg1R9tu;78QJg6g4C39(Bpl1gcg`|k^eNeIcG#@w%gf}sYsPL(&T zQ}N`$0`xSXDkbgtj30DC;_I$YBkWdfJQL?e^%0aj&qB3eMCicPpVu_gf-75`OxlvrP34$>y;;RS!ys? z8vr7y=ijPNAza#V{*(a~cnu%dJ9u{mhGz1*wOFfCPQAZ!h)Av!o_)`(2rnh{EeZb~ z?v$XA2~t5OJ6)W%jOoMR>7aTA-yY%n%SD`hZdB8RMrZ(I6{um9s~fZ`IWb$UzMBA_ z7l`<7kHGb!V{0I0$;n0Hwu&2fy?)7uooIEE>Lj4swJKj*DepTM+B;4V^2Zlc#aJX5iIIG(3>x7&w7Oe2Nj(Qy)Y9(*!<-Qxj3LIllZ~4k z&TdnpIyg86zhW*11rNj~i@+W0R>OXKcZ+J4BadAm9lm(KC?7HJ%!#n5@D*Dc)_(+n zS|ap$1TV&${7|e0!~0M(S~L<_xVewv%P1IHZ@h$>4BNue>u|dlwBpzp5<9rs*FnQADsl}(LH>bI{@H)^5o~%PGjIv z`qFiMpM46$B1C9HEQZ{R;Sc|d2*gik3k_6Pa%3;2PJS$$pU)b=EdS%s@yi3qfBG5z zy47RTUg3?r_<3{hx)$&Ne%XOne7Yi*FQjpz-L*|Tht|=zkt0@5+cpW>Q59pVLcY7k z^DA$l`hbb99tz>iMGbLprdY^$V~g~xx(VhfPC^?u+2`r)^q2GQ9Ma9FZ4Hl=r5^Jw zo&=7KpiXi^!rjirM(`k;b+0?F4IsMLUJ^q}#97n<1AE0YKkHjR1~Cs=-M-B}6n9%lDlhtEp$IR4xU?q9o$qg+o$0@I_VN``T-X+B zI=XeTmQ(p~lB^r;9!dcR4i(k!%J!)MQSR@Ew0?SQfH-*9VN4~9^`2sxA7F(KT3 zBP)BhlF-IY)peyv*kdh|R9nx2sb!CHJbyW}&!FCB%y8jD|IiM))8bDs3z>}!FLx z%-6uaIEqfC#d~=^^(ndyxd4Z!yUAYn$-JTA2GB*ON4IbBiHpCsK;7LpW;-*To@S%! ziUwf$qL}g}3~ylGPL9<^7GlCjxhyZfM*`BT9kNdT!q_!gfZ)3d$HRV@`Bse=J_94R zTHnO}8hizv%m%9S(dO*6kt?GcBlx8_^#svLGtH^>=mnF7nk3Pw<(j2JCLjo%3BT&TVhSO@2p&8=yGS*Jt z@t|5Wt1Tr+2aHYwul-Mgdv?98Q5j(TtnAfT$fHKzK**SNFYEgbM)v~N2D# z?n^+z#Wq!(@TR|iP>&KdhVM9@Fzyl_FxnyX72ll4aOQRyA~3IYCYcDiBPu&m2Bf$-&kO`rPnu$2X!DYwbHCvw{YVzc?9cV}mFX)lG^ zLC*%HVt3ua%jPH{*>loOWTT?637&&EFJ7T|mpZg4kjr8@arzO$kLgYyU~q(P&XOEc zQ~cc|PP7IS#XeteG)2a8J&Kpu;Tj<1r@@D1!77(FZECB1`KDj_RKUZydz-@xBkUUl z4Y5as9Er8&u7wM4O_vTfR-Hv{cL|kw${2-+7%}uo8YyYf9Z;=a4s+xjLS7$kM`jFG z(|PI&+Bf!o&bfv_{T$YC)>^DjGP%pycn!Pb@w&gZ&otTq;m(Dnlw=qzT(V3eZK=TB z+2P{=4@5i@dezNdMYPD*4*qOh8~W<9_(bn5p~jtb1F*#Xhgc@DeK<9L5b0xz>Gz=? zvD##j1hurC#~at7rJLy^cD1GBo3Ih}W}YFvQ7iY3h2%m`McG6pJ$sX>$qPW`ZK}36 zA0To=mKF)h{82j$lH<3}m4s|@Kkj6*$w3&A-?zA0Kc~{g*IqclvtwM^xOI#;51qU}zF^6sC&O8j^+!>2fxlR&e?AfU{~*$w(0+#8!qId)UI zVXDBOnu0Qy5K{_ED5!0o(f3EigM-!Aq(E~{t|IzhdJzMmR#osji|}rU+xllEj+y%@ z>N~I{igq4fV_?BoNmeVWy-BO$5_r6j=KyokN-uOSnQe5`Z z+5*?iFz1edgjfNI4_Fw!_6b3Yt3wxXd`hSmHKBw3);U^6L$XrCHw8&IrW6=C#p5Ly z$i|0b_N@`*+f|=&3Y#@#&{pQb-3N#b%ttbeFdCWKqmyBqlU}CtE&Wo5`?{b((M#<` z|Eyb1kE#F~oMgqXDzKeT3aC@;oDc^I?xg#<>Tvq%vDSAYyLv8Wka;$RJ`S!p0vIvJ zP>D2Ex)F&NXjJLb#`1ia%S4;AyB9E4l#&!v7Xs@%0X>Zdj^H?V+Ko(cjnsX>RMar_ z=+<6WHcdMc;faAoPdW=DX0)I!u5NwD8Gu;!T<6WzHQXW8#6qA333G*# z;Az^HIBzo_25JvU z&>t<9@Jl&+MFFh`!sn)+!cJzPI+Pre_u%AQ@;bs1-_sNn$d4M@vUTM!uZvR; z%fMADCKl9`*G)g*pxBzjbx_mx9L)HC*>&jKQ@c1zQbRFMw+(n-|Ea@P#ZU(U^vS#I z3*6ji=C?B{O;>X0Om^W5NPy203hMjcAnYTdUsScE4VP6}OSK*DKeC`4zzb5ICuk58 zgmvfGSu%5HgsfjnF(Dw_yz91FJYnQOg+$8&-8Xx8KWvr`GR%7yS#3%JQ4^(Ge!XFq z#icWh-pq>&`7-zwOcINe^!cpWZ@S46R>YGCD&O-y17@>bOGFWTPTsG=t9C@*VH!$9 zT6k1mNWO*eq12ApzT5h>MHExcPU?=Mtty6j1upCt?KR9PNK=%p2OGBl)~7$_+yiei}uknc^1CfxC`hI4G>Zm9fW*5SUpTnPd~r zq2v$=_ip*LH*A@7DpqH4zq*(xmd1iQ!>TOu&THqTH8Ys#)3&>E5g4$=OY<~bOfaRF zaNb-ba4)%=%RMxc&qd`q)mNUTXi8f-M~A4$A)S$_8EjnH+g4SpjoLJy&#jP`!P`nt zJ9E+UJg5`B0qWMSY?|%q^^!US-S7HcZ1%Pjs$L$&D^|fejX!-xBHhnsKXxxzO90j3 zkJ;U>NLJL0ojLfHM2KK{SaB=l&(yyIjNSLs^LH9~^s=a+*%;0+MYuS9REJM)UY zlkeQ~R|#=x`Z&jPPmfA=Jc|(0Q6<^czx%XOVC zb;1>kC116d;IHiuVv^xl%r;!Yyk;3Pzh$nUHxSR>d1`!I!Q&cX%I-d!U4`@}GS;Ft<9Gtd%bRkNUP zQ_tZRef46=Y-MZlR&X`w4YYJVzYRYn8gq;k87#;O4hivW;b;Z3Ku|9E1{5a_56e<+ zP5h|`)92G|V7>i{F65i7;Dv=E+>U3g*-b9TRH{Vj&3iWB{EbC~d96kPYbD6>zyL3oKOTvj{`Agg56-pA{EF@q*66(wk;@sd^ zyop<(1Mg)O&{C3SfDO=Fa7_(Y^Q-aQxhEIHzeAtl1+U%N`{i3BL+sEGMTouy+-0}R zJR0hX9Ma<&-#m%<&&3N942_I(zuj!t0heP@0jYL%BG%ypaiO|i)vQ;9bGS zqmUVTqyol)n6t7qFk$C>WFHz#+1^RbPa>M{|7Fdu#tB=aQ*FtKV6FVL)5{CAka$?C#o;iP_ z#=={Kjqh|7KCuD!`J>qW9-1w7hw|MQ%5UkCakBUc-XtwjHmJQcuFHU9_K>b1-sJI7 zmc;oN`9--1aVF$vI`hcQCPIxS+XBjyn+!P$Qy%bvsZD zy#&#IMi$sn%bo>-ORsc{1!8=mV=`Pb(|;cj!Nc{bQgVL~(=G6>E_`r5C*ckje0kDKJB4TH>Y9 z*LiJ0eSxU<^@dE$aK)7T1LN?uIH;+0mWK+}cw4*bA_R1LMHvflk`JEL+XN zKA5Q#LN&f^KpyqGP14xGQ7wcIU|b52AANB3b~#wnLSCQlx_bP0fKHtfL_;-Ht*7T_ zb%xas9E)nqD#?c~&fgn&PqPvIjz*LuB_%Q^@FN$OYK0`6#(H{FSCOG2ifD?)Bx;#k z7b6R*=V(s!61eTQ%U!fXxg8HcOGHvVQG+%^RI@2<*IgW% z>3Q1;DcH31x0q2Iv8t95iHCY9b&jf|oE5#iEg8 zPF0qA+wp_&lll|d{oq0$DY*TnxHbmG?6Y0IREbM(M91)Xd$3TX0d>!%#>|360>p+u- z&I8;5#G4D*+2osTh(VLsrZe8IvN8Sw^G(dU9l^Ky;gi&7pAv_`i`}9gr&Ufb{c(gs zS9JHqWD{sy^UW-TX@TVVUHRw}MK7FL?GCu2i`d^E>|sQWC48BbX?j89cDW&V!ZJ|b zH{YA>rIZj5Q#0LEH6=j%(w9=K_84Ezvk%|xhte{&E9E=YI%xSxc?*C{IvT2AAIE5Z zP!7Tq8_JjW#4_^6Ruh9Sf+advk3yd3MPoZfPHpH;;Ryw3gVeU20oLKmZN?&hR)NXKv^doBAqkLYE}Mq`ei{d&%-? zQl;K&U#`4PKbiyt`|nV3ZgZ)Vu{Og_{J9T(v7RLqUL?U0Qy_OMd{L3?qE`HM^jYR9 zU&aF(f{@!QTy+h#PnZ}i39*Vl3p^rIL$POyRn(Xvq+TN^wp;>qk@7{Qc>)PK$BnlA zG^D?3`Ew-%G#CYVBPyvCT)_`5j`&VUu`Di(OWn~RDdU+KyQ<$U zH2y=@nJ0vscBqaWivqu<4xl`PStz$g7oV_*@BPT`7pv|sXp*_KK2VwH)tJeA8WCUO zb-C=8y44WVw|8O_G8w}C*B3{siWD0~BVt#fu8SEUbLpM^_MCg^N6yLy@AuG9b(KvU z9b|wkOGK9!Vz=DF4d%~7Mu$kT_}uIC0ya7=`L6CwT)TbgZZa!jDuZ>rSs^o8EqC+Q zX_Y{Ael9RG)f#p$&863ug*6{LtO&m8ulfvjGKpH?_9qv>_3L)FMRszO1j$$9ZV3kq z52qkEb$eZ4T-78y)oBhzSgfjIkQzX$fkKUgM#z_LpN)pPUBQ4iBQ|Ox#14OEQ1)8Q zOGugZs?f7u%};I$*N}W53fkfXtA=y!>#Lv6hHq758k;hxg93WRA0j)j-7PVmnvuAL zFQ;st`!GCn5a>|a+&t9@JYC}wq&KQ}v4=!xqrSUj`np(7vLRSTEQtMB_QRRME43m2 z=LWlpaQW0lL{E$alW|1J%;y2@XtHy8pv)yM?`Q&)21$Kl-7wU8 zX<10aPyP*j;#g~?dMKoJ>3xk}z3lr|5Sy8*(6ZxQaL7Cw$)8^i&Y5_9IqP!pRn&*A zB|sEBV8vf&>a`|}U&(=N0l1TBExbSr{F7n_$xBBFBe4SFw(6>$+}3>^A)P>yYOufL zvmAR`J0ItxsoP~)N^3*VFpj;7r>&7G#sg17Z^>NEUUz8GYuwvO7kEu{vB=apoMD(zVNj8NVO^wS_vs!xW=;p-?rI7?u7Yw|bo_Aj)Z6oKaZor*j}4=XXg*=kP^WRY?i;f#Ia@%MQzwTV7xW_#-#$+-2J(m+Mx%H3`! zT=v2OkERh=9o%c=)ix>6L{E^|UAi)T4(qkj>E5G~WB%Dh*U*`)m=53}3AMAOT(}MV zTz(>641_3&WZ8IGm8nI_Qmy4H=JMP9-TWuJ1c9g!{LdUGy{UyL<$mqe7K4U|_X~Rs z4Y|BLgmF}9$9>~A)QJwUOz2fDa#7okX{V-9v^T-is&%r^KnqW?C7M&`Mn0MglmMVBF7nwVezh&6hUD8_X^HhM@-peS} zCNR}XG<)cBsqkbA2?ewpK|2QX(oO=KIGR118e0cGKa0k$iX}tOSw2%bQLPGEzc9#~ zJ&%Wl*(28H>vWI;wlr}U>UG7?ps8vbjUv&g`DH-PqK54Fh8Cya0Xp|2+)gwyF}~%~ z1KFHeH^f{84SsK6lm@>RYQ8ayP1B|+b8bh#a$DgJG?gG`cx$9vBnJ0|_f~kEJDZZXY6vKU>q~}@G!`<6Msnj>3*kMF{O;m_lQguKeWpTS zKZFR{58s15Fn}OHZQK2oUKt>2#a?F!0cWtBmP;AeJuDM4rpd~vDVb=;BO79nYV1>N z^)V<2z4rDq{b?rHx;nt5aPij{b4r(K{FF)~Aocxn0dCiF6?2gN7DMg{pZp-qu8?IX zxW4M4fX~RDDsT!zNi_|jRV<2?Zt0Gh7TQG9>CY2qx3vul*2m@FW1=~6xnC_Bx#QRN zS+vu?IzKaNdBK?@ew!pQ)*hz&9J45ZI`QLSwWG*)lzj3;n&=P_PS=ul!*uWR>!9vl z-(iQWy<-Dh&vQ~UTn@2wr))nB&8mEZD#>Z~Ue6Zl>TuLt%Ow6y+ViNjX8q?iE28!Y zY0djV$7qObbZgTPu|Ya`jIdi$lz|3t;6Vl?CCoaGvwrDCq^*Xsl3it8IGk!iba4ih zpm6maJuDjv{Bst2#i2|##+Xmj>9183!ytlwTISsb+MYWAa(ke7R_oaw(cvHf)%r|J zyZS|EHjEuzSyN&!1)VDybZel8v^MY@s2K8Fq9jL^3Z?TVG$~}W>7v)YvN0k!S@Xs; z_+s!B7IvybvSz|X{hNI_WIob|NzPxRB99%=M>ZbDuh&%sq}1&KcQR0n>0J^Qb7H8aG1&12g(cXOdJ7 ztWr2&OFHUyX%jtHh2`t0l0RG{7{?NN)#{>@ zp3KuTgW@`H@00T2+n4xkQ(6@4%B-r^sPVlMIyGL-iGC~J%{yL$Z>(!oYh%3E74V|@ zOs6Fq@CZUkF!HnGW_TCMF<1gxICL5IC9K>#xtPKHk@nO~Es@4zfdJh7_M1S2uv+|K zjO=tBC7cc~b6T|M@@7z3LYiWe!2^ap*fa%os+nyJZI=quD~rFVuZ%C$kX&v<3W1o_ zyiFb6*=Z-NJXI?Xo*Yk0MHKE`-Gv@(aes2jxBT3g#mj;!FsAt}z zrsrlYuXH;}cfaE!oxq_8diHhXM1@PhMYB_zkP#_EcH4(_!`N-Z^??q8CKm%}u1RI2 zz3K4>#{9{f%c62Hcwb^v1OE+G52ZVlB&cCWd5qLdB*FN2M`vQ;+f576SgG*X%W_j` zYKpwAfb>o!IjnDS6V? zB@?ldxAG7$y6!z1s)c&GOHHnLmV7m;jaiSw8}*_+SQb4eCf?J9q!HHIqi5!f9pqJw zcGbS@#pWo;VSnd7XMmaFx<>H=!vzxeI8_f!13GVGLE4zJLAXZfARegIo9ky`#BT{j zHbo@(oZ#-7F+!cfXxQAXIY`^W#FZ5tR@OZ0$mm!F2U}@sxVRICFLj}hmf3;46zaA` z?#&5h-qvtsx2q7500u~VOJkwMjEno`otqlQsRXZV-~<$m*|dnorGQMYR+$7UV<971 zyxC@j<7m*%4pup14@}1M&Zq!72c%IVmln=%yi~3dNI?>)oPHs8c{6*ZPo1y?do~|? zO|BzhrjaB3$p$x4!-D6~VsaZ_{89)GW^KH zp+dR+U?hX{oPMIit37wKG$blvUWuxvmpFe2Pt4DenH|;y*{$(>EAaiC4#923PTO5+ z#)BJujKfSiCCF?`%BJI^M|+M9iwWlND_9vS@Q8cc z`we&b;HM};DClIdDm*^5WgB)}zofL7hC<(gQ9wppYF`jnO2Ml9J3L(M{DF3M5L*NE zBsbAiIxblQLJH!?%s1Y}JjbVk69M>7WcHb)hfx`QQmMxcr5=S1nAmjCaeS~L7fHbk zm9s?|>nb}>PaNTy?GnqaxDSdpVx>6y?0v_1@WmJm?e@16``)tf&8ufv56=Vdvv|_{ zRWx>XbEgwA6ULtkPhr`{p)}Gi#RtA+cs51zdWe!^9K47zL9GUfY`$~#K1dW7ZuP9sAYV%?&)KIv56Vuv!-1F3 zD#q&xX|Vi60~oFlMuKbP*j=u??V4qM5*2-;+&sP<7LFTTW&uu7JhbU8JF+brsIkjj z@YCqlRy-LBHV*6#l^9rO%X0r5@H?=FNK^qH{o0m#H2VlQ;)eT|&?#7>x&&;An=B0y zXNCT)TLhGRTcAe$e2m3@h9;9NNv*jKly9+KzKq;iU%f;7pPD^wJ|i$x+LDD+gJfYX z&^3Lx(s9<%V{N4qNA(Y_7+0}-$Fgx#>9LeuINqTcfhll?BZ+XVNg8yD4TC2hD3l}% zNN*j&z`%;oUMnr4pyT}|67tj$su+2%C09qGL<7906NjJ?=YbS%-q$D7x? zBjYS6AbsYnJ14U6?r?dEfQl(&19^*!7GB>#*v0(7R;hmaaMO8k05IB&ZPiSH-_s(# z&t29X>7(uj8IVgYM8)*87U+wbeq{C+V(2jRD_;!iEe3#YG<(voA&X^dTLJhBgLdu6 zQ!2H}pwdX-!G3rATAQS;h&WpAJVOxlrImpC^PleSTiX{)inGv?Ham|oOBxapj2H}q zfa@a1{kX}cT(eJwazK~GEO0{CE6!Z}Y-C9{E;JM_YrR-+vCX6OI`|83&B*n|Q~H&= zd+L+vQI9H>B)(8_JgS254+h4-T<2TKMpI_peK}J@#ytQk^m}snS`KCQb{HGsE1u)m z>iufNCEnCRHi>NiR?w8i2P1ZEsE316#^)`sz6~Xjtt&nZ@GcHof-YE46a@PXNEyiVR+;Vsq3J@?7UA3&1QJNHMo;9)FEI0RFg@K{n zq@5pO2T)7?kk*}TNZ$o34xXjzc2nAZ_?dR>f0V{T>^((A45i#DS)QoY#l%n4HyImO zYaYUl=LKx9znC7+Ez6_%xOgQohXSg+lEFYfKo;)2miN3o84~KP7+Wt>p@w9bhDZ2D zl|O!Kwz?ng*KS|F00i@O1$N=i&YAQml!)|R$!TGGKlm(+71Wcg#qH6P5YP6J8pPic z#3Z`R4Rl$h17Z^$rN$AZ&wtQGJ{|LlxlC@*NHMLTIN$QdqCaeahN zK4otnA~V3&Bkb!=2Y1Z*qG+`51{j@b^!cGijNMFHdy#CvSJK&4NtivL-kpTN7hZvH z_o5)4u8}hwP9=Z_O>(-_ZQ({YB~IAO6a9}kufBtv&_8BfuHs&@p(`{2$^xqADFR9= zIUlo|j0R!xGZ)Rs0U4PefJ6~u81Ok6aCui93jy9}QBG11tXwPT^HXReOT+vJi&|4i6L^jqk z?%Cu}fb5`od40Ps`AO7_7VEn8qjCDJE?8Rw8H}QF>99WFtAm)QF3MsbPP2GuUI@CT z2UP;~txGJxQXG^RI-3)9HKh-+LC!b}&M9I|p{y3>#SiDVBrMXEQ8u_y@aO7uDq??< zte^W2H)wu3Y&B1g?D0L6bfSN^>7f)nLH&;BSN{xHbNJICgUCjhZ^{#qtvjT>c3}!KF z?g$zqA(H{dp)IUZ$^^3*gv@3+eene%g_y!=dP-<>mX|wjbN&0^;yQs6(}rM^d}vGK zrFFWhPdDXIa|i>ush53k(yS8Z75J0TJnFBXNYA4MJLDdwodNlOJ_du3yN1jflAC&I zM`?X0oHq(X<3HGq$;?3e)&~o)4_)N~w6vSuSz{x-Gy>^szGH#7X71ukP_zCV8{atC z1iS?i1Usec@Aj?GQ@Nk`4j6QkGT_Z8#eUh8(>DhH^>BsxC#ebbdYi%B(S8}}+v8nF zkiVK3lU9KzmX@9?&HlFaelx#AHDB+V5H`KdgzR>7CvsM~^s6(iXv(hW*;)IF)k_!6!f&4PsFwC^@mlawd>IxRoAm6y95-@g8vOKmm%- zRB(BdlWXaMc^UXD>WR3t z@ojpWyTbt+D%GVK^-_W@Av3Ud(JnbBUdI?(EjZ(zkQ2_4c{4W|3)68f(Y-AF?A4`UF$#D?fs1Qjet@^JhuW#pa8Y)peM&U2+j zO_`%xC!-9|qrjcOHAzUEw=I64RXN1P0p^%!yo+xe$%v@iC3Y`J0h zJMLeaY)py0Wx_>)yL@8QvQSk+TJq2Xo-}~D=fB@dbNGC&+_zc)lRu9EB4)9^t(JIe z>^=hdY0xJ`QacqVr7*9ONb9W%`8aF|_@W`inOy0)bYU`n15!gwaBrl4 z*%g_2Y#IO=n59hL4)&h%BI^WRq4kB2Yy?W&XV5c5%;8TeC#`=i#^pPTSZ%vsdJ>PP~#O4;QE_!Ad7reFx;f;w|oEh zWRQq2tGjPzTrE+vFF4*nx@TFKfMsZ3AsN>Ab#XN~1bf2)tB$%Vyl5|-u^Q}eL6+H^ zZWwsVI?OpW2c)-gIC9ilhdJFy=gV*wb&3?f7vu*99JQyN*^*E=eXm5}20c2hb(cW0fb zC~^kKe7*WQ)*j5wt;5=!&LOx6Sz*XoLdXfZL65UVE4;6lZpe_6P@S?X3k0S&`4l}r zGH5OJQ?4lFG=5!#BMIKXVvi2=+C@VlQ6r`otx(gbaU1EvC^t7B&cozR?cD7}=X1n- zMeLx1^qFUwwF;eK8>+MskR|%Qq^f}sY>7d?-2$cU6Kcfv3r^3m6Wl>w8rwjA9~KRU zHyPl`2!lQR6)cw0F>fV_U}KY4rt_Ek^`;-6;ey!`x|70e*dvip0K>wd>7@Or2Y9qb zcP^%hojQw&AZZc>3=hf|uzGsaw#ez6cVf{2)4yfvV{cnhob@}lVH`dj(Ti#?NCb;s zk`-w0%KTh{*Xm#y!GDHf#B>Yup~bfxxA;-$d$rLO{4!Zh_2b={Tt6VG)G(BT-jjS1 z4;loNbprlO`i1LgSYj>F1@CNjMbDFnqGHBy<5a;OgK#yN0+}r|0YGHXfSSH8)Z-FQ z>EuO$>=k>~K6gM5QN8aQhM5|ALk<^{u6eDWKEGyFcUnSSxce1FaE6qy={-)3gTFE* zRd+5xqP5LRC%2*uk&+M={r;&lIyC0n5=3JD37@I~g)kHm{`H_Bc3iT4PSM?t7{SgG zeIJ`ffkqhrO)v5n2%lXteY7|C=9o!B#-Iyi8MCNe!WU!}7_34tdGe-=fbFUB$9?QI zsY+GJE>IteHqFvTcfVK7e9zJ4%M;04tvZ~h>r6C3My^|rB6r4e=o!pzS&y7MM~XTs zmvukM>hN8KJ&anu&yijJFkg04^;yJuDC2H!LP*-o3WeLPBX%^kwy;O#43VdThvs8+ z&*<0g&2bcM=w2JX=ejUXN7o4tx%A?U#cIQ%cu3~?R(NCV$E<+?BMCi#1Z$m*pUk9P zxUc{z?&zU028+TQpbb@y>CVR2;7znH1Q#(X5NsZ`<4zSK74WoR4-02n9tXiT%y~!%Jqz)GQ;e6aHhG;C>o9Q|yYy#;=Tv?T^ z)r~KtQ3~R87}xE@Cx-hbdOfh9Lu|%IfYzbmJ)t05##;l!}m#kzY@B}@D)o7FwamHQC~%YvkYpG_M6Gm;(^ z0c4QV@HkyQmr$#k2No15W0C!eXl|eXjL2Fk+b%WJ*V*>&Q?o1hhMdSEOcZzTT>bUyQ@w-=W5kX(>aNX z`rPegJ_NAIghe^7c^TwJMdq6OFVF}_O!|FTbMmF|K9i+-^t#D>YvRPD)y;X93VOP# zQll=%N5yv=ktLOp9m5HlDeOFUT@UGH#bYW2HSZCTSlr5rCN|p%eeaeopM)5%!f5TP zdr_b|D55oP{edye8!;O+tUCwcZaa&%A9O6|HEMQTW$(q!2~!E&VEfYC#~{d&Kv5mM z9M>>txyAY*vL!CF$BOFh74IT|9J9zymZhAmQJ*j%6h&s$7MZ%Z+N~G|cmcF%0zlCE zZO&~ehmB&{7*{FS6SyWsJ9Ou!z8bu?$HJB6eC}e<_O^c8 zPZg}_PcKqOZ?6!+wZ;i`@F^Q1-^Z`*@sE*yRZ>Xqyb!oty09RIwOk>X)M zk}Z&6y(1KssaB0bgr2!v`fLnX1q~(h{g|GJ#RzDaR^2 zjJKkFGD#;E%Vb2?hb;56R6h;=DTHby1tj;AZGCZjtS4l*BqbuNT*-mn9L9Tl&r}BD zm)9kA#AC=&(15EII@npXfVDIy`OHcKE^x$&68Q15%C&`s>6UHg1?~xo9XKw+QmKcQ z_vh~-U5XKD^ZxrDBF}i7sBdmE&UsYa(D4;hZ)G$_&F9p(qUmLzk9n(0j*wX?MCwp< zo2!)ZgbbavHc{`L>|rf;6xDa%Q*qjuP$~StyOTLaNHGCxZk$93wHw1K%dc;mL7eX2 z;EEXnn!VoS^aB!z41F1esCqDU5>L-4$WmDv9`R^MpP}oUva<^xH6i`CMqz#waciFy zpq=Cr4X&Y8>)mGPg0JWjV2=??c7SzSQ<&RXh$Fx|mT*PV-mgQN|CCeDJYE(%3JhfQySje6 zKtN|s21XdjeH&7D_Jf_GH7q$H1?On$ahNG!M8^e?^bIVkHgQg%=Ui%~R4P{&nkv8e z-sjZaDO?7mIGa@Y{?NJu@{Bi(t*e|>%-!99AX~IJizBEbQT_c4y;ghv^0#JiIqEXJ#%N+@pj@U5= z-nho=nW50|YUa2Im$1}x5uXrW>ZU|5E`s#y#>+Jv3Dw5FgIkCLHi|ng5D4_>EO_Yx zG{j5m8A+>deyv9)>GWX*EiQ}b%RYceO674zh^EQ2l?+;++U8inRFG{r^E}m6lrH?1Uk7maHxLUqpm zmZGnN4k1jTIsD`uLh!MZJN~(&520z@Pm9~Z8&rD!+MngbdR0IZExAD=;o0Z6W3%BD z$o3G}5ix7J;>w~MZNHl7r}A`udaKxtXsm*mP5Uzh+C>!}7~I~W3{gTIB%9}*`a9*> zB1^F@K=828Myaw&z>rWIAqi01Pxe(kj)ljJ3u_^$2p(V#9~e3DJb#Tc$< zX_~>#!7a>-IfgfdZ6(KG6!fZ@uhgFP<55G>Wj$6hd8#>gptiq3`m{#9=mPq+5SD=* zwA&s+LSz|Eu`Nr!yQAQ3$C3PgbwcFkU3m6bcy?Aws5i74hqVBePrT z!cGYX$j%#n&*>}%Pds^LD)x6*iv}vNFa^vMP4O?~LVXMJST-Qr<5o>WJ9_pP#FlJ8 zM-#))NNEZ7(I)hifBi4wSX29Ek%{wCb{^M#jx$;Dw2St~*@&|iD+trnA+JxL^9!mZ z&3r;+I`9nZE5QaBF$9gO;5<{pqPMUVXy$GGQ{q z*>Pm_98Ua+jT2x81ccG7>M0R5>f4-yGW#dRHccmn847Qh9xG1Z#ZPKC=e?0kvKrjy zzr35DYNzC{gHZqs>qhwSUnyN+2rh-G?|tdjVeKD2bJ=7gl%2Fq-!#vL^l?E4Y#MW(R%(o&MnEB>0WW_1Pa*zrP>kYJH zz^ic6L>%nO64*Ecor4wD@l#mEqb#)9G_cg5eL~4`(*jK8<$w^Gmt#2z^0$$|v?o~k zg&eqZdmTuDD0wxWAbQZgMkgY%hyzNwh7Q_9kee4v$487V@EHCc# zfg8GPKlFMSrZ(sy%RBpy)&pxOZbKHYn!^cY%iy#zB5)8J1PZCgx+e>@xymKUglSZr zi*_U6oxksE!|tS~xiD9w+lP^1Tai z-YQhvxv+bI*EIA6=)BP?SY|XG)*Gsa#Q4bq6!Jtg`Wm+5tCD?zdwXS;QOKcg?9Pp)7XKw$vXvdRb5E z`d^HXu#m(fAKmC!Byk%%Z(dF3h5IQ&`qA{PocD$^GDG--X~c!djmVg7uzNS(@r$@E z-75%)a^&t5`owmKzl-^PKiEx0`*Lt4%<`tDPx5uyCZNb&r zfF7w&$09nc{!rkwnS&$wml-#lWE^c-`H|%TXQ)|uaf|aq2A|H;?YYXJf|FI^1g)3s z!*@U!0RXfclRKHYYs0oV*M`OFd2*h11rBEn_MiC6W+KHmWL>_G7DGh}+#gd?;F3m} z1RX^{S1j;zXYYnDzA>!b4!7#p+`pZwi+J?HKg7*D*>w&ml>e%%T z?qlj|o>24mzv8nD8${C4rNT`ft8TV)&1kDW2Ryk=@i-nH6eN6b@YcdQqs~T6(qz~<(noiydGGN1!? zY4#pan=t~j>b(jtg3GZhfYnhA%rrjZ1|dE%N5Sdsm@GhRBH)b(aVBSK+pf6;Tu@yDZNDI$*pL&T)u(6WU)aF)F55YlF}8G~9i!xg&63U$;BqkdybL zj_iFXM)IMXK4uV37bD`J@2%6;X4jo1t0a0)Zw}v`)v^)q`$l)Ga6vQR8kA#DLU1R^ zaSTM0goX_}QhuP9J*egt5l#XKdj*r#!$I+*yb%Z~0-s1=b2)Co;gNkm zwdx?dV*A6TeyRT|5^!7q6yV{C_zT~d)+;+rSCr+plb~a7XCv_ey0m`;f66F;4(P!Q zJVUv=!=ltmaj?3MLW07OZG|ITuhh3ob!5`*1{~D@On%a)aQ{nYM_oh?O={+Tun%dT zpi1^=B4uMp1cNM1p}kD|T2EX;+y`I|?Yo*)G?>Q>5QcDgy~)(^_=+ zkyrpiZ(#Y@2Ca$2hO2e7w#?q;5K+Jq6}b|DQS#d~#tl9_)iAAcYpeS31;tCN9&$|D zb#p~EDp3$wbwj3dhhH9%sx>mz>>Ta2#SpNo;#Pm$2t6_vFjZs=i|THP9RvW_&$+gN z1?jzZ1PPX=_Z0T-fiTwatZ3)Qid}}NEe8@dD0Vh;>X5C=%CBcVv{})DKgd{9@|5c1 z+fFgzPt3DfyJ9Bf4+Jyf{WzLNGD4F9^W1~LxGwm(Eb?|q#W?dhZjgiH=xvE_ZD>F= ze2%k{!(r_Ghi$|+^kP=ggZs9@f#$VsSCWj&IMANM2+PG~{ze3t%qjD>JOYEtfh=2y%Xepj2^+_E^%0CC|X0%5A0C zPnv|9qy8I&(seX8oy_o$P$037(I3O_c`r7DHM$`@_ zvVrk!S6h)x13o|)hZVW6&MvEn&0XGAC{L<>VniheMMXkArDmpzx$3f^eE0eLUO|ZJ z@f}Vzk+cuz?jZv@t?E*U9p%$Kxb5av88FOB-UKq-Y&K+u8A&MwT8nrEVeU7YupzTz zkfv>8y^qihX><`&=6-aF8$mvGW6jRX>EJ zSOi|S2+>nRzFn-j%b0NK%RY$`bOpH&6i}f!nLqXfy5#LJ^z(x^HF~5@8W-PI`lcVL zxNRe$eC#w<;09jpakwc8{|0Aj$An8gx1RFa#XxR)8dx zwTbES$V^x#2z#ic#qJtU#Iu_s|lHBtnCeGRrsS2Yx6)TQ=tPz=sA

;r`>9&E-Gij_an(Ln;jR0lA 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 section { + max-width: var(--g-max-content); + margin: 0 auto; + padding: var(--g-space-16) var(--g-space-6); +} +.section-sub { color: var(--g-text-2); max-width: var(--g-max-prose); margin-top: 0; } + +/* ------------------------------------------------------------------ hero */ + +.hero { + display: grid; + grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); + gap: var(--g-space-12); + align-items: center; + padding-top: var(--g-space-12); +} +.eyebrow { + color: var(--g-text-2); + font-size: 0.85rem; + letter-spacing: 0.02em; + margin: 0 0 var(--g-space-4); +} +.hero h1 { + font-size: clamp(2rem, 5vw, 3.25rem); + font-weight: 700; + margin: 0 0 var(--g-space-4); + background: var(--g-gradient); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.hero .sub { color: var(--g-text-2); margin: 0 0 var(--g-space-6); } +.cta { display: flex; gap: var(--g-space-3); flex-wrap: wrap; } +.button { + display: inline-block; + padding: 0.55rem 1.3rem; + border-radius: var(--g-radius-chip); + border: 1px solid var(--g-border-hi); + color: var(--g-text); + font-weight: 600; +} +.button:hover { border-color: var(--g-blue); text-decoration: none; } +.button.primary { + background: var(--g-gradient); + border: none; + color: #06090f; +} +.hero-art svg { width: 100%; height: auto; display: block; } +.art-controls { text-align: right; margin: var(--g-space-2) 0 0; } +.art-controls button { + font: inherit; + font-size: 0.8rem; + color: var(--g-text-3); + background: none; + border: 1px solid var(--g-border); + border-radius: var(--g-radius-chip); + padding: 0.15rem 0.7rem; + cursor: pointer; +} +.art-controls button:hover { color: var(--g-text); border-color: var(--g-border-hi); } + +@media (max-width: 900px) { + .hero { grid-template-columns: 1fr; gap: var(--g-space-8); } +} + +/* --------------------------------------------------------- hero SVG marks */ + +#run text { + font-family: var(--g-font-mono); + font-size: 13px; + fill: var(--g-text-2); + text-anchor: middle; +} +#run .planner rect { fill: var(--g-card); stroke: var(--g-violet-hi); stroke-width: 1.5; } +#run .planner text { fill: var(--g-violet-hi); } +#run .gate-bar { stroke: url(#arc); stroke-width: 4; stroke-linecap: round; } +#run .gate-label { font-size: 10px; letter-spacing: 0.12em; fill: var(--g-text-3); text-transform: uppercase; } + +#run .prop rect { + fill: none; + stroke: var(--g-status-proposed); + stroke-width: 1.3; + stroke-dasharray: 4 3; +} +#run .prop text { font-size: 11px; fill: var(--g-status-proposed); } +#run .prop { opacity: 0; } /* proposals are transient; final frame shows none */ + +#run .chip rect { fill: var(--g-surface); stroke-width: 1.3; } +#run .chip text { font-size: 11px; letter-spacing: 0.02em; } +#run .chip-rejected rect { stroke: var(--g-status-rejected); } +#run .chip-rejected text { fill: var(--g-status-rejected); } +#run .chip-admitted { opacity: 0; } +#run .chip-admitted rect { stroke: var(--g-status-done); } +#run .chip-admitted text { fill: var(--g-status-done); } +#run .chip-stop rect { stroke: var(--g-status-done); fill: none; } +#run .chip-stop text { fill: var(--g-status-done); font-size: 12px; } + +#run .wire { fill: none; stroke: #3d4250; stroke-width: 1.4; } +#run .node rect { + fill: rgba(52, 211, 153, 0.07); + stroke: var(--g-status-done); + stroke-width: 1.5; +} +#run .node text { fill: var(--g-text); font-size: 12.5px; } +#run .node .spend { font-size: 10px; fill: var(--g-teal); } + +#run .meter .track { fill: var(--g-border); } +#run .meter .fill { fill: var(--g-budget-fill); transform-box: fill-box; transform-origin: left; } +#run .meter .readout { font-size: 11px; fill: var(--g-text-2); text-anchor: end; } + +/* ----------------------------------------------------------------- cards */ + +.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--g-space-6); margin-top: var(--g-space-8); } +.card { + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: var(--g-radius-card); + padding: var(--g-space-6); + border-top: 3px solid transparent; + background-clip: padding-box; + position: relative; +} +.card::before { + content: ""; + position: absolute; + inset: -1px -1px auto -1px; + height: 3px; + border-radius: var(--g-radius-card) var(--g-radius-card) 0 0; + background: var(--g-gradient); +} +.card h3 { margin-top: 0; } +.card p { color: var(--g-text-2); margin-bottom: 0; } + +@media (max-width: 900px) { .cards { grid-template-columns: 1fr; } } + +/* ------------------------------------------------------------- comparison */ + +.compare { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--g-space-4); + margin-top: var(--g-space-8); +} +.compare article { + background: var(--g-surface); + border: 1px solid var(--g-border); + border-radius: var(--g-radius-card); + padding: var(--g-space-4) var(--g-space-4) var(--g-space-2); +} +.compare article.self { border-color: var(--g-teal); background: var(--g-card); } +.compare h3 { margin: 0 0 var(--g-space-2); font-size: 1rem; } +.compare p { color: var(--g-text-2); font-size: 0.92rem; } + +@media (max-width: 900px) { + .compare { grid-template-columns: 1fr 1fr; } +} +@media (max-width: 560px) { + .compare { grid-template-columns: 1fr; } +} + +/* ------------------------------------------------------------- quickstart */ + +.term { + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: var(--g-radius-card); + margin: var(--g-space-4) 0; + position: relative; + overflow: hidden; +} +.term-bar { display: flex; gap: 6px; padding: 10px 14px 0; } +.term-bar span { width: 10px; height: 10px; border-radius: 50%; background: var(--g-border-hi); } +.term pre { margin: 0; padding: var(--g-space-4); overflow-x: auto; } +.term code { background: none; border: none; padding: 0; font-size: 0.88rem; color: var(--g-text); } +.term .cm { color: var(--g-text-3); } +.term.output pre { padding-top: var(--g-space-4); } +.term .rej { color: var(--g-status-rejected); font-weight: 600; } +.term .adm { color: var(--g-status-done); font-weight: 600; } +.term .copy { + position: absolute; + top: 8px; + right: 10px; + font: inherit; + font-size: 0.75rem; + color: var(--g-text-3); + background: var(--g-surface); + border: 1px solid var(--g-border); + border-radius: var(--g-radius-chip); + padding: 0.1rem 0.6rem; + cursor: pointer; +} +.term .copy:hover { color: var(--g-text); } + +.demo { margin: var(--g-space-8) 0 0; } +.demo video { max-width: 100%; height: auto; border-radius: var(--g-radius-card); border: 1px solid var(--g-border); } +.demo figcaption { color: var(--g-text-2); font-size: 0.9rem; margin-top: var(--g-space-2); } + +/* ----------------------------------------------------------------- status */ + +.status { + background: var(--g-surface); + border-radius: var(--g-radius-card); + max-width: var(--g-max-content); +} +.status p { max-width: var(--g-max-prose); color: var(--g-text-2); } + +/* ----------------------------------------------------------------- footer */ + +footer { + max-width: var(--g-max-content); + margin: 0 auto; + padding: var(--g-space-12) var(--g-space-6) var(--g-space-16); + color: var(--g-text-3); + font-size: 0.9rem; +} +footer nav { display: flex; gap: var(--g-space-4); flex-wrap: wrap; margin: var(--g-space-3) 0; } +footer nav a { color: var(--g-text-2); } + +/* ==================================================== the hero animation + * + * One 12s clock shared by every element — delays are keyframe percentages, + * so the scene can never drift out of sync. Everything uses + * animation-fill-mode: both, and each keyframe set begins from the element's + * pre-run state, overriding the static final-frame styling above only while + * animations actually run. + */ + +@media (prefers-reduced-motion: no-preference) { + #run .scene, + #run .prop, #run .gate, #run .gate-bar, + #run .chip-rejected, #run .chip-admitted, #run .chip-stop, + #run .wire, #run .node rect, #run .node .spend, + #run .meter .fill, #run .meter .readout { + animation-duration: 12s; + animation-iteration-count: infinite; + animation-fill-mode: both; + animation-timing-function: ease-in-out; + } + /* replay/pause hooks (site.js) */ + #run.reset * { animation: none !important; } + #run.offscreen * { animation-play-state: paused; } + + #run .scene { animation-name: g-scene; } + #run .prop-1 { animation-name: g-prop1; } + #run .prop-2 { animation-name: g-prop2; } + #run .gate { animation-name: g-gate-shake; } + #run .gate-bar { animation-name: g-gate; } + #run .chip-rejected { animation-name: g-chip-rej; transform-box: fill-box; transform-origin: center; } + #run .chip-admitted { animation-name: g-chip-adm; transform-box: fill-box; transform-origin: center; } + #run .chip-stop { animation-name: g-chip-stop; transform-box: fill-box; transform-origin: center; } + #run .wire { animation-name: g-wire; } + #run .meter .fill { animation-name: g-meter; } + #run .meter .readout { animation-name: g-fade-late; } + #run .n-a rect { animation-name: g-run-a; } + #run .n-b rect { animation-name: g-run-b; } + #run .n-c rect { animation-name: g-run-b; } + #run .n-d rect { animation-name: g-run-d; } + #run .n-e rect { animation-name: g-run-e; } + #run .n-f rect { animation-name: g-run-f; } + #run .n-a .spend { animation-name: g-spend-a; } + #run .n-b .spend, #run .n-c .spend { animation-name: g-spend-b; } + #run .n-d .spend { animation-name: g-spend-d; } + #run .n-e .spend { animation-name: g-spend-e; } + #run .n-f .spend { animation-name: g-spend-f; } + + @keyframes g-scene { + 0% { opacity: 0.25; } 2% { opacity: 1; } 96% { opacity: 1; } 100% { opacity: 0.25; } + } + + /* proposal 1 flies to the gate, recoils, dissolves. Nothing ran: the + meter (below) stays at zero until the admitted graph executes. */ + @keyframes g-prop1 { + 0%, 2% { opacity: 0; transform: translateX(-28px); } + 4% { opacity: 1; transform: translateX(-22px); } + 8%, 10% { opacity: 1; transform: translateX(28px); } + 13%, 100% { opacity: 0; transform: translateX(2px); } + } + @keyframes g-prop2 { + 0%, 25% { opacity: 0; transform: translateX(-28px); } + 27% { opacity: 1; transform: translateX(-22px); } + 31%, 35% { opacity: 1; transform: translateX(28px); } + 40%, 100% { opacity: 0; transform: translateX(34px); } + } + @keyframes g-gate { + 0%, 8% { stroke: url(#arc); } + 9%, 12% { stroke: var(--g-status-rejected); } + 14%, 32% { stroke: url(#arc); } + 33%, 37% { stroke: var(--g-status-done); } + 39%, 100% { stroke: url(#arc); } + } + @keyframes g-gate-shake { + 0%, 8.5%, 12%, 100% { transform: translateX(0); } + 9.5% { transform: translateX(-3px); } + 10.5% { transform: translateX(3px); } + 11.2% { transform: translateX(-2px); } + } + @keyframes g-chip-rej { + 0%, 13% { opacity: 0; transform: scale(0.85); } + 15%, 100% { opacity: 1; transform: scale(1); } + } + @keyframes g-chip-adm { + 0%, 36% { opacity: 0; transform: scale(0.85); } + 38%, 44% { opacity: 1; transform: scale(1); } + 47%, 100% { opacity: 0; transform: scale(1); } + } + @keyframes g-chip-stop { + 0%, 84% { opacity: 0; transform: scale(0.85); } + 86%, 100% { opacity: 1; transform: scale(1); } + } + @keyframes g-wire { + 0%, 42% { opacity: 0; } + 46%, 100% { opacity: 1; } + } + + /* the admitted nodes: ghost in blue, run amber, land green */ + @keyframes g-run-a { + 0%, 38% { opacity: 0; stroke: var(--g-status-admitted); fill: var(--g-card); } + 44%, 45% { opacity: 1; stroke: var(--g-status-admitted); fill: var(--g-card); } + 47%, 52% { opacity: 1; stroke: var(--g-status-running); fill: rgba(251, 191, 36, 0.10); } + 54%, 100% { opacity: 1; stroke: var(--g-status-done); fill: rgba(52, 211, 153, 0.07); } + } + @keyframes g-run-b { + 0%, 38% { opacity: 0; stroke: var(--g-status-admitted); fill: var(--g-card); } + 44%, 53% { opacity: 1; stroke: var(--g-status-admitted); fill: var(--g-card); } + 55%, 61% { opacity: 1; stroke: var(--g-status-running); fill: rgba(251, 191, 36, 0.10); } + 63%, 100% { opacity: 1; stroke: var(--g-status-done); fill: rgba(52, 211, 153, 0.07); } + } + @keyframes g-run-d { + 0%, 38% { opacity: 0; stroke: var(--g-status-admitted); fill: var(--g-card); } + 44%, 62% { opacity: 1; stroke: var(--g-status-admitted); fill: var(--g-card); } + 64%, 69% { opacity: 1; stroke: var(--g-status-running); fill: rgba(251, 191, 36, 0.10); } + 71%, 100% { opacity: 1; stroke: var(--g-status-done); fill: rgba(52, 211, 153, 0.07); } + } + @keyframes g-run-e { + 0%, 38% { opacity: 0; stroke: var(--g-status-admitted); fill: var(--g-card); } + 44%, 70% { opacity: 1; stroke: var(--g-status-admitted); fill: var(--g-card); } + 72%, 76% { opacity: 1; stroke: var(--g-status-running); fill: rgba(251, 191, 36, 0.10); } + 78%, 100% { opacity: 1; stroke: var(--g-status-done); fill: rgba(52, 211, 153, 0.07); } + } + @keyframes g-run-f { + 0%, 38% { opacity: 0; stroke: var(--g-status-admitted); fill: var(--g-card); } + 44%, 77% { opacity: 1; stroke: var(--g-status-admitted); fill: var(--g-card); } + 79%, 83% { opacity: 1; stroke: var(--g-status-running); fill: rgba(251, 191, 36, 0.10); } + 85%, 100% { opacity: 1; stroke: var(--g-status-done); fill: rgba(52, 211, 153, 0.07); } + } + + /* spend chips fade in as each node lands */ + @keyframes g-spend-a { 0%, 54% { opacity: 0; } 57%, 100% { opacity: 1; } } + @keyframes g-spend-b { 0%, 63% { opacity: 0; } 66%, 100% { opacity: 1; } } + @keyframes g-spend-d { 0%, 71% { opacity: 0; } 74%, 100% { opacity: 1; } } + @keyframes g-spend-e { 0%, 78% { opacity: 0; } 81%, 100% { opacity: 1; } } + @keyframes g-spend-f { 0%, 85% { opacity: 0; } 88%, 100% { opacity: 1; } } + + /* the meter fills in steps — spend is per node, not a smooth ramp */ + @keyframes g-meter { + 0%, 53% { transform: scaleX(0); } + 54%, 62% { transform: scaleX(0.14); } + 63%, 70% { transform: scaleX(0.46); } + 71%, 77% { transform: scaleX(0.62); } + 78%, 84% { transform: scaleX(0.80); } + 85%, 100% { transform: scaleX(1); } + } + @keyframes g-fade-late { 0%, 52% { opacity: 0; } 56%, 100% { opacity: 1; } } +} diff --git a/docs/site/assets/site.js b/docs/site/assets/site.js new file mode 100644 index 0000000..e8f0065 --- /dev/null +++ b/docs/site/assets/site.js @@ -0,0 +1,43 @@ +/* Progressive enhancement only: the page is complete without this file. + * Copy buttons appear when a clipboard exists; the hero animation pauses + * off-screen and gains a replay button. */ + +"use strict"; + +// Copy-to-clipboard on the terminal blocks. +if (navigator.clipboard) { + for (const button of document.querySelectorAll(".copy[data-copy]")) { + button.hidden = false; + button.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(button.dataset.copy); + button.textContent = "copied"; + setTimeout(() => { button.textContent = "copy"; }, 1500); + } catch { /* the command is right there to select */ } + }); + } +} + +const hero = document.getElementById("run"); +const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches; + +if (hero && !reducedMotion) { + // Battery: pause the 12s loop while the hero is not on screen. + if ("IntersectionObserver" in window) { + new IntersectionObserver((entries) => { + for (const entry of entries) { + hero.classList.toggle("offscreen", !entry.isIntersecting); + } + }).observe(hero); + } + + const replay = document.getElementById("replay-hero"); + if (replay) { + replay.hidden = false; + replay.addEventListener("click", () => { + hero.classList.add("reset"); + void hero.getBoundingClientRect(); // flush, so removal restarts the clock + hero.classList.remove("reset"); + }); + } +} diff --git a/docs/site/assets/tokens.css b/docs/site/assets/tokens.css new file mode 100644 index 0000000..d67db76 --- /dev/null +++ b/docs/site/assets/tokens.css @@ -0,0 +1,58 @@ +/* GraphARC design tokens — the single source of truth for both surfaces: + * this site and the live run view (grapharc/server/static/view.css copies + * the same values; change both). Grounded in docs/brand/README.md. */ + +:root { + /* surfaces */ + --g-bg: #0B0F19; + --g-surface: #111827; + --g-card: #151C2C; + --g-border: rgba(231, 236, 243, 0.08); + --g-border-hi: rgba(231, 236, 243, 0.16); + + /* text */ + --g-text: #E7ECF3; + --g-text-2: #94A3B8; + --g-text-3: #64748B; + + /* brand accents (logo gradient stops) */ + --g-teal: #2DD4BF; + --g-blue: #3B82F6; + --g-violet: #8B5CF6; + --g-teal-hi: #5EEAD4; + --g-blue-hi: #60A5FA; + --g-violet-hi: #A78BFA; + --g-gradient: linear-gradient(135deg, var(--g-teal), var(--g-blue) 50%, var(--g-violet)); + + /* node/run status — the README's grey/amber/green language */ + --g-status-proposed: #64748B; + --g-status-admitted: #60A5FA; + --g-status-running: #FBBF24; + --g-status-done: #34D399; + --g-status-rejected: #F87171; + --g-status-approval: #A78BFA; + --g-budget-fill: var(--g-teal); + --g-budget-warn: #FBBF24; + + /* type */ + --g-font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + --g-font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, + "Liberation Mono", monospace; + + /* rhythm: 4px base */ + --g-space-1: 0.25rem; + --g-space-2: 0.5rem; + --g-space-3: 0.75rem; + --g-space-4: 1rem; + --g-space-6: 1.5rem; + --g-space-8: 2rem; + --g-space-12: 3rem; + --g-space-16: 4rem; + --g-space-24: 6rem; + --g-radius-chip: 6px; + --g-radius-card: 12px; + --g-radius-pill: 999px; + --g-max-content: 1080px; + --g-max-prose: 720px; +} diff --git a/docs/site/index.html b/docs/site/index.html new file mode 100644 index 0000000..93eee63 --- /dev/null +++ b/docs/site/index.html @@ -0,0 +1,257 @@ + + + + + +GraphARC — the admission gate for agent graphs + + + + + + + + + + + + + + +

+ GraphARC + +
+ +
+ + +
+
+

Python 3.12+ · MIT · built on LangGraph · pip install grapharc

+

The admission gate for agent graphs.

+

A planner proposes a subgraph. A deterministic checker admits it — or + refuses with reasons. Only then does anything execute. GraphARC is a governed agent + runtime on LangGraph: every transition permitted, every loop bounded, and afterwards + you can prove what happened and why it stopped.

+

+ Get started + View on GitHub +

+
+
+ + + One governed GraphARC run + A planner proposes a subgraph containing a deploy node. + The admission gate rejects it with the reason edge_denied and nothing runs. + A second proposal with a patch node instead is admitted; its nodes execute + one by one, each stamping its spend on a budget meter, until the run stops + with goal met. The rejected round stays on the record. + + + + + + + + + + + + + + + planner + + + + + + admission + + + + + + + + + + + + REJECTED · edge_denied + + + + + + + + + + + + + triage$0.0031 + pull logs$0.0058 + pull metrics$0.0044 + correlate$0.0072 + patch$0.0049 + verify$0.0056 + + + + + + stop: goal_met + + + + + + + $0.031 / $0.05 + + + +

+
+
+ + +
+

Enforced by the library, not left to convention.

+

Each of these is a mechanism with a test you can run — not a policy document.

+
+
+

Admission before execution

+

Every proposed subgraph passes five deterministic checks — registry membership, + edge policy, worst-case budget fit, depth, acyclicity — before a single node is + built. Work discovered mid-run re-enters the same gate; there is no + already-approved path. A rejection is data: codes and remedies handed back to the + planner. grapharc run --check-only is the gate as a linter — it + executes nothing.

+
+
+

Budgets that bind before the bill

+

Cost ceilings are checked against a worst-case, execution-frequency-aware + estimate before anything runs. Actual per-node spend is stamped on the + trace even on error or cancellation — an overspent run still says what it spent. + recorded_cost_usd is never an estimate.

+
+
+

One trace file, every answer

+

Replay, diff, metrics, viz, cost attribution and OpenTelemetry spans all read + the same append-only JSONL file. Runs are replayable and comparable, and the + dashboard cannot disagree with the audit trail — they are the same record.

+
+
+
+ + +
+

Different tools, different jobs.

+

None of these is a competitor to be beaten. This is where GraphARC sits.

+
+

Claude Code

+

An interactive single-agent coding loop. Permissions are decided live, by you, + per action. Superb at its job; not a multi-node runtime. GraphARC's default + backend drives the Claude CLI.

+

OpenClaw

+

A personal AI assistant gateway. Safety comes from how you configure it. + GraphARC borrowed its policy-before-schema tool gating — and put it behind + enforcement.

+

Raw LangGraph

+

The mechanism GraphARC is built on. It gives you graphs; conventions like + write discipline, budgets and admission are yours to uphold. GraphARC upholds + them for you and raises when you don't.

+

GraphARC

+

The governance layer for multi-node agent graphs: propose → admit → execute → + replan, with the refusals on the record.

+
+
+ + +
+

Zero keys to try it.

+

The demo planner is scripted, so this run is free, deterministic, and identical on your machine.

+
+
+
$ pip install grapharc
+$ grapharc demo stage0          # deterministic DAG, costs nothing, needs no key
+$ grapharc plan "investigate the checkout outage"
+ +
+
+
   round 1: rejected  nodes=2 executed=False  rejected: edge_denied
+   round 2: admitted  nodes=3 executed=True
+
+

Round 1 wanted to deploy and never executed. Round 2 went through the + same checker and ran. Then watch any run redraw live in the browser:

+
+
$ grapharc serve --live-root .grapharc   # the live view, self-contained, no CDN
+ +
+
+ +
One question in, a governed graph out: a local model proposes the topology, + the admission gate and a human approval decide, and the live view shows every node run — + amber while executing, green when done.
+
+
+ + +
+

Measured claims only.

+

GraphARC is early (0.1.x) and the API is not stable. The README keeps a + Status and limits section that is re-derived by running each item, not by + reading the commit log. A sample of what it says: admission authorises a node's + kind, not its arguments; the in-process sandbox is defense in depth, not a + kernel boundary — ContainerExecutor is the boundary where one is needed; + the HTTP API does not yet use the durable session layer. If a sentence on this page + over-promises, the README wins.

+

Read the full list →

+
+ +
+ +
+ + +

Built on LangGraph. Design lineage: OpenClaw, Hermes Agent, Claude Code, + OpenRouter — studied from public documentation.

+
+ + + + diff --git a/grapharc/cli/config.py b/grapharc/cli/config.py index b0186db..0db0581 100644 --- a/grapharc/cli/config.py +++ b/grapharc/cli/config.py @@ -55,6 +55,7 @@ "memory": str, "max_rounds": int, "max_tokens": int, + "max_planning_failures": int, "max_iterations": int, "max_seconds": float, "max_concurrency": int, diff --git a/grapharc/cli/generate.py b/grapharc/cli/generate.py index ffcb301..68d1cbf 100644 --- a/grapharc/cli/generate.py +++ b/grapharc/cli/generate.py @@ -31,6 +31,7 @@ from __future__ import annotations +import re from pathlib import Path from typing import Any @@ -97,9 +98,21 @@ def targets(action: Decision) -> list[str]: return ", ".join(parts) -def generated_policy_path(workdir: Path | None = None) -> Path: - """Where a generated policy lives. Not created until something is written.""" - return Path(workdir or Path.cwd()) / GENERATED_DIR / GENERATED_POLICY +def generated_policy_path(workdir: Path | None = None, *, registry: str = "") -> Path: + """Where a generated policy lives. Not created until something is written. + + Keyed by the registry target that generated it. A policy is a statement + about one registry's kinds — a file generated for the incident demo's + `deploy` said nothing about the stdlib's `apply_change`, yet the un-keyed + cache served it anyway, silently overriding the stdlib registry's own + deny of its mutating kind. The un-keyed name is kept only so callers can + recognise (and refuse) legacy files; nothing writes it anymore. + """ + base = Path(workdir or Path.cwd()) / GENERATED_DIR + if not registry: + return base / GENERATED_POLICY + slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", registry) + return base / f"generated-policy.{slug}.toml" def build_policy_toml( @@ -148,6 +161,7 @@ def resolve_or_generate_policy( mutating: tuple[str, ...] = (), fallback: Any = None, fallback_label: str = "", + registry_target: str = "", ) -> tuple[Any, str, str]: """Return `(gate_policy, description, source)`. @@ -182,25 +196,35 @@ def resolve_or_generate_policy( policy, description = resolve_policy(policy_path, tenant=tenant) return policy, description, "flag-or-config" - cached = generated_policy_path(workdir) + cached = generated_policy_path(workdir, registry=registry_target) if cached.is_file(): # Read back as an ordinary document, node rules included: a generated # file the operator has since edited is theirs, not the generator's. policy, description = resolve_policy(cached, tenant=tenant) return policy, f"{description} [previously generated]", "generated-cached" + # A legacy un-keyed file cannot prove which registry it was generated for, + # so it governs nothing implicitly — fail closed to generation or the + # registry's own default, and say so. The operator's edits survive on disk + # and are one `--policy` flag away. + legacy = generated_policy_path(workdir) + legacy_note = ( + f" (ignoring un-keyed {legacy} — pass --policy to use it)" + if registry_target and legacy.is_file() + else "" + ) def _settled() -> tuple[Any, str, str]: if fallback is not None: label = fallback_label or "registry default" return ( GatePolicy(edge=fallback), - f"{label} ({describe_policy(fallback)})", + f"{label} ({describe_policy(fallback)}){legacy_note}", "registry-default", ) builtin = stdlib.default_edge_policy() return ( GatePolicy(edge=builtin), - f"built-in default ({describe_policy(builtin)})", + f"built-in default ({describe_policy(builtin)}){legacy_note}", "builtin-default", ) diff --git a/grapharc/cli/init_cmd.py b/grapharc/cli/init_cmd.py new file mode 100644 index 0000000..74c7dd8 --- /dev/null +++ b/grapharc/cli/init_cmd.py @@ -0,0 +1,421 @@ +"""`grapharc init` — scaffold a working directory an operator can grow. + +Three artifacts, all in cwd: a heavily-commented `registry.py` (the authoring +surface — node kinds, bodies, write permissions, edge policy, goal check), a +`grapharc.toml` pointing `plan`/`go` at it, and the `.grapharc/runs/` directory +the live server serves. The templates are string constants rather than packaged +data files, so nothing new can fall out of the wheel. + +Refusal semantics: never overwrite, no `--force`. The two files are *authored* +surfaces — the registry is code that will execute — and a scaffold that can +silently replace an operator's kinds is the one failure mode this command must +not have. `.grapharc/runs/` is ensured unconditionally: it is run output, not +authorship, and re-running `init` after a `serve` must not fail on it. +""" + +from __future__ import annotations + +from pathlib import Path + +from grapharc.cli import style +from grapharc.cli.output import EXIT_OK, EXIT_UNAVAILABLE, emit + +REGISTRY_FILENAME = "registry.py" +CONFIG_FILENAME = "grapharc.toml" +RUNS_DIR = Path(".grapharc") / "runs" + +REGISTRY_TEMPLATE = '''\ +"""Your GraphARC registry — the node kinds a planner may propose. + +Read this top to bottom once; it is the whole authoring surface. + +The contract, in one paragraph: a *registry* is an allowlist of node kinds. +Each kind owns its body (a Python function YOU wrote), a description the +planning model reads, and a worst-case cost the admission gate budgets +against. When you run `grapharc plan "some goal"`, a model proposes a graph — +which kinds, wired how — and a deterministic checker admits or refuses it +against this file and your policy. A proposal names kinds; it carries no +code, no arguments, no paths. Absence from this list is refusal; there is no +wildcard. + +Try it now, before editing anything (free — `--scripted` swaps the model +for this file's own canned planner replies, so no AI is involved): + + grapharc plan "review what is in this directory" --scripted + +Round 1 proposes the `apply` kind below and is refused (the edge policy +denies it); round 2 replans without it and is admitted — then everything +STOPS, planned but unexecuted. Look at it, then execute it: + + grapharc go + +That refusal is the point of the whole tool; keep one dangerous kind around +so you can watch the gate work. + +Then make it yours: rename the kinds, rewrite the bodies, grow the State. +`grapharc.toml` next door points `plan` at this file. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel + +from grapharc.harness.permissions import Decision +from grapharc.observe.trace import TraceRecorder +from grapharc.planner import ( + AdmissionChecker, + AdmissionLimits, + CostEstimate, + EdgePolicy, + EdgeRule, + GovernedLoop, + LoopLimits, + Materializer, + NodeRegistry, + NodeSpec, + PlannerNode, +) +from grapharc.runtime.budget import Budget + +# ── 1. State ──────────────────────────────────────────────────────────────── +# One typed contract for the whole run, however the topology changes between +# rounds. Every field a node writes must be declared here AND granted in +# WRITES below — a kind nobody declared writes for may write nothing. + + +class State(BaseModel): + goal: str = "" # filled from the CLI argument; the planner reads it + notes: list[str] = [] # the working record every kind appends to + report: str = "" # the deliverable; the goal check below watches it + + +# ── 2. Bodies ─────────────────────────────────────────────────────────────── +# Operator code. The planner never sees these and can never supply one — the +# registry is the only source of a body. Bodies read `state` and return a +# dict of the fields they write. + + +def _gather(state: State) -> dict: + """Collect raw material — really. Lists this directory so the report node + has actual evidence; replace with your own reading/searching code.""" + from pathlib import Path as _Path + + root = _Path.cwd() + entries = sorted(p for p in root.iterdir() if not p.name.startswith(".")) + dirs = [p.name + "/" for p in entries if p.is_dir()] + files = [p.name for p in entries if p.is_file()] + note = ( + f"gather: {root.name}/ holds {len(dirs)} director(y/ies) " + f"({', '.join(dirs[:12]) or 'none'}) and {len(files)} file(s) " + f"({', '.join(files[:12]) or 'none'})" + ) + return {"notes": [*state.notes, note]} + + +def _analyse(state: State) -> dict: + """Work over what gather found. Deterministic on purpose: cheap, testable.""" + from pathlib import Path as _Path + + suffixes: dict[str, int] = {} + for p in _Path.cwd().rglob("*"): + if p.is_file() and not any(part.startswith(".") for part in p.parts): + suffixes[p.suffix or "(none)"] = suffixes.get(p.suffix or "(none)", 0) + 1 + top = sorted(suffixes.items(), key=lambda kv: -kv[1])[:6] + note = "analyse: file types by count — " + ", ".join( + f"{ext} x{count}" for ext, count in top + ) + return {"notes": [*state.notes, note]} + + +def _report_for(model: Any): + """The one body with a model in it, built per-run so it closes over the + run's own backend. With no real model (scripted runs) it says so honestly + instead of pretending — and still writes `report`, so the goal check + passes and a free run completes end to end.""" + + def body(state: State) -> dict: + # A scripted double is the free path's planner, not a writer — its + # replies are plans, and asking it for prose exhausts the script. + scripted = "Scripted" in type(model).__name__ + if model is None or scripted or not hasattr(model, "invoke"): + return { + "report": "report: run with --model SPEC for a model-written report", + "notes": [*state.notes, "report: written without a model"], + } + try: + reply = model.invoke( + f"Goal: {state.goal!r}. Evidence:\\n" + "\\n".join(state.notes) + + "\\n\\nWrite one short report that satisfies the goal, grounded " + "only in the evidence above." + ) + text = str(getattr(reply, "content", reply)).strip()[:2000] + except Exception as exc: # a failed call is a note, not a crash + text = f"report: model call failed ({exc}); notes stand" + return {"report": text, "notes": [*state.notes, "report: written"]} + + return body + + +def _apply(state: State) -> dict: + """The deliberately dangerous kind: in your registry this would change + things — write files, call APIs, deploy. It is REGISTERED (a planner may + propose it) and DENIED by the edge policy below (no admitted graph may + reach it) until you decide otherwise. Keep the pattern even after you + rename it: a gate with nothing to refuse proves nothing.""" + return {"notes": [*state.notes, "apply: this should not have run"]} + + +# ── 3. Write permissions ──────────────────────────────────────────────────── +# Enforced by the materializer, not trusted to the bodies. A body that writes +# an unlisted field is refused at build time. + +WRITES: dict[str, set[str]] = { + "gather": {"notes"}, + "analyse": {"notes"}, + "report": {"report", "notes"}, + "apply": {"notes"}, +} + +# ── 4. The registry ───────────────────────────────────────────────────────── +# `description` is what the planning model reads when choosing kinds — write +# it for the model. `worst_case` is what admission budgets against — estimate +# it honestly; a lowball here is a budget the gate cannot keep. + + +def build_registry(model: Any = None) -> NodeRegistry: + """Accepting `model` is the CLI's contract: a factory with a positional + parameter is handed the run's backend.""" + + def factory(build: Any) -> Any: + # `build.kind` is what this registry licensed; `build.name` is the + # instance name the planner chose. Behaviour keys on the kind. + if build.kind == "report": + return _report_for(model) + return {"gather": _gather, "analyse": _analyse, "apply": _apply}[build.kind] + + return NodeRegistry( + [ + NodeSpec( + name="gather", + description="collect raw material for the goal", + factory=factory, + worst_case=CostEstimate(iterations=1, tokens=400), + ), + NodeSpec( + name="analyse", + description="work over what gather collected", + factory=factory, + worst_case=CostEstimate(iterations=1, tokens=800), + ), + NodeSpec( + name="report", + description="write the deliverable from the notes; the run is " + "complete once this has run", + factory=factory, + worst_case=CostEstimate(iterations=1, tokens=2500), + ), + NodeSpec( + name="apply", + description="act on the report (changes things)", + factory=factory, + worst_case=CostEstimate(iterations=1, tokens=500), + ), + ] + ) + + +# ── 5. Edge policy ────────────────────────────────────────────────────────── +# What may be wired to what when no --policy document is given. Deny beats +# allow. The one rule below is the demo refusal; delete it the day you mean it. + + +def default_edge_policy() -> EdgePolicy: + return EdgePolicy( + rules=( + EdgeRule(action=Decision.DENY, target="apply"), # nothing reaches apply + EdgeRule(action=Decision.ALLOW), # everything else may flow + ) + ) + + +# ── 6. Dangerous kinds ────────────────────────────────────────────────────── +# Read by the policy generator on a first --model run so a generated policy +# knows what is worth denying. Empty means "nothing here mutates" — say so +# only if it is true. + +MUTATING_KINDS: tuple[str, ...] = ("apply",) + +# ── 7. The scripted planner ───────────────────────────────────────────────── +# What `grapharc plan` uses when no --model is given, so the free path +# exercises this registry — refusal included — and spends nothing. + + +def scripted_planner_replies() -> list[str]: + import json + + from grapharc.runtime.graph import END, START + + def chain(*kinds: str) -> str: + stops = [START, *kinds, END] + return json.dumps( + { + "nodes": [{"name": k} for k in kinds], + "edges": [ + {"source": a, "target": b} + for a, b in zip(stops, stops[1:], strict=False) + ], + } + ) + + # Round 1 reaches for `apply` and is refused; round 2 replans without it. + return [chain("gather", "apply"), chain("gather", "analyse", "report")] + + +# ── 8. The loop ───────────────────────────────────────────────────────────── +# Owning build_loop is what gives this file its own goal check. Without one, +# `grapharc plan` falls back to the incident demo's check (len(notes) >= 3) — +# a documented trap for any state that counts differently. + + +def build_loop( + model, + *, + edge_policy=None, + node_policy=None, + trace: TraceRecorder | None = None, + budget: Budget | None = None, + limits: LoopLimits | None = None, + registry: NodeRegistry | None = None, + state_schema=None, + writes=None, + approval=None, +) -> GovernedLoop: + registry = registry or build_registry(model) + registry.freeze() # the same object every round + edge_policy = edge_policy or default_edge_policy() + return GovernedLoop( + planner=PlannerNode( + model, + name="planner", + catalog=registry.catalog(), + edge_policy=edge_policy, + node_policy=node_policy, + trace=trace, + ), + checker=AdmissionChecker( + registry=registry, + edge_policy=edge_policy, + node_policy=node_policy, + trace=trace, + limits=AdmissionLimits(require_entry=True), + ), + materializer=Materializer( + registry=registry, + state_schema=state_schema or State, + writes=writes if writes is not None else WRITES, + trace=trace, + ), + budget=budget, + limits=limits, + trace=trace, + name="my_loop", + # Deterministic code, never a model: "am I done" is exactly the + # question a run must not talk itself into answering yes. + goal_reached=lambda state: bool(getattr(state, "report", "")), + approval=approval, + ) + + +STATE_SCHEMA = State +''' + +CONFIG_TEMPLATE = '''\ +# grapharc.toml — defaults for the flags nobody wants to retype. +# Read from THIS directory only; parent directories are never searched, so a +# run cannot be governed by a file you did not know about. Flags beat +# environment (GRAPHARC_*) beat this file beat built-ins. + +[grapharc] +# The node kinds a planner may propose: a module:attr, or a .py file next to +# this config (cwd-relative — run grapharc from this directory). +registry = "registry.py:build_registry" + +# Uncomment to plan with a real model instead of the free scripted planner. +# `grapharc models --check` shows what this machine can use. +# model = "ollama/qwen3:8b" + +# Uncomment to pin a policy document instead of the registry's own default +# (or the one a first --model run generates into .grapharc/). +# policy = "policy.toml" + +# max_rounds = 8 +# max_tokens = 100000 +''' + + +def init(*, as_json: bool = False) -> int: + """Scaffold a working directory: registry.py, grapharc.toml, .grapharc/runs/.""" + registry = Path(REGISTRY_FILENAME) + config = Path(CONFIG_FILENAME) + + existing = [str(p) for p in (registry, config) if p.exists()] + if existing: + message = ( + f"refusing to overwrite: {', '.join(existing)} — move it aside, " + "or start in an empty directory" + ) + if as_json: + emit({"ok": False, "command": "init", "error": message}, [], as_json=True) + else: + import sys + + print(f"error: {message}", file=sys.stderr) + return EXIT_UNAVAILABLE # exit 2: could not run at all + + registry.write_text(REGISTRY_TEMPLATE, encoding="utf-8") + config.write_text(CONFIG_TEMPLATE, encoding="utf-8") + RUNS_DIR.mkdir(parents=True, exist_ok=True) + + payload = { + "ok": True, + "command": "init", + "registry": str(registry), + "config": str(config), + "runs": str(RUNS_DIR), + } + width = style.LABEL_WIDTH + lines = [ + style.kv( + "wrote", + f"{registry} (your node kinds — open it, it is heavily commented)", + width=width, + tint=style.accent, + ), + style.kv( + "wrote", + f"{config} (points plan at registry.py:build_registry)", + width=width, + tint=style.accent, + ), + style.kv( + "created", + f"{RUNS_DIR}/ (traces land here; serve it as the live root)", + width=width, + ), + "", + style.kv( + "next", + 'grapharc plan "review what is in this directory" --scripted', + width=width, + ), + style.kv("", "grapharc go (executes the plan it saved)", width=width), + style.kv("", f"grapharc serve --live-root {RUNS_DIR} (second terminal)", width=width), + style.kv("", 'grapharc go "your real goal" --model ollama/qwen3:8b', width=width), + ] + emit(payload, lines, as_json=as_json) + return EXIT_OK + + +__all__ = ["CONFIG_TEMPLATE", "REGISTRY_TEMPLATE", "init"] diff --git a/grapharc/cli/main.py b/grapharc/cli/main.py index b59e029..3aed159 100644 --- a/grapharc/cli/main.py +++ b/grapharc/cli/main.py @@ -357,10 +357,86 @@ def _cmd_plan(args: argparse.Namespace) -> int: run_id=args.run_id, max_rounds=args.max_rounds, max_tokens=args.max_tokens, + max_planning_failures=args.max_planning_failures, + workspace=args.workspace, + model_arg_pairs=args.model_arg, config_path=args.config, approve=args.approve, approval_timeout=args.approval_timeout, as_json=args.json, + scripted=args.scripted, + go_after=args.go_after, + use_default_registry=args.default_registry, + ) + + +def _cmd_init(args: argparse.Namespace) -> int: + from grapharc.cli.init_cmd import init + + return init(as_json=args.json) + + +def _cmd_start(args: argparse.Namespace) -> int: + from grapharc.cli.start import start + + return start(as_json=args.json) + + +def _cmd_go(args: argparse.Namespace) -> int: + from grapharc.cli.plan import PLAN_FILENAME, execute_plan, plan + + # Two natures, one word. Bare `go` — or `go ` — executes a plan + # `grapharc plan` saved; `go "some goal"` is plan-and-execute in one run. + target = args.goal + if target is None: + return execute_plan( + None, + model_spec=args.model, + model_arg_pairs=args.model_arg, + workspace=args.workspace, + policy_path=args.policy, + tenant=args.tenant, + run_id=args.run_id, + max_tokens=args.max_tokens, + config_path=args.config, + as_json=args.json, + ) + candidate = Path(target) + if candidate.name == PLAN_FILENAME and candidate.is_file() or ( + candidate.is_dir() and (candidate / PLAN_FILENAME).is_file() + ): + return execute_plan( + target, + model_spec=args.model, + model_arg_pairs=args.model_arg, + workspace=args.workspace, + policy_path=args.policy, + tenant=args.tenant, + run_id=args.run_id, + max_tokens=args.max_tokens, + config_path=args.config, + as_json=args.json, + ) + return plan( + target, + model_spec=args.model, + registry_target=args.registry, + policy_path=args.policy, + tenant=args.tenant, + trace_path=args.trace, + run_id=args.run_id, + max_rounds=args.max_rounds, + max_tokens=args.max_tokens, + max_planning_failures=args.max_planning_failures, + workspace=args.workspace, + model_arg_pairs=args.model_arg, + config_path=args.config, + approve=args.approve, + approval_timeout=args.approval_timeout, + as_json=args.json, + command="go", + go_after=True, + use_default_registry=args.default_registry, ) @@ -594,10 +670,85 @@ def _cmd_viz(args: argparse.Namespace) -> int: # -- parser ------------------------------------------------------------------- +def _add_planning_flags(parser: argparse.ArgumentParser) -> None: + """The flags `plan` and `go` share, added identically to both. + + One list, two parsers: a flag that exists on one and not the other is a + doc bug waiting to be filed, and the handlers thread every one of these + into the same `plan()` call. + """ + parser.add_argument( + "--policy", + type=Path, + default=None, + metavar="PATH", + help="TOML policy document whose edge rules become the admission gate's EdgePolicy", + ) + parser.add_argument( + "--tenant", default=None, metavar="NAME", help="tenant to compile --policy for" + ) + parser.add_argument("--trace", type=Path, default=None, help="trace JSONL output path") + parser.add_argument( + "--run-id", default=None, help="name this run; refused if --trace already holds it" + ) + parser.add_argument( + "--max-rounds", type=int, default=None, + help="planning rounds the loop may take (default: 8)", + ) + parser.add_argument( + "--max-tokens", type=int, default=None, + help="run token ceiling across every round (default: 100000)", + ) + parser.add_argument( + "--max-planning-failures", type=int, default=None, metavar="N", + help="consecutive unusable planner replies before giving up (default: 3)", + ) + parser.add_argument( + "--workspace", + type=Path, + default=None, + metavar="DIR", + help=( + "confine the registry's tool-using kinds (and its workspace listing) " + "to DIR; refused when the registry cannot take one" + ), + ) + parser.add_argument( + "--model-arg", + action="append", + default=None, + metavar="KEY=VALUE", + help="constructor argument for --model (repeatable), e.g. --model-arg temperature=0", + ) + parser.add_argument( + "--approve", + action="store_true", + help="pause each admitted round until `grapharc approve` answers next to the trace", + ) + parser.add_argument( + "--approval-timeout", + type=float, + default=None, + metavar="SECONDS", + help="how long --approve waits before the round counts as unapproved (default: 300)", + ) + parser.add_argument( + "--default", + action="store_true", + dest="default_registry", + help=( + "use the built-in general-purpose kinds (read/edit files, write a " + "report) instead of a registry.py in this directory" + ), + ) + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="grapharc", description=__doc__) parser.add_argument("--version", action="version", version=f"grapharc {__version__}") - sub = parser.add_subparsers(dest="command", required=True) + # Not required: bare `grapharc` orients a newcomer (exit 0, like `-h`) + # instead of erroring — `main()` handles the no-command branch. + sub = parser.add_subparsers(dest="command", required=False) # `--json` is attached to every subcommand rather than to the top level, so # `grapharc run stage0 --json` works — the position a shell user reaches for. @@ -749,7 +900,7 @@ def build_parser() -> argparse.ArgumentParser: plan = sub.add_parser( "plan", parents=[common, configurable], - help="drive the governed planning loop against a goal", + help="rehearse the governed planning loop against a goal (see also: go)", ) plan.add_argument("goal", help="what the planner should plan for") plan.add_argument( @@ -765,40 +916,67 @@ def build_parser() -> argparse.ArgumentParser: help=f"the node kinds a planner may propose (default: {DEFAULT_REGISTRY})", ) plan.add_argument( - "--policy", - type=Path, - default=None, - metavar="PATH", - help="TOML policy document whose edge rules become the admission gate's EdgePolicy", - ) - plan.add_argument( - "--tenant", default=None, metavar="NAME", help="tenant to compile --policy for" + "--go", + action="store_true", + dest="go_after", + help="plan and immediately execute it, in one run", ) - plan.add_argument("--trace", type=Path, default=None, help="trace JSONL output path") plan.add_argument( - "--run-id", default=None, help="name this run; refused if --trace already holds it" + "--scripted", + action="store_true", + help=( + "rehearse with the registry's built-in stand-in planner instead of " + "a model — free, deterministic, no AI involved; contradicts --model" + ), ) - plan.add_argument( - "--max-rounds", type=int, default=None, - help="planning rounds the loop may take (default: 8)", + _add_planning_flags(plan) + plan.set_defaults(handler=_cmd_plan) + + # `go` is `plan` with doing-defaults: the stdlib registry (real tool-using + # kinds) and a required model — go means do, and the scripted planner does + # nothing worth doing. Same handler underneath; only the defaults differ. + go = sub.add_parser( + "go", + parents=[common, configurable], + help="plan AND do: a model proposes the graph, stdlib kinds do real work", ) - plan.add_argument( - "--max-tokens", type=int, default=None, - help="run token ceiling across every round (default: 100000)", + go.add_argument( + "goal", + nargs="?", + default=None, + help=( + "what to get done — or a run directory holding a saved plan.json; " + "with nothing, the newest unexecuted plan runs" + ), ) - plan.add_argument( - "--approve", - action="store_true", - help="pause each admitted round until `grapharc approve` answers next to the trace", + go.add_argument( + "--model", + default=None, + metavar="SPEC", + help="required: the model that plans and works (see `grapharc models --check`)", ) - plan.add_argument( - "--approval-timeout", - type=float, + go.add_argument( + "--registry", default=None, - metavar="SECONDS", - help="how long --approve waits before the round counts as unapproved (default: 300)", + metavar="MODULE:ATTR", + help="the node kinds a planner may propose (default: grapharc.stdlib:build_registry)", ) - plan.set_defaults(handler=_cmd_plan) + _add_planning_flags(go) + go.set_defaults(handler=_cmd_go) + + ini = sub.add_parser( + "init", + parents=[common], + help="scaffold a registry, a config and a runs directory in this directory", + ) + ini.set_defaults(handler=_cmd_init) + + st = sub.add_parser( + "start", + parents=[common], + help="the guided tour: concept, first run, live view", + ) + st.set_defaults(handler=_cmd_start) ap = sub.add_parser( "approve", @@ -946,12 +1124,30 @@ def build_parser() -> argparse.ArgumentParser: def main(argv: list[str] | None = None) -> int: + raw = sys.argv[1:] if argv is None else argv + # `grapharc help` is what people type; making it an argparse error taught + # nothing. It is `-h`, and `help ` is ` -h`. + if raw[:1] == ["help"]: + parser = build_parser() + rest = raw[1:] + if rest: + return main([*rest, "-h"]) + parser.print_help() + return EXIT_OK args = build_parser().parse_args(argv) # `--json` disables colour too, belt and braces: a JSON run has no human # reader, and `tests/test_cli.py` requires that exactly one document reaches # stdout and nothing at all reaches stderr. Turning styling off at the source # means no future call site can leak an escape sequence into a payload. - style.configure(no_color=args.no_color or args.json) + style.configure( + no_color=getattr(args, "no_color", False) or getattr(args, "json", False) + ) + if args.command is None: + from grapharc.cli.start import orientation + + for line in orientation(): + print(line) + return EXIT_OK if args.command == "models" and args.check and args.spec: return fail( "`models --check` probes the configured backends and `models ` " diff --git a/grapharc/cli/plan.py b/grapharc/cli/plan.py index 5ca5ca4..1a7af0e 100644 --- a/grapharc/cli/plan.py +++ b/grapharc/cli/plan.py @@ -28,7 +28,6 @@ from __future__ import annotations -import importlib import tempfile from dataclasses import dataclass from pathlib import Path @@ -42,12 +41,392 @@ from grapharc.cli.runid import refuse_reused_run_id DEFAULT_REGISTRY = "grapharc.examples.plan_incident:build_registry" +STDLIB_REGISTRY = "grapharc.stdlib:build_registry" +PLAN_FILENAME = "plan.json" + + +def resolve_registry_target( + configured: str | None, *, scripted: bool, use_default: bool +) -> str: + """Which registry governs this run, by one visible chain. + + Flag/config first, always. Then, unconfigured: a `registry.py` in this + directory is *yours* and wins — that is what `grapharc init` scaffolds + and what an operator expects to be running. `--default` forces the + built-in general-purpose kinds past all of that. With nothing at all: + the built-ins for real runs; the shipped rehearsal registry for + `--scripted`, whose canned replies exist to demonstrate a refusal. + """ + if use_default: + return STDLIB_REGISTRY + if configured: + return configured + if Path("registry.py").is_file(): + return "registry.py:build_registry" + if scripted: + return DEFAULT_REGISTRY + return STDLIB_REGISTRY + + +def default_trace_path() -> Path: + """`.grapharc/runs/-<6 hex>/trace.jsonl` under cwd. + + The same shape the Slack gate mints, and — deliberately — inside the + directory `grapharc serve --live-root .grapharc/runs` serves, so a default + run is watchable without anyone passing `--trace`. The random suffix keeps + two plans started in the same second apart. Falls back to the old tempdir + when cwd is unwritable: a read-only checkout must not make `plan` exit 2 + over a file the operator never asked for. + """ + import uuid + from datetime import UTC, datetime + + stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") + run_dir = Path(".grapharc") / "runs" / f"{stamp}-{uuid.uuid4().hex[:6]}" + try: + run_dir.mkdir(parents=True, exist_ok=True) + except OSError: + return Path(tempfile.mkdtemp(prefix="grapharc-plan-")) / "trace.jsonl" + return run_dir / "trace.jsonl" + + +def watch_url(trace_path: Path, *, run_id: str | None = None, timeout: float = 0.25) -> str | None: + """The live-view URL for this trace, when a server is up to serve it. + + `.grapharc/live-server.json` (written by `grapharc serve --live-root`) + names the server; the URL is returned only when (a) the trace resolves + under the marker's live root and (b) one loopback TCP connect — never + slower than `timeout` — answers, so a marker left by a crashed server is + harmless. None on every other path; the caller decides what hint to print + instead, because a guessed URL is worse than an honest instruction. + """ + import json + import socket + from urllib.parse import quote + + marker = Path(".grapharc") / "live-server.json" + try: + record = json.loads(marker.read_text(encoding="utf-8")) + root = Path(record["live_root"]) + base = str(record["url"]) + host, port = str(record["host"]), int(record["port"]) + except (OSError, ValueError, KeyError): + return None + try: + rel = trace_path.resolve().relative_to(root) + except ValueError: + return None + try: + with socket.create_connection((host, port), timeout=timeout): + pass + except OSError: + return None + url = f"{base}/live/view?trace={quote(rel.as_posix(), safe='')}" + if run_id: + url += f"&run={quote(run_id)}" + return url + + +def watch_hint(trace_path: Path) -> str: + """What to print when no live server answers: the command, then the URL. + + The user asked for the link to always exist — so when it cannot be exact, + it is an instruction that produces the exact one. + """ + try: + rel = trace_path.resolve().relative_to((Path(".grapharc") / "runs").resolve()) + from urllib.parse import quote + + would_be = f"http://127.0.0.1:8000/live/view?trace={quote(rel.as_posix(), safe='')}" + return f"run `grapharc serve --live-root .grapharc/runs` then open {would_be}" + except ValueError: + return ( + f"run `grapharc serve --live-root {trace_path.parent}` " + f"then open http://127.0.0.1:8000/live" + ) class PlanSetupError(Exception): """Raised before anything runs, so a bad flag never half-executes a plan.""" +def _write_plan_file(run_dir: Path, *, goal, registry_target, model_spec, result) -> None: + """Persist the admitted-but-unexecuted plan next to its trace. + + What `grapharc go` reads. The proposal is stored whole and re-judged by + admission at execution time — a hand-edited plan.json is a new proposal, + not a pre-approved one. + """ + import json + from datetime import UTC, datetime + + admitted = next( + (r for r in reversed(result.rounds) if r.proposal is not None and r.admission + and r.admission.status.value == "admitted"), + None, + ) + if admitted is None: + return + (run_dir / PLAN_FILENAME).write_text( + json.dumps( + { + "goal": goal, + "registry": registry_target, + "model": model_spec, + "fingerprint": admitted.proposal.fingerprint(), + "proposal": admitted.proposal.model_dump(mode="json"), + "planned_at": datetime.now(UTC).isoformat(), + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + + +def find_unexecuted_plan(runs_root: Path | None = None) -> Path | None: + """The newest saved plan `go` has not executed yet, or None.""" + import json + + root = runs_root or Path(".grapharc") / "runs" + candidates = sorted(root.glob(f"*/{PLAN_FILENAME}"), reverse=True) + for candidate in candidates: + try: + record = json.loads(candidate.read_text(encoding="utf-8")) + except (OSError, ValueError): + continue + if not record.get("executed_run_id"): + return candidate + return None + + +def execute_plan( + target: str | None, + *, + model_spec: str | None = None, + model_arg_pairs: list[str] | None = None, + workspace: Path | None = None, + policy_path: Path | None = None, + tenant: str | None = None, + run_id: str | None = None, + max_tokens: int | None = None, + config_path: Path | None = None, + as_json: bool = False, +) -> int: + """`grapharc go []` — execute a plan `grapharc plan` saved. + + The stored proposal is replayed through the full governed loop — a + scripted planner whose one reply *is* the plan — so admission judges it + again on the way in: what runs is what the gate admits now, not what a + file claims was admitted before. Executing is the human approval; there + is no second gate. + """ + import json + + from grapharc.planner import LoopLimits + from grapharc.runtime.budget import Budget + from grapharc.testing import ScriptedChatModel + + if target is None: + plan_file = find_unexecuted_plan() + if plan_file is None: + return fail( + "no unexecuted plan under .grapharc/runs — run `grapharc plan " + '""` first, or pass a run directory', + as_json=as_json, + command="go", + code=EXIT_FAILED, + ) + else: + candidate = Path(target) + plan_file = candidate if candidate.name == PLAN_FILENAME else candidate / PLAN_FILENAME + if not plan_file.is_file(): + return fail( + f"no {PLAN_FILENAME} in {candidate} — `grapharc plan` writes one " + "next to its trace", + as_json=as_json, + command="go", + ) + + try: + record = json.loads(plan_file.read_text(encoding="utf-8")) + from grapharc.planner import Subgraph + + proposal = Subgraph.model_validate(record["proposal"]) + goal = str(record.get("goal", "")) + registry_target = str(record["registry"]) + except (OSError, ValueError, KeyError) as exc: + return fail(f"unreadable plan file {plan_file}: {exc}", as_json=as_json, command="go") + + run_dir = plan_file.parent + trace_path = run_dir / "trace.jsonl" + + try: + settings = load_settings(config_path) + model_spec = settings.resolve("model", model_spec, record.get("model")) + tenant = settings.resolve("tenant", tenant, "default") + max_tokens = settings.resolve("max_tokens", max_tokens, 100_000) + model_args = _parse_model_args(model_arg_pairs) + # The registry gets the real model (agent-backed kinds need one); the + # PLANNER gets a stand-in whose only reply is the saved plan. + model = None + model_description = "none (deterministic bodies only)" + if model_spec: + from grapharc.gateway import get_model + + model = get_model(model_spec, **model_args) + model_description = model_spec + bundle = resolve_registry(registry_target, model, workspace=workspace) + gate_policy, policy_description, policy_source = resolve_or_generate_policy( + policy_path, + tenant=tenant, + model=None, # executing a saved plan never generates policy + goal=goal, + catalog=bundle.registry.catalog(), + mutating=bundle.mutating, + fallback=bundle.default_policy, + fallback_label=f"{registry_target} default", + registry_target=registry_target, + ) + except (ConfigError, PlanSetupError) as exc: + return fail(str(exc), as_json=as_json, command="go", goal=goal) + except Exception as exc: # noqa: BLE001 — a backend that will not load is a setup failure + return fail(f"could not load the plan's setup: {exc}", as_json=as_json, command="go") + + from grapharc.examples.plan_incident import IncidentState + from grapharc.examples.plan_incident import build_loop as incident_build_loop + from grapharc.observe.trace import TraceRecorder + + replay = { + "nodes": [n.model_dump(mode="json", exclude={"subgraph"}) for n in proposal.nodes], + "edges": [e.model_dump(mode="json") for e in proposal.edges], + "rationale": proposal.rationale, + } + planner_model = ScriptedChatModel(responses=[json.dumps(replay)]) + schema = bundle.state_schema or IncidentState + trace = TraceRecorder(trace_path) + build_loop = bundle.build_loop or incident_build_loop + loop = build_loop( + planner_model, + edge_policy=gate_policy.edge, + node_policy=gate_policy.node, + trace=trace, + budget=Budget(max_tokens=max_tokens), + limits=LoopLimits(max_rounds=1), + registry=bundle.registry, + state_schema=schema, + writes=bundle.writes, + approval=None, + ) + initial = schema(goal=goal) if "goal" in schema.model_fields else schema() + result = loop.run(goal, initial, run_id=run_id) + + executed = any(r.executed for r in result.rounds) + if executed: + record["executed_run_id"] = result.run_id + plan_file.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8") + + url = watch_url(trace_path, run_id=result.run_id) + payload = { + "ok": executed, + "command": "go", + "goal": goal, + "plan": str(plan_file), + "registry": registry_target, + "model": model_description, + "policy": policy_description, + "policy_source": policy_source, + "stop": result.stop.value, + "detail": result.detail, + "executed": executed, + "run_id": result.run_id, + "trace": str(trace_path), + "watch_url": url, + "state": result.state.model_dump() if hasattr(result.state, "model_dump") else result.state, + } + stop_tint = style.ok if executed else style.warn + lines = [ + style.kv("plan", str(plan_file), width=style.LABEL_WIDTH), + style.kv("goal", goal, width=style.LABEL_WIDTH), + style.kv("model", model_description, width=style.LABEL_WIDTH, tint=style.accent), + style.kv("registry", registry_target, width=style.LABEL_WIDTH, tint=style.accent), + style.kv( + "policy", + f"{policy_description} {style.dim(f'[{policy_source}]')}", + width=style.LABEL_WIDTH, + ), + "", + style.kv( + "stopped", + f"{stop_tint(result.stop.value)} {style.dim(f'({result.detail})')}", + width=style.LABEL_WIDTH, + ), + style.kv("state", str(result.state), width=style.LABEL_WIDTH), + style.kv("trace", str(trace_path), width=style.LABEL_WIDTH, tint=style.accent), + style.kv( + "watch", + url if url else style.dim(watch_hint(trace_path)), + width=style.LABEL_WIDTH, + tint=style.accent if url else None, + ), + ] + emit(payload, lines, as_json=as_json) + return EXIT_OK if executed else EXIT_FAILED + + +def _registry_module(target: str) -> tuple[Any, str]: + """The module a `--registry` target names, and the attribute after the colon. + + Two forms, one rule — before the colon: an importable module name, or a + `.py` file; after it: the attribute. The path form (detected by a `.py` + suffix or a path separator) is what `grapharc init`'s scaffold uses: + `registry.py:build_registry`, cwd-relative like everything else here. + + Loaded via `spec_from_file_location` under a deterministic private name + derived from the resolved path, registered in `sys.modules` *before* + `exec_module` (pydantic model creation looks the module up by name + mid-exec), and reused from `sys.modules` on a second call — which is what + keeps `resolve_registry` and `_model_for` looking at one module object. + `sys.path` is never touched: the file may import installed packages but + not unlisted siblings; a package of kinds should use `module:attr`. + """ + import hashlib + import importlib.util + import os + import sys + + module_name, separator, attribute = target.partition(":") + if not separator or not attribute: + raise PlanSetupError( + f"--registry expects module:attr or path/to/file.py:attr, got {target!r}" + ) + if not (module_name.endswith(".py") or os.sep in module_name): + try: + return importlib.import_module(module_name), attribute + except ImportError as exc: + raise PlanSetupError(f"--registry {target!r}: {exc}") from exc + + path = Path(module_name) + if not path.is_file(): + raise PlanSetupError(f"--registry {target!r}: no such file: {path}") + resolved = path.resolve() + private = f"_grapharc_registry_{hashlib.sha1(str(resolved).encode()).hexdigest()[:12]}" + cached = sys.modules.get(private) + if cached is not None: + return cached, attribute + spec = importlib.util.spec_from_file_location(private, resolved) + if spec is None or spec.loader is None: + raise PlanSetupError(f"--registry {target!r}: could not load {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[private] = module + try: + spec.loader.exec_module(module) + except Exception as exc: # noqa: BLE001 — user code failing at import is a setup failure + sys.modules.pop(private, None) + raise PlanSetupError(f"--registry {target!r}: {exc}") from exc + return module, attribute + + @dataclass class RegistryBundle: """Everything a registry module supplies, travelling together. @@ -75,7 +454,9 @@ class RegistryBundle: build_loop: Any = None -def resolve_registry(target: str, model: Any = None) -> RegistryBundle: +def resolve_registry( + target: str, model: Any = None, *, workspace: Path | None = None +) -> RegistryBundle: """Import `module:attr` and return `(registry, state_schema, writes)`. A callable attribute is called, so both `mypkg:build_registry` and @@ -97,18 +478,25 @@ def resolve_registry(target: str, model: Any = None) -> RegistryBundle: silently paired with a schema its nodes cannot write to, or with a policy written for someone else's kinds, is worse than one that refuses to load. """ - module_name, separator, attribute = target.partition(":") - if not separator or not attribute: - raise PlanSetupError(f"--registry expects module:attr, got {target!r}") - try: - module = importlib.import_module(module_name) - except ImportError as exc: - raise PlanSetupError(f"--registry {target!r}: {exc}") from exc + module, attribute = _registry_module(target) registry = getattr(module, attribute, None) if registry is None: - raise PlanSetupError(f"--registry {target!r}: {module_name} has no {attribute!r}") + raise PlanSetupError(f"--registry {target!r}: {module.__name__} has no {attribute!r}") if callable(registry): - registry = registry(model) if _accepts_an_argument(registry) else registry() + kwargs: dict[str, Any] = {} + if workspace is not None: + # Fail closed, never silently un-confined: a factory that has no + # `workspace` parameter cannot honour the flag, and running anyway + # would leave the tools on the process cwd the operator asked to + # leave behind. + if not _accepts_keyword(registry, "workspace"): + raise PlanSetupError( + f"--workspace: registry {target!r} does not accept a workspace" + ) + kwargs["workspace"] = workspace + registry = ( + registry(model, **kwargs) if _accepts_an_argument(registry) else registry(**kwargs) + ) default_policy = getattr(module, "default_edge_policy", None) return RegistryBundle( registry=registry, @@ -135,6 +523,46 @@ def _accepts_an_argument(factory: Any) -> bool: ) +def _accepts_keyword(factory: Any, name: str) -> bool: + """Whether `factory` can take `name` as a keyword argument.""" + import inspect + + try: + signature = inspect.signature(factory) + except (TypeError, ValueError): + return False + for parameter in signature.parameters.values(): + if parameter.kind is inspect.Parameter.VAR_KEYWORD: + return True + if parameter.name == name and parameter.kind in ( + inspect.Parameter.KEYWORD_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ): + return True + return False + + +def _parse_model_args(pairs: list[str] | None) -> dict[str, Any]: + """`--model-arg KEY=VALUE` pairs into backend-constructor kwargs. + + Values are read as JSON first (`temperature=0` is the number zero, not the + string), falling back to the raw string. A pair with no `=` is a setup + error before anything runs. + """ + import json as _json + + kwargs: dict[str, Any] = {} + for pair in pairs or []: + key, separator, raw = pair.partition("=") + if not separator or not key: + raise PlanSetupError(f"--model-arg expects KEY=VALUE, got {pair!r}") + try: + kwargs[key] = _json.loads(raw) + except ValueError: + kwargs[key] = raw + return kwargs + + @dataclass(frozen=True) class GatePolicy: """The gate objects one policy document compiles to, travelling together. @@ -210,7 +638,11 @@ def compile_policy(engine: Any, *, tenant: str) -> GatePolicy: ) -def _model_for(spec: str | None, registry_target: str = DEFAULT_REGISTRY) -> tuple[Any, str]: +def _model_for( + spec: str | None, + registry_target: str = DEFAULT_REGISTRY, + model_args: dict[str, Any] | None = None, +) -> tuple[Any, str]: """The scripted planner by default; a real backend when asked for one. The scripted replies come from the registry module when it supplies @@ -223,19 +655,15 @@ def _model_for(spec: str | None, registry_target: str = DEFAULT_REGISTRY) -> tup if spec is None: from grapharc.testing import ScriptedChatModel - module_name = registry_target.split(":", 1)[0] - try: - module = importlib.import_module(module_name) - except ImportError as exc: - raise PlanSetupError(f"--registry {registry_target!r}: {exc}") from exc + module, _ = _registry_module(registry_target) replies = getattr(module, "scripted_planner_replies", None) if replies is None: from grapharc.examples.plan_incident import scripted_planner_replies as replies - return ScriptedChatModel(responses=replies()), "scripted" + return ScriptedChatModel(responses=replies()), "scripted stand-in (--scripted)" from grapharc.gateway import get_model - return get_model(spec), spec + return get_model(spec, **(model_args or {})), spec def plan( @@ -249,24 +677,39 @@ def plan( run_id: str | None = None, max_rounds: int | None = None, max_tokens: int | None = None, + max_planning_failures: int | None = None, + workspace: Path | None = None, + model_arg_pairs: list[str] | None = None, config_path: Path | None = None, settings: Settings | None = None, approve: bool = False, approval_timeout: float | None = None, as_json: bool = False, + command: str = "plan", + scripted: bool = False, + go_after: bool = False, + use_default_registry: bool = False, ) -> int: - """Run one governed planning loop against `goal`. Returns the exit code.""" + """Run one governed planning loop against `goal`. Returns the exit code. + + A real command runs a real model, full stop: with no `--model` (and none + in `grapharc.toml`), both `plan` and `go` refuse before anything happens. + The one exception is opt-in and named out loud — `plan --scripted` runs + the registry's canned planner replies so the machinery (admission, + refusal, approval, the live page) can be rehearsed for free. `go` has no + such flag: go means do. + """ from grapharc.observe.trace import TraceRecorder - from grapharc.planner import LoopLimits + from grapharc.planner import LoopLimits, LoopStop from grapharc.runtime.budget import Budget - trace_path = trace_path or Path(tempfile.mkdtemp(prefix="grapharc-plan-")) / "trace.jsonl" + trace_path = trace_path or default_trace_path() # Before the setup, because this one is about the file the setup would start # writing into: a run id already in that file merges this plan with an # earlier one under a single name. reused = refuse_reused_run_id( - trace_path, run_id, command="plan", as_json=as_json, goal=goal + trace_path, run_id, command=command, as_json=as_json, goal=goal ) if reused is not None: return reused @@ -277,13 +720,43 @@ def plan( if settings is None: settings = load_settings(config_path) model_spec = settings.resolve("model", model_spec) - registry_target = settings.resolve("registry", registry_target, DEFAULT_REGISTRY) + registry_target = resolve_registry_target( + settings.resolve("registry", registry_target, None), + scripted=scripted, + use_default=use_default_registry, + ) + if scripted and model_spec: + raise PlanSetupError( + "--scripted and --model contradict each other: the scripted " + "rehearsal never calls a model — drop one of the two" + ) + if not model_spec and not scripted: + if command == "go": + raise PlanSetupError( + "go plans with a real model and there is no scripted " + "fallback: pass --model SPEC (see `grapharc models --check` " + "for what this machine can use)" + ) + raise PlanSetupError( + "plan needs a real model: pass --model SPEC (see `grapharc " + "models --check`), set `model` in grapharc.toml — or add " + "--scripted to rehearse the machinery with the registry's " + "built-in stand-in planner (free, deterministic, no AI)" + ) policy_path = settings.resolve_path("policy", policy_path) tenant = settings.resolve("tenant", tenant, "default") max_rounds = settings.resolve("max_rounds", max_rounds, 8) max_tokens = settings.resolve("max_tokens", max_tokens, 100_000) - model, model_description = _model_for(model_spec, registry_target) - bundle = resolve_registry(registry_target, model) + max_planning_failures = settings.resolve( + "max_planning_failures", max_planning_failures, 3 + ) + if workspace is not None: + workspace = Path(workspace).resolve() + if not workspace.is_dir(): + raise PlanSetupError(f"--workspace: not a directory: {workspace}") + model_args = _parse_model_args(model_arg_pairs) + model, model_description = _model_for(model_spec, registry_target, model_args) + bundle = resolve_registry(registry_target, model, workspace=workspace) registry, state_schema, writes = bundle.registry, bundle.state_schema, bundle.writes gate_policy, policy_description, policy_source = resolve_or_generate_policy( policy_path, @@ -297,11 +770,12 @@ def plan( mutating=bundle.mutating, fallback=bundle.default_policy, fallback_label=f"{registry_target} default", + registry_target=registry_target, ) except (ConfigError, PlanSetupError) as exc: - return fail(str(exc), as_json=as_json, command="plan", goal=goal) + return fail(str(exc), as_json=as_json, command=command, goal=goal) except Exception as exc: # noqa: BLE001 — a backend that will not load is a setup failure - return fail(f"could not build the plan: {exc}", as_json=as_json, command="plan", goal=goal) + return fail(f"could not build the plan: {exc}", as_json=as_json, command=command, goal=goal) from grapharc.examples.plan_incident import IncidentState from grapharc.examples.plan_incident import build_loop as incident_build_loop @@ -314,6 +788,8 @@ def plan( from grapharc.planner.approval_file import DEFAULT_TIMEOUT_SECONDS, file_approval + watch_shown = False + def _announce(message: str) -> None: # Printed *and flushed* before the run parks: a terminal user (or a # log tailer) must learn how to answer without waiting for the exit. @@ -321,6 +797,18 @@ def _announce(message: str) -> None: # a notice printed ahead of it makes the whole output unparseable. if as_json: return + # The live link first, once: the page is where the parked proposal + # is drawn, and it should be open while the human decides. + nonlocal watch_shown + if not watch_shown: + watch_shown = True + url = watch_url(trace_path, run_id=run_id) + if url: + print( + style.kv("watch", url, width=style.LABEL_WIDTH, tint=style.accent), + flush=True, + file=sys.stdout, + ) print(message, flush=True, file=sys.stdout) approval = file_approval( @@ -337,7 +825,10 @@ def _announce(message: str) -> None: node_policy=gate_policy.node, trace=trace, budget=Budget(max_tokens=max_tokens), - limits=LoopLimits(max_rounds=max_rounds), + limits=LoopLimits( + max_rounds=max_rounds, + max_consecutive_planning_failures=max_planning_failures, + ), registry=registry, state_schema=schema, writes=writes, @@ -346,8 +837,20 @@ def _announce(message: str) -> None: # `goal` is set when the schema has somewhere to put it; a custom schema is # not required to carry one, and the planner is told the goal regardless. initial = schema(goal=goal) if "goal" in schema.model_fields else schema() + # `plan` plans; `go` (and `plan --go`) executes. The attribute rather + # than a ctor param keeps every registry module's build_loop signature. + loop.plan_only = command == "plan" and not go_after result = loop.run(goal, initial, run_id=run_id) + if result.stop is LoopStop.PLANNED: + _write_plan_file( + trace_path.parent, + goal=goal, + registry_target=registry_target, + model_spec=model_spec, + result=result, + ) + rounds = [ { "round": record.round, @@ -359,8 +862,8 @@ def _announce(message: str) -> None: for record in result.rounds ] payload = { - "ok": result.succeeded, - "command": "plan", + "ok": result.succeeded or result.stop is LoopStop.PLANNED, + "command": command, "goal": goal, "model": model_description, "registry": registry_target, @@ -416,14 +919,37 @@ def _announce(message: str) -> None: f"{style.dim('nodes=')}{record['nodes']} " f"{style.dim('executed=')}{record['executed']}{note}" ) + # The live link, always: the exact URL when a serve is discoverable and + # reachable, otherwise the one-line instruction that produces it. Filtered + # out (with the trace line) by the README byte-comparison, which is why it + # may vary per machine. + url = watch_url(trace_path, run_id=run_id) + payload["watch_url"] = url lines += [ "", style.kv("state", str(result.state), width=style.LABEL_WIDTH), style.kv("trace", str(trace_path), width=style.LABEL_WIDTH, tint=style.accent), + style.kv( + "watch", + url if url else style.dim(watch_hint(trace_path)), + width=style.LABEL_WIDTH, + tint=style.accent if url else None, + ), ] + if result.stop is LoopStop.PLANNED: + payload["plan_file"] = str(trace_path.parent / PLAN_FILENAME) + lines += [ + style.kv( + "execute", + f"grapharc go (or: grapharc go {trace_path.parent})", + width=style.LABEL_WIDTH, + tint=style.accent, + ), + ] emit(payload, lines, as_json=as_json) - return EXIT_OK if result.succeeded else EXIT_FAILED + done = result.succeeded or result.stop is LoopStop.PLANNED + return EXIT_OK if done else EXIT_FAILED __all__ = [ diff --git a/grapharc/cli/serve.py b/grapharc/cli/serve.py index 51b0c8b..b743495 100644 --- a/grapharc/cli/serve.py +++ b/grapharc/cli/serve.py @@ -13,6 +13,8 @@ from __future__ import annotations import importlib +import json +import os import sys from pathlib import Path from typing import Any @@ -149,13 +151,50 @@ def serve( tint=style.warn, ), ) + # A discovery marker, so `grapharc plan`/`go` can print the exact live-view + # URL for their trace. Best-effort in both directions: an unwritable cwd + # must not stop the server, and the marker never holds the token — it is a + # convenience pointer, not a credential store. Written in cwd only, the + # same rule grapharc.toml follows. + marker: Path | None = None + if live_root is not None: + try: + marker = Path(".grapharc") / "live-server.json" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text( + json.dumps( + { + "url": f"http://{host}:{port}", + "host": host, + "port": port, + "live_root": str(Path(live_root).resolve()), + "pid": os.getpid(), + } + ) + + "\n", + encoding="utf-8", + ) + payload["live_marker"] = str(marker) + except OSError: + marker = None + # Printed *and flushed* before the server blocks: a caller watching stdout for # the URL would otherwise wait for the process to exit to learn it. Nothing # here is buffered, deferred, or drawn on a timer for the same reason. emit(payload, lines, as_json=as_json) sys.stdout.flush() - runner(app, host=host, port=port, log_level=log_level) + try: + runner(app, host=host, port=port, log_level=log_level) + finally: + # Only our own marker: a second serve that replaced it owns it now. + if marker is not None: + try: + written = json.loads(marker.read_text(encoding="utf-8")) + if written.get("pid") == os.getpid(): + marker.unlink() + except (OSError, ValueError): + pass return EXIT_OK diff --git a/grapharc/cli/start.py b/grapharc/cli/start.py new file mode 100644 index 0000000..7137d65 --- /dev/null +++ b/grapharc/cli/start.py @@ -0,0 +1,187 @@ +"""`grapharc start` — the guided tour, and the bare-invocation orientation. + +The `-h` text is a contract document (flags, exit codes, colour policy); this +is the onboarding one, written for someone who installed five minutes ago and +knows none of the project's words. Every term is defined where it is used, in +plain language — a tour that explains jargon with more jargon teaches nothing, +which was the first complaint a real user filed against the first version of +this text. + +Text follows the style contract — same bytes on a terminal and in a pipe, +colour the only difference — so it joins the style tests like any command. +""" + +from __future__ import annotations + +from grapharc.cli import style +from grapharc.cli.output import EXIT_OK, emit + + +def orientation() -> list[str]: + """The short block bare `grapharc` prints: what this is, where to begin.""" + return [ + "grapharc — you give a goal in English; a language model turns it into", + "a small workflow; grapharc checks that workflow against rules you set", + "and only then runs it, recording every step to a file you can watch", + "live in a browser and replay afterwards.", + "", + f" {style.accent('grapharc start')} the guided tour (ten minutes, free)", + f" {style.accent('grapharc init')} create the starter files in this directory", + f" {style.accent('grapharc help')} every command and flag", + ] + + +_WORDS = [ + ( + "node", + "one box of work: read some files, write a report, apply a change", + ), + ( + "edge", + "one arrow between boxes: which box is allowed to run after which", + ), + ( + "graph", + "the whole workflow — boxes plus arrows — sharing one typed state", + ), + ( + "registry", + "a plain Python file listing the KINDS of box a model may use. The " + "model only picks from this menu; it can never write code of its " + "own. You do not write this file from scratch: `grapharc init` " + "creates a working, heavily-commented one (registry.py) to edit", + ), + ( + "plan", + "PLANS ONLY: a model proposes the workflow, admission checks it, and " + "the plan is saved — nothing executes. Needs --model, or --scripted " + "for a free rehearsal with canned replies instead of AI", + ), + ( + "go", + "EXECUTES: bare `go` runs the newest saved plan (`go ` for a " + "specific one); `go \"a goal\"` plans and executes in one shot. So " + "does `plan --go`", + ), + ( + "admission", + "the checker: before ANY box runs, deterministic code tests the " + "proposed workflow against your registry, rules and budget, and a " + "refusal comes back as a written reason the model must fix", + ), + ( + "approval", + "plan → look at it → go IS the approval. For one-shot runs, " + "--approve pauses mid-run until `grapharc approve ` answers", + ), + ( + "live view", + "a browser page drawing the run as it happens: violet boxes waiting " + "for your approval, amber while running, green when done — with " + "your goal in the header and each box's token bill on the box", + ), +] + +_PATH = [ + ( + "1", + "grapharc init", + "creates three things here: registry.py (the menu of box-kinds — " + "open it, every section is explained), grapharc.toml (saved " + "settings), and .grapharc/runs/ (where each run's record lands)", + ), + ( + "2", + "grapharc serve --live-root .grapharc/runs", + "in a second terminal — this is the browser page's server " + "(pip install 'grapharc[server]' if it says the extra is missing)", + ), + ( + "3", + 'grapharc plan "review what is in this directory" --scripted', + "plans and STOPS: the proposed boxes are saved and drawn in the " + "browser, nothing has run. --scripted says it out loud: no AI here — " + "the planner replies are canned text from your registry.py. It " + "prints watch : http://127.0.0.1:8000/… — open that link", + ), + ( + "4", + "grapharc go", + "executes the plan you just looked at; the page goes live as the " + "boxes run, and when it finishes, drag the slider to replay any " + "moment", + ), +] + +_REAL = [ + ( + "check", + "grapharc models --check — which model backends this machine can use", + ), + ( + "exact tag", + "for local models the name must be EXACTLY what `ollama list` shows: " + "ollama/qwen3:8b works; ollama/qwen3:8 is a different, missing model " + "and the run stops immediately saying so", + ), + ( + "go", + 'grapharc go "summarize how this repo is laid out" ' + "--model ollama/qwen3:8b --model-arg temperature=0", + ), + ( + "author", + "open registry.py and make it yours: rename the kinds, rewrite the " + "bodies, grow the State — the file explains each part where it sits", + ), + ( + "policy", + "the first --model run writes .grapharc/generated-policy.*.toml — " + "the rules it guessed from your goal. Read it; edit it; it is yours", + ), +] + + +def start(*, as_json: bool = False) -> int: + """Print the guided tour. The payload mirrors the prose for `--json`.""" + lines: list[str] = [ + "grapharc, in one paragraph", + style.dim( + " You give a goal in English. A language model turns it into a small" + ), + style.dim( + " workflow of boxes and arrows. grapharc runs that workflow only after" + ), + style.dim( + " deterministic checks pass — and, if you ask, only after you say yes —" + ), + style.dim( + " recording every step to a file you can watch live and replay." + ), + "", + "the words you will meet", + ] + for label, text in _WORDS: + lines.append(f" {style.accent(f'{label:<10}')}{style.dim(text)}") + lines += ["", "try it in ten minutes (free — nothing here calls a paid model)"] + for number, command, note in _PATH: + lines.append(f" {number} {style.accent(command)}") + lines.append(f" {style.dim(note)}") + lines += ["", "do it for real (a real model plans; real tools do the work)"] + for label, text in _REAL: + lines.append(f" {style.accent(f'{label:<10}')}{style.dim(text)}") + lines += ["", style.dim("the long version: README.md · docs/cookbook/")] + + payload = { + "ok": True, + "command": "start", + "words": dict(_WORDS), + "path": [{"step": n, "command": c, "note": note} for n, c, note in _PATH], + "going_real": dict(_REAL), + "docs": ["README.md", "docs/cookbook/"], + } + emit(payload, lines, as_json=as_json) + return EXIT_OK + + +__all__ = ["orientation", "start"] diff --git a/grapharc/gateway/ollama.py b/grapharc/gateway/ollama.py index 6c3ed3c..7db0968 100644 --- a/grapharc/gateway/ollama.py +++ b/grapharc/gateway/ollama.py @@ -32,7 +32,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, ClassVar from grapharc.gateway.config import ollama_api_key, ollama_base_url from grapharc.gateway.openai_compat import OpenAICompatChatModel @@ -50,6 +50,14 @@ class OllamaError(Exception): class OllamaChatModel(OpenAICompatChatModel): """A LangChain chat model over a local Ollama server.""" + #: Read by `PlannerNode` (duck-typed, like `disclosure()`): Ollama compiles + #: a strict `json_schema` into its grammar-constrained decoder, and + #: `Subgraph`'s schema — recursive, every field required, docstring-laden — + #: reliably breaks small models there; `json_mode` suppresses the reasoning + #: phase models like qwen3 plan with. The text path with the slim proposal + #: shape is the one that works, so this backend asks for it. + reliable_structured_output: ClassVar[bool] = False + def __init__(self, model: str, /, **kwargs: Any) -> None: if not model: raise OllamaError( diff --git a/grapharc/observe/__init__.py b/grapharc/observe/__init__.py index 1046851..a39ef7e 100644 --- a/grapharc/observe/__init__.py +++ b/grapharc/observe/__init__.py @@ -40,6 +40,7 @@ replay, replay_thread, ) +from grapharc.observe.status import NodeState, NodeStatus, node_states from grapharc.observe.trace import TraceEvent, TraceReadError, TraceRecorder, load_events __all__ = [ @@ -48,6 +49,8 @@ "NodeCost", "NodeDiff", "NodeExecution", + "NodeState", + "NodeStatus", "NullSpanExporter", "OTelSpanExporter", "OTelUnavailable", @@ -72,6 +75,7 @@ "format_diff", "format_replay", "load_events", + "node_states", "replay", "replay_thread", "summarize", diff --git a/grapharc/observe/cost.py b/grapharc/observe/cost.py index 166e06b..214f8c9 100644 --- a/grapharc/observe/cost.py +++ b/grapharc/observe/cost.py @@ -3,12 +3,11 @@ Two sources of money, kept apart on purpose: - **Recorded** — a `cost_usd` a producer wrote onto the trace event. This is - the provider's own number and is used as-is. *Nothing in GraphARC writes it - today*: the field exists on `TraceEvent` and `TraceRecorder.event` accepts - it, but the kernel's node wrapper and `AgentNode` do not pass it, so on a - trace produced by today's runtime `recorded_cost_usd` is always None. Both - gateways already capture `cost_usd` per model call, so closing the gap is one - argument at each `trace.event(...)` call site. + the provider's own number and is used as-is. The kernel's node wrapper + stamps it on a node's terminal event when the meter carries one, and + `AgentNode` stamps its model calls — so a gateway-priced run carries + recorded cost per node. A trace from a producer that never priced its calls + still reads as `recorded_cost_usd = None`, never as an estimate. - **Estimated** — tokens × a `RateCard` the caller supplies. Priced off the *total* token count, because that is what the trace records: there is no input/output split on a trace event, so a card carries one blended rate per diff --git a/grapharc/observe/layout.py b/grapharc/observe/layout.py new file mode 100644 index 0000000..8135697 --- /dev/null +++ b/grapharc/observe/layout.py @@ -0,0 +1,264 @@ +"""Positioned geometry for a `GraphSnapshot`, computed server-side. + +The live page stays dumb by design (`server.live` docstring) — so the layout +runs here, in pure Python with no dependencies, and the browser receives +coordinates it merely draws. That buys three things at once: the layout is +ordinary tested code rather than the only untested JavaScript in the repo; +it is deterministic, so two snapshots of the same topology carry identical +geometry and the page can patch statuses in place without anything moving; +and it is importable without the `server` extra, so a static SVG export can +reuse it later. + +The algorithm is a small Sugiyama: longest-path ranks over the forward edges, +a few barycenter sweeps for in-rank order, then top-down coordinates. Cycles +are a fact of these graphs — a LangGraph router looping back to an earlier +node is normal — so back-edges are found by DFS first and excluded from +ranking. That step is a hard requirement, not an optimization: the snapshot +builder runs inside the live server's poll thread, and a hand-written cyclic +trace must terminate, never hang it. + +Text width is estimated from character count rather than measured — at the +graph sizes this serves (tens of nodes), a clamped estimate and an ellipsis +in the renderer beat shipping a font metrics table. +""" + +from __future__ import annotations + +from grapharc.observe.viewmodel import EdgeView, GraphSnapshot, NodeView + +NODE_H = 52.0 +NODE_W_MIN = 150.0 +NODE_W_MAX = 230.0 +TERMINAL_SIZE = 28.0 +GAP_X = 36.0 +GAP_Y = 56.0 +MARGIN = 24.0 +CLUSTER_PAD = 20.0 +CLUSTER_GAP = 40.0 +#: Barycenter passes: down, up, down. More sweeps buy nothing at these sizes. +ORDER_SWEEPS = 3 + + +def _node_width(node: NodeView) -> float: + if node.role != "node": + return TERMINAL_SIZE + return min(NODE_W_MAX, max(NODE_W_MIN, 24 + 7.5 * len(node.label))) + + +def _back_edges(order: list[str], adjacency: dict[str, list[str]]) -> set[tuple[str, str]]: + """Edges that point at an ancestor of the DFS path — iterative, cycle-safe.""" + back: set[tuple[str, str]] = set() + visited: set[str] = set() + on_path: set[str] = set() + for root in order: + if root in visited: + continue + # (node, iterator-index) pairs; an explicit stack instead of recursion + # so a long chain cannot hit the interpreter's recursion limit. + stack: list[tuple[str, int]] = [(root, 0)] + on_path.add(root) + visited.add(root) + while stack: + node, index = stack[-1] + targets = adjacency.get(node, []) + if index >= len(targets): + stack.pop() + on_path.discard(node) + continue + stack[-1] = (node, index + 1) + target = targets[index] + if target in on_path: + back.add((node, target)) + elif target not in visited: + visited.add(target) + on_path.add(target) + stack.append((target, 0)) + return back + + +def _ranks( + ids: list[str], edges: list[tuple[str, str]] +) -> tuple[dict[str, int], set[tuple[str, str]]]: + """Longest-path rank per node over the forward edges; also the back set.""" + adjacency: dict[str, list[str]] = {i: [] for i in ids} + for source, target in edges: + if source in adjacency and target in adjacency: + adjacency[source].append(target) + back = _back_edges(ids, adjacency) + forward = [(s, t) for s, t in edges if (s, t) not in back and s in adjacency and t in adjacency] + + indegree = dict.fromkeys(ids, 0) + for _, target in forward: + indegree[target] += 1 + rank = dict.fromkeys(ids, 0) + queue = [i for i in ids if indegree[i] == 0] + while queue: + node = queue.pop(0) + for source, target in forward: + if source != node: + continue + rank[target] = max(rank[target], rank[node] + 1) + indegree[target] -= 1 + if indegree[target] == 0: + queue.append(target) + return rank, back + + +def _ordered_rows( + ids: list[str], rank: dict[str, int], edges: list[tuple[str, str]] +) -> list[list[str]]: + """Nodes per rank, ordered by a few barycenter sweeps; ties keep declaration order.""" + rows: list[list[str]] = [[] for _ in range(max(rank.values(), default=0) + 1)] + for i in ids: + rows[rank[i]].append(i) + + neighbors_down: dict[str, list[str]] = {} + neighbors_up: dict[str, list[str]] = {} + for source, target in edges: + neighbors_down.setdefault(source, []).append(target) + neighbors_up.setdefault(target, []).append(source) + + def sweep(rows: list[list[str]], neighbors: dict[str, list[str]], indices: range) -> None: + for row_index in indices: + adjacent = rows[row_index - 1] if neighbors is neighbors_up else rows[row_index + 1] + position = {name: i for i, name in enumerate(adjacent)} + keyed: dict[str, float] = {} + for i, name in enumerate(rows[row_index]): + anchors = [position[n] for n in neighbors.get(name, []) if n in position] + # A node with no neighbor in the adjacent rank keeps its place. + keyed[name] = sum(anchors) / len(anchors) if anchors else float(i) + rows[row_index].sort(key=lambda name: keyed[name]) # stable: ties hold order + + for pass_index in range(ORDER_SWEEPS): + if len(rows) < 2: + break + if pass_index % 2 == 0: + sweep(rows, neighbors_up, range(1, len(rows))) + else: + sweep(rows, neighbors_down, range(len(rows) - 2, -1, -1)) + return rows + + +def _place_scope( + nodes: list[NodeView], edges: list[EdgeView] +) -> tuple[float, float]: + """Lay out one scope (the whole graph, or one cluster) at origin. + + Returns (width, height) of the laid-out block. Mutates node geometry and + the points of edges whose endpoints are both in this scope. + """ + by_id = {n.id: n for n in nodes} + pairs = [(e.source, e.target) for e in edges if e.source in by_id and e.target in by_id] + rank, back = _ranks(list(by_id), pairs) + rows = _ordered_rows(list(by_id), rank, [p for p in pairs if p not in back]) + + for node in nodes: + node.w = _node_width(node) + node.h = TERMINAL_SIZE if node.role != "node" else NODE_H + + row_widths = [ + sum(by_id[i].w for i in row) + GAP_X * max(0, len(row) - 1) for row in rows + ] + block_width = max(row_widths, default=0.0) + y = 0.0 + for row, row_width in zip(rows, row_widths, strict=True): + x = (block_width - row_width) / 2 + row_height = max((by_id[i].h for i in row), default=NODE_H) + for i in row: + node = by_id[i] + node.x = x + # Terminals are shorter than nodes; center them on the row. + node.y = y + (row_height - node.h) / 2 + x += node.w + GAP_X + y += row_height + GAP_Y + block_height = max(0.0, y - GAP_Y) + + for edge in edges: + source, target = by_id.get(edge.source), by_id.get(edge.target) + if source is None or target is None: + continue + if edge.source == edge.target: + # Self-loop: a small bulge off the node's right edge. + rx, cy = source.x + source.w, source.y + source.h / 2 + edge.points = [ + [rx, cy - 8], + [rx + 40, cy - 22], + [rx + 40, cy + 22], + [rx, cy + 8], + ] + elif (edge.source, edge.target) in back: + # Back-edge: arc around the right of the block, bottom to top. + sx, sy = source.x + source.w, source.y + source.h / 2 + tx, ty = target.x + target.w, target.y + target.h / 2 + arc_x = block_width + 48 + edge.points = [[sx, sy], [arc_x, sy], [arc_x, ty], [tx, ty]] + else: + sx, sy = source.x + source.w / 2, source.y + source.h + tx, ty = target.x + target.w / 2, target.y + bend = min(GAP_Y / 2, max(12.0, (ty - sy) / 2)) + edge.points = [[sx, sy], [sx, sy + bend], [tx, ty - bend], [tx, ty]] + return block_width, block_height + + +def layout_graph(snapshot: GraphSnapshot) -> GraphSnapshot: + """Fill geometry in place and return the snapshot. + + Clusters are laid out independently and stacked vertically — each round + of a planner run really is its own graph — with the dotted `state` edge + drawn frame-to-frame between them. + """ + if not snapshot.nodes: + snapshot.width = snapshot.height = 0.0 + return snapshot + + if snapshot.clusters: + y_offset = MARGIN + max_width = 0.0 + for cluster in snapshot.clusters: + members = [n for n in snapshot.nodes if n.cluster == cluster.id] + member_ids = {n.id for n in members} + scoped_edges = [ + e + for e in snapshot.edges + if e.kind != "state" and e.source in member_ids and e.target in member_ids + ] + width, height = _place_scope(members, scoped_edges) + for node in members: + node.x += MARGIN + CLUSTER_PAD + node.y += y_offset + CLUSTER_PAD + for edge in scoped_edges: + edge.points = [ + [x + MARGIN + CLUSTER_PAD, y + y_offset + CLUSTER_PAD] + for x, y in edge.points + ] + cluster.x = MARGIN + cluster.y = y_offset + cluster.w = width + 2 * CLUSTER_PAD + cluster.h = height + 2 * CLUSTER_PAD + max_width = max(max_width, cluster.w) + y_offset += cluster.h + CLUSTER_GAP + frames = {c.id: c for c in snapshot.clusters} + for edge in snapshot.edges: + if edge.kind != "state": + continue + source, target = frames.get(edge.source), frames.get(edge.target) + if source is None or target is None: + continue + sx, sy = source.x + source.w / 2, source.y + source.h + tx, ty = target.x + target.w / 2, target.y + edge.points = [[sx, sy], [sx, sy + 16], [tx, ty - 16], [tx, ty]] + snapshot.width = max_width + 2 * MARGIN + 56 # room for back-edge arcs + snapshot.height = y_offset - CLUSTER_GAP + MARGIN + else: + width, height = _place_scope(snapshot.nodes, snapshot.edges) + for node in snapshot.nodes: + node.x += MARGIN + node.y += MARGIN + for edge in snapshot.edges: + edge.points = [[x + MARGIN, y + MARGIN] for x, y in edge.points] + snapshot.width = width + 2 * MARGIN + 56 + snapshot.height = height + 2 * MARGIN + return snapshot + + +__all__ = ["layout_graph"] diff --git a/grapharc/observe/metrics.py b/grapharc/observe/metrics.py index ddafb35..8ef3fa2 100644 --- a/grapharc/observe/metrics.py +++ b/grapharc/observe/metrics.py @@ -25,6 +25,7 @@ from pydantic import BaseModel from grapharc.observe.replay import replay +from grapharc.observe.status import node_states from grapharc.observe.trace import TraceRecorder @@ -147,7 +148,7 @@ def to_mermaid(recorder: TraceRecorder, run_id: str) -> str: work, so its recorded events are the path instead of an empty diagram. """ run = replay(recorder, run_id) - topologies = _latest_topologies(run.events) + topologies = latest_topologies(run.events) if topologies: return _topology_mermaid(run, topologies) events = [e for e in run.events if e.phase in ("end", "error")] @@ -189,7 +190,7 @@ def node_ref(ev) -> str: return "\n".join(dict.fromkeys(lines)) -def _latest_topologies(events: list) -> list[tuple[str, dict[str, Any]]]: +def latest_topologies(events: list) -> list[tuple[str, dict[str, Any]]]: """One merged (graph, delta) per graph, in first-appearance order. Two emitters restate the same graph: the loop (whose delta carries @@ -244,19 +245,12 @@ def ref(name: str, prefix: str = prefix, ids: dict[str, str] = ids) -> str: for name in nodes: cluster.append(f"{indent}{ref(name)}") - # Per-node status from this graph's events. Parallel instances of one - # node collapse to the worst-informative status: any error wins, then - # running, then done. - started: dict[str, int] = {} - ended: dict[str, int] = {} - errored: dict[str, int] = {} + # Per-node status from this graph's events, by the shared rule in + # `observe.status`: parallel instances of one node collapse to the + # worst-informative status — any error wins, then running, then done. + states = node_states(graph_events) for event in graph_events: - if event.phase == "start": - started[event.node] = started.get(event.node, 0) + 1 - elif event.phase == "end": - ended[event.node] = ended.get(event.node, 0) + 1 - elif event.phase == "error": - errored[event.node] = errored.get(event.node, 0) + 1 + if event.phase == "error": cluster.append( f"{indent}{ref(event.node)} -.->|error| " f'err{error_index}{{"{_label(event.error or "error")}"}}' @@ -269,19 +263,13 @@ def ref(name: str, prefix: str = prefix, ids: dict[str, str] = ids) -> str: targeted = {e[1] for e in edges if len(e) == 3} for source in fanout_sources: for name in nodes: - if name != source and name not in targeted and name in started: + started = name in states and states[name].starts > 0 + if name != source and name not in targeted and started: cluster.append(f"{indent}{ref(source)} -.-> {ref(name)}") for name in nodes: node_id = ids[name] - if errored.get(name): - status = "errored" - elif started.get(name, 0) > ended.get(name, 0): - status = "running" - elif ended.get(name): - status = "done" - else: - status = "pending" + status = states[name].status if name in states else "pending" class_members.setdefault(status, []).append(node_id) if clustered: diff --git a/grapharc/observe/status.py b/grapharc/observe/status.py new file mode 100644 index 0000000..c43530e --- /dev/null +++ b/grapharc/observe/status.py @@ -0,0 +1,85 @@ +"""One rule for what a node's events say about it. + +Three renderers derive a per-node status from the same trace — the Mermaid +overlay in `metrics`, the Slack narration in `slack.live`, and the structured +graph snapshot in `viewmodel` — and for a while each carried its own copy of +the counting. The copies agreed by luck, not by construction. This module is +the single statement of the rule they all share: + + errored > running > done > pending + +Any error wins: a node that failed and was retried to success still shows the +failure, because "it failed at some point" is what an operator scanning a live +view needs to notice first. `running` means more starts than ends — under +fan-out several instances of one node may be open at once and one still-open +instance keeps the name running. `done` means at least one end. A declared +node with no events at all is `pending`, which is the whole point of drawing +the declared graph rather than the executed path. + +Statuses are computed from `start`/`end`/`error` events only. Sub-step phases +(`model`, `tool`, `stop`, the planner's paperwork) describe work *inside* a +node, never the node's own lifecycle. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Literal + +from pydantic import BaseModel + +from grapharc.observe.trace import TraceEvent + +NodeStatus = Literal["pending", "running", "done", "errored"] + + +class NodeState(BaseModel): + """What one node's own events add up to.""" + + node: str + starts: int = 0 + ends: int = 0 + errors: int = 0 + # The most recent terminal events, kept whole: the Slack line wants the + # last end's duration and tokens, the snapshot wants the last error's text. + last_end: TraceEvent | None = None + last_error: TraceEvent | None = None + + @property + def status(self) -> NodeStatus: + if self.errors: + return "errored" + if self.starts > self.ends: + return "running" + if self.ends: + return "done" + return "pending" + + +def node_states(events: Iterable[TraceEvent]) -> dict[str, NodeState]: + """Fold events into per-node states, keyed by node name. + + The caller chooses the scope: pass one graph's events and statuses are that + graph's own — a multi-round planner run must not let round 1's finished + node wear round 2's still-running state. + """ + states: dict[str, NodeState] = {} + for event in events: + if event.phase not in ("start", "end", "error"): + continue + state = states.get(event.node) + if state is None: + state = states[event.node] = NodeState(node=event.node) + if event.phase == "start": + state.starts += 1 + elif event.phase == "end": + state.ends += 1 + state.last_end = event + else: + state.errors += 1 + state.last_error = event + + return states + + +__all__ = ["NodeState", "NodeStatus", "node_states"] diff --git a/grapharc/observe/viewmodel.py b/grapharc/observe/viewmodel.py new file mode 100644 index 0000000..83bf951 --- /dev/null +++ b/grapharc/observe/viewmodel.py @@ -0,0 +1,436 @@ +"""A structured view of one run, for renderers that draw rather than count. + +`to_mermaid` renders the run as text; this module renders it as data — nodes +with a status and a bill, edges with a kind, clusters for a multi-round +planner run, and a timeline of when each node was open. The live web view +draws its SVG from this; nothing here is a new source of truth, and the three +shapes mirror `to_mermaid`'s three branches exactly: + +- a trace with `topology` events becomes the *declared* graph with execution + status overlaid (kind ``"topology"``), +- a trace with executions but no topology becomes the executed path in event + order (kind ``"path"``), +- a planner run that admitted nothing becomes an honest empty diagram + (kind ``"empty"``), with the reason in `note`. + +What is deliberately *not* here: `state_delta` contents, event payloads, or +anything else a node wrote. The exposure is the same as the Mermaid path's — +node names, statuses, counts, durations, tokens, recorded cost, and error +labels truncated the way `metrics._label` truncates them. The live server +serializes this model to the browser, so the boundary is enforced by the +model's fields, not by a filter somewhere else. + +Geometry fields (`x`/`y`/`w`/`h`, edge `points`, canvas `width`/`height`) +are zeroed here and filled by `observe.layout`, which is a pure function of +the topology — so two snapshots of the same shape carry identical coordinates +and a page can patch statuses in place without anything moving. +""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, Field + +from grapharc.observe.metrics import latest_topologies +from grapharc.observe.replay import ReplayedRun +from grapharc.observe.status import NodeStatus, node_states +from grapharc.observe.trace import TraceEvent + +#: Same truncation the Mermaid labels use: flatten whitespace, cap the length. +#: An error message is the one free-text string this model carries. +_ERROR_LIMIT = 120 + +#: Phases that describe the run rather than doing work (mirrors `metrics`). +_SHAPE_PHASES = frozenset({"topology", "approval_request", "approval_response"}) +_LOOP_PHASES = frozenset({"plan", "admission", "round"}) + +_NO_GRAPH_NOTE = "no graph ran: no proposal was admitted and built" + + +class NodeView(BaseModel): + """One drawable node: identity, status, and its share of the bill.""" + + id: str + label: str + cluster: str | None = None + #: Terminals (`start`/`end` rings) are drawn, not statused. + role: Literal["node", "start", "end"] = "node" + status: NodeStatus = "pending" + executions: int = 0 + tokens: int = 0 + #: Tokens reported by sub-steps of a still-open execution — what a running + #: node has spent *so far*, before its terminal event exists to say so. + live_tokens: int = 0 + #: Recorded cost only, summed off terminal events. Never an estimate. + cost_usd: float | None = None + duration_ms: float | None = None + error: str | None = None + x: float = 0.0 + y: float = 0.0 + w: float = 0.0 + h: float = 0.0 + + +class EdgeView(BaseModel): + source: str + target: str + kind: Literal["static", "conditional", "fanout", "state"] = "static" + #: Cubic bezier control points, filled by layout: [[x,y], ×4]. + points: list[list[float]] = Field(default_factory=list) + + +class ClusterView(BaseModel): + """One admitted round's frame in a multi-round planner run.""" + + id: str + label: str + round: int | None = None + x: float = 0.0 + y: float = 0.0 + w: float = 0.0 + h: float = 0.0 + + +class TimelineSpan(BaseModel): + """When one node execution was open, as ms offsets from the run's start. + + Names and times only — this is the replay scrubber's fuel, and it must + stay as free of payload as the rest of the model. + """ + + node: str + t0: float + t1: float | None = None + #: None while open, True for `end`, False for `error`. + ok: bool | None = None + + +class GraphSnapshot(BaseModel): + kind: Literal["topology", "path", "empty"] + width: float = 0.0 + height: float = 0.0 + nodes: list[NodeView] = Field(default_factory=list) + edges: list[EdgeView] = Field(default_factory=list) + clusters: list[ClusterView] = Field(default_factory=list) + timeline: list[TimelineSpan] = Field(default_factory=list) + #: The timeline's horizon: the largest t1, or the run's wall span. + duration_ms: float | None = None + note: str | None = None + + +def _flatten(text: str | None, fallback: str = "error") -> str: + return " ".join(str(text or fallback).split())[:_ERROR_LIMIT] + + +def _parse_ts(ts: str) -> datetime | None: + try: + return datetime.fromisoformat(ts) + except ValueError: + return None + + +def _offsets(run: ReplayedRun) -> dict[str, float]: + """Event timestamp → ms offset from the run's first parseable stamp.""" + stamps = [t for t in (_parse_ts(e.ts) for e in run.events) if t is not None] + if not stamps: + return {} + origin = min(stamps) + return { + e.ts: (t - origin).total_seconds() * 1000 + for e in run.events + if (t := _parse_ts(e.ts)) is not None + } + + +def _timeline( + run: ReplayedRun, resolve: Callable[[TraceEvent], str | None] +) -> tuple[list[TimelineSpan], float | None]: + """One span per node execution, paired the way `replay` pairs them. + + `resolve` maps a lifecycle event to the NodeView id its span points at; + executions of nodes the view does not draw resolve to None and are skipped + rather than left dangling. + """ + offsets = _offsets(run) + open_spans: dict[tuple[str, str, int, int], TimelineSpan] = {} + spans: list[TimelineSpan] = [] + horizon: float | None = None + for event in run.events: + if event.phase not in ("start", "end", "error"): + continue + target = resolve(event) + if target is None or event.ts not in offsets: + continue + at = offsets[event.ts] + horizon = at if horizon is None else max(horizon, at) + key = (event.graph, event.node, event.step, event.attempt) + if event.phase == "start": + span = TimelineSpan(node=target, t0=at) + open_spans[key] = span + spans.append(span) + continue + span = open_spans.pop(key, None) + if span is None: + # A terminal with no start — a budget-refused node. Zero-width + # span at the moment of refusal; it still scrubs as errored. + span = TimelineSpan(node=target, t0=at) + spans.append(span) + span.t1 = at + span.ok = event.phase == "end" + return spans, horizon + + +def _node_measures(node: NodeView, graph_events: list[TraceEvent], name: str) -> None: + """Fill tokens/cost/duration off terminal events, live tokens off sub-steps. + + Terminal events (`end` *and* `error`) are the measure, exactly as + `metrics.summarize` counts a run: the kernel stamps a node's terminal event + with what it spent, whichever way it ended. Sub-step tokens are a breakdown + of a total that will exist once the node closes — they are surfaced only + while it is open (`live_tokens`), never added to `tokens`. + """ + costs: list[float] = [] + prefix = f"{name}:" + for event in graph_events: + if event.node == name and event.phase in ("end", "error"): + node.tokens += event.tokens or 0 + if event.duration_ms is not None: + node.duration_ms = (node.duration_ms or 0.0) + event.duration_ms + if event.cost_usd is not None: + costs.append(event.cost_usd) + node.executions += 1 + elif node.status == "running" and ( + event.node == name or event.node.startswith(prefix) + ): + if event.phase not in ("start",) and event.tokens: + node.live_tokens += event.tokens + if costs: + node.cost_usd = round(sum(costs), 6) + if node.duration_ms is not None: + node.duration_ms = round(node.duration_ms, 2) + + +def build_graph_view(run: ReplayedRun) -> GraphSnapshot: + """The structured equivalent of `to_mermaid`, minus geometry.""" + topologies = latest_topologies(run.events) + if topologies: + return _topology_view(run, topologies) + return _path_view(run) + + +def _topology_view(run: ReplayedRun, topologies: list) -> GraphSnapshot: + clustered = len(topologies) > 1 + nodes: list[NodeView] = [] + edges: list[EdgeView] = [] + clusters: list[ClusterView] = [] + node_id: dict[tuple[str, str], str] = {} + + for graph_index, (graph, delta) in enumerate(topologies): + prefix = f"g{graph_index}." if clustered else "" + cluster_id = f"g{graph_index}" if clustered else None + declared = [ + str(n) for n in delta.get("nodes", []) if n not in ("__start__", "__end__") + ] + raw_edges = [tuple(e) for e in delta.get("edges", []) if len(tuple(e)) == 3] + fanout_sources = [str(s) for s in delta.get("fanout_sources", [])] + graph_events = [e for e in run.events if e.graph == graph] + states = node_states(graph_events) + + if clustered: + label = delta.get("round") + clusters.append( + ClusterView( + id=cluster_id, + label=f"round {label}" if label else graph, + round=label if isinstance(label, int) else None, + ) + ) + + def ref(name: str, prefix: str = prefix, cluster_id: str | None = cluster_id) -> str: + if name in ("__start__", "__end__"): + terminal_id = f"{prefix}{name}" + if all(n.id != terminal_id for n in nodes): + nodes.append( + NodeView( + id=terminal_id, + label="start" if name == "__start__" else "end", + cluster=cluster_id, + role="start" if name == "__start__" else "end", + ) + ) + return terminal_id + return f"{prefix}{name}" + + for name in declared: + state = states.get(name) + node = NodeView( + id=f"{prefix}{name}", + label=name, + cluster=cluster_id, + status=state.status if state else "pending", + ) + if state is not None: + if state.last_error is not None: + node.error = _flatten(state.last_error.error) + _node_measures(node, graph_events, name) + nodes.append(node) + node_id[(graph, name)] = node.id + + for source, target, kind in raw_edges: + edges.append( + EdgeView( + source=ref(str(source)), + target=ref(str(target)), + kind="conditional" if kind == "conditional" else "static", + ) + ) + # Fan-out targets are dynamic, so the topology names only the sources; + # workers are wired to whichever declared nodes actually started — + # the same rule the Mermaid overlay draws by. + targeted = {str(e[1]) for e in raw_edges} + for source in fanout_sources: + for name in declared: + started = name in states and states[name].starts > 0 + if name != source and name not in targeted and started: + edges.append( + EdgeView(source=ref(source), target=ref(name), kind="fanout") + ) + + if clustered and graph_index: + edges.append( + EdgeView( + source=f"g{graph_index - 1}", + target=f"g{graph_index}", + kind="state", + ) + ) + + timeline, horizon = _timeline(run, lambda e: node_id.get((e.graph, e.node))) + return GraphSnapshot( + kind="topology", + nodes=nodes, + edges=edges, + clusters=clusters, + timeline=timeline, + duration_ms=horizon, + ) + + +def _feed_view(run: ReplayedRun, feed: list[TraceEvent]) -> GraphSnapshot: + """A run with no node spans at all: chain its flat feed in event order.""" + nodes: list[NodeView] = [] + seen: dict[tuple[str, int], str] = {} + + def ref(event: TraceEvent) -> str: + key = (event.node, event.step) + existing = seen.get(key) + if existing is not None: + return existing + view = NodeView( + id=f"{event.node}@{event.step}", + label=event.node, + status="errored" if event.error else "done", + tokens=event.tokens or 0, + cost_usd=event.cost_usd, + duration_ms=event.duration_ms, + error=_flatten(event.error) if event.error else None, + executions=1, + ) + nodes.append(view) + seen[key] = view.id + return view.id + + edges: list[EdgeView] = [] + for a, b in zip(feed, feed[1:], strict=False): + source, target = ref(a), ref(b) + if not a.error and source != target: + edges.append(EdgeView(source=source, target=target)) + ref(feed[-1]) + + timeline, horizon = _timeline(run, lambda e: seen.get((e.node, e.step))) + return GraphSnapshot( + kind="path", + nodes=nodes, + edges=edges, + timeline=timeline, + duration_ms=horizon, + ) + + +def _path_view(run: ReplayedRun) -> GraphSnapshot: + """The executed path, in event order — `to_mermaid`'s fallback, as data. + + Built from `run.executions`, not from terminal events, because an + execution that *started and has not finished* is the most important thing + on a live page — a delegated executor is silent mid-flight, and building + from `end`/`error` alone rendered exactly that run as "no events" while + its one node was busy working. + """ + executions = list(run.executions) + if not executions: + feed = [ + e + for e in run.orphan_sub_events + if e.phase not in _SHAPE_PHASES and e.phase not in _LOOP_PHASES + ] + if all(e.phase == "stop" for e in feed): + feed = [] + if not feed and any(e.phase in _LOOP_PHASES for e in run.events): + return GraphSnapshot(kind="empty", note=_NO_GRAPH_NOTE) + if not feed: + return GraphSnapshot(kind="empty", note="no events") + return _feed_view(run, feed) + + nodes: list[NodeView] = [] + seen: dict[tuple[str, int], str] = {} + for execution in executions: + key = (execution.node, execution.step) + if key in seen: + continue + if execution.error is not None: + status = "errored" + elif execution.completed: + status = "done" + else: + status = "running" + view = NodeView( + id=f"{execution.node}@{execution.step}", + label=execution.node, + status=status, + tokens=execution.tokens, + live_tokens=execution.sub_tokens if status == "running" else 0, + cost_usd=execution.cost_usd, + duration_ms=execution.duration_ms, + error=_flatten(execution.error) if execution.error is not None else None, + executions=1, + ) + nodes.append(view) + seen[key] = view.id + + edges: list[EdgeView] = [] + for a, b in zip(executions, executions[1:], strict=False): + source, target = seen[(a.node, a.step)], seen[(b.node, b.step)] + if a.error is None and source != target: + edges.append(EdgeView(source=source, target=target)) + + timeline, horizon = _timeline(run, lambda e: seen.get((e.node, e.step))) + return GraphSnapshot( + kind="path", + nodes=nodes, + edges=edges, + timeline=timeline, + duration_ms=horizon, + ) + + +__all__ = [ + "ClusterView", + "EdgeView", + "GraphSnapshot", + "NodeView", + "TimelineSpan", + "build_graph_view", +] diff --git a/grapharc/planner/__init__.py b/grapharc/planner/__init__.py index 4431f4c..55bbae8 100644 --- a/grapharc/planner/__init__.py +++ b/grapharc/planner/__init__.py @@ -74,9 +74,11 @@ ) from grapharc.planner.proposal import ( DEFAULT_PLANNER_SYSTEM_PROMPT, + PROPOSAL_EXAMPLE, PlannerConfigError, PlannerNode, PlanningOutcome, + PlanProposal, ProposedEdge, ProposedNode, Subgraph, @@ -84,6 +86,7 @@ __all__ = [ "DEFAULT_PLANNER_SYSTEM_PROMPT", + "PROPOSAL_EXAMPLE", "AdmissionChecker", "AdmissionLimits", "AdmissionRejected", @@ -108,6 +111,7 @@ "Planner", "PlannerConfigError", "PlannerNode", + "PlanProposal", "PlanningOutcome", "ProposedEdge", "ProposedNode", diff --git a/grapharc/planner/loop.py b/grapharc/planner/loop.py index 2c92fed..3cc4ebc 100644 --- a/grapharc/planner/loop.py +++ b/grapharc/planner/loop.py @@ -105,7 +105,7 @@ RemainingBudget, ) from grapharc.planner.materialize import MaterializationError, Materializer -from grapharc.planner.proposal import PlanningOutcome, Subgraph +from grapharc.planner.proposal import PROPOSAL_EXAMPLE, PlanningOutcome, Subgraph from grapharc.runtime.budget import Budget, BudgetExceeded, BudgetMeter from grapharc.runtime.graph import RunContext, topology_delta @@ -127,6 +127,10 @@ class LoopStop(StrEnum): # The planner was admitted while proposing nothing, and no goal check said # the goal was met. It has run out of work; that is an answer, not a fault. NO_FURTHER_WORK = "no_further_work" + # Plan-only mode: an admitted, materialisable graph exists and — by + # request — nothing was executed. `grapharc plan` stops here; `grapharc + # go` picks the saved plan up. + PLANNED = "planned" MAX_ROUNDS = "max_rounds" BUDGET_EXHAUSTED = "budget_exhausted" NO_PROGRESS = "no_progress" @@ -145,7 +149,11 @@ class LoopStop(StrEnum): # Stops that mean the run did what it was asked, for the trace's `error` field: # everything else is recorded as a refusal so an audit can find it by grepping # for errors rather than by knowing this enum. -_CLEAN_STOPS = frozenset({LoopStop.GOAL_MET, LoopStop.NO_FURTHER_WORK}) +_CLEAN_STOPS = frozenset({LoopStop.GOAL_MET, LoopStop.NO_FURTHER_WORK, LoopStop.PLANNED}) + +#: How much of an unusable planner reply the retry note echoes back. Bounded +#: because the note rides in the next round's prompt. +_SNIPPET_LIMIT = 400 class Planner(Protocol): @@ -325,6 +333,11 @@ def __init__( # charges nothing — thinking time is not the run's spend. None means # what it always meant: admission is the only gate. self.approval = approval + # Plan-only: stop each run at its first admitted, materialised + # graph instead of executing it. Set by `grapharc plan` after + # construction (an attribute, not a ctor param, so every shipped + # build_loop factory keeps its signature). + self.plan_only = False self._halt = threading.Event() self._halt_reason = "" @@ -429,9 +442,20 @@ def close(**fields: Any) -> None: if not outcome.ok or outcome.proposal is None: unplanned_in_a_row += 1 + # The retry note shows the model what it actually said and what + # was wanted. The bare error string alone ("no JSON object + # found…") left a local model guessing at the shape and burning + # its three allowed failures on the same mistake; the snippet + # is truncated because the note lands verbatim in the next + # prompt — the same bound the rejection log keeps. + snippet = (outcome.raw or "").strip() + if len(snippet) > _SNIPPET_LIMIT: + snippet = snippet[:_SNIPPET_LIMIT] + " …[truncated]" note = ( - "Your previous reply could not be used as a proposal: " - f"{outcome.error}. Reply with the proposal object and nothing else." + f"Your previous reply could not be used as a proposal: {outcome.error}.\n" + + (f"It began:\n---\n{snippet}\n---\n" if snippet else "") + + "Reply with exactly one JSON object in this shape and nothing else:\n" + + PROPOSAL_EXAMPLE ) if unplanned_in_a_row >= self.limits.max_consecutive_planning_failures: stop = LoopStop.PLANNING_FAILED @@ -482,7 +506,13 @@ def close(**fields: Any) -> None: before = self._progress_of(current) attempt = self._execute( - verdict, proposal, current, meter=meter, ctx=ctx, round_number=round_number + verdict, + proposal, + current, + meter=meter, + ctx=ctx, + round_number=round_number, + goal=goal, ) current = attempt.state progressed = attempt.executed and self._progress_of(current) != before @@ -605,6 +635,7 @@ def _execute( meter: BudgetMeter, ctx: RunContext, round_number: int, + goal: str = "", ) -> _Execution: """Build and run the admitted subgraph. Spend is charged back either way. @@ -637,12 +668,32 @@ def _execute( "round": round_number, "proposal_id": proposal.proposal_id, "fingerprint": proposal.fingerprint(), + # The operator's own question, so a live view can say what + # this graph is *for*. Deliberate exposure of operator- + # supplied text — never something a node wrote. + "goal": goal, }, ) + if self.plan_only: + # The graph is admitted, materialisable, and on the trace — which + # is exactly what "planned" means. Executing it is `grapharc go`'s + # job, in its own process, whenever the operator says. The + # approval gate is skipped on purpose: a plan that executes + # nothing has nothing to approve; the act of running `go` *is* + # the approval. + return _Execution( + state=state, + executed=False, + hard_stop=LoopStop.PLANNED, + execution_error="awaiting `grapharc go`", + ) + if self.approval is not None: parked = time.monotonic() - decision = self._request_approval(proposal, verdict, ctx, round_number) + decision = self._request_approval( + proposal, verdict, ctx, round_number, goal=goal + ) # The wait charges nothing, seconds included: `elapsed_seconds` is # wall clock from run() start, and without this credit a human who # took longer than `max_seconds` to say yes handed the round a @@ -701,6 +752,7 @@ def _request_approval( verdict: AdmissionResult, ctx: RunContext, round_number: int, + goal: str = "", ) -> str: """Ask the configured gate; both the question and the answer are audited. @@ -719,6 +771,7 @@ def _request_approval( "fingerprint": proposal.fingerprint(), "nodes": [n.name for n in proposal.nodes], "edges": [[e.source, e.target] for e in proposal.edges], + "goal": goal, }, ) try: diff --git a/grapharc/planner/proposal.py b/grapharc/planner/proposal.py index cfea5f0..c30d4b4 100644 --- a/grapharc/planner/proposal.py +++ b/grapharc/planner/proposal.py @@ -30,6 +30,7 @@ from __future__ import annotations import hashlib +import json import re import time import uuid @@ -257,6 +258,107 @@ def fingerprint(self) -> str: ProposedNode.model_rebuild() +# -- the slim proposal shape -------------------------------------------------- +# +# What text-path backends are asked for. `Subgraph`'s own JSON schema is wrong +# for a local model's grammar-constrained decoder: it is recursive +# (`ProposedNode.subgraph` refers back to `Subgraph`), strict mode makes every +# field required — including `proposal_id` and `origin`, which `_stamp` +# discards on arrival — and every multi-paragraph docstring rides along. The +# slim shape is three keys a small model can hit, and everything it accepts is +# re-validated through the real constructors before admission ever sees it: +# tolerance in reading, zero tolerance in what gets judged. + + +class SlimNode(BaseModel): + """One node as a small model states it: a name, optionally a kind.""" + + model_config = ConfigDict(extra="ignore") + + name: str + kind: str = "" + + +class SlimEdge(BaseModel): + model_config = ConfigDict(extra="ignore") + + source: str + target: str + + +class PlanProposal(BaseModel): + """The proposal shape asked of backends that cannot hit `Subgraph`.""" + + model_config = ConfigDict(extra="ignore") + + nodes: list[SlimNode] = Field(default_factory=list) + edges: list[SlimEdge] = Field(default_factory=list) + rationale: str = "" + + @field_validator("edges", mode="before") + @classmethod + def _normalise_edges(cls, value: Any) -> Any: + """Accept `{"source","target"}`, `{"from","to"}`, and `["a","b"]` pairs. + + Local models produce all three, and the difference carries no meaning + an admission check would care about. + """ + if not isinstance(value, (list, tuple)): + return value + normalised = [] + for entry in value: + if isinstance(entry, (list, tuple)) and len(entry) == 2: + normalised.append({"source": entry[0], "target": entry[1]}) + elif isinstance(entry, dict) and "from" in entry and "to" in entry: + normalised.append({"source": entry["from"], "target": entry["to"]}) + else: + normalised.append(entry) + return normalised + + def to_subgraph(self) -> Subgraph: + """Re-issue through the real constructors — name/sentinel/duplicate + validation happens there, so a bad slim proposal fails with the same + named reason a bad full one does.""" + return Subgraph( + nodes=tuple(ProposedNode(name=n.name, kind=n.kind) for n in self.nodes), + edges=tuple(ProposedEdge(source=e.source, target=e.target) for e in self.edges), + rationale=self.rationale, + ) + + +#: Deliberately a diamond, not a chain: small models imitate the example far +#: more than they follow the rules, and a chain example taught them to +#: serialize work that had no reason to wait. Two branches sharing a +#: predecessor run *at the same time*; the join waits for both. +PROPOSAL_EXAMPLE = json.dumps( + { + "nodes": [ + {"name": "prepare", "kind": ""}, + {"name": "branch_a", "kind": ""}, + {"name": "branch_b", "kind": ""}, + {"name": "combine", "kind": ""}, + ], + "edges": [ + [START, "prepare"], + ["prepare", "branch_a"], + ["prepare", "branch_b"], + ["branch_a", "combine"], + ["branch_b", "combine"], + ["combine", END], + ], + "rationale": "branch_a and branch_b are independent, so they run in parallel", + }, + indent=2, +) + +TEXT_FORMAT_INSTRUCTIONS = ( + "Reply with exactly one JSON object and nothing else — no prose before or " + "after it. The object has three keys: `nodes` (a list of {name, kind}), " + "`edges` (a list of [source, target] pairs), and `rationale` (one " + f"sentence). Example:\n{PROPOSAL_EXAMPLE}" +) + + # -- the planner node --------------------------------------------------------- DEFAULT_PLANNER_SYSTEM_PROMPT = ( @@ -283,9 +385,10 @@ def fingerprint(self) -> str: f"edge from {START!r} to the first node, and every other node is reachable " f"by following edges from there. A node nothing leads to would never run.\n" f"Give the last node an edge to {END!r}.\n" - "Nodes that should run at the same time all take an edge from the same " - "predecessor; nodes that must wait for several others all take an edge " - "into the same successor. That is how you express parallelism and joins.\n" + "Two nodes that do not need each other's output should NOT be chained: " + "give them the same predecessor and they run at the same time; give " + "their successor an edge from each and it waits for both. Chain nodes " + "only when one truly needs what the other produced.\n" "Leave `subgraph` unset on every node.\n" "Propose no nodes at all when there is no further work to do.\n" "Admission is deterministic code, not a conversation: arguing with a " @@ -319,6 +422,10 @@ def fingerprint(self) -> str: "permissiondenied", "ratelimit", "insufficient", + # A model name the backend does not have (`ollama/qwen3:8` for `qwen3:8b`) + # is as dead as a dead socket: the 404 is deterministic, and retrying it + # burned every allowed round on the same typo before this marker existed. + "notfound", "quota", "serviceunavailable", ) @@ -450,8 +557,14 @@ def __init__( prompt_fn: Callable[[Any], str] | None = None, trace: TraceRecorder | None = None, origin: str | None = None, + structured: bool | None = None, ) -> None: self.model = model + # None asks the backend: a gateway that knows its wire cannot carry + # `Subgraph`'s strict schema (Ollama's grammar decoder) declares + # `reliable_structured_output = False` and gets the text path with the + # slim shape instead. True/False is the operator overriding either way. + self.structured = structured self.name = name self.catalog = catalog # The gates this planner's proposals will be checked against, held only @@ -511,8 +624,8 @@ def propose( ctx = RunContext( run_id=uuid.uuid4().hex[:12], graph=self.name, meter=BudgetMeter(Budget()) ) - messages = self._messages(task, feedback) runnable, structured = self._runnable() + messages = self._messages(task, feedback, structured=structured) started = time.perf_counter() raw_message: BaseMessage | None = None @@ -575,7 +688,9 @@ def propose( # -- internals ------------------------------------------------------------ - def _messages(self, task: str, feedback: str) -> list[BaseMessage]: + def _messages( + self, task: str, feedback: str, *, structured: bool = True + ) -> list[BaseMessage]: system = f"{self.system_prompt}\n\nAvailable node kinds:\n{_catalog_text(self.catalog)}" # Immediately after the catalog, because the two are one statement: here # is what exists, and here is what may not be wired. A model shown only @@ -587,6 +702,11 @@ def _messages(self, task: str, feedback: str) -> list[BaseMessage]: system = f"{system}\n\n{denied}" if self.instructions: system = f"{system}\n\n{self.instructions}" + if not structured: + # The text path carries its own format contract: the slim shape + # with a worked example, because a backend on this path was never + # handed a schema to decode against. + system = f"{system}\n\n{TEXT_FORMAT_INSTRUCTIONS}" messages: list[BaseMessage] = [SystemMessage(content=system), HumanMessage(content=task)] if feedback: messages.append( @@ -604,15 +724,21 @@ def _messages(self, task: str, feedback: str) -> list[BaseMessage]: def _runnable(self) -> tuple[Any, bool]: """The structured-output runnable and whether it is one, computed once.""" if self._structured_cache is _UNSET: - try: - self._structured_cache = self.model.with_structured_output( - Subgraph, include_raw=True - ) - except (NotImplementedError, ValueError, TypeError): - # `with_structured_output` raises NotImplementedError on backends - # without tool calling (the Claude CLI adapter); the other two - # cover a backend that rejects the schema outright. + wants = self.structured + if wants is None: + wants = getattr(self.model, "reliable_structured_output", True) + if not wants: self._structured_cache = None + else: + try: + self._structured_cache = self.model.with_structured_output( + Subgraph, include_raw=True + ) + except (NotImplementedError, ValueError, TypeError): + # `with_structured_output` raises NotImplementedError on backends + # without tool calling (the Claude CLI adapter); the other two + # cover a backend that rejects the schema outright. + self._structured_cache = None runnable = self._structured_cache return (runnable, True) if runnable is not None else (self.model, False) @@ -620,8 +746,17 @@ def _from_text(self, text: str) -> tuple[Subgraph | None, str]: data = extract_json(text) if data is None: return None, "no JSON object found in the planner's reply" + # Full shape first: a capable backend's raw-text fallback may + # legitimately carry `args` or nested subgraphs, and the slim reading + # would drop them. Only a reply that fails the full shape is read slim + # — and the reported error is the slim model's, because that is the + # shape the text path asked for. try: return Subgraph.model_validate(data), "" + except ValidationError: + pass + try: + return PlanProposal.model_validate(data).to_subgraph(), "" except ValidationError as exc: return None, f"proposal did not validate: {_first_validation_error(exc)}" diff --git a/grapharc/runtime/parsing.py b/grapharc/runtime/parsing.py index c1d8ca8..95e2c8b 100644 --- a/grapharc/runtime/parsing.py +++ b/grapharc/runtime/parsing.py @@ -6,10 +6,22 @@ extraction) it silently drops every claim. Both were observed against live models. +Reasoning models add a second failure shape: a `` block that +*contains JSON* — drafts, worked examples, half-answers — ahead of the real +reply. The visible text after stripping those blocks is scanned first, and the +original text is scanned only when the visible text yields nothing parseable. +Second-class rather than deleted, because some chat templates put the *only* +copy of the answer inside the block; first-class would resurrect the bug this +ordering exists to fix — a longer draft inside the think block outranking the +real answer outside it. + This is parsing latitude, not correctness latitude: the JSON that comes back must still be valid and still say what it says. Nothing here makes a malformed or missing answer look like a good one — an unrecoverable reply returns None -and the caller's fail-closed path runs exactly as before. +and the caller's fail-closed path runs exactly as before. The one repair +performed (trailing commas) only ever runs on a candidate that already failed +to parse, and a trailing comma is never valid JSON, so no valid document can +be rewritten. """ from __future__ import annotations @@ -20,6 +32,27 @@ _FENCE = re.compile(r"```(?:json|JSON)?\s*(.*?)```", re.DOTALL) +#: The tag names reasoning models wrap their scratch work in. Matched as a +#: pair with a backreference so `` cannot be closed by ``. +_THINK_NAMES = "think|thinking|thought|reasoning|reason" +_THINK = re.compile( + rf"<({_THINK_NAMES})\b[^>]*>.*?", re.DOTALL | re.IGNORECASE +) +#: Some chat templates strip the opening tag and the visible reply *starts* +#: with a bare closer; and a truncated reply can open a block it never closes. +_ORPHAN_CLOSE = re.compile(rf"^\s*", re.IGNORECASE) +_ORPHAN_OPEN = re.compile( + rf"<({_THINK_NAMES})\b[^>]*>(?!.*).*\Z", re.DOTALL | re.IGNORECASE +) + + +def _without_reasoning(text: str) -> str: + """The reply minus its reasoning blocks — what the model meant to show.""" + visible = _THINK.sub("", text) + visible = _ORPHAN_CLOSE.sub("", visible) + visible = _ORPHAN_OPEN.sub("", visible) + return visible.strip() + def _span_from(text: str, start: int) -> str | None: """The balanced {...} or [...] region opening at `start`, or None if unclosed.""" @@ -79,6 +112,50 @@ def _balanced_spans(text: str) -> list[str]: return sorted(spans, key=lambda span: (span[0] != "{", -len(span))) +def _strip_trailing_commas(candidate: str) -> str: + """Remove commas whose next non-whitespace char (outside strings) closes a + scope — the one syntax error local models make that has exactly one honest + reading. Only ever called on text that already failed `json.loads`.""" + out: list[str] = [] + in_string = False + escaped = False + for i, ch in enumerate(candidate): + if in_string: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + in_string = False + out.append(ch) + continue + if ch == '"': + in_string = True + out.append(ch) + continue + if ch == ",": + rest = candidate[i + 1 :].lstrip() + if rest[:1] in ("}", "]"): + continue # the comma before a closer: drop it + out.append(ch) + return "".join(out) + + +def _candidates(text: str) -> list[str]: + """Whole reply first, then every fence in order, then balanced spans. + + The earlier a candidate is, the more of the model's reply it accounts for, + so a fenced top-level array still wins over any span found inside it. + Every fence is tried, not only the first: a model that shows a draft in + one fence and the answer in the next must not be stranded on the draft. + """ + found = [text] + for fence in _FENCE.finditer(text): + found.append(fence.group(1).strip()) + found.extend(_balanced_spans(text)) + return found + + def extract_json(content: Any) -> Any | None: """Best-effort JSON from a model reply. None when nothing valid is found.""" text = content if isinstance(content, str) else str(content) @@ -86,18 +163,30 @@ def extract_json(content: Any) -> Any | None: if not text: return None - # Whole reply first, then the fence, then balanced spans: the earlier a - # candidate is, the more of the model's reply it accounts for, so a fenced - # top-level array still wins over any span found inside it. + # Tier 1: the whole reply, untouched. A reply that is already valid JSON + # must never be rewritten — if it contains a reasoning tag, that tag lives + # inside a string and is data, not scratch work. + # Tier 2: the visible text (reasoning blocks stripped), whole/fences/spans + # — so a draft inside can never outrank the shown answer. + # Tier 3: fences and spans of the original, for the reply whose only copy + # of the answer sits inside the reasoning block. + visible = _without_reasoning(text) candidates = [text] - fenced = _FENCE.search(text) - if fenced: - candidates.append(fenced.group(1).strip()) - candidates.extend(_balanced_spans(text)) + if visible and visible != text: + candidates.extend(_candidates(visible)) + candidates.extend(_candidates(text)[1:]) for candidate in candidates: + if not candidate: + continue try: return json.loads(candidate) except json.JSONDecodeError: - continue + pass + repaired = _strip_trailing_commas(candidate) + if repaired != candidate: + try: + return json.loads(repaired) + except json.JSONDecodeError: + continue return None diff --git a/grapharc/server/live.py b/grapharc/server/live.py index df79566..bb02a49 100644 --- a/grapharc/server/live.py +++ b/grapharc/server/live.py @@ -12,8 +12,10 @@ events. That is a security decision as much as a simplicity one — `state_delta` can hold anything a run's nodes wrote, and its *contents* are deliberately not served. The exposure is the same as the `viz`/`metrics` commands': node names, -error labels, counts, and the one state field `summarize` lifts out of the -delta — `termination_reason`, a short reason string by convention. +error labels, counts, and two state fields shown by convention — +`termination_reason` (a short reason string) and `goal` (the operator's own +question, from the loop's labelled topology/approval events; text the operator +typed, never something a node wrote). Reachability is the operator's problem by design: bind stays loopback unless they choose otherwise, and the recommended remote path is a tunnel or tailnet @@ -30,8 +32,10 @@ from __future__ import annotations import asyncio +import functools import hashlib import html +import importlib.resources import math import secrets from datetime import datetime @@ -44,9 +48,11 @@ from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse from pydantic import BaseModel +from grapharc.observe.layout import layout_graph from grapharc.observe.metrics import RunMetrics, summarize, to_mermaid from grapharc.observe.replay import replay from grapharc.observe.trace import TailRecorder, TraceEvent +from grapharc.observe.viewmodel import GraphSnapshot, build_graph_view from grapharc.slack.format import mermaid_live_url #: How often the stream re-stats the trace file. Coarser than the session @@ -84,6 +90,29 @@ REPLAY_MAX_SPEED = 10_000.0 +#: The static assets the router serves, and as what. An allowlist, not a +#: directory walk: any other name under /live/static is a 404, so the route +#: can never be talked into serving a neighbouring file. +STATIC_ASSETS = { + "view.css": "text/css; charset=utf-8", + "view.js": "text/javascript; charset=utf-8", +} + + +@functools.cache +def _static(name: str) -> str: + """One page or asset from the packaged static directory. + + `importlib.resources`, not a path relative to `__file__`: the files must + load from an installed wheel, not only from a checkout. + """ + return ( + importlib.resources.files("grapharc.server") + .joinpath("static", name) + .read_text(encoding="utf-8") + ) + + class LivePathError(Exception): """The requested trace is not one this server will read.""" @@ -157,6 +186,19 @@ class LiveSnapshot(BaseModel): #: None for a run that never planned — an ordinary graph invocation renders #: exactly as it did before this field existed. planning: PlanningView | None = None + #: The structured, positioned graph the SVG view draws — same exposure as + #: `mermaid` (names, statuses, counts, spend, error labels), never deltas. + graph: GraphSnapshot | None = None + #: The operator's own question, lifted from the loop's topology/ + #: approval_request deltas the way `termination_reason` is lifted by + #: `summarize` — the second state field shown by convention, and the only + #: other one: it is text the operator typed, never something a node wrote. + goal: str | None = None + #: The command that answers a parked run, shown only while one is parked. + #: Reveals the trace's directory to authenticated viewers — accepted: it is + #: the one string correct from any shell on this machine, the view is + #: loopback/token/tunnel-gated, and it vanishes once the run is answered. + approve_command: str | None = None def _as_int(value: Any, fallback: int = 0) -> int: @@ -260,7 +302,15 @@ def build_snapshot(root: Path, rel: str, run_id: str | None) -> LiveSnapshot: size, quiet_for = stat.st_size, time() - stat.st_mtime except OSError: size, quiet_for = 0, float("inf") - return compose_snapshot(rel, recorder, events, run_id, size=size, quiet_for=quiet_for) + return compose_snapshot( + rel, + recorder, + events, + run_id, + size=size, + quiet_for=quiet_for, + trace_dir=path.parent, + ) def compose_snapshot( @@ -271,6 +321,7 @@ def compose_snapshot( *, size: int, quiet_for: float, + trace_dir: Path | None = None, ) -> LiveSnapshot: """The snapshot for one already-read set of events. Pure; run it in a thread. @@ -287,6 +338,7 @@ def compose_snapshot( mermaid = to_mermaid(recorder, chosen) run = replay(recorder, chosen) + graph = layout_graph(build_graph_view(run)) # Finished means: something wrote a termination reason, OR a driver wrote # its terminal `stop` event — the planner writes the latter and never the # former, and keying on `termination_reason` alone held the SSE stream @@ -326,6 +378,19 @@ def compose_snapshot( awaiting = False if awaiting and not done: active = True + # The goal, from the loop's labelled events only. Last one wins — a + # multi-round run restates it, and the current round's is the answer. + goal = None + for event in run_events: + if event.phase in ("topology", "approval_request"): + value = (event.state_delta or {}).get("goal") + if value: + goal = str(value) + approve_command = ( + f"grapharc approve {trace_dir}" + if awaiting and not done and trace_dir is not None + else None + ) return LiveSnapshot( trace=rel, run_id=chosen, @@ -341,6 +406,9 @@ def compose_snapshot( done=done, awaiting_approval=awaiting, planning=planning, + graph=graph, + goal=goal, + approve_command=approve_command, ) @@ -541,7 +609,8 @@ def _return_to(request: Request) -> str: def _sign_in(target: str, reason: str) -> HTMLResponse: page = ( - SIGNIN_HTML.replace("__REASON__", html.escape(reason)) + _static("signin.html") + .replace("__REASON__", html.escape(reason)) .replace("__NEXT__", html.escape(target, quote=True)) ) return HTMLResponse(page, status_code=401) @@ -604,7 +673,7 @@ def index(request: Request) -> HTMLResponse: f"{runs}{item['size']}" ) body = "\n".join(rows) or 'no trace files yet' - return HTMLResponse(INDEX_HTML.replace("__ROWS__", body)) + return HTMLResponse(_static("index.html").replace("__ROWS__", body)) @router.get("/api/runs") def runs(request: Request) -> dict[str, Any]: @@ -619,7 +688,20 @@ def view(request: Request, trace: str = Query(...)) -> HTMLResponse: if refusal: return _sign_in(_return_to(request), refusal) _resolved(trace) - return HTMLResponse(VIEW_HTML) + return HTMLResponse(_static("view.html")) + + @router.get("/static/{name}", include_in_schema=False) + def static_asset(name: str) -> Response: + """CSS/JS for the pages above. Deliberately outside the token gate: + these are the same bytes anyone can read in the published wheel, and + the sign-in page itself needs its stylesheet before any credential + exists. The allowlist is the whole route — no path from here reaches + the filesystem or the trace root. + """ + content_type = STATIC_ASSETS.get(name) + if content_type is None: + raise HTTPException(status_code=404, detail="no such asset") + return Response(_static(name), media_type=content_type) @router.get("/api/stream") async def stream( @@ -751,200 +833,11 @@ async def frames(): return router -# The pages are plain strings with sentinel replacement, not str.format — -# JS/CSS braces would fight the format machinery. Self-contained except for -# the pinned mermaid CDN import; without it (offline viewer, blocked CDN) the -# page falls back to the raw Mermaid source plus the mermaid.live link that -# every snapshot carries. - -INDEX_HTML = """ -grapharc live - - -

grapharc live

-

Trace files under the live root, newest first. This page refreshes itself.

- -__ROWS__ -
tracerunsbytes
-""" - -SIGNIN_HTML = """ -grapharc live · sign in - - -

grapharc live

-

__REASON__

-
- - - -
-

The token is exchanged for a cookie scoped to /live, -so it never appears in a URL — where it would be copied into access logs, -browser history and referrer headers. Scripts can send -Authorization: Bearer TOKEN instead.

- -""" - -VIEW_HTML = """ -grapharc live view - - -
- connecting… - - - -
-
waiting for the run to start…
- - -
- -""" +# The pages live in `static/` as real files, packaged with the wheel and +# loaded through `_static`. They keep sentinel replacement (`__ROWS__`, +# `__REASON__`, `__NEXT__`), not str.format — JS/CSS braces would fight the +# format machinery. Fully self-contained: no CDN, no external request; the +# mermaid.live link every snapshot carries remains the degraded path. __all__ = [ "ACTIVE_WINDOW_SECONDS", diff --git a/grapharc/server/static/index.html b/grapharc/server/static/index.html new file mode 100644 index 0000000..9200e83 --- /dev/null +++ b/grapharc/server/static/index.html @@ -0,0 +1,22 @@ + +grapharc live + + + + +
+ GraphARC + live traces +
+
+

Trace files under the live root, newest first. This page refreshes itself.

+ +__ROWS__ +
tracerunsbytes
diff --git a/grapharc/server/static/signin.html b/grapharc/server/static/signin.html new file mode 100644 index 0000000..5fd8caa --- /dev/null +++ b/grapharc/server/static/signin.html @@ -0,0 +1,28 @@ + +grapharc live · sign in + + + + +
GraphARCsign in
+
+

__REASON__

+
+ + + +
+

The token is exchanged for a cookie scoped to /live, +so it never appears in a URL — where it would be copied into access logs, +browser history and referrer headers. Scripts can send +Authorization: Bearer TOKEN instead.

+
diff --git a/grapharc/server/static/view.css b/grapharc/server/static/view.css new file mode 100644 index 0000000..5e5f38a --- /dev/null +++ b/grapharc/server/static/view.css @@ -0,0 +1,349 @@ +/* GraphARC live view. + * + * The design tokens below are shared with the public site + * (docs/site/assets/tokens.css is the source of truth — change both). + * Status is never color alone: every state also carries a glyph and a label. + */ + +:root { + --g-bg: #0B0F19; + --g-surface: #111827; + --g-card: #151C2C; + --g-border: rgba(231, 236, 243, 0.08); + --g-border-hi: rgba(231, 236, 243, 0.16); + --g-text: #E7ECF3; + --g-text-2: #94A3B8; + --g-text-3: #64748B; + --g-teal: #2DD4BF; + --g-blue: #3B82F6; + --g-violet: #8B5CF6; + --g-gradient: linear-gradient(135deg, #2DD4BF, #3B82F6 50%, #8B5CF6); + --g-status-pending: #64748B; + --g-status-running: #FBBF24; + --g-status-done: #34D399; + --g-status-errored: #F87171; + --g-status-approval: #A78BFA; + --g-font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + --g-font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, + "Liberation Mono", monospace; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--g-font-sans); + background: var(--g-bg); + color: var(--g-text); + font-size: 15px; + line-height: 1.5; +} + +a { color: var(--g-blue); text-decoration: none; } +a:hover { text-decoration: underline; } + +header { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + padding: 0.75rem 1.25rem; + background: var(--g-surface); + border-bottom: 1px solid var(--g-border); +} + +.brand { + font-weight: 700; + letter-spacing: -0.01em; + background: var(--g-gradient); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.livedot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--g-text-3); + flex: none; +} +.livedot.running { background: var(--g-status-done); animation: pulse 1.2s ease-in-out infinite; } +.livedot.awaiting { background: var(--g-status-approval); animation: pulse 1.2s ease-in-out infinite; } +.livedot.done { background: var(--g-status-done); } +.livedot.planning { background: var(--g-status-running); animation: pulse 1.2s ease-in-out infinite; } + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +#status { font-weight: 600; } +.muted { color: var(--g-text-2); } +.mono { font-family: var(--g-font-mono); font-size: 0.85em; } + +#runpick { + font: inherit; + font-size: 0.85em; + font-family: var(--g-font-mono); + color: var(--g-text-2); + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: 6px; + padding: 0.15rem 0.4rem; +} + +#badge { + padding: 0.05rem 0.5rem; + border: 1px solid var(--g-status-running); + border-radius: 999px; + color: var(--g-status-running); + font-size: 0.78rem; +} + +main { padding: 1rem 1.25rem 2rem; max-width: 1200px; margin: 0 auto; } + +.tiles { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin: 0.25rem 0 1rem; +} +.tile { + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: 12px; + padding: 0.5rem 0.9rem; + min-width: 6.5rem; +} +.tile b { + display: block; + font-size: 1.25rem; + font-weight: 600; + font-variant-numeric: tabular-nums; +} +.tile span { + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--g-text-2); +} +.tile.errors b { color: var(--g-status-errored); } +.tile[hidden] { display: none; } + +#panel { + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: 12px; + padding: 1rem; + overflow: auto; +} +#panel .empty { color: var(--g-text-2); padding: 2rem 1rem; text-align: center; } + +svg { display: block; margin: 0 auto; } + +/* --- graph marks ---------------------------------------------------------- */ + +.node rect { + fill: var(--g-card); + stroke-width: 1.5; + rx: 8; + transition: stroke 300ms ease, fill 300ms ease; +} +.node text { font-family: var(--g-font-sans); } +.node .name { font-size: 13px; font-weight: 600; fill: var(--g-text); } +.node .meta { + font-size: 11px; + fill: var(--g-text-2); + font-family: var(--g-font-mono); + font-variant-numeric: tabular-nums; +} + +.node.st-pending rect { stroke: var(--g-status-pending); stroke-dasharray: 4 3; } +.node.st-pending .name { fill: var(--g-text-2); } +.node.st-proposed rect { + stroke: var(--g-status-approval); + stroke-dasharray: 4 3; + fill: rgba(139, 92, 246, 0.05); +} +.node.st-proposed .name { fill: var(--g-text-2); } +.node.st-proposed .meta { fill: var(--g-status-approval); } +.node.st-running rect { stroke: var(--g-status-running); } +.node.st-done rect { stroke: var(--g-status-done); fill: rgba(52, 211, 153, 0.06); } +.node.st-errored rect { stroke: var(--g-status-errored); fill: rgba(248, 113, 113, 0.10); } +.node.st-errored .meta { fill: var(--g-status-errored); } + +.node .halo { + fill: none; + stroke: var(--g-status-running); + stroke-width: 2; + opacity: 0; + rx: 10; +} +.node.st-running .halo { animation: halo 1.6s ease-in-out infinite; } + +@keyframes halo { + 0%, 100% { opacity: 0.12; } + 50% { opacity: 0.55; } +} + +.term circle { fill: var(--g-surface); stroke: var(--g-text-3); stroke-width: 1.5; } +.term text { + font-size: 9px; + fill: var(--g-text-3); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.edge { + fill: none; + stroke: #3d4250; + stroke-width: 1.5; + transition: stroke 300ms ease; +} +.edge.conditional { stroke-dasharray: 5 4; } +.edge.fanout { stroke-dasharray: 2 4; } +.edge.state { stroke-dasharray: 6 5; stroke: var(--g-text-3); } +.edge.flow { + stroke: var(--g-status-running); + stroke-dasharray: 6 5; + animation: flow 900ms linear infinite; +} + +@keyframes flow { + to { stroke-dashoffset: -11; } +} + +.cluster rect { + fill: none; + stroke: var(--g-border-hi); + stroke-dasharray: 3 4; + rx: 12; +} +.cluster text { + font-size: 11px; + fill: var(--g-text-2); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* --- approval banner ------------------------------------------------------ */ + +#approvalbar { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + margin: 0 0 1rem; + background: var(--g-card); + border: 1px solid var(--g-status-approval); + border-radius: 12px; + padding: 0.5rem 0.9rem; +} +#approvalbar[hidden] { display: none; } +#approvalbar code { + font-family: var(--g-font-mono); + font-size: 0.85rem; + background: var(--g-surface); + border: 1px solid var(--g-border); + border-radius: 6px; + padding: 0.15rem 0.5rem; + user-select: all; +} +#approvalbar button { + font: inherit; + font-size: 0.8rem; + background: var(--g-surface); + color: var(--g-text); + border: 1px solid var(--g-border-hi); + border-radius: 6px; + padding: 0.15rem 0.7rem; + cursor: pointer; +} +#approvalbar button:hover { border-color: var(--g-status-approval); } + +/* --- playback ------------------------------------------------------------- */ + +#playback { + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: 0.9rem; + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: 12px; + padding: 0.5rem 0.9rem; +} +#playback[hidden] { display: none; } +#playback button { + font: inherit; + background: var(--g-surface); + color: var(--g-text); + border: 1px solid var(--g-border-hi); + border-radius: 6px; + padding: 0.2rem 0.8rem; + cursor: pointer; +} +#playback button:hover { border-color: var(--g-blue); } +#playback input[type="range"] { flex: 1; accent-color: var(--g-teal); } +#playback select { + font: inherit; + background: var(--g-surface); + color: var(--g-text); + border: 1px solid var(--g-border); + border-radius: 6px; + padding: 0.15rem 0.3rem; +} +#clock { font-family: var(--g-font-mono); font-variant-numeric: tabular-nums; color: var(--g-text-2); min-width: 5.5ch; text-align: right; } + +/* --- planning panel ------------------------------------------------------- */ + +#planning { margin-top: 1rem; } +#planning h2 { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--g-text-2); + margin: 0 0 0.5rem; +} +#planning table { + border-collapse: collapse; + background: var(--g-card); + border: 1px solid var(--g-border); + border-radius: 12px; + overflow: hidden; + width: 100%; +} +#planning td, #planning th { + padding: 0.35rem 0.8rem; + border-bottom: 1px solid var(--g-border); + text-align: left; + font-size: 0.88rem; +} +#planning th { color: var(--g-text-2); font-weight: 500; } +#planning td.rejected { color: var(--g-status-errored); } +#planning tr.inflight td { background: rgba(251, 191, 36, 0.08); } +#planning .stop { margin: 0.5rem 0 0; color: var(--g-status-errored); } +#planning .stop.ok { color: var(--g-status-done); } + +/* --- source drawer -------------------------------------------------------- */ + +#drawer { margin-top: 1rem; color: var(--g-text-2); } +#drawer summary { cursor: pointer; font-size: 0.88rem; } +#drawer pre { + background: var(--g-surface); + border: 1px solid var(--g-border); + border-radius: 12px; + padding: 1rem; + overflow-x: auto; + font-family: var(--g-font-mono); + font-size: 0.82rem; + color: var(--g-text-2); +} + +@media (prefers-reduced-motion: reduce) { + .livedot, .node .halo, .edge.flow { animation: none; } + .node rect, .edge { transition: none; } + .node.st-running .halo { opacity: 0.45; } +} diff --git a/grapharc/server/static/view.html b/grapharc/server/static/view.html new file mode 100644 index 0000000..3c53640 --- /dev/null +++ b/grapharc/server/static/view.html @@ -0,0 +1,48 @@ + +grapharc live view + + + + +
+ GraphARC + + connecting… + + + + +
+
+ +
+ + + + + + +
+
waiting for the run to start…
+ + +
+ diagram source · +

+ 
+
+ + diff --git a/grapharc/server/static/view.js b/grapharc/server/static/view.js new file mode 100644 index 0000000..5a430b7 --- /dev/null +++ b/grapharc/server/static/view.js @@ -0,0 +1,483 @@ +/* GraphARC live view renderer. + * + * Draws the positioned graph the server ships in each SSE snapshot. The page + * stays dumb on purpose: no layout, no counting, no trace parsing — statuses, + * spend and geometry all arrive computed, and everything written into the DOM + * goes through textContent (names and error labels come out of a trace file + * this server merely reads). + * + * Flicker-free by construction: the SVG is built once per topology (keyed on + * node ids + edges + canvas size) and later snapshots only patch classes and + * text in place. A new planner round changes the key and rebuilds, which is + * the one moment things may move. + */ + +"use strict"; + +const SVG = "http://www.w3.org/2000/svg"; + +const el = (id) => document.getElementById(id); +const dot = el("dot"), statusEl = el("status"), runEl = el("run"); +const goalEl = el("goal"); +const runPick = el("runpick"), badge = el("badge"), panel = el("panel"); +const planningEl = el("planning"), drawer = el("drawer"); +const srcEl = el("src"), mlive = el("mlive"); +const playback = el("playback"), playBtn = el("play"); +const speedPick = el("speed"), scrub = el("scrub"), clock = el("clock"); +const approvalBar = el("approvalbar"), approveCmd = el("approvecmd"); +const copyApprove = el("copyapprove"); + +// Whether the current snapshot is parked at an approval gate. Proposed nodes +// (status pending while parked) render in the approval colour, not plain grey. +let awaiting = false; + +const params = new URLSearchParams(location.search); +if (params.get("replay")) badge.hidden = false; + +/* ---------------------------------------------------------------- helpers */ + +function fmtTokens(n) { + if (n >= 100_000) return Math.round(n / 1000) + "k"; + if (n >= 10_000) return (n / 1000).toFixed(1) + "k"; + return String(n); +} + +function fmtMs(ms) { + if (ms == null) return ""; + if (ms < 1000) return Math.round(ms) + "ms"; + if (ms < 90_000) return (ms / 1000).toFixed(1) + "s"; + return Math.round(ms / 60_000) + "m" + Math.round((ms % 60_000) / 1000) + "s"; +} + +function fmtCost(c) { + if (c == null) return null; + return "$" + (c < 0.01 ? c.toFixed(4) : c.toFixed(2)); +} + +function make(tag, className, parent) { + const node = document.createElementNS(SVG, tag); + if (className) node.setAttribute("class", className); + if (parent) parent.appendChild(node); + return node; +} + +/* ------------------------------------------------------------- stat tiles */ + +function tile(id, value) { + const box = el(id); + if (value === null || value === undefined || value === "") { + box.hidden = true; + return; + } + box.hidden = false; + box.querySelector("b").textContent = String(value); +} + +function renderTiles(s) { + const g = s.graph; + let nodesDone = null; + if (g && g.kind === "topology") { + const drawn = g.nodes.filter((n) => n.role === "node"); + nodesDone = drawn.filter((n) => n.status === "done").length + "/" + drawn.length; + } else if (s.stats) { + nodesDone = String(s.stats.nodes_executed); + } + tile("t-nodes", nodesDone); + tile("t-tokens", s.stats && s.stats.tokens ? fmtTokens(s.stats.tokens) : null); + tile("t-cost", fmtCost(s.cost_usd)); + tile("t-wall", s.wall_ms != null ? fmtMs(s.wall_ms) : null); + tile("t-errors", s.stats && s.stats.errors ? s.stats.errors : null); + tile("t-events", s.stats ? s.stats.events : null); +} + +/* ------------------------------------------------------------- the graph */ + +const GLYPH = { pending: "○", running: "▸", done: "✓", errored: "✗", proposed: "◇" }; +let topologyKey = null; +let nodeGroups = new Map(); // node id -> {group, rect, name, meta, title} +let edgePaths = new Map(); // "src->dst#i" -> {path, edge} +let lastGraph = null; + +function graphKey(g) { + return [ + g.width, g.height, + g.nodes.map((n) => n.id).join(","), + g.edges.map((e) => e.source + ">" + e.target + ":" + e.kind).join(","), + ].join("|"); +} + +function nodeMeta(node) { + if (node.status === "running") { + const live = node.live_tokens ? fmtTokens(node.live_tokens) + " tok · " : ""; + return live + "running…"; + } + if (node.status === "errored") { + return node.error ? node.error.slice(0, 34) : "failed"; + } + if (node.status === "done") { + const parts = []; + if (node.tokens) parts.push(fmtTokens(node.tokens) + " tok"); + const cost = fmtCost(node.cost_usd); + if (cost) parts.push(cost); + if (node.duration_ms != null) parts.push(fmtMs(node.duration_ms)); + return parts.join(" · ") || "done"; + } + return "pending"; +} + +function patchNode(node, statusOverride) { + const entry = nodeGroups.get(node.id); + if (!entry) return; + const status = statusOverride || node.status; + // A pending node in a parked run is "proposed": the plan exists and a human + // has not said yes. Replay scrubbing (statusOverride) is exempt — a finished + // run is no longer awaiting anything. + const shown = + awaiting && status === "pending" && !statusOverride ? "proposed" : status; + entry.group.setAttribute("class", "node st-" + shown); + entry.name.textContent = GLYPH[shown] + " " + entry.shortLabel; + entry.meta.textContent = + shown === "proposed" + ? "awaiting approval" + : status === node.status + ? nodeMeta(node) + : nodeMeta({ ...node, status, live_tokens: 0 }); + entry.title.textContent = + node.label + (node.error ? "\n" + node.error : ""); +} + +function patchEdges(g, statusAt) { + const running = new Set( + g.nodes.filter((n) => (statusAt ? statusAt(n.id) : n.status) === "running") + .map((n) => n.id) + ); + for (const { path, edge } of edgePaths.values()) { + const flow = running.has(edge.target) && edge.kind !== "state"; + path.setAttribute( + "class", + "edge " + edge.kind + (flow ? " flow" : "") + ); + } +} + +function buildGraph(g) { + nodeGroups = new Map(); + edgePaths = new Map(); + panel.textContent = ""; + const svg = make("svg", null, panel); + svg.setAttribute("viewBox", `0 0 ${g.width} ${g.height}`); + svg.setAttribute("width", g.width); + svg.style.maxWidth = "100%"; + svg.style.height = "auto"; + + const defs = make("defs", null, svg); + for (const [id, color] of [["arrow", "#3d4250"], ["arrow-flow", "#FBBF24"]]) { + const marker = make("marker", null, defs); + marker.setAttribute("id", id); + marker.setAttribute("viewBox", "0 0 10 10"); + marker.setAttribute("refX", "9"); + marker.setAttribute("refY", "5"); + marker.setAttribute("markerWidth", "5.5"); + marker.setAttribute("markerHeight", "5.5"); + marker.setAttribute("orient", "auto-start-reverse"); + const tip = make("path", null, marker); + tip.setAttribute("d", "M 0 0 L 10 5 L 0 10 z"); + tip.setAttribute("fill", color); + } + + for (const cluster of g.clusters) { + const group = make("g", "cluster", svg); + const rect = make("rect", null, group); + rect.setAttribute("x", cluster.x); + rect.setAttribute("y", cluster.y); + rect.setAttribute("width", cluster.w); + rect.setAttribute("height", cluster.h); + const label = make("text", null, group); + label.setAttribute("x", cluster.x + 10); + label.setAttribute("y", cluster.y - 6); + label.textContent = cluster.label; + } + + g.edges.forEach((edge, index) => { + if (!edge.points || edge.points.length < 4) return; + const [p0, p1, p2, p3] = edge.points; + const path = make("path", "edge " + edge.kind, svg); + path.setAttribute( + "d", + `M ${p0[0]} ${p0[1]} C ${p1[0]} ${p1[1]}, ${p2[0]} ${p2[1]}, ${p3[0]} ${p3[1]}` + ); + path.setAttribute("marker-end", "url(#arrow)"); + edgePaths.set(edge.source + ">" + edge.target + "#" + index, { path, edge }); + }); + + for (const node of g.nodes) { + if (node.role !== "node") { + const group = make("g", "term", svg); + const circle = make("circle", null, group); + circle.setAttribute("cx", node.x + node.w / 2); + circle.setAttribute("cy", node.y + node.h / 2); + circle.setAttribute("r", node.w / 2); + const label = make("text", null, group); + label.setAttribute("x", node.x + node.w / 2); + label.setAttribute("y", node.y + node.h + 11); + label.setAttribute("text-anchor", "middle"); + label.textContent = node.label; + continue; + } + const group = make("g", "node st-" + node.status, svg); + const halo = make("rect", "halo", group); + halo.setAttribute("x", node.x - 3); + halo.setAttribute("y", node.y - 3); + halo.setAttribute("width", node.w + 6); + halo.setAttribute("height", node.h + 6); + halo.setAttribute("rx", 10); + const rect = make("rect", null, group); + rect.setAttribute("x", node.x); + rect.setAttribute("y", node.y); + rect.setAttribute("width", node.w); + rect.setAttribute("height", node.h); + rect.setAttribute("rx", 8); + const title = make("title", null, group); + const name = make("text", "name", group); + name.setAttribute("x", node.x + 10); + name.setAttribute("y", node.y + 21); + const meta = make("text", "meta", group); + meta.setAttribute("x", node.x + 10); + meta.setAttribute("y", node.y + 39); + // Ellipsize by character count; the full name lives in the tooltip. + const room = Math.floor((node.w - 26) / 7.5); + const shortLabel = + node.label.length > room ? node.label.slice(0, room - 1) + "…" : node.label; + nodeGroups.set(node.id, { group, rect, name, meta, title, shortLabel }); + patchNode(node); + } + patchEdges(g); +} + +function renderGraph(g, note) { + if (!g || !g.nodes.length) { + topologyKey = null; + panel.textContent = ""; + const empty = document.createElement("div"); + empty.className = "empty"; + empty.textContent = note || (g && g.note) || "waiting for the run to start…"; + panel.appendChild(empty); + return; + } + const key = graphKey(g); + if (key !== topologyKey) { + topologyKey = key; + buildGraph(g); + } else { + for (const node of g.nodes) if (node.role === "node") patchNode(node); + patchEdges(g); + } + lastGraph = g; +} + +/* -------------------------------------------------------- planning panel */ + +function cell(tag, text, className) { + const node = document.createElement(tag); + node.textContent = text; + if (className) node.className = className; + return node; +} + +function renderPlanning(p) { + planningEl.textContent = ""; + if (!p || (!p.rounds.length && !p.stop)) { + planningEl.hidden = true; + return; + } + planningEl.append(cell("h2", p.has_topology ? "planning history" : "planning")); + const table = document.createElement("table"); + const head = document.createElement("tr"); + for (const h of ["round", "status", "nodes", "why", "tokens"]) head.append(cell("th", h)); + table.append(head); + for (const r of p.rounds) { + const tr = document.createElement("tr"); + if (r.in_flight) tr.className = "inflight"; + tr.append(cell("td", "round " + r.round)); + tr.append(cell("td", r.in_flight ? (r.status || "planning…") : (r.status || "—"), + r.status === "rejected" ? "rejected" : "")); + tr.append(cell("td", String(r.nodes || 0))); + const why = [...(r.rejections || []), ...(r.failed_checks || [])]; + tr.append(cell("td", why.length ? [...new Set(why)].join(", ") : (r.error || "—"))); + tr.append(cell("td", String(r.tokens || 0))); + table.append(tr); + } + planningEl.append(table); + if (p.stop) { + const clean = + p.stop === "goal_met" || p.stop === "no_further_work" || p.stop === "planned"; + planningEl.append( + cell( + "p", + "stopped: " + p.stop + (p.stop_detail ? " — " + p.stop_detail : ""), + clean ? "stop ok" : "stop" + ) + ); + } + planningEl.hidden = false; +} + +/* --------------------------------------------------------------- replay */ + +let horizon = 0; +let playing = null; // {t0Real, t0Scrub} while the animation runs + +function statusAtTime(spans, t) { + // Mirrors the server's status rule (errored > running > done > pending), + // evaluated against what had happened by time t. + return (id) => { + const list = spans.get(id); + if (!list) return "pending"; + let running = false, done = false, errored = false; + for (const span of list) { + if (span.t0 > t) continue; + if (span.t1 == null || span.t1 > t) running = true; + else if (span.ok === false) errored = true; + else done = true; + } + if (errored) return "errored"; + if (running) return "running"; + if (done) return "done"; + return "pending"; + }; +} + +function scrubTo(t) { + if (!lastGraph || !lastGraph.timeline) return; + const spans = new Map(); + for (const span of lastGraph.timeline) { + if (!spans.has(span.node)) spans.set(span.node, []); + spans.get(span.node).push(span); + } + const at = statusAtTime(spans, t); + for (const node of lastGraph.nodes) { + if (node.role === "node") patchNode(node, at(node.id)); + } + patchEdges(lastGraph, at); + clock.textContent = fmtMs(t); +} + +function stopPlaying() { + playing = null; + playBtn.textContent = "▶ replay"; +} + +function tick(now) { + if (!playing) return; + const speed = Number(speedPick.value) || 4; + const t = Math.min(horizon, playing.t0Scrub + (now - playing.t0Real) * speed); + scrub.value = String(Math.round((t / horizon) * 1000)); + scrubTo(t); + if (t >= horizon) { stopPlaying(); return; } + requestAnimationFrame(tick); +} + +playBtn.addEventListener("click", () => { + if (playing) { stopPlaying(); return; } + const from = Number(scrub.value) >= 1000 ? 0 : (Number(scrub.value) / 1000) * horizon; + playing = { t0Real: performance.now(), t0Scrub: from }; + playBtn.textContent = "⏸ pause"; + requestAnimationFrame(tick); +}); + +scrub.addEventListener("input", () => { + stopPlaying(); + scrubTo((Number(scrub.value) / 1000) * horizon); +}); + +function offerPlayback(s) { + const g = s.graph; + if (!s.done || !g || !g.timeline || !g.timeline.length || !g.duration_ms) { + playback.hidden = true; + return; + } + horizon = g.duration_ms; + playback.hidden = false; + if (Number(scrub.value) >= 1000) clock.textContent = fmtMs(horizon); +} + +/* ------------------------------------------------------------ the stream */ + +let es = null; + +function connect() { + if (es) es.close(); + es = new EventSource("/live/api/stream?" + params.toString()); + es.addEventListener("snapshot", (event) => { + const s = JSON.parse(event.data); + const planningOnly = s.planning && !s.planning.has_topology; + awaiting = Boolean(s.awaiting_approval); + runEl.textContent = s.run_id ? "run " + s.run_id : ""; + goalEl.textContent = s.goal ? "· " + s.goal : ""; + if (awaiting && s.approve_command) { + approveCmd.textContent = s.approve_command; + approvalBar.hidden = false; + } else { + approvalBar.hidden = true; + } + statusEl.textContent = s.done ? "finished" + : (s.awaiting_approval ? "awaiting approval" + : (s.active ? (planningOnly ? "planning" : "running") + : (s.run_id ? "idle" : "waiting"))); + dot.className = "livedot " + (s.done ? "done" + : (s.awaiting_approval ? "awaiting" + : (s.active ? (planningOnly ? "planning" : "running") : ""))); + renderTiles(s); + renderPlanning(s.planning); + if (s.run_ids && s.run_ids.length > 1) { + runPick.hidden = false; + if (runPick.length !== s.run_ids.length) { + runPick.textContent = ""; + for (const id of s.run_ids) { + const option = document.createElement("option"); + option.value = id; + option.textContent = id; + runPick.append(option); + } + } + runPick.value = s.run_id || ""; + } + if (planningOnly && !s.done) { + // The "no graph ran" placeholder is an answer for a finished run, not + // for one whose planner is still thinking. + renderGraph(null, "planning…"); + return; + } + renderGraph(s.graph); + offerPlayback(s); + if (s.mermaid) { + srcEl.textContent = s.mermaid; + if (s.mermaid_live_url) { mlive.href = s.mermaid_live_url; mlive.hidden = false; } + } + }); + es.addEventListener("done", () => { + es.close(); + statusEl.textContent = "finished"; + dot.className = "livedot done"; + }); + es.onerror = () => { + if (es.readyState === EventSource.CONNECTING) statusEl.textContent = "reconnecting…"; + }; +} + +copyApprove.addEventListener("click", async () => { + if (!navigator.clipboard) return; // non-secure context: the text is selectable + try { + await navigator.clipboard.writeText(approveCmd.textContent); + copyApprove.textContent = "copied"; + setTimeout(() => { copyApprove.textContent = "copy"; }, 1500); + } catch { /* the command is right there to select */ } +}); + +runPick.addEventListener("change", () => { + params.set("run", runPick.value); + topologyKey = null; + connect(); +}); + +connect(); diff --git a/grapharc/slack/bot.py b/grapharc/slack/bot.py index bcebc49..85a5257 100644 --- a/grapharc/slack/bot.py +++ b/grapharc/slack/bot.py @@ -96,7 +96,8 @@ def _update(message: str) -> bool: return False settings = LiveSettings(update_interval=config.live_interval_seconds) - with LiveTail(tpath, argv, _update, settings): + view_url = live_view_url(argv, base=config.live_url_base, workdir=config.workdir) + with LiveTail(tpath, argv, _update, settings, view_url=view_url): result = run_command( argv, workdir=config.workdir, timeout_seconds=config.timeout_seconds ) @@ -113,27 +114,30 @@ def _with_final_links( config: SlackBotConfig, prior_runs: frozenset[str] = frozenset(), ) -> str: - """Keep the diagram and run-page links on the *final* message. + """Keep the run-page (or diagram) link on the *final* message. The final result edits over the live status message, which is where the "watch live" link lived — without this, finishing a run is what makes its - links disappear. Both are best-effort: a link that cannot be computed is + links disappear. The operator's own run page is the primary link; the + mermaid.live fragment link is the fallback for a bot with no live server + configured. Both are best-effort: a link that cannot be computed is simply absent. """ if tpath is None: return final lines = [final] - try: - recorder = TailRecorder(tpath) - new_runs = [r for r in recorder.run_ids() if r not in prior_runs] - if new_runs: - diagram = to_mermaid(recorder, new_runs[-1]) - lines.append(f"<{mermaid_live_url(diagram)}|final diagram>") - except Exception: - pass url = live_view_url(argv, base=config.live_url_base, workdir=config.workdir) if url: lines.append(f"run page: {url}") + else: + try: + recorder = TailRecorder(tpath) + new_runs = [r for r in recorder.run_ids() if r not in prior_runs] + if new_runs: + diagram = to_mermaid(recorder, new_runs[-1]) + lines.append(f"<{mermaid_live_url(diagram)}|final diagram>") + except Exception: + pass return "\n".join(lines) diff --git a/grapharc/slack/command.py b/grapharc/slack/command.py index c594728..75f5ed2 100644 --- a/grapharc/slack/command.py +++ b/grapharc/slack/command.py @@ -123,7 +123,7 @@ class CommandSpec: "--max-tokens": False, "--approval-timeout": False, }, - bool_flags=frozenset({"--approve"}), + bool_flags=frozenset({"--approve", "--scripted"}), model_flags=frozenset({"--model"}), choice_flags={"--registry": PLAN_REGISTRIES}, ), diff --git a/grapharc/slack/live.py b/grapharc/slack/live.py index 047913e..941d574 100644 --- a/grapharc/slack/live.py +++ b/grapharc/slack/live.py @@ -28,6 +28,7 @@ from grapharc.observe.metrics import to_mermaid from grapharc.observe.replay import NodeExecution, ReplayedRun, replay +from grapharc.observe.status import NodeState, node_states from grapharc.observe.trace import TailRecorder, TraceEvent from grapharc.slack.format import fence, mermaid_live_url, truncate @@ -80,10 +81,13 @@ def __init__( argv: list[str], update: Callable[[str], bool], settings: LiveSettings | None = None, + *, + view_url: str | None = None, ) -> None: self._path = trace_path self._argv = list(argv) self._update = update + self._view_url = view_url self._settings = settings or LiveSettings() try: self._offset = trace_path.stat().st_size @@ -147,6 +151,7 @@ def _render(self, run_id: str) -> str | None: argv=self._argv, elapsed_s=time.monotonic() - self._started_at, diagram=diagram, + view_url=self._view_url, ) def _read_new_run_id(self) -> str | None: @@ -233,6 +238,17 @@ def _sub_event_line(event: TraceEvent) -> str: _SHAPE_PHASES = frozenset({"topology", "approval_request", "approval_response"}) +def _goal(run: ReplayedRun) -> str | None: + """The goal the loop recorded on its topology/approval events, if any.""" + found: str | None = None + for event in run.events: + if event.phase in ("topology", "approval_request"): + value = (event.state_delta or {}).get("goal") + if value: + found = str(value) + return found + + def _pending_approval(run: ReplayedRun) -> TraceEvent | None: """The latest approval request no response has answered yet, if any.""" pending: TraceEvent | None = None @@ -268,37 +284,29 @@ def _planned_lines(run: ReplayedRun) -> list[str] | None: round_no = delta.get("round") lines.append(f"round {round_no}:" if round_no else f"{graph}:") graph_events = [e for e in run.events if e.graph == graph] + states = node_states(graph_events) for name in delta.get("nodes", []): - lines.append(_node_status_line(str(name), graph_events)) + lines.append(_node_status_line(str(name), states.get(str(name)))) return lines -def _node_status_line(name: str, events: list[TraceEvent]) -> str: - """One mark per declared node, from its own graph's events only.""" - starts = ends = 0 - last_end: TraceEvent | None = None - last_error: TraceEvent | None = None - for event in events: - if event.node != name: - continue - if event.phase == "start": - starts += 1 - elif event.phase == "end": - ends += 1 - last_end = event - elif event.phase == "error": - last_error = event - if last_error is not None: - detail = " ".join((last_error.error or "error").split())[:80] +def _node_status_line(name: str, state: NodeState | None) -> str: + """One mark per declared node, from the shared `observe.status` rule.""" + status = state.status if state is not None else "pending" + if status == "errored": + error = state.last_error.error if state.last_error else None + detail = " ".join((error or "error").split())[:80] return f"✗ {name} err: {detail}" - if starts > ends: + if status == "running": return f"▸ {name} running…" - if last_end is not None: + if status == "done": parts = [f"✓ {name}"] - if last_end.duration_ms is not None: - parts.append(f" {_duration(last_end.duration_ms)}") - if last_end.tokens: - parts.append(f" {last_end.tokens} tok") + last_end = state.last_end + if last_end is not None: + if last_end.duration_ms is not None: + parts.append(f" {_duration(last_end.duration_ms)}") + if last_end.tokens: + parts.append(f" {last_end.tokens} tok") return "".join(parts) return f"⬜ {name} pending" @@ -309,6 +317,7 @@ def render_progress( argv: list[str], elapsed_s: float, diagram: str | None = None, + view_url: str | None = None, ) -> str: """One Slack message describing the run so far. @@ -325,6 +334,9 @@ def render_progress( ) else: header = f"`{shlex.join(['grapharc', *argv])}` — running ({elapsed_s:.0f}s)" + goal = _goal(run) + if goal: + header += f" · {goal[:80]}" planned = _planned_lines(run) if planned is not None: @@ -362,12 +374,17 @@ def render_progress( if approval is not None: trace_arg = _trace_argument(argv) if trace_arg: + where = "live view" if view_url else "diagram" parts.append( - f"planned graph is in the diagram link — approve with " + f"planned graph is in the {where} link — approve with " f"`/grapharc approve {trace_arg}`, refuse with " f"`/grapharc approve {trace_arg} --deny`" ) - if diagram: + # The operator's own live view is the primary link; the mermaid.live + # fragment link is the fallback for a bot with no live server configured. + if view_url: + parts.append(f"<{view_url}|open live view>") + elif diagram: parts.append(f"<{mermaid_live_url(diagram)}|current diagram>") return "\n".join(parts) diff --git a/grapharc/stdlib.py b/grapharc/stdlib.py index e87fa97..2ac2476 100644 --- a/grapharc/stdlib.py +++ b/grapharc/stdlib.py @@ -143,25 +143,36 @@ class WorkState(BaseModel): } -def _collect_context(spec: Any) -> Any: - """List the workspace, so a run has somewhere to start with no model.""" - - def body(state: WorkState) -> dict: - from pathlib import Path - - root = Path.cwd() - names = sorted( - str(p.relative_to(root)) - for p in root.iterdir() - if not p.name.startswith(".") - )[:50] - found = f"workspace contains {len(names)} visible entries: {', '.join(names)}" - # Only the new item: `findings` carries an `operator.add` reducer, so - # returning the accumulated list would append it to itself. - return {"findings": [found]} - - body.writes = {"findings"} - return body +def _collect_context_for(workspace: Any = None) -> Any: + """A factory listing *the run's* workspace, so a run has somewhere to + start with no model. Parameterised rather than hard-coded to `Path.cwd()`: + a `--workspace` that moved the tools but not this listing would report a + directory the agents cannot reach — the one lie a context phase must not + tell.""" + + def factory(spec: Any) -> Any: + def body(state: WorkState) -> dict: + from pathlib import Path + + root = Path(workspace) if workspace is not None else Path.cwd() + names = sorted( + str(p.relative_to(root)) + for p in root.iterdir() + if not p.name.startswith(".") + )[:50] + found = f"workspace contains {len(names)} visible entries: {', '.join(names)}" + # Only the new item: `findings` carries an `operator.add` reducer, + # so returning the accumulated list would append it to itself. + return {"findings": [found]} + + body.writes = {"findings"} + return body + + return factory + + +#: The unparameterised form, kept for direct importers. +_collect_context = _collect_context_for() def _checkpoint(spec: Any) -> Any: @@ -266,7 +277,9 @@ def default_harness(tools: tuple[str, ...], workspace: Any = None) -> Any: return Harness(registry=registry, policy=policy, executor=LocalExecutor()) -def build_registry(model: Any = None, *, harness_for: Any = None) -> Any: +def build_registry( + model: Any = None, *, harness_for: Any = None, workspace: Any = None +) -> Any: """The shipped registry. Deterministic kinds always; agent kinds with a model. With no model the agent-backed kinds are **left out entirely** rather than @@ -274,15 +287,19 @@ def build_registry(model: Any = None, *, harness_for: Any = None) -> Any: `unknown kind` and the list of what is allowed — which tells the truth. A registered-but-bodyless kind would instead pass the gate and fail at materialisation, which is a worse error at a later moment. + + `workspace` confines the agent kinds' tools — and the `collect_context` + listing — to one directory instead of the process cwd. Ignored when the + caller supplies its own `harness_for`, which already decided that. """ from grapharc.planner import CostEstimate, NodeRegistry, NodeSpec - harness_for = harness_for or default_harness + harness_for = harness_for or (lambda tools: default_harness(tools, workspace)) specs = [ NodeSpec( name="collect_context", description="list the workspace so later phases have somewhere to start", - factory=_collect_context, + factory=_collect_context_for(workspace), worst_case=CostEstimate(iterations=1), ), NodeSpec( @@ -293,6 +310,15 @@ def build_registry(model: Any = None, *, harness_for: Any = None) -> Any: ), ] if model is not None: + # The catalog is what a planner actually chooses from, so `summarize` + # states the completion rule outright rather than leaving the model to + # infer it from failed rounds. + described = { + "summarize": ( + "write the final human-facing report; the run is complete " + "once this has run" + ), + } for kind, tokens in ( ("investigate", 4000), ("apply_change", 6000), @@ -302,7 +328,7 @@ def build_registry(model: Any = None, *, harness_for: Any = None) -> Any: specs.append( NodeSpec( name=kind, - description=_PROMPTS[kind].split(".")[0], + description=described.get(kind, _PROMPTS[kind].split(".")[0]), factory=_agent_factory(model, harness_for, kind), worst_case=CostEstimate(iterations=1, tokens=tokens), ) @@ -329,6 +355,24 @@ def catalog_for_prompt(model: Any = None) -> dict[str, str]: return build_registry(model).catalog() +#: Told to the planner verbatim. The completion rule below is deterministic +#: code the model cannot argue with; a model that was never told it burned its +#: rounds on investigate-only plans that could never finish, then stopped on +#: `no_progress` with the work done and the run reported failed. +_PLANNER_INSTRUCTIONS = ( + "The run is judged complete by deterministic code when a report lands in " + "`notes`, and only `apply_change` and `summarize` write there. " + "Investigation alone never finishes the run: end every plan with a " + "`summarize` node that takes an edge from your other phases and writes " + "the final report. A goal with several independent parts requires one " + "`investigate` node PER part — named after its part, e.g. " + "investigate_repo, investigate_docs — all taking an edge from the same " + "predecessor so they execute in parallel, with `summarize` joining " + "them. A single combined investigate node for a multi-part goal is " + "wrong: it serializes work the goal asked to run simultaneously." +) + + def goal_met(state: Any) -> bool: """Done when a report landed in `notes`. @@ -429,6 +473,7 @@ def build_loop( edge_policy=edge_policy, node_policy=node_policy, trace=trace, + instructions=_PLANNER_INSTRUCTIONS, ), checker=AdmissionChecker( registry=registry, diff --git a/tests/test_approval.py b/tests/test_approval.py index 6b7c045..59df539 100644 --- a/tests/test_approval.py +++ b/tests/test_approval.py @@ -311,7 +311,7 @@ def test_plan_approve_in_json_mode_emits_one_document(tmp_path, capsys): trace = tmp_path / "run" / "trace.jsonl" code = main( - ["plan", "ship it", "--approve", "--approval-timeout", "0.2", + ["plan", "ship it", "--scripted", "--go", "--approve", "--approval-timeout", "0.2", "--trace", str(trace), "--json"] ) captured = capsys.readouterr() @@ -327,6 +327,16 @@ def test_plan_approve_in_text_mode_still_announces_how_to_answer(tmp_path, capsy """Silencing the notice in JSON mode must not silence it for a human.""" trace = tmp_path / "run" / "trace.jsonl" - main(["plan", "ship it", "--approve", "--approval-timeout", "0.2", "--trace", str(trace)]) + main([ + "plan", + "ship it", + "--scripted", + "--go", + "--approve", + "--approval-timeout", + "0.2", + "--trace", + str(trace), + ]) assert "grapharc approve" in capsys.readouterr().out diff --git a/tests/test_cli.py b/tests/test_cli.py index 0ef548d..f3c8ac1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1144,9 +1144,14 @@ def test_the_durable_store_survives_a_real_process_boundary(tmp_path): """ -def test_plan_runs_the_governed_loop_and_reports_every_round(tmp_path, capsys): +def test_plan_runs_the_governed_loop_and_reports_every_round(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # (registry.py + grapharc.toml) must not steer these runs. + monkeypatch.chdir(tmp_path) code, out, _ = call( - ["plan", "look into the outage", "--trace", str(tmp_path / "t.jsonl")], capsys + ["plan", "look into the outage", "--scripted", "--go", "--trace", + str(tmp_path / "t.jsonl")], + capsys, ) assert code == 0 @@ -1156,7 +1161,10 @@ def test_plan_runs_the_governed_loop_and_reports_every_round(tmp_path, capsys): assert "round 2: admitted" in out -def test_plan_traces_the_plan_and_every_node_it_executed(tmp_path, capsys): +def test_plan_traces_the_plan_and_every_node_it_executed(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # (registry.py + grapharc.toml) must not steer these runs. + monkeypatch.chdir(tmp_path) """README's "the trace holds an `admission` event per round, a `round` event per round, the executed nodes' own `start`/`end` pairs and one `stop` event". @@ -1170,7 +1178,14 @@ def test_plan_traces_the_plan_and_every_node_it_executed(tmp_path, capsys): from grapharc.observe.trace import TraceRecorder path = tmp_path / "t.jsonl" - code, payload, _ = call_json(["plan", "look into the outage", "--trace", str(path)], capsys) + code, payload, _ = call_json([ + "plan", + "look into the outage", + "--scripted", + "--go", + "--trace", + str(path), + ], capsys) assert code == 0 recorder = TraceRecorder(path) @@ -1196,9 +1211,14 @@ def test_plan_traces_the_plan_and_every_node_it_executed(tmp_path, capsys): assert cost.attribute(recorder, run_id).tokens == summary.tokens -def test_plan_json_carries_the_rounds_and_the_stop_reason(tmp_path, capsys): +def test_plan_json_carries_the_rounds_and_the_stop_reason(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # (registry.py + grapharc.toml) must not steer these runs. + monkeypatch.chdir(tmp_path) code, payload, _ = call_json( - ["plan", "look into the outage", "--trace", str(tmp_path / "t.jsonl")], capsys + ["plan", "look into the outage", "--scripted", "--go", "--trace", + str(tmp_path / "t.jsonl")], + capsys, ) assert code == 0 @@ -1211,13 +1231,16 @@ def test_plan_json_carries_the_rounds_and_the_stop_reason(tmp_path, capsys): assert "deploy" in payload["kinds"], "the denied kind is registered — it is the edge that fails" -def test_a_policy_document_is_what_refuses_the_transition(tmp_path, capsys): +def test_a_policy_document_is_what_refuses_the_transition(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # (registry.py + grapharc.toml) must not steer these runs. + monkeypatch.chdir(tmp_path) """§12.2 end to end: the TOML file constrains the run, not Python.""" doc = tmp_path / "policy.toml" doc.write_text(_DENY_DEPLOY, encoding="utf-8") code, payload, _ = call_json( - ["plan", "look into the outage", "--policy", str(doc), + ["plan", "look into the outage", "--scripted", "--go", "--policy", str(doc), "--trace", str(tmp_path / "t.jsonl")], capsys, ) @@ -1227,7 +1250,10 @@ def test_a_policy_document_is_what_refuses_the_transition(tmp_path, capsys): assert str(doc) in payload["policy"] -def test_a_document_that_denies_a_node_kind_stops_it_running(tmp_path, capsys): +def test_a_document_that_denies_a_node_kind_stops_it_running(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # (registry.py + grapharc.toml) must not steer these runs. + monkeypatch.chdir(tmp_path) """Issue #66, end to end: a `resource = "node"` deny rule is enforced. The shipped script proposes `deploy` in round 1. Before the fix the whole @@ -1239,7 +1265,7 @@ def test_a_document_that_denies_a_node_kind_stops_it_running(tmp_path, capsys): doc.write_text(_DENY_DEPLOY_NODE, encoding="utf-8") code, payload, _ = call_json( - ["plan", "fix the outage", "--policy", str(doc), + ["plan", "fix the outage", "--scripted", "--go", "--policy", str(doc), "--trace", str(tmp_path / "t.jsonl")], capsys, ) @@ -1255,14 +1281,25 @@ def test_a_document_that_denies_a_node_kind_stops_it_running(tmp_path, capsys): assert "1 edge rule(s), 2 node rule(s)" in payload["policy"] -def test_a_node_denial_is_traced_with_the_reason_the_document_gave(tmp_path, capsys): +def test_a_node_denial_is_traced_with_the_reason_the_document_gave(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # (registry.py + grapharc.toml) must not steer these runs. + monkeypatch.chdir(tmp_path) from grapharc.observe.trace import TraceRecorder doc = tmp_path / "nodepolicy.toml" doc.write_text(_DENY_DEPLOY_NODE, encoding="utf-8") path = tmp_path / "t.jsonl" - call(["plan", "fix the outage", "--policy", str(doc), "--trace", str(path)], capsys) + call([ + "plan", + "fix the outage", + "--scripted", + "--policy", + str(doc), + "--trace", + str(path), + ], capsys) admissions = [e for e in TraceRecorder(path).read_events() if e.phase == "admission"] assert admissions, "the gate's decision has to be on the record" @@ -1275,7 +1312,7 @@ def test_a_permissive_document_admits_what_the_strict_one_refused(tmp_path, caps doc.write_text(_PERMISSIVE, encoding="utf-8") code, payload, _ = call_json( - ["plan", "look into the outage", "--policy", str(doc), + ["plan", "look into the outage", "--scripted", "--go", "--policy", str(doc), "--trace", str(tmp_path / "t.jsonl")], capsys, ) @@ -1289,7 +1326,7 @@ def test_a_permissive_document_admits_what_the_strict_one_refused(tmp_path, caps def test_plan_stops_short_with_a_reason_and_a_failure_code(tmp_path, capsys): """One round is not enough to reach the goal; that is a stop, not a crash.""" code, payload, _ = call_json( - ["plan", "look into the outage", "--max-rounds", "1", + ["plan", "look into the outage", "--scripted", "--max-rounds", "1", "--trace", str(tmp_path / "t.jsonl")], capsys, ) @@ -1301,7 +1338,15 @@ def test_plan_stops_short_with_a_reason_and_a_failure_code(tmp_path, capsys): def test_plan_refuses_a_registry_that_does_not_resolve(tmp_path, capsys): code, _, err = call( - ["plan", "x", "--registry", "no.such.module:thing", "--trace", str(tmp_path / "t.jsonl")], + [ + "plan", + "x", + "--scripted", + "--registry", + "no.such.module:thing", + "--trace", + str(tmp_path / "t.jsonl"), + ], capsys, ) @@ -1311,7 +1356,15 @@ def test_plan_refuses_a_registry_that_does_not_resolve(tmp_path, capsys): def test_plan_refuses_a_registry_target_with_no_colon(tmp_path, capsys): code, _, err = call( - ["plan", "x", "--registry", "grapharc.examples", "--trace", str(tmp_path / "t.jsonl")], + [ + "plan", + "x", + "--scripted", + "--registry", + "grapharc.examples", + "--trace", + str(tmp_path / "t.jsonl"), + ], capsys, ) @@ -1321,7 +1374,7 @@ def test_plan_refuses_a_registry_target_with_no_colon(tmp_path, capsys): def test_plan_refuses_a_policy_file_that_is_not_there(tmp_path, capsys): code, _, err = call( - ["plan", "x", "--policy", str(tmp_path / "nope.toml"), + ["plan", "x", "--scripted", "--policy", str(tmp_path / "nope.toml"), "--trace", str(tmp_path / "t.jsonl")], capsys, ) @@ -1332,7 +1385,15 @@ def test_plan_refuses_a_policy_file_that_is_not_there(tmp_path, capsys): def test_every_plan_round_is_auditable_from_the_trace_alone(tmp_path, capsys): trace_path = tmp_path / "t.jsonl" - call(["plan", "look into the outage", "--trace", str(trace_path), "--run-id", "p1"], capsys) + call([ + "plan", + "look into the outage", + "--scripted", + "--trace", + str(trace_path), + "--run-id", + "p1", + ], capsys) events = TraceRecorder(trace_path).read_events("p1") phases = [e.phase for e in events] @@ -1412,7 +1473,10 @@ def _write_graph(tmp_path, document, name="graph.json"): return path -def test_run_executes_a_topology_the_operator_wrote(tmp_path, capsys): +def test_run_executes_a_topology_the_operator_wrote(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # must not steer these runs. + monkeypatch.chdir(tmp_path) graph = _write_graph(tmp_path, _LEGAL_GRAPH) code, payload, _ = call_json( @@ -1426,7 +1490,10 @@ def test_run_executes_a_topology_the_operator_wrote(tmp_path, capsys): assert payload["state"]["notes"] == ["triage ran", "fix ran", "verify ran"] -def test_a_hand_written_graph_is_refused_like_any_other_proposal(tmp_path, capsys): +def test_a_hand_written_graph_is_refused_like_any_other_proposal(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # must not steer these runs. + monkeypatch.chdir(tmp_path) """The gate does not care who authored the topology.""" graph = _write_graph(tmp_path, _DENIED_GRAPH) @@ -1441,7 +1508,10 @@ def test_a_hand_written_graph_is_refused_like_any_other_proposal(tmp_path, capsy assert "state" not in payload, "nothing may run when the gate refused" -def test_check_only_validates_without_executing(tmp_path, capsys): +def test_check_only_validates_without_executing(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # must not steer these runs. + monkeypatch.chdir(tmp_path) """Admission as a linter: legal or not, and nothing runs either way.""" graph = _write_graph(tmp_path, _LEGAL_GRAPH) @@ -1455,7 +1525,10 @@ def test_check_only_validates_without_executing(tmp_path, capsys): assert "state" not in payload -def test_check_only_still_fails_on_an_illegal_topology(tmp_path, capsys): +def test_check_only_still_fails_on_an_illegal_topology(tmp_path, capsys, monkeypatch): + # A scratch cwd: a developer's own `grapharc init` in the checkout + # must not steer these runs. + monkeypatch.chdir(tmp_path) graph = _write_graph(tmp_path, _DENIED_GRAPH) code, payload, _ = call_json( @@ -1466,7 +1539,8 @@ def test_check_only_still_fails_on_an_illegal_topology(tmp_path, capsys): assert [r["code"] for r in payload["rejections"]] == ["edge_denied"] -def test_a_toml_topology_works_the_same_as_json(tmp_path, capsys): +def test_a_toml_topology_works_the_same_as_json(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this graph = tmp_path / "graph.toml" graph.write_text( '[[nodes]]\nname = "triage"\n\n' @@ -1532,7 +1606,8 @@ def test_run_reports_a_graph_file_that_is_not_utf8(tmp_path, capsys): assert err == "" -def test_a_policy_document_gates_a_hand_written_graph_too(tmp_path, capsys): +def test_a_policy_document_gates_a_hand_written_graph_too(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """§12.2 on the deterministic path: the TOML file decides here as well.""" graph = _write_graph(tmp_path, _DENIED_GRAPH) permissive = tmp_path / "allow.toml" @@ -1555,7 +1630,8 @@ def test_a_policy_document_gates_a_hand_written_graph_too(tmp_path, capsys): # -------------------------------------------------------------------------- -def test_run_records_the_execution_it_says_it_performed(tmp_path, capsys): +def test_run_records_the_execution_it_says_it_performed(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """It reported "ADMITTED and executed" and wrote only the admission event — `Materializer` takes a `trace=` and the call omitted it.""" graph = _write_graph(tmp_path, _LEGAL_GRAPH) @@ -1568,7 +1644,8 @@ def test_run_records_the_execution_it_says_it_performed(tmp_path, capsys): assert "admission" in phases -def test_run_honours_the_run_id_it_was_given(tmp_path, capsys): +def test_run_honours_the_run_id_it_was_given(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """`--run-id` was accepted and discarded, so `metrics ` found nothing under the id the operator chose.""" graph = _write_graph(tmp_path, _LEGAL_GRAPH) @@ -1590,12 +1667,20 @@ def test_a_reused_run_id_is_refused_before_the_second_run_writes_anything(tmp_pa one run: doubled tokens from `metrics`, a welded path from `viz`.""" trace = tmp_path / "t.jsonl" first, _, _ = call( - ["plan", "goal one", "--trace", str(trace), "--run-id", "r1"], capsys + ["plan", "goal one", "--scripted", "--trace", str(trace), "--run-id", "r1"], capsys ) assert first == 0 before = TraceRecorder(trace).read_events("r1") - code, _, err = call(["plan", "goal two", "--trace", str(trace), "--run-id", "r1"], capsys) + code, _, err = call([ + "plan", + "goal two", + "--scripted", + "--trace", + str(trace), + "--run-id", + "r1", + ], capsys) assert code == 2 assert "r1" in err and str(trace) in err @@ -1606,10 +1691,10 @@ def test_a_reused_run_id_is_refused_before_the_second_run_writes_anything(tmp_pa def test_a_reused_run_id_fails_as_one_json_document(tmp_path, capsys): trace = tmp_path / "t.jsonl" - call(["plan", "goal one", "--trace", str(trace), "--run-id", "r1"], capsys) + call(["plan", "goal one", "--scripted", "--trace", str(trace), "--run-id", "r1"], capsys) code, payload, err = call_json( - ["plan", "goal two", "--trace", str(trace), "--run-id", "r1"], capsys + ["plan", "goal two", "--scripted", "--trace", str(trace), "--run-id", "r1"], capsys ) assert code == 2 @@ -1659,8 +1744,24 @@ def test_different_run_ids_in_one_trace_stay_supported(tmp_path, capsys): reused is the defect.""" trace = tmp_path / "t.jsonl" - assert call(["plan", "goal one", "--trace", str(trace), "--run-id", "r1"], capsys)[0] == 0 - assert call(["plan", "goal two", "--trace", str(trace), "--run-id", "r2"], capsys)[0] == 0 + assert call([ + "plan", + "goal one", + "--scripted", + "--trace", + str(trace), + "--run-id", + "r1", + ], capsys)[0] == 0 + assert call([ + "plan", + "goal two", + "--scripted", + "--trace", + str(trace), + "--run-id", + "r2", + ], capsys)[0] == 0 assert TraceRecorder(trace).run_ids() == ["r1", "r2"] @@ -1669,8 +1770,8 @@ def test_a_generated_run_id_is_never_guarded(tmp_path, capsys): """Fresh by construction, so it must not pay for a scan of the file either.""" trace = tmp_path / "t.jsonl" - assert call(["plan", "goal one", "--trace", str(trace)], capsys)[0] == 0 - assert call(["plan", "goal two", "--trace", str(trace)], capsys)[0] == 0 + assert call(["plan", "goal one", "--scripted", "--trace", str(trace)], capsys)[0] == 0 + assert call(["plan", "goal two", "--scripted", "--trace", str(trace)], capsys)[0] == 0 assert len(TraceRecorder(trace).run_ids()) == 2 @@ -1697,7 +1798,8 @@ def test_the_guard_counts_a_line_it_can_read_and_skips_the_rest(tmp_path): def test_check_only_refuses_a_topology_that_passes_the_gate_but_cannot_be_built( tmp_path, capsys -): +, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """A linter that says ADMITTED and then crashes on the same file is worse than no linter. All three of these pass admission and fail materialisation.""" unbuildable = { @@ -1718,7 +1820,10 @@ def test_check_only_refuses_a_topology_that_passes_the_gate_but_cannot_be_built( assert payload["error"], label -def test_a_topology_that_cannot_be_built_reports_rather_than_crashing(tmp_path, capsys): +def test_a_topology_that_cannot_be_built_reports_rather_than_crashing( + tmp_path, capsys, monkeypatch +): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """`MaterializationError` escaped `run_graph` as a raw traceback, leaving stdout empty in --json mode.""" graph = _write_graph(tmp_path, {"nodes": [{"name": "triage"}], "edges": []}) @@ -1732,7 +1837,8 @@ def test_a_topology_that_cannot_be_built_reports_rather_than_crashing(tmp_path, assert "has no edge out of START" in payload["error"] -def test_run_can_be_given_a_token_ceiling(tmp_path, capsys): +def test_run_can_be_given_a_token_ceiling(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """A comment claimed the budget dimension was bounded; `Budget()` is unlimited on every dimension, so a 400,000-token worst case was admitted.""" chain = { @@ -1756,7 +1862,8 @@ def test_run_can_be_given_a_token_ceiling(tmp_path, capsys): -def test_run_budget_flags_reach_json_payload(tmp_path, capsys): +def test_run_budget_flags_reach_json_payload(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this """All four Budget dimensions are settable from `grapharc run` (#5).""" graph = _write_graph(tmp_path, _LEGAL_GRAPH) code, payload, _ = call_json( @@ -1784,7 +1891,8 @@ def test_run_budget_flags_reach_json_payload(tmp_path, capsys): assert payload["max_concurrency"] == 2 -def test_run_unset_budget_flags_stay_unlimited_in_json(tmp_path, capsys): +def test_run_unset_budget_flags_stay_unlimited_in_json(tmp_path, capsys, monkeypatch): + monkeypatch.chdir(tmp_path) # a checkout-level grapharc.toml must not steer this graph = _write_graph(tmp_path, _LEGAL_GRAPH) code, payload, _ = call_json( ["run", str(graph), "--check-only", "--trace", str(tmp_path / "t.jsonl")], @@ -1834,9 +1942,400 @@ def test_an_unusable_memory_path_reports_rather_than_crashing(tmp_path, capsys): def test_config_is_only_accepted_by_commands_that_read_it(capsys): """It was on the shared parser, so all eleven accepted it and nine ignored it — including erroring on a missing file for two of them and not the rest.""" - for command in (["plan", "g"], ["demo", "stage0"], ["run", "x.json"]): + for command in (["plan", "g", "--scripted"], ["demo", "stage0"], ["run", "x.json"]): code, _, err = call([*command, "--config", "/no/such/file.toml"], capsys) assert code == 2 and "--config" in err, command for command in (["models"], ["trace", "f"], ["viz", "f", "r"]): with pytest.raises(SystemExit): main([*command, "--config", "/no/such/file.toml"]) + + +# --------------------------------------------------------------------------- +# The default trace lands under the live root, and the watch line finds it. +# --------------------------------------------------------------------------- + + +def test_the_default_trace_lands_under_grapharc_runs(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + code = main(["plan", "look into it", "--scripted", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + trace = Path(payload["trace"]).resolve() + assert (tmp_path / ".grapharc" / "runs").resolve() in trace.parents + assert trace.name == "trace.jsonl" + assert trace.is_file() + + +def test_without_a_server_the_watch_line_is_an_instruction(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + code = main(["plan", "look into it", "--scripted"]) + printed = capsys.readouterr().out + assert code == 0 + watch = next(line for line in printed.splitlines() if line.startswith("watch")) + assert "grapharc serve --live-root .grapharc/runs" in watch + assert "http://127.0.0.1:8000/live/view?trace=" in watch + + +def _write_live_marker(tmp_path, *, host="127.0.0.1", port=0): + import json as _json + + marker = tmp_path / ".grapharc" / "live-server.json" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text( + _json.dumps( + { + "url": f"http://{host}:{port}", + "host": host, + "port": port, + "live_root": str((tmp_path / ".grapharc" / "runs").resolve()), + "pid": 999999, + } + ), + encoding="utf-8", + ) + return marker + + +def test_a_reachable_server_yields_the_exact_watch_url(tmp_path, monkeypatch, capsys): + import socket + + monkeypatch.chdir(tmp_path) + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + listener.listen(1) + port = listener.getsockname()[1] + try: + _write_live_marker(tmp_path, port=port) + code = main(["plan", "look into it", "--scripted", "--json"]) + payload = json.loads(capsys.readouterr().out) + finally: + listener.close() + assert code == 0 + url = payload["watch_url"] + assert url is not None + assert url.startswith(f"http://127.0.0.1:{port}/live/view?trace=") + # The trace path in the URL is relative to the marker's live root. + from urllib.parse import unquote + + rel = unquote(url.split("trace=", 1)[1]) + assert (tmp_path / ".grapharc" / "runs" / rel).is_file() + + +def test_a_stale_marker_with_no_listener_falls_back_to_the_hint( + tmp_path, monkeypatch, capsys +): + import socket + + monkeypatch.chdir(tmp_path) + # A port that was just released: nothing is listening on it. + probe = socket.socket() + probe.bind(("127.0.0.1", 0)) + dead_port = probe.getsockname()[1] + probe.close() + _write_live_marker(tmp_path, port=dead_port) + code = main(["plan", "look into it", "--scripted", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert payload["watch_url"] is None + + +def test_a_trace_outside_the_live_root_gets_no_exact_url(tmp_path, monkeypatch, capsys): + import socket + + monkeypatch.chdir(tmp_path) + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + listener.listen(1) + try: + _write_live_marker(tmp_path, port=listener.getsockname()[1]) + elsewhere = tmp_path / "elsewhere" / "trace.jsonl" + code = main(["plan", "look into it", "--scripted", "--trace", str(elsewhere), "--json"]) + payload = json.loads(capsys.readouterr().out) + finally: + listener.close() + assert code == 0 + assert payload["watch_url"] is None + + +def test_serve_writes_a_discovery_marker_and_removes_it( + monkeypatch, capsys, tmp_path +): + """`plan`/`go` find the live server through this marker; it must exist + while the server runs, carry no token, and be gone afterwards.""" + record: dict = {} + stub = ModuleType("grapharc.server") + + def create_app(**kwargs): + record["create_app"] = kwargs + return "THE-APP" + + def running_serve(app, **kwargs): + marker = Path(".grapharc") / "live-server.json" + record["marker_during"] = json.loads(marker.read_text(encoding="utf-8")) + record["marker_bytes"] = marker.read_bytes() + + stub.create_app = create_app + stub.serve = running_serve + monkeypatch.setitem(sys.modules, "grapharc.server", stub) + monkeypatch.chdir(tmp_path) + root = tmp_path / "runs" + root.mkdir() + + code, _, _ = call( + ["serve", "--live-root", str(root), "--live-token", "s3cret"], capsys + ) + assert code == 0 + marker = record["marker_during"] + assert marker["url"] == "http://127.0.0.1:8000" + assert marker["live_root"] == str(root.resolve()) + assert marker["pid"] == __import__("os").getpid() + assert b"s3cret" not in record["marker_bytes"] + # Unlinked on the way out. + assert not (tmp_path / ".grapharc" / "live-server.json").exists() + + +def test_serve_without_a_live_root_writes_no_marker(monkeypatch, capsys, tmp_path): + record: dict = {} + monkeypatch.setitem(sys.modules, "grapharc.server", _server_stub(record)) + monkeypatch.chdir(tmp_path) + code, _, _ = call(["serve"], capsys) + assert code == 0 + assert not (tmp_path / ".grapharc").exists() + + +# -- go: plan with doing-defaults --------------------------------------------- + + +def test_go_requires_a_model(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + code = main(["go", "get it done", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 2 + assert payload["ok"] is False + assert "models --check" in payload["error"] + + +def test_go_defaults_to_the_stdlib_registry(tmp_path, monkeypatch, capsys): + """With a model spec that cannot construct, the failure message proves the + resolution order: the model is reached before any registry import.""" + monkeypatch.chdir(tmp_path) + recorded = {} + + def fake_plan(goal, **kwargs): + recorded.update(kwargs, goal=goal) + return 0 + + import grapharc.cli.plan as plan_module + + monkeypatch.setattr(plan_module, "plan", fake_plan) + code = main(["go", "get it done", "--model", "ollama/fake"]) + assert code == 0 + assert recorded["command"] == "go" + assert recorded["go_after"] is True, "go executes; plan-only is plan's job" + assert recorded["model_spec"] == "ollama/fake" + + +def test_go_and_plan_share_every_planning_flag(): + parser = build_parser() + plan_actions = { + a.option_strings[0] + for a in parser._subparsers._group_actions[0].choices["plan"]._actions + if a.option_strings + } + go_actions = { + a.option_strings[0] + for a in parser._subparsers._group_actions[0].choices["go"]._actions + if a.option_strings + } + # `--scripted` and `--go` are plan-only by design: go means do (no + # scripted doing), and go needs no flag to do what its name says. + assert plan_actions - go_actions == {"--scripted", "--go"} + assert go_actions - plan_actions == set() + + +# -- init: the scaffold ------------------------------------------------------- + + +def test_init_scaffolds_a_working_directory(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + code = main(["init", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert (tmp_path / "registry.py").is_file() + assert (tmp_path / "grapharc.toml").is_file() + assert (tmp_path / ".grapharc" / "runs").is_dir() + assert payload["registry"] == "registry.py" + # The template compiles and the config parses. + compile((tmp_path / "registry.py").read_text(encoding="utf-8"), "registry.py", "exec") + from grapharc.cli.config import load + + assert load(tmp_path / "grapharc.toml").values["registry"] == "registry.py:build_registry" + + +def test_init_refuses_to_overwrite_and_names_the_files(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + assert main(["init"]) == 0 + capsys.readouterr() + code = main(["init"]) + err = capsys.readouterr().err + assert code == 2 + assert "registry.py" in err and "grapharc.toml" in err + # The runs dir alone never blocks a scaffold. + assert (tmp_path / ".grapharc" / "runs").is_dir() + + +def test_an_init_scaffold_plans_end_to_end(tmp_path, monkeypatch, capsys): + """The money test: template + path-form loader + build_loop handoff + + scripted replies compose into the refuse-then-admit first run.""" + monkeypatch.chdir(tmp_path) + assert main(["init"]) == 0 + capsys.readouterr() + code = main(["plan", "try it", "--scripted", "--go"]) + printed = capsys.readouterr().out + assert code == 0 + assert "round 1: rejected" in printed and "edge_denied" in printed + assert "round 2: admitted" in printed + assert "goal_met" in printed + + +def test_the_path_form_registry_shares_one_module_object(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + (tmp_path / "reg.py").write_text( + "COUNTER = []\n" + "def build_registry(model=None):\n" + " from grapharc.planner import NodeRegistry\n" + " COUNTER.append(1)\n" + " return NodeRegistry([])\n", + encoding="utf-8", + ) + from grapharc.cli.plan import _registry_module + + first, attr = _registry_module("reg.py:build_registry") + second, _ = _registry_module("reg.py:build_registry") + assert first is second + assert attr == "build_registry" + + +def test_a_missing_registry_file_exits_2_naming_the_path(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + code = main(["plan", "x", "--scripted", "--registry", "./nope.py:build_registry", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 2 + assert "no such file" in payload["error"] + assert "nope.py" in payload["error"] + + +# -- start and the bare invocation -------------------------------------------- + + +def test_bare_invocation_orients_and_exits_zero(capsys): + code = main([]) + out, err = capsys.readouterr().out, capsys.readouterr().err + assert code == 0 + assert "grapharc start" in out + assert err == "" + + +def test_start_prints_the_guided_path(capsys): + code = main(["start"]) + out = capsys.readouterr().out + assert code == 0 + for expected in ("init", "serve --live-root", "plan", "go", "approve", "watch"): + assert expected in out, expected + + +def test_start_json_is_one_document(capsys): + code = main(["start", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert payload["ok"] is True + assert payload["command"] == "start" + assert any("init" in step["command"] for step in payload["path"]) + + +def test_help_is_a_command_not_an_error(capsys): + """`grapharc help` is what people type; it must be `-h`, not an argparse + scolding.""" + code = main(["help"]) + out = capsys.readouterr().out + assert code == 0 + assert "usage: grapharc" in out + assert "plan" in out and "go" in out + + +# -- plan plans; go goes ------------------------------------------------------ + + +def test_plan_plans_only_and_saves_the_plan(tmp_path, monkeypatch, capsys): + """`plan` executes nothing: the gate runs, the plan lands on disk, and + the nodes wait for `go`.""" + monkeypatch.chdir(tmp_path) + code = main(["plan", "look into it", "--scripted", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert payload["ok"] is True + assert payload["stop"] == "planned" + assert all(r["executed"] is False for r in payload["rounds"]) + plan_file = Path(payload["plan_file"]) + record = json.loads(plan_file.read_text(encoding="utf-8")) + assert record["goal"] == "look into it" + assert record["proposal"]["nodes"], "the admitted proposal is stored whole" + + +def test_go_executes_the_newest_saved_plan_and_marks_it(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + main(["plan", "look into it", "--scripted", "--json"]) + planned = json.loads(capsys.readouterr().out) + code = main(["go", "--json"]) + executed = json.loads(capsys.readouterr().out) + assert code == 0 + assert executed["executed"] is True + assert executed["stop"] == "goal_met" + record = json.loads(Path(planned["plan_file"]).read_text(encoding="utf-8")) + assert record["executed_run_id"] == executed["run_id"] + # And a second bare `go` finds nothing left to do. + assert main(["go", "--json"]) == 1 + assert json.loads(capsys.readouterr().out)["ok"] is False + + +def test_go_takes_a_specific_run_directory(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + main(["plan", "look into it", "--scripted", "--json"]) + planned = json.loads(capsys.readouterr().out) + run_dir = str(Path(planned["plan_file"]).parent) + code = main(["go", run_dir, "--json"]) + executed = json.loads(capsys.readouterr().out) + assert code == 0 + assert executed["plan"].startswith(run_dir) + + +def test_plan_go_is_one_shot(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + code = main(["plan", "look into it", "--scripted", "--go", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert payload["stop"] == "goal_met" + assert any(r["executed"] for r in payload["rounds"]) + + +def test_a_registry_py_in_cwd_wins_when_nothing_is_configured( + tmp_path, monkeypatch, capsys +): + monkeypatch.chdir(tmp_path) + assert main(["init"]) == 0 + (tmp_path / "grapharc.toml").unlink() # no config: detection must carry it + capsys.readouterr() + code = main(["plan", "look into it", "--scripted", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert payload["registry"] == "registry.py:build_registry" + + +def test_default_flag_forces_the_builtin_kinds(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + assert main(["init"]) == 0 + capsys.readouterr() + code = main(["plan", "look into it", "--scripted", "--default", "--json"]) + payload = json.loads(capsys.readouterr().out) + assert code == 0 + assert payload["registry"] == "grapharc.stdlib:build_registry" diff --git a/tests/test_cli_style.py b/tests/test_cli_style.py index bfc15e1..331bfd0 100644 --- a/tests/test_cli_style.py +++ b/tests/test_cli_style.py @@ -34,7 +34,7 @@ # here: `models --check` exits 1 when the host can reach no real provider, which # is correct and is what a machine with no credentials does. STYLED = [ - pytest.param(["plan", "investigate the checkout outage"], id="plan"), + pytest.param(["plan", "investigate the checkout outage", "--scripted"], id="plan"), pytest.param(["models"], id="models"), pytest.param(["models", "--check"], id="models-check"), pytest.param(["demo", "stage0"], id="demo-stage0"), @@ -55,6 +55,9 @@ # sets TMPDIR elsewhere would otherwise leave the paths unnormalised and the # comparison below would fail for a reason that has nothing to do with styling. _TMPDIR = re.compile(re.escape(tempfile.gettempdir()) + r"/grapharc-[A-Za-z0-9_.-]+") +# The default trace directory stamp: two invocations of one command are two +# runs with two stamps, and the comparison is about styling, not clocks. +_RUNDIR = re.compile(r"\d{8}-\d{6}-[0-9a-f]{6}") def _env(**extra: str) -> dict[str, str]: @@ -123,7 +126,8 @@ def _on_pty(args: list[str], **extra: str) -> tuple[str, int]: def _normalise(text: str) -> str: - return _TMPDIR.sub("/tmp/grapharc-NORMALISED", text) + text = _TMPDIR.sub("/tmp/grapharc-NORMALISED", text) + return _RUNDIR.sub("RUNDIR-NORMALISED", text) @pytest.mark.parametrize("args", STYLED) @@ -159,10 +163,10 @@ def test_stripping_the_escapes_reproduces_the_piped_output_exactly(args): @pytest.mark.parametrize( ("label", "args", "extra"), [ - ("NO_COLOR", ["plan", "x"], {"NO_COLOR": "1"}), - ("TERM=dumb", ["plan", "x"], {"TERM": "dumb"}), - ("--no-color", ["plan", "x", "--no-color"], {}), - ("--json", ["plan", "x", "--json"], {}), + ("NO_COLOR", ["plan", "x", "--scripted"], {"NO_COLOR": "1"}), + ("TERM=dumb", ["plan", "x", "--scripted"], {"TERM": "dumb"}), + ("--no-color", ["plan", "x", "--scripted", "--no-color"], {}), + ("--json", ["plan", "x", "--scripted", "--json"], {}), ], ) def test_every_opt_out_silences_styling_even_on_a_terminal(label, args, extra): @@ -180,11 +184,11 @@ def test_json_on_a_terminal_is_still_one_clean_document(): """ import json - out, err, _ = _piped(["plan", "x", "--json"]) + out, err, _ = _piped(["plan", "x", "--scripted", "--json"]) assert err == "" assert json.loads(out)["ok"] is True - on_pty, _ = _on_pty(["plan", "x", "--json"]) + on_pty, _ = _on_pty(["plan", "x", "--scripted", "--json"]) assert "\x1b" not in on_pty assert json.loads(on_pty)["ok"] is True diff --git a/tests/test_config.py b/tests/test_config.py index 1f16172..079ccac 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -47,7 +47,7 @@ def project(tmp_path, monkeypatch): def _plan_payload(capsys, *args): - code = main(["plan", "a goal", "--json", *args]) + code = main(["plan", "a goal", "--scripted", "--json", *args]) return code, json.loads(capsys.readouterr().out) @@ -350,9 +350,14 @@ def test_sources_agrees_with_policy_source(tmp_path, monkeypatch, capsys): `default`. A reviewer reading `sources` alone would conclude no file was involved.""" monkeypatch.chdir(tmp_path) - cached = tmp_path / ".grapharc" - cached.mkdir() - (cached / "generated-policy.toml").write_text(DENY_DEPLOY, encoding="utf-8") + from grapharc.cli.generate import generated_policy_path + from grapharc.cli.plan import DEFAULT_REGISTRY + + # Keyed to the registry the run will use: an un-keyed file is a statement + # about an unknown registry and is deliberately ignored now. + cached = generated_policy_path(tmp_path, registry=DEFAULT_REGISTRY) + cached.parent.mkdir() + cached.write_text(DENY_DEPLOY, encoding="utf-8") _, payload = _plan_payload(capsys) diff --git a/tests/test_generate.py b/tests/test_generate.py index b4f2610..1cd06f0 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -103,7 +103,7 @@ def test_the_command_puts_the_source_in_its_payload(tmp_path, monkeypatch, capsy """The trace is what an incident review reads; a banner is seen once.""" monkeypatch.chdir(tmp_path) - main(["plan", "look into it", "--json"]) + main(["plan", "look into it", "--scripted", "--json"]) payload = json.loads(capsys.readouterr().out) assert "policy_source" in payload @@ -119,7 +119,7 @@ def test_the_command_puts_the_source_in_its_payload(tmp_path, monkeypatch, capsy def test_the_human_view_names_the_source_too(tmp_path, monkeypatch, capsys): monkeypatch.chdir(tmp_path) - main(["plan", "look into it"]) + main(["plan", "look into it", "--scripted"]) out = capsys.readouterr().out assert "policy :" in out @@ -273,11 +273,14 @@ def test_a_scripted_run_never_generates(tmp_path, monkeypatch, capsys): that produced a different policy each run would not be one.""" monkeypatch.chdir(tmp_path) - main(["plan", "look into it", "--json"]) + main(["plan", "look into it", "--scripted", "--json"]) payload = json.loads(capsys.readouterr().out) assert payload["policy_source"] != "generated" - assert not (tmp_path / GENERATED_DIR).exists() + # `.grapharc/` itself now exists (the default trace lands under runs/); + # what a scripted run must never do is write a *policy* there. + generated = list((tmp_path / GENERATED_DIR).glob("generated-policy*.toml")) + assert generated == [] # -- the description is derived, not written --------------------------------- @@ -316,3 +319,72 @@ def test_a_description_names_every_tier_that_has_rules(): assert "deny -> deploy" in described assert "ask -> patch" in described assert "otherwise deny" in described + + +# -- the cache is keyed by the registry that generated it --------------------- + + +STALE_INCIDENT_POLICY = """\ +version = "1" +default = "allow" + +[[rule]] +id = "deny-deploy" +resource = "edge" +match = "*->deploy" +effect = "deny" +reason = "generated for the incident registry, knows nothing of stdlib" +""" + + +def test_a_cached_policy_is_keyed_to_the_registry_that_generated_it(tmp_path): + first = _generate(tmp_path, registry_target="pkg.one:build_registry") + keyed = generated_policy_path(tmp_path, registry="pkg.one:build_registry") + assert keyed.is_file() + assert "pkg.one-build_registry" in keyed.name + # A different registry's run does not read it. + _, _, source = _generate(tmp_path, registry_target="pkg.two:build_registry") + assert source == "generated" + assert generated_policy_path(tmp_path, registry="pkg.two:build_registry").is_file() + # The same registry's second run does. + _, _, source = _generate(tmp_path, registry_target="pkg.one:build_registry") + assert source == "generated-cached" + assert first # the first result existed; silences the unused warning + + +def test_a_stale_incident_policy_cannot_neuter_the_stdlib_mutating_deny(tmp_path): + """The proof test for the reproduced safety bug: an un-keyed cache file + generated for another registry must not override this registry's own + default deny of its mutating kind.""" + from grapharc.harness.permissions import Decision + + legacy = generated_policy_path(tmp_path) + legacy.parent.mkdir(parents=True, exist_ok=True) + legacy.write_text(STALE_INCIDENT_POLICY, encoding="utf-8") + + policy, description, source = resolve_or_generate_policy( + None, + tenant="default", + model=None, # scripted run: no generation, fallback must win + workdir=tmp_path, + fallback=stdlib.default_edge_policy(), + fallback_label="grapharc.stdlib:build_registry default", + registry_target="grapharc.stdlib:build_registry", + ) + assert source == "registry-default" + assert policy.edge.decide("investigate", "apply_change") is Decision.DENY + assert "ignoring un-keyed" in description + # The operator's file survives untouched for an explicit --policy. + assert legacy.read_text(encoding="utf-8") == STALE_INCIDENT_POLICY + + +def test_a_legacy_unkeyed_cache_is_still_honoured_without_a_registry_target(tmp_path): + """Direct callers that never pass a target keep the old behavior — the + refusal is scoped to runs that *can* say which registry they are.""" + legacy = generated_policy_path(tmp_path) + legacy.parent.mkdir(parents=True, exist_ok=True) + legacy.write_text(STALE_INCIDENT_POLICY, encoding="utf-8") + _, _, source = resolve_or_generate_policy( + None, tenant="default", workdir=tmp_path + ) + assert source == "generated-cached" diff --git a/tests/test_graph_layout.py b/tests/test_graph_layout.py new file mode 100644 index 0000000..38a9fa2 --- /dev/null +++ b/tests/test_graph_layout.py @@ -0,0 +1,129 @@ +"""The server-side layout: deterministic, downward, and cycle-safe. + +The layout runs inside the live server's snapshot thread, so the properties +under test are load-bearing: identical input must give identical geometry +(the page patches in place on that promise), and a cyclic topology must +terminate rather than hang the thread. +""" + +from __future__ import annotations + +from grapharc.observe.layout import layout_graph +from grapharc.observe.viewmodel import ClusterView, EdgeView, GraphSnapshot, NodeView + + +def _diamond(status: str = "pending") -> GraphSnapshot: + return GraphSnapshot( + kind="topology", + nodes=[ + NodeView(id="a", label="a", status=status), + NodeView(id="b", label="b"), + NodeView(id="c", label="c"), + NodeView(id="d", label="d"), + ], + edges=[ + EdgeView(source="a", target="b"), + EdgeView(source="a", target="c"), + EdgeView(source="b", target="d"), + EdgeView(source="c", target="d"), + ], + ) + + +def _geometry(snapshot: GraphSnapshot) -> list[tuple]: + return [(n.id, n.x, n.y, n.w, n.h) for n in snapshot.nodes] + [ + (e.source, e.target, tuple(map(tuple, e.points))) for e in snapshot.edges + ] + + +def test_identical_input_gives_identical_geometry(): + assert _geometry(layout_graph(_diamond())) == _geometry(layout_graph(_diamond())) + + +def test_geometry_is_stable_when_only_statuses_change(): + # The flicker-free contract: a status flip must not move a single node. + pending = layout_graph(_diamond()) + running = layout_graph(_diamond(status="running")) + assert [(n.id, n.x, n.y) for n in pending.nodes] == [ + (n.id, n.x, n.y) for n in running.nodes + ] + + +def test_forward_edges_rank_downward(): + snapshot = layout_graph(_diamond()) + y = {n.id: n.y for n in snapshot.nodes} + assert y["a"] < y["b"] == y["c"] < y["d"] + + +def test_no_overlap_within_a_rank(): + snapshot = layout_graph(_diamond()) + b, c = (next(n for n in snapshot.nodes if n.id == i) for i in ("b", "c")) + left, right = (b, c) if b.x < c.x else (c, b) + assert left.x + left.w <= right.x + + +def test_cyclic_topology_terminates_and_marks_the_back_edge(): + snapshot = GraphSnapshot( + kind="topology", + nodes=[NodeView(id=i, label=i) for i in ("extract", "verify", "retry")], + edges=[ + EdgeView(source="extract", target="verify"), + EdgeView(source="verify", target="retry"), + EdgeView(source="retry", target="extract"), # the loop back + ], + ) + laid = layout_graph(snapshot) # must return, not hang + y = {n.id: n.y for n in laid.nodes} + assert y["extract"] < y["verify"] < y["retry"] + # The back-edge routed as a side arc: strictly right of the block's nodes. + back = next(e for e in laid.edges if (e.source, e.target) == ("retry", "extract")) + rightmost = max(n.x + n.w for n in laid.nodes) + assert max(x for x, _ in back.points) > rightmost + + +def test_self_loop_terminates_and_bulges_off_the_node(): + snapshot = GraphSnapshot( + kind="topology", + nodes=[NodeView(id="loop", label="loop")], + edges=[EdgeView(source="loop", target="loop")], + ) + laid = layout_graph(snapshot) + node = laid.nodes[0] + assert max(x for x, _ in laid.edges[0].points) > node.x + node.w + + +def test_clusters_stack_vertically_and_contain_their_nodes(): + snapshot = GraphSnapshot( + kind="topology", + clusters=[ + ClusterView(id="g0", label="round 1", round=1), + ClusterView(id="g1", label="round 2", round=2), + ], + nodes=[ + NodeView(id="g0.work", label="work", cluster="g0"), + NodeView(id="g1.work", label="work", cluster="g1"), + ], + edges=[EdgeView(source="g0", target="g1", kind="state")], + ) + laid = layout_graph(snapshot) + first, second = laid.clusters + assert first.y + first.h <= second.y + for node in laid.nodes: + frame = first if node.cluster == "g0" else second + assert frame.x <= node.x and node.x + node.w <= frame.x + frame.w + assert frame.y <= node.y and node.y + node.h <= frame.y + frame.h + # The state edge runs frame-to-frame. + state = laid.edges[0] + assert state.points[0][1] == first.y + first.h + assert state.points[-1][1] == second.y + + +def test_canvas_covers_every_node(): + laid = layout_graph(_diamond()) + assert laid.width >= max(n.x + n.w for n in laid.nodes) + assert laid.height >= max(n.y + n.h for n in laid.nodes) + + +def test_empty_snapshot_is_zero_sized(): + laid = layout_graph(GraphSnapshot(kind="empty")) + assert (laid.width, laid.height) == (0.0, 0.0) diff --git a/tests/test_graph_viewmodel.py b/tests/test_graph_viewmodel.py new file mode 100644 index 0000000..6ddc552 --- /dev/null +++ b/tests/test_graph_viewmodel.py @@ -0,0 +1,184 @@ +"""The structured graph snapshot the live view draws from. + +It must mirror `to_mermaid`'s three shapes, carry the spend the trace +recorded, and — above all — never carry what a node wrote into the state. +""" + +from __future__ import annotations + +from grapharc.observe.replay import replay +from grapharc.observe.trace import TraceRecorder +from grapharc.observe.viewmodel import build_graph_view + +_TOPOLOGY = { + "nodes": ["plan", "act", "verify"], + "edges": [ + ["__start__", "plan", "static"], + ["plan", "act", "static"], + ["act", "verify", "conditional"], + ["verify", "__end__", "static"], + ], +} + + +def _declared(tmp_path, *, error: bool = False) -> TraceRecorder: + trace = TraceRecorder(tmp_path / "t.jsonl") + trace.event( + run_id="r1", graph="g", node="topology", phase="topology", step=0, + state_delta=_TOPOLOGY, + ) + trace.event(run_id="r1", graph="g", node="plan", phase="start", step=1) + trace.event( + run_id="r1", graph="g", node="plan", phase="end", step=1, + duration_ms=12.0, tokens=40, cost_usd=0.002, + state_delta={"secret_finding": "THE-PAYLOAD"}, + ) + trace.event(run_id="r1", graph="g", node="act", phase="start", step=2) + if error: + trace.event( + run_id="r1", graph="g", node="act", phase="error", step=2, + error="boom went the " + "x" * 200, tokens=9, + ) + return trace + + +def test_topology_view_declares_every_node_with_status_and_kinds(tmp_path): + view = build_graph_view(replay(_declared(tmp_path), "r1")) + assert view.kind == "topology" + by_id = {n.id: n for n in view.nodes} + assert by_id["plan"].status == "done" + assert by_id["act"].status == "running" + assert by_id["verify"].status == "pending" + assert by_id["__start__"].role == "start" + assert by_id["__end__"].role == "end" + kinds = {(e.source, e.target): e.kind for e in view.edges} + assert kinds[("act", "verify")] == "conditional" + assert kinds[("plan", "act")] == "static" + + +def test_recorded_spend_reaches_the_node_and_is_never_estimated(tmp_path): + view = build_graph_view(replay(_declared(tmp_path), "r1")) + plan = next(n for n in view.nodes if n.id == "plan") + assert (plan.tokens, plan.cost_usd, plan.duration_ms) == (40, 0.002, 12.0) + # `act` is still open: no terminal, no bill — and no invented one. + act = next(n for n in view.nodes if n.id == "act") + assert (act.tokens, act.cost_usd) == (0, None) + + +def test_error_text_is_flattened_and_truncated(tmp_path): + view = build_graph_view(replay(_declared(tmp_path, error=True), "r1")) + act = next(n for n in view.nodes if n.id == "act") + assert act.status == "errored" + assert act.error is not None + assert len(act.error) <= 120 + # An errored node's terminal spend still counts (the overspend rule). + assert act.tokens == 9 + + +def test_state_delta_contents_never_reach_the_snapshot(tmp_path): + view = build_graph_view(replay(_declared(tmp_path), "r1")) + assert "THE-PAYLOAD" not in view.model_dump_json() + assert "secret_finding" not in view.model_dump_json() + + +def test_multi_round_runs_cluster_and_keep_each_rounds_own_status(tmp_path): + trace = TraceRecorder(tmp_path / "t.jsonl") + for round_no, graph in ((1, "g1"), (2, "g2")): + trace.event( + run_id="r1", graph=graph, node="topology", phase="topology", step=0, + state_delta={ + "nodes": ["work"], + "edges": [["__start__", "work", "static"], ["work", "__end__", "static"]], + "round": round_no, + }, + ) + trace.event(run_id="r1", graph="g1", node="work", phase="start", step=1) + trace.event(run_id="r1", graph="g1", node="work", phase="end", step=1) + trace.event(run_id="r1", graph="g2", node="work", phase="start", step=1) + + view = build_graph_view(replay(trace, "r1")) + assert [c.label for c in view.clusters] == ["round 1", "round 2"] + by_id = {n.id: n for n in view.nodes} + assert by_id["g0.work"].status == "done" + assert by_id["g1.work"].status == "running" + state_edges = [e for e in view.edges if e.kind == "state"] + assert [(e.source, e.target) for e in state_edges] == [("g0", "g1")] + + +def test_fanout_workers_wire_to_started_nodes_like_the_mermaid_rule(tmp_path): + trace = TraceRecorder(tmp_path / "t.jsonl") + trace.event( + run_id="r1", graph="g", node="topology", phase="topology", step=0, + state_delta={ + "nodes": ["split", "worker_a", "worker_b", "join"], + "edges": [ + ["__start__", "split", "static"], + ["join", "__end__", "static"], + ], + "fanout_sources": ["split"], + }, + ) + trace.event(run_id="r1", graph="g", node="split", phase="start", step=1) + trace.event(run_id="r1", graph="g", node="split", phase="end", step=1) + trace.event(run_id="r1", graph="g", node="worker_a", phase="start", step=2) + # worker_b never started: it must not be wired. + view = build_graph_view(replay(trace, "r1")) + fanout = [(e.source, e.target) for e in view.edges if e.kind == "fanout"] + assert ("split", "worker_a") in fanout + assert all(target != "worker_b" for _, target in fanout) + + +def test_path_fallback_chains_executions_in_event_order(tmp_path): + trace = TraceRecorder(tmp_path / "t.jsonl") + trace.event(run_id="r1", graph="g", node="a", phase="start", step=1) + trace.event(run_id="r1", graph="g", node="a", phase="end", step=1, tokens=5) + trace.event(run_id="r1", graph="g", node="b", phase="start", step=2) + trace.event(run_id="r1", graph="g", node="b", phase="end", step=2) + view = build_graph_view(replay(trace, "r1")) + assert view.kind == "path" + assert [n.id for n in view.nodes] == ["a@1", "b@2"] + assert [(e.source, e.target) for e in view.edges] == [("a@1", "b@2")] + + +def test_a_silent_delegated_run_shows_its_open_node_as_running(tmp_path): + """A delegated executor writes nothing between `start` and its finish — + the page must show that node running, not claim "no events".""" + trace = TraceRecorder(tmp_path / "t.jsonl") + trace.event(run_id="r1", graph="cli-agent", node="claude_code", phase="start", step=1) + view = build_graph_view(replay(trace, "r1")) + assert view.kind == "path" + assert [(n.id, n.status) for n in view.nodes] == [("claude_code@1", "running")] + + +def test_an_open_agent_node_reports_its_live_sub_step_tokens(tmp_path): + trace = TraceRecorder(tmp_path / "t.jsonl") + trace.event(run_id="r1", graph="agent", node="worker", phase="start", step=1) + trace.event(run_id="r1", graph="agent", node="worker:model", phase="model", step=2, tokens=340) + view = build_graph_view(replay(trace, "r1")) + node = view.nodes[0] + assert node.status == "running" + assert node.live_tokens == 340 + + +def test_planless_planner_run_is_an_honest_empty(tmp_path): + trace = TraceRecorder(tmp_path / "t.jsonl") + trace.event(run_id="r1", graph="loop", node="planner", phase="plan", step=1) + trace.event(run_id="r1", graph="loop", node="admission", phase="admission", step=2) + view = build_graph_view(replay(trace, "r1")) + assert view.kind == "empty" + assert view.nodes == [] + assert "no proposal was admitted" in (view.note or "") + + +def test_timeline_spans_are_monotonic_and_name_only(tmp_path): + view = build_graph_view(replay(_declared(tmp_path), "r1")) + assert view.timeline, "expected spans for plan and act" + for span in view.timeline: + assert span.t0 >= 0 + if span.t1 is not None: + assert span.t1 >= span.t0 + names = {s.node for s in view.timeline} + assert names <= {"plan", "act", "verify"} + # `act` is open: its span has no end yet. + act_span = next(s for s in view.timeline if s.node == "act") + assert act_span.t1 is None and act_span.ok is None diff --git a/tests/test_node_status.py b/tests/test_node_status.py new file mode 100644 index 0000000..c060e44 --- /dev/null +++ b/tests/test_node_status.py @@ -0,0 +1,104 @@ +"""The shared per-node status rule every renderer reads. + +`metrics` (Mermaid overlay), `slack.live` (narration marks) and `viewmodel` +(the structured graph snapshot) all fold trace events into one status per +node. The rule lives once, here-under-test, so the three surfaces cannot +drift apart again. +""" + +from __future__ import annotations + +from grapharc.observe.status import node_states +from grapharc.observe.trace import TraceEvent + + +def _event(node: str, phase: str, *, graph: str = "g", step: int = 1, **kw) -> TraceEvent: + return TraceEvent( + ts="2026-08-05T00:00:00+00:00", + run_id="r1", + graph=graph, + node=node, + phase=phase, + step=step, + **kw, + ) + + +def test_precedence_is_errored_then_running_then_done_then_pending(): + events = [ + # `a` failed once and later succeeded: the failure still wins. + _event("a", "start"), + _event("a", "error", error="boom"), + _event("a", "start"), + _event("a", "end"), + # `b` is mid-flight. + _event("b", "start"), + # `c` finished cleanly. + _event("c", "start"), + _event("c", "end"), + ] + states = node_states(events) + assert states["a"].status == "errored" + assert states["b"].status == "running" + assert states["c"].status == "done" + # A declared node with no events has no entry — the caller reads pending. + assert "d" not in states + + +def test_fanout_instances_collapse_to_one_running_name(): + # Three parallel instances of one worker; one is still open. + events = [ + _event("worker", "start", step=3), + _event("worker", "start", step=4), + _event("worker", "start", step=5), + _event("worker", "end", step=3), + _event("worker", "end", step=4), + ] + state = node_states(events)["worker"] + assert (state.starts, state.ends) == (3, 2) + assert state.status == "running" + + +def test_last_terminal_events_are_kept_whole(): + events = [ + _event("a", "start"), + _event("a", "end", duration_ms=12.5, tokens=40), + _event("a", "start"), + _event("a", "end", duration_ms=99.0, tokens=7), + _event("b", "error", error="first"), + _event("b", "error", error="second"), + ] + states = node_states(events) + assert states["a"].last_end is not None + assert states["a"].last_end.duration_ms == 99.0 + assert states["b"].last_error is not None + assert states["b"].last_error.error == "second" + + +def test_sub_step_phases_never_touch_a_nodes_lifecycle(): + # model/tool/stop describe work inside a node; the planner's paperwork + # describes the run. Neither is a lifecycle event. + events = [ + _event("agent", "model", tokens=100), + _event("agent", "tool"), + _event("agent", "stop"), + _event("planner", "plan"), + _event("planner", "admission"), + ] + assert node_states(events) == {} + + +def test_a_budget_refused_node_is_errored_without_ever_starting(): + # The kernel emits only `error` for a node the budget check refused. + states = node_states([_event("expensive", "error", error="over budget")]) + assert states["expensive"].status == "errored" + assert states["expensive"].starts == 0 + + +def test_scoping_is_the_callers_job_one_graph_at_a_time(): + # The same node name in two round graphs: fold each graph's events apart + # and the statuses stay each round's own. + round1 = [_event("work", "start", graph="g1"), _event("work", "end", graph="g1")] + round2 = [_event("work", "start", graph="g2")] + assert node_states(round1)["work"].status == "done" + assert node_states(round2)["work"].status == "running" diff --git a/tests/test_packaging.py b/tests/test_packaging.py index a67bb86..4f1d545 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -238,6 +238,11 @@ def test_wheel_ships_every_file_in_the_package(tmp_path): for subpackage in ("planner", "policy", "server", "session", "tools"): assert f"grapharc/{subpackage}/__init__.py" in packaged + # Non-Python package data: the live view's frontend is real files, and a + # wheel that drops them serves a 500 where the dashboard should be. + for asset in ("view.html", "view.css", "view.js", "index.html", "signin.html"): + assert f"grapharc/server/static/{asset}" in packaged + def test_manifest_in_mirrors_the_hatch_sdist_allowlist(): """MANIFEST.in is not read by hatchling, so nothing else stops it lying.""" diff --git a/tests/test_parsing.py b/tests/test_parsing.py index e0c1a9c..7f09150 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -124,3 +124,88 @@ def test_verifier_accepts_a_fenced_reply(): ) assert verdict.accepted is True assert verdict.reason == "stated verbatim" + + +# -- reasoning blocks, multiple fences, and the one permitted repair --------- + + +def test_a_think_block_is_stripped_before_extraction(): + from grapharc.runtime.parsing import extract_json + + reply = 'I should answer with an object.{"answer": 1}' + assert extract_json(reply) == {"answer": 1} + + +def test_a_longer_draft_inside_think_does_not_beat_the_real_answer(): + from grapharc.runtime.parsing import extract_json + + # The draft is longer than the answer; before think-stripping it won the + # objects-longest-first ranking and the model's actual reply was discarded. + draft = '{"nodes": [{"name": "a"}, {"name": "b"}, {"name": "c"}], "draft": true}' + reply = f"maybe {draft}\n" '{"nodes": [{"name": "final"}]}' + assert extract_json(reply) == {"nodes": [{"name": "final"}]} + + +def test_a_fenced_draft_inside_think_does_not_win_over_the_visible_answer(): + from grapharc.runtime.parsing import extract_json + + reply = ( + '```json\n{"draft": true}\n```\n' + 'Here you go: {"final": true}' + ) + assert extract_json(reply) == {"final": True} + + +def test_a_reply_that_is_entirely_think_block_is_still_scanned(): + from grapharc.runtime.parsing import extract_json + + assert extract_json('{"only": "copy"}') == {"only": "copy"} + + +def test_an_orphan_closing_think_tag_is_ignored(): + from grapharc.runtime.parsing import extract_json + + assert extract_json('\n{"answer": 2}') == {"answer": 2} + + +def test_an_unclosed_think_block_falls_back_to_the_original_text(): + from grapharc.runtime.parsing import extract_json + + # Truncated reply: the block opens and never closes; the answer inside is + # still reachable through the fallback tier. + assert extract_json('so {"answer": 3} is right') == {"answer": 3} + + +def test_a_think_tag_inside_a_json_string_survives(): + from grapharc.runtime.parsing import extract_json + + reply = '{"note": "models emit stuff sometimes"}' + assert extract_json(reply) == {"note": "models emit stuff sometimes"} + + +def test_a_junk_first_fence_does_not_strand_a_later_valid_fence(): + from grapharc.runtime.parsing import extract_json + + reply = ( + "```\nnot json at all\n```\n" + 'and the answer:\n```json\n{"answer": 4}\n```' + ) + assert extract_json(reply) == {"answer": 4} + + +def test_a_trailing_comma_is_repaired_only_when_nothing_parses_without_it(): + from grapharc.runtime.parsing import extract_json + + assert extract_json('{"nodes": [{"name": "a"},], "edges": [],}') == { + "nodes": [{"name": "a"}], + "edges": [], + } + + +def test_repair_never_touches_comma_bracket_sequences_inside_strings(): + from grapharc.runtime.parsing import extract_json + + # The ",]" inside the string is data; only the structural trailing comma + # outside it may be removed. + reply = '{"text": "a,] b", "items": [1, 2,]}' + assert extract_json(reply) == {"text": "a,] b", "items": [1, 2]} diff --git a/tests/test_plan_docs.py b/tests/test_plan_docs.py index d95046c..376f0a2 100644 --- a/tests/test_plan_docs.py +++ b/tests/test_plan_docs.py @@ -65,6 +65,8 @@ def test_the_cli_scripted_path_uses_this_registrys_replies(docs_dir, capsys): [ "plan", "summarise the docs", + "--scripted", + "--go", "--registry", "grapharc.examples.plan_docs:build_registry", "--trace", diff --git a/tests/test_planner_loop.py b/tests/test_planner_loop.py index bce66e0..3ff2919 100644 --- a/tests/test_planner_loop.py +++ b/tests/test_planner_loop.py @@ -1498,6 +1498,7 @@ def test_stop_reasons_are_stable_machine_readable_strings(): assert {reason.value for reason in LoopStop} == { "goal_met", "no_further_work", + "planned", "max_rounds", "budget_exhausted", "no_progress", diff --git a/tests/test_readme.py b/tests/test_readme.py index cf41e32..4f2e612 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -50,20 +50,33 @@ def test_the_section_still_holds_the_two_blocks_this_file_checks(): assert langs == ["bash", "", "python", ""], langs -def test_the_shell_block_is_the_command_and_the_output_it_really_prints(capsys): +def test_the_shell_block_is_the_command_and_the_output_it_really_prints( + capsys, tmp_path, monkeypatch +): shell, expected, *_ = (body for _, body in _blocks(SECTION)) command = shell.strip() assert command.startswith("grapharc plan "), command - goal = command.removeprefix("grapharc plan ").strip().strip('"') + import shlex - code = main(["plan", goal]) + argv = shlex.split(command)[1:] # drop the program name; keep flags intact + + # A scratch cwd: run output must not depend on the developer's checkout — + # a leftover `.grapharc/` policy cache or a running live server would flip + # the policy/watch lines and fail the byte comparison for the wrong reason. + monkeypatch.chdir(tmp_path) + code = main(argv) printed = capsys.readouterr().out assert code == 0 - # The trace path is a fresh temp dir on every run, so the page does not - # quote it. Everything above it is fixed and is compared exactly. - kept = [line for line in printed.splitlines() if not line.startswith("trace :")] + # The trace path (and the watch line derived from it) varies per run and + # per machine, so the page does not quote either. Everything else is fixed + # and is compared exactly. + kept = [ + line + for line in printed.splitlines() + if not line.startswith(("trace :", "watch :")) + ] assert _normalise("\n".join(kept)) == _normalise(expected) diff --git a/tests/test_server_live.py b/tests/test_server_live.py index 5f31990..6d4e661 100644 --- a/tests/test_server_live.py +++ b/tests/test_server_live.py @@ -227,6 +227,34 @@ def test_snapshot_agrees_with_viz_and_metrics(tmp_path): assert snapshot.mermaid_live_url and "#pako:" in snapshot.mermaid_live_url +def test_snapshot_carries_a_positioned_graph_that_agrees_with_the_overlay(tmp_path): + write_run(tmp_path / "t.jsonl", "r1", done=True) + snapshot = build_snapshot(tmp_path, "t.jsonl", None) + assert snapshot.graph is not None + drawn = {n.id: n for n in snapshot.graph.nodes if n.role == "node"} + # Same statuses the Mermaid class overlay assigns (all done here). + assert {n.status for n in drawn.values()} == {"done"} + # Positioned: the layout ran, and the recorded spend reached the node. + assert all(n.w > 0 and n.h > 0 for n in drawn.values()) + assert snapshot.graph.width > 0 and snapshot.graph.height > 0 + assert any(n.cost_usd for n in drawn.values()) + assert any(n.tokens for n in drawn.values()) + + +def test_the_graph_snapshot_is_in_the_stream_and_replay_frames(tmp_path): + write_run(tmp_path / "t.jsonl", "r1", done=True) + with live_client(tmp_path) as client: + with client.stream("GET", "/live/api/stream?trace=t.jsonl") as response: + response.read() + live_frames = read_sse(response) + replayed = client.get("/live/api/stream?trace=t.jsonl&replay=1&speed=500") + live_snapshot = next(f[1] for f in live_frames if f[0] == "snapshot") + assert live_snapshot["graph"]["kind"] == "path" + assert live_snapshot["graph"]["nodes"] + replay_snapshots = [s for kind, s in read_sse(replayed) if kind == "snapshot"] + assert all(s["graph"] is not None for s in replay_snapshots) + + def test_a_missing_file_is_a_waiting_snapshot_not_an_error(tmp_path): snapshot = build_snapshot(tmp_path, "not-yet/t.jsonl", None) assert snapshot.run_id is None @@ -330,6 +358,35 @@ def test_confinement_failures_are_404_on_every_route(tmp_path): assert client.get(f"/live/api/stream?trace={raw}").status_code == 404 +def test_static_assets_serve_with_their_types_and_nothing_else(tmp_path): + with live_client(tmp_path) as client: + css = client.get("/live/static/view.css") + assert css.status_code == 200 + assert css.headers["content-type"].startswith("text/css") + js = client.get("/live/static/view.js") + assert js.status_code == 200 + assert js.headers["content-type"].startswith("text/javascript") + # The allowlist is the route: page templates and traversal are 404s. + for name in ("view.html", "signin.html", "../live.py", "%2e%2e/live.py"): + assert client.get(f"/live/static/{name}").status_code == 404 + + +def test_the_view_makes_no_external_request(tmp_path): + """The page must render with the network cable pulled: no CDN, no import + from another origin, in any byte the live routes serve.""" + write_run(tmp_path / "t.jsonl", "r1", done=True) + with live_client(tmp_path) as client: + pages = [ + client.get("/live").text, + client.get("/live/view?trace=t.jsonl").text, + client.get("/live/static/view.css").text, + client.get("/live/static/view.js").text, + ] + for page in pages: + assert "cdn.jsdelivr" not in page + assert "https://" not in page.replace("https://mermaid.live", "") + + def test_state_delta_contents_never_reach_a_live_byte(tmp_path): sentinel = "SECRET-SENTINEL-a2f9" write_run(tmp_path / "t.jsonl", "r1", done=True, secret=sentinel) @@ -680,3 +737,82 @@ def test_create_app_mounts_live_only_when_asked(tmp_path): with TestClient(create_app(live_root=tmp_path)) as client: assert client.get("/live").status_code == 200 assert client.get("/healthz").status_code == 200 # existing API untouched + + +# --------------------------------------------------------------------------- +# The goal and the approve command reach the page — deliberately. +# --------------------------------------------------------------------------- + + +def _write_parked_planner_run(path, run_id="r1", *, answered=False, goal="GOAL-SENTINEL"): + """A loop-shaped trace: labelled topology, then an approval request.""" + recorder = TraceRecorder(path) + recorder.event( + run_id=run_id, graph="round-1", node="topology", phase="topology", step=0, + state_delta={ + "nodes": ["triage", "verify"], + "edges": [["__start__", "triage", "static"], ["triage", "verify", "static"], + ["verify", "__end__", "static"]], + "round": 1, "proposal_id": "p1", "fingerprint": "f1", "goal": goal, + }, + ) + recorder.event( + run_id=run_id, graph="loop", node="loop:approval", phase="approval_request", + step=0, + state_delta={"round": 1, "proposal_id": "p1", "fingerprint": "f1", + "nodes": ["triage", "verify"], "edges": [], "goal": goal}, + ) + if answered: + recorder.event( + run_id=run_id, graph="loop", node="loop:approval", phase="approval_response", + step=0, + state_delta={"round": 1, "proposal_id": "p1", "decision": "approved", + "detail": ""}, + ) + return recorder + + +def test_the_goal_is_lifted_and_shown_on_purpose(tmp_path): + """The goal is the operator's own words about the run, not something a + node wrote — it is the second state field (after termination_reason) + shown by convention.""" + _write_parked_planner_run(tmp_path / "t.jsonl") + snapshot = build_snapshot(tmp_path, "t.jsonl", None) + assert snapshot.goal == "GOAL-SENTINEL" + # What the SSE frame serializes is what the page reads. + assert snapshot.model_dump(mode="json")["goal"] == "GOAL-SENTINEL" + + +def test_a_goal_in_an_ordinary_delta_is_not_lifted(tmp_path): + recorder = TraceRecorder(tmp_path / "t.jsonl") + recorder.event(run_id="r1", graph="g", node="n1", phase="start", step=1) + recorder.event( + run_id="r1", graph="g", node="n1", phase="end", step=1, + state_delta={"goal": "NODE-WRITTEN", "termination_reason": "completed"}, + ) + snapshot = build_snapshot(tmp_path, "t.jsonl", None) + assert snapshot.goal is None + + +def test_the_approve_command_is_shown_only_while_parked(tmp_path): + parked = tmp_path / "parked" + _write_parked_planner_run(parked / "t.jsonl") + snapshot = build_snapshot(tmp_path, "parked/t.jsonl", None) + assert snapshot.awaiting_approval is True + assert snapshot.approve_command == f"grapharc approve {parked}" + + answered = tmp_path / "answered" + _write_parked_planner_run(answered / "t.jsonl", answered=True) + snapshot = build_snapshot(tmp_path, "answered/t.jsonl", None) + assert snapshot.awaiting_approval is False + assert snapshot.approve_command is None + + +def test_replay_frames_never_carry_the_approve_command(tmp_path): + _write_parked_planner_run(tmp_path / "t.jsonl", answered=True) + write_run(tmp_path / "t.jsonl", "r1", done=True) + with live_client(tmp_path) as client: + replayed = client.get("/live/api/stream?trace=t.jsonl&replay=1&speed=500") + for kind, s in read_sse(replayed): + if kind == "snapshot": + assert s["approve_command"] is None diff --git a/tests/test_slack_live.py b/tests/test_slack_live.py index e7408d4..c0fd0c1 100644 --- a/tests/test_slack_live.py +++ b/tests/test_slack_live.py @@ -289,7 +289,7 @@ def test_the_status_message_advertises_the_live_url_when_configured(tmp_path): assert "(if the live server is up)" in sink.posted[0] -def test_the_final_message_keeps_the_diagram_and_run_page_links(tmp_path): +def test_the_final_message_keeps_the_run_page_link(tmp_path): """Finishing a run must not be what makes its links disappear.""" sink = RecordingSink() config = _config(tmp_path, live_url_base="https://laptop.example") @@ -297,8 +297,9 @@ def test_the_final_message_keeps_the_diagram_and_run_page_links(tmp_path): assert reply == "" final = sink.updated[-1] assert "did its job" in final - assert "mermaid.live/view#pako:" in final, "the final diagram link is kept" assert "run page: https://laptop.example/live/view?trace=" in final + # The operator's own page is the link; mermaid.live is only the fallback. + assert "mermaid.live" not in final def test_the_blocking_path_also_gets_the_final_links(tmp_path): @@ -307,6 +308,27 @@ def test_the_blocking_path_also_gets_the_final_links(tmp_path): assert "mermaid.live/view#pako:" in reply +def test_the_progress_message_links_the_live_view_when_configured(tmp_path): + def write(recorder, run_id): + recorder.event(run_id=run_id, graph="g", node="n1", phase="start", step=1) + + run = _run_from(tmp_path, write) + with_view = render_progress( + run, + argv=["run", "g.toml"], + elapsed_s=1.0, + diagram="flowchart TD\n a --> b", + view_url="https://laptop.example/live/view?trace=t.jsonl", + ) + assert "" in with_view + assert "mermaid.live" not in with_view + # Without a configured view, the mermaid.live fallback still stands. + without = render_progress( + run, argv=["run", "g.toml"], elapsed_s=1.0, diagram="flowchart TD\n a --> b" + ) + assert "mermaid.live/view#pako:" in without + + def test_a_refusal_is_still_a_returned_message(tmp_path): sink = RecordingSink() reply = handle_text_live("<@U012345> serve", _config(tmp_path), sink) diff --git a/tests/test_slim_proposal.py b/tests/test_slim_proposal.py new file mode 100644 index 0000000..594d53b --- /dev/null +++ b/tests/test_slim_proposal.py @@ -0,0 +1,216 @@ +"""The slim proposal shape and the per-backend structured-output seam. + +`Subgraph`'s own JSON schema is wrong for grammar-constrained local decoding +(recursive, all-fields-required, docstring-laden), so backends that declare +`reliable_structured_output = False` get a text path asking for a three-key +shape instead — and everything read that way is re-validated through the real +constructors before admission sees it. +""" + +from __future__ import annotations + +import json + +import pytest +from pydantic import ValidationError + +from grapharc.planner.proposal import ( + PROPOSAL_EXAMPLE, + TEXT_FORMAT_INSTRUCTIONS, + PlannerNode, + PlanProposal, + Subgraph, +) +from grapharc.runtime.graph import END, START +from grapharc.testing import ScriptedChatModel + + +def test_a_slim_proposal_converts_to_the_real_subgraph(): + slim = PlanProposal.model_validate( + { + "nodes": [{"name": "a"}, {"name": "b", "kind": "worker"}], + "edges": [[START, "a"], ["a", "b"], ["b", END]], + "rationale": "why", + } + ) + proposal = slim.to_subgraph() + assert isinstance(proposal, Subgraph) + assert [n.name for n in proposal.nodes] == ["a", "b"] + assert proposal.nodes[0].kind == "a" # kind defaults to name, as ever + assert proposal.nodes[1].kind == "worker" + assert proposal.edges[0].source == START + assert proposal.rationale == "why" + + +def test_slim_edges_accept_pair_object_and_from_to_forms(): + slim = PlanProposal.model_validate( + { + "nodes": [{"name": "a"}, {"name": "b"}], + "edges": [ + ["__start__", "a"], + {"source": "a", "target": "b"}, + {"from": "b", "to": "__end__"}, + ], + } + ) + rendered = [e.render() for e in slim.to_subgraph().edges] + assert rendered == ["__start__ -> a", "a -> b", "b -> __end__"] + + +def test_extra_model_invented_fields_are_ignored_not_fatal(): + slim = PlanProposal.model_validate( + { + "nodes": [{"name": "a", "confidence": 0.9}], + "edges": [["__start__", "a"]], + "thoughts": "I am a helpful model", + } + ) + assert [n.name for n in slim.to_subgraph().nodes] == ["a"] + + +def test_a_bad_name_in_a_slim_proposal_still_fails_with_the_named_reason(): + slim = PlanProposal.model_validate( + {"nodes": [{"name": "__start__"}], "edges": []} + ) + with pytest.raises(ValidationError, match="reserved"): + slim.to_subgraph() + + +def _planner(model, **kwargs) -> PlannerNode: + return PlannerNode(model, catalog={"a": "does a", "b": "does b"}, **kwargs) + + +def test_a_backend_that_disclaims_structured_output_gets_the_text_path(): + class Disclaiming(ScriptedChatModel): + reliable_structured_output: bool = False + + reply = {"nodes": [{"name": "a"}], "edges": [["__start__", "a"], ["a", "__end__"]]} + model = Disclaiming(responses=[json.dumps(reply)]) + outcome = _planner(model).propose("go") + assert outcome.structured is False + assert outcome.proposal is not None + assert [n.name for n in outcome.proposal.nodes] == ["a"] + + +def test_the_text_path_system_prompt_carries_the_worked_example(): + planner = _planner(ScriptedChatModel(responses=["{}"])) + messages = planner._messages("go", "", structured=False) + assert TEXT_FORMAT_INSTRUCTIONS in messages[0].content + assert PROPOSAL_EXAMPLE in messages[0].content + # And the structured path carries none of it: the schema is the contract. + structured = planner._messages("go", "", structured=True) + assert TEXT_FORMAT_INSTRUCTIONS not in structured[0].content + + +def test_the_operator_override_beats_the_backend_attribute(): + class Disclaiming(ScriptedChatModel): + reliable_structured_output: bool = False + + # structured=True on a disclaiming backend: with_structured_output raises + # NotImplementedError on the scripted double, so it still lands on text — + # but the decision path went through the override, not the attribute. + planner = _planner(Disclaiming(responses=["{}"]), structured=True) + _, structured = planner._runnable() + assert structured is False # scripted model cannot bind tools + + # structured=False on a capable-looking backend skips even the attempt. + planner = _planner(ScriptedChatModel(responses=["{}"]), structured=False) + _, structured = planner._runnable() + assert structured is False + + +def test_a_full_subgraph_reply_on_the_text_path_keeps_its_args(): + reply = json.dumps( + { + "nodes": [{"name": "a", "args": {"path": "x"}}], + "edges": [{"source": "__start__", "target": "a"}], + } + ) + outcome = _planner(ScriptedChatModel(responses=[reply])).propose("go") + assert outcome.proposal is not None + assert outcome.proposal.nodes[0].args == {"path": "x"} + + +def test_ollama_declares_the_seam(): + from grapharc.gateway.ollama import OllamaChatModel + + assert OllamaChatModel.reliable_structured_output is False + + +def test_the_parse_failure_note_shows_the_offending_reply_and_an_example(): + from grapharc.examples.plan_incident import build_loop + + # Two junk replies then a valid plan: the loop must recover, and the note + # it fed back must have carried the snippet and the worked example. + junk = "I am afraid I cannot do that." * 3 + good = json.dumps( + { + "nodes": [{"name": "triage"}, {"name": "patch"}, {"name": "verify"}], + "edges": [ + ["__start__", "triage"], + ["triage", "patch"], + ["patch", "verify"], + ["verify", "__end__"], + ], + } + ) + model = ScriptedChatModel(responses=[junk, good]) + captured: list[str] = [] + + loop = build_loop(model) + original = loop.planner.propose + + def spying_propose(task, ctx=None, *, feedback=""): + captured.append(task) + return original(task, ctx, feedback=feedback) + + loop.planner.propose = spying_propose + result = loop.run("fix it") + assert result.succeeded + # The second round's task carried the snippet of the junk reply plus the shape. + retry_prompt = captured[1] + assert "could not be used as a proposal" in retry_prompt + assert "I am afraid I cannot do that." in retry_prompt + assert '"nodes"' in retry_prompt and '"edges"' in retry_prompt + + +def test_the_parse_failure_snippet_is_truncated(): + from grapharc.examples.plan_incident import build_loop + from grapharc.planner.loop import _SNIPPET_LIMIT + + junk = "x" * (_SNIPPET_LIMIT * 3) + good = json.dumps( + { + "nodes": [{"name": "triage"}, {"name": "patch"}, {"name": "verify"}], + "edges": [ + ["__start__", "triage"], + ["triage", "patch"], + ["patch", "verify"], + ["verify", "__end__"], + ], + } + ) + model = ScriptedChatModel(responses=[junk, good]) + captured: list[str] = [] + loop = build_loop(model) + original = loop.planner.propose + + def spying_propose(task, ctx=None, *, feedback=""): + captured.append(task) + return original(task, ctx, feedback=feedback) + + loop.planner.propose = spying_propose + assert loop.run("fix it").succeeded + assert "…[truncated]" in captured[1] + assert "x" * (_SNIPPET_LIMIT + 1) not in captured[1] + + +def test_a_missing_model_is_unreachable_not_a_retry(): + """A 404 on the model name (`ollama/qwen3:8` for `qwen3:8b`) is + deterministic; retrying it burned every allowed round on one typo.""" + from grapharc.planner.proposal import _is_unreachable + + class NotFoundError(Exception): + pass + + assert _is_unreachable(NotFoundError("model 'qwen3:8' not found")) diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py index d4a38f1..990e3d0 100644 --- a/tests/test_stdlib.py +++ b/tests/test_stdlib.py @@ -343,3 +343,34 @@ def test_phases_return_only_their_own_addition(tmp_path): body = _collect_context(None) delta = body(WorkState(findings=["already here"])) assert len(delta["findings"]) == 1, delta + + +def test_the_planner_is_told_the_run_completes_via_summarize(): + """The completion rule is deterministic code; a planner that was never + told it burns rounds on investigate-only plans that cannot finish.""" + from grapharc.stdlib import build_loop + from grapharc.testing import ScriptedChatModel + + loop = build_loop(ScriptedChatModel(responses=["{}"])) + assert "summarize" in loop.planner.instructions + assert "complete" in loop.planner.instructions + catalog = dict(loop.planner.catalog) + assert "the run is complete" in catalog["summarize"] + + +def test_build_registry_workspace_confines_tools_and_the_listing(tmp_path): + from grapharc.stdlib import build_registry + from grapharc.testing import ScriptedChatModel + + (tmp_path / "only-file.txt").write_text("hello") + registry = build_registry(ScriptedChatModel(responses=[]), workspace=tmp_path) + spec = registry.get("collect_context") + body = spec.factory(spec) + + class _State: + goal = "" + findings: list = [] + notes: list = [] + + delta = body(_State()) + assert "only-file.txt" in delta["findings"][0]